title_bar: Move the organization items inside the user menu (#52343)

Danilo Leal and Gaauwe Rombouts created

Closes CLO-339

This PR moves the organization menu items inside the user menu, instead
of a standalone button within the title bar. We did it so because 1)
we're not fully sure how useful _always_ seeing in which org you are in
will be, and 2) seeing in which org you're in and seeing in which _plan_
you are in are very similar in terms of use case, and we don't currently
display the plan in the title bar... thus, that served as argument for
the same level of visibility.

---

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:

- N/A

---------

Co-authored-by: Gaauwe Rombouts <mail@grombouts.nl>

Change summary

crates/title_bar/src/collab.rs    |  38 ++--
crates/title_bar/src/title_bar.rs | 230 +++++++++++++++-----------------
2 files changed, 125 insertions(+), 143 deletions(-)

Detailed changes

crates/title_bar/src/collab.rs 🔗

@@ -383,10 +383,29 @@ impl TitleBar {
             ConnectionQuality::Poor => (IconName::SignalMedium, Some(Color::Warning), "Poor"),
             ConnectionQuality::Lost => (IconName::SignalLow, Some(Color::Error), "Lost"),
         };
+
         let quality_label: SharedString = quality_label.into();
+
+        children.push(
+            h_flex()
+                .gap_1()
+                .child(
+                    IconButton::new("leave-call", IconName::Exit)
+                        .style(ButtonStyle::Subtle)
+                        .tooltip(Tooltip::text("Leave Call"))
+                        .icon_size(IconSize::Small)
+                        .on_click(move |_, _window, cx| {
+                            ActiveCall::global(cx)
+                                .update(cx, |call, cx| call.hang_up(cx))
+                                .detach_and_log_err(cx);
+                        }),
+                )
+                .child(Divider::vertical().color(DividerColor::Border))
+                .into_any_element(),
+        );
+
         children.push(
             IconButton::new("call-quality", signal_icon)
-                .style(ButtonStyle::Subtle)
                 .icon_size(IconSize::Small)
                 .when_some(signal_color, |button, color| button.icon_color(color))
                 .tooltip(move |_window, cx| {
@@ -413,23 +432,6 @@ impl TitleBar {
                 })
                 .into_any_element(),
         );
-        children.push(
-            h_flex()
-                .gap_1()
-                .child(
-                    IconButton::new("leave-call", IconName::Exit)
-                        .style(ButtonStyle::Subtle)
-                        .tooltip(Tooltip::text("Leave Call"))
-                        .icon_size(IconSize::Small)
-                        .on_click(move |_, _window, cx| {
-                            ActiveCall::global(cx)
-                                .update(cx, |call, cx| call.hang_up(cx))
-                                .detach_and_log_err(cx);
-                        }),
-                )
-                .child(Divider::vertical().color(DividerColor::Border))
-                .into_any_element(),
-        );
 
         if is_local && can_share_projects && !is_connecting_to_project {
             let is_sharing_disabled = channel.is_some_and(|channel| match channel.visibility {

crates/title_bar/src/title_bar.rs 🔗

@@ -27,9 +27,9 @@ use client::{Client, UserStore, zed_urls};
 use cloud_api_types::Plan;
 
 use gpui::{
-    Action, AnyElement, App, Context, Corner, Element, Empty, Entity, Focusable,
-    InteractiveElement, IntoElement, MouseButton, ParentElement, Render,
-    StatefulInteractiveElement, Styled, Subscription, WeakEntity, Window, actions, div,
+    Action, AnyElement, App, Context, Corner, Element, Entity, Focusable, InteractiveElement,
+    IntoElement, MouseButton, ParentElement, Render, StatefulInteractiveElement, Styled,
+    Subscription, WeakEntity, Window, actions, div,
 };
 use onboarding_banner::OnboardingBanner;
 use project::{Project, git_store::GitStoreEvent, trusted_worktrees::TrustedWorktrees};
@@ -260,7 +260,6 @@ impl Render for TitleBar {
                     user.is_none() && TitleBarSettings::get_global(cx).show_sign_in,
                     |this| this.child(self.render_sign_in_button(cx)),
                 )
-                .child(self.render_organization_menu_button(cx))
                 .when(TitleBarSettings::get_global(cx).show_user_menu, |this| {
                     this.child(self.render_user_menu_button(cx))
                 })
@@ -1132,110 +1131,70 @@ impl TitleBar {
             })
     }
 
-    pub fn render_organization_menu_button(&mut self, cx: &mut Context<Self>) -> AnyElement {
-        let Some(organization) = self.user_store.read(cx).current_organization() else {
-            return Empty.into_any_element();
-        };
-
-        PopoverMenu::new("organization-menu")
-            .anchor(Corner::TopRight)
-            .menu({
-                let user_store = self.user_store.clone();
-                move |window, cx| {
-                    ContextMenu::build(window, cx, |mut menu, _window, cx| {
-                        menu = menu.header("Organizations").separator();
-
-                        let current_organization = user_store.read(cx).current_organization();
-
-                        for organization in user_store.read(cx).organizations() {
-                            let organization = organization.clone();
-                            let plan = user_store.read(cx).plan_for_organization(&organization.id);
-
-                            let is_current =
-                                current_organization
-                                    .as_ref()
-                                    .is_some_and(|current_organization| {
-                                        current_organization.id == organization.id
-                                    });
-
-                            menu = menu.custom_entry(
-                                {
-                                    let organization = organization.clone();
-                                    move |_window, _cx| {
-                                        h_flex()
-                                            .w_full()
-                                            .gap_1()
-                                            .child(
-                                                div()
-                                                    .flex_none()
-                                                    .when(!is_current, |parent| parent.invisible())
-                                                    .child(Icon::new(IconName::Check)),
-                                            )
-                                            .child(
-                                                h_flex()
-                                                    .w_full()
-                                                    .gap_3()
-                                                    .justify_between()
-                                                    .child(Label::new(&organization.name))
-                                                    .child(PlanChip::new(
-                                                        plan.unwrap_or(Plan::ZedFree),
-                                                    )),
-                                            )
-                                            .into_any_element()
-                                    }
-                                },
-                                {
-                                    let user_store = user_store.clone();
-                                    let organization = organization.clone();
-                                    move |_window, cx| {
-                                        user_store.update(cx, |user_store, cx| {
-                                            user_store
-                                                .set_current_organization(organization.clone(), cx);
-                                        });
-                                    }
-                                },
-                            );
-                        }
-
-                        menu
-                    })
-                    .into()
-                }
-            })
-            .trigger_with_tooltip(
-                Button::new("organization-menu", &organization.name)
-                    .selected_style(ButtonStyle::Tinted(TintColor::Accent))
-                    .label_size(LabelSize::Small),
-                Tooltip::text("Toggle Organization Menu"),
-            )
-            .anchor(gpui::Corner::TopRight)
-            .into_any_element()
-    }
-
     pub fn render_user_menu_button(&mut self, cx: &mut Context<Self>) -> impl Element {
-        let show_update_badge = self.update_version.read(cx).show_update_in_menu_bar();
+        let show_update_button = self.update_version.read(cx).show_update_in_menu_bar();
 
-        let user_store = self.user_store.read(cx);
-        let user = user_store.current_user();
+        let user_store = self.user_store.clone();
+        let user_store_read = user_store.read(cx);
+        let user = user_store_read.current_user();
 
         let user_avatar = user.as_ref().map(|u| u.avatar_uri.clone());
         let user_login = user.as_ref().map(|u| u.github_login.clone());
 
         let is_signed_in = user.is_some();
 
-        let has_subscription_period = user_store.subscription_period().is_some();
-        let plan = user_store.plan().filter(|_| {
+        let has_subscription_period = user_store_read.subscription_period().is_some();
+        let plan = user_store_read.plan().filter(|_| {
             // Since the user might be on the legacy free plan we filter based on whether we have a subscription period.
             has_subscription_period
         });
 
+        let has_organization = user_store_read.current_organization().is_some();
+
+        let current_organization = user_store_read.current_organization();
+        let organizations: Vec<_> = user_store_read
+            .organizations()
+            .iter()
+            .map(|org| {
+                let plan = user_store_read.plan_for_organization(&org.id);
+                (org.clone(), plan)
+            })
+            .collect();
+
+        let show_user_picture = TitleBarSettings::get_global(cx).show_user_picture;
+
+        let trigger = if is_signed_in && show_user_picture {
+            let avatar = user_avatar.map(|avatar| Avatar::new(avatar)).map(|avatar| {
+                if show_update_button {
+                    avatar.indicator(
+                        div()
+                            .absolute()
+                            .bottom_0()
+                            .right_0()
+                            .child(Indicator::dot().color(Color::Accent)),
+                    )
+                } else {
+                    avatar
+                }
+            });
+
+            ButtonLike::new("user-menu").children(avatar)
+        } else {
+            ButtonLike::new("user-menu")
+                .child(Icon::new(IconName::ChevronDown).size(IconSize::Small))
+        };
+
         PopoverMenu::new("user-menu")
-            .anchor(Corner::TopRight)
+            .trigger(trigger)
             .menu(move |window, cx| {
-                ContextMenu::build(window, cx, |menu, _, _cx| {
-                    let user_login = user_login.clone();
+                let user_login = user_login.clone();
+                let current_organization = current_organization.clone();
+                let organizations = organizations.clone();
+                let user_store = user_store.clone();
 
+                ContextMenu::build(window, cx, |menu, _, _cx| {
                     menu.when(is_signed_in, |this| {
+                        let user_login = user_login.clone();
                         this.custom_entry(
                             move |_window, _cx| {
                                 let user_login = user_login.clone().unwrap_or_default();
@@ -1253,7 +1212,7 @@ impl TitleBar {
                         )
                         .separator()
                     })
-                    .when(show_update_badge, |this| {
+                    .when(show_update_button, |this| {
                         this.custom_entry(
                             move |_window, _cx| {
                                 h_flex()
@@ -1274,6 +1233,58 @@ impl TitleBar {
                         )
                         .separator()
                     })
+                    .when(has_organization, |this| {
+                        let mut this = this.header("Organization");
+
+                        for (organization, plan) in &organizations {
+                            let organization = organization.clone();
+                            let plan = *plan;
+
+                            let is_current =
+                                current_organization
+                                    .as_ref()
+                                    .is_some_and(|current_organization| {
+                                        current_organization.id == organization.id
+                                    });
+
+                            this = this.custom_entry(
+                                {
+                                    let organization = organization.clone();
+                                    move |_window, _cx| {
+                                        h_flex()
+                                            .w_full()
+                                            .gap_4()
+                                            .justify_between()
+                                            .child(
+                                                h_flex()
+                                                    .gap_1()
+                                                    .child(Label::new(&organization.name))
+                                                    .when(is_current, |this| {
+                                                        this.child(
+                                                            Icon::new(IconName::Check)
+                                                                .color(Color::Accent),
+                                                        )
+                                                    }),
+                                            )
+                                            .child(PlanChip::new(plan.unwrap_or(Plan::ZedFree)))
+                                            .into_any_element()
+                                    }
+                                },
+                                {
+                                    let user_store = user_store.clone();
+                                    let organization = organization.clone();
+                                    move |_window, cx| {
+                                        user_store.update(cx, |user_store, cx| {
+                                            user_store
+                                                .set_current_organization(organization.clone(), cx);
+                                        });
+                                    }
+                                },
+                            );
+                        }
+
+                        this.separator()
+                    })
                     .action("Settings", zed_actions::OpenSettings.boxed_clone())
                     .action("Keymap", Box::new(zed_actions::OpenKeymap))
                     .action(
@@ -1295,37 +1306,6 @@ impl TitleBar {
                 })
                 .into()
             })
-            .map(|this| {
-                if is_signed_in && TitleBarSettings::get_global(cx).show_user_picture {
-                    let avatar =
-                        user_avatar
-                            .clone()
-                            .map(|avatar| Avatar::new(avatar))
-                            .map(|avatar| {
-                                if show_update_badge {
-                                    avatar.indicator(
-                                        div()
-                                            .absolute()
-                                            .bottom_0()
-                                            .right_0()
-                                            .child(Indicator::dot().color(Color::Accent)),
-                                    )
-                                } else {
-                                    avatar
-                                }
-                            });
-                    this.trigger_with_tooltip(
-                        ButtonLike::new("user-menu").children(avatar),
-                        Tooltip::text("Toggle User Menu"),
-                    )
-                } else {
-                    this.trigger_with_tooltip(
-                        IconButton::new("user-menu", IconName::ChevronDown)
-                            .icon_size(IconSize::Small),
-                        Tooltip::text("Toggle User Menu"),
-                    )
-                }
-            })
-            .anchor(gpui::Corner::TopRight)
+            .anchor(Corner::TopRight)
     }
 }