Popover buttons titlebar (#3732)

Piotr Osiewicz created

Migrate project picker/vcs menu to use popover_menu.

Release Notes:
- N/A

Change summary

crates/collab_ui2/src/collab_titlebar_item.rs  | 84 ++++++++-----------
crates/recent_projects2/src/recent_projects.rs | 43 ++++++---
crates/vcs_menu2/src/lib.rs                    | 78 ++++++++++--------
3 files changed, 106 insertions(+), 99 deletions(-)

Detailed changes

crates/collab_ui2/src/collab_titlebar_item.rs 🔗

@@ -3,10 +3,10 @@ use auto_update::AutoUpdateStatus;
 use call::{ActiveCall, ParticipantLocation, Room};
 use client::{proto::PeerId, Client, ParticipantIndex, User, UserStore};
 use gpui::{
-    actions, canvas, div, overlay, point, px, rems, Action, AnyElement, AppContext, DismissEvent,
-    Div, Element, FocusableView, Hsla, InteractiveElement, IntoElement, Model, ParentElement, Path,
-    Render, Stateful, StatefulInteractiveElement, Styled, Subscription, View, ViewContext,
-    VisualContext, WeakView, WindowBounds,
+    actions, canvas, div, point, px, rems, Action, AnyElement, AppContext, Div, Element, Hsla,
+    InteractiveElement, IntoElement, Model, ParentElement, Path, Render, Stateful,
+    StatefulInteractiveElement, Styled, Subscription, View, ViewContext, VisualContext, WeakView,
+    WindowBounds,
 };
 use project::{Project, RepositoryEntry};
 use recent_projects::RecentProjects;
@@ -52,8 +52,8 @@ pub struct CollabTitlebarItem {
     user_store: Model<UserStore>,
     client: Arc<Client>,
     workspace: WeakView<Workspace>,
-    branch_popover: Option<View<BranchList>>,
-    project_popover: Option<recent_projects::RecentProjects>,
+    branch_popover: Option<(View<BranchList>, Subscription)>,
+    project_popover: Option<(View<recent_projects::RecentProjects>, Subscription)>,
     _subscriptions: Vec<Subscription>,
 }
 
@@ -329,9 +329,8 @@ impl CollabTitlebarItem {
         };
 
         let name = util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH);
-
-        div()
-            .child(
+        popover_menu("project_name_trigger")
+            .trigger(
                 Button::new("project_name_trigger", name)
                     .style(ButtonStyle::Subtle)
                     .tooltip(move |cx| Tooltip::text("Recent Projects", cx))
@@ -339,16 +338,12 @@ impl CollabTitlebarItem {
                         this.toggle_project_menu(&ToggleProjectMenu, cx);
                     })),
             )
-            .children(self.project_popover.as_ref().map(|popover| {
-                overlay().child(
-                    div()
-                        .min_w_56()
-                        .on_mouse_down_out(cx.listener_for(&popover.picker, |picker, _, cx| {
-                            picker.cancel(&Default::default(), cx)
-                        }))
-                        .child(popover.picker.clone()),
-                )
-            }))
+            .when_some(
+                self.project_popover
+                    .as_ref()
+                    .map(|(project, _)| project.clone()),
+                |this, project| this.menu(move |_| project.clone()),
+            )
     }
 
     pub fn render_project_branch(&self, cx: &mut ViewContext<Self>) -> Option<impl Element> {
@@ -366,10 +361,9 @@ impl CollabTitlebarItem {
             .as_ref()
             .and_then(RepositoryEntry::branch)
             .map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH))?;
-
         Some(
-            div()
-                .child(
+            popover_menu("project_branch_trigger")
+                .trigger(
                     Button::new("project_branch_trigger", branch_name)
                         .color(Color::Muted)
                         .style(ButtonStyle::Subtle)
@@ -381,11 +375,16 @@ impl CollabTitlebarItem {
                                 cx,
                             )
                         })
-                        .on_click(
-                            cx.listener(|this, _, cx| this.toggle_vcs_menu(&ToggleVcsMenu, cx)),
-                        ),
+                        .on_click(cx.listener(|this, _, cx| {
+                            this.toggle_vcs_menu(&ToggleVcsMenu, cx);
+                        })),
                 )
-                .children(self.render_branches_popover_host()),
+                .when_some(
+                    self.branch_popover
+                        .as_ref()
+                        .map(|(branch, _)| branch.clone()),
+                    |this, branch| this.menu(move |_| branch.clone()),
+                ),
         )
     }
 
@@ -461,29 +460,19 @@ impl CollabTitlebarItem {
             .log_err();
     }
 
-    fn render_branches_popover_host<'a>(&'a self) -> Option<AnyElement> {
-        self.branch_popover.as_ref().map(|child| {
-            overlay()
-                .child(div().min_w_64().child(child.clone()))
-                .into_any()
-        })
-    }
-
     pub fn toggle_vcs_menu(&mut self, _: &ToggleVcsMenu, cx: &mut ViewContext<Self>) {
         if self.branch_popover.take().is_none() {
             if let Some(workspace) = self.workspace.upgrade() {
                 let Some(view) = build_branch_list(workspace, cx).log_err() else {
                     return;
                 };
-                cx.subscribe(&view, |this, _, _, cx| {
-                    this.branch_popover = None;
-                    cx.notify();
-                })
-                .detach();
                 self.project_popover.take();
                 let focus_handle = view.focus_handle(cx);
                 cx.focus(&focus_handle);
-                self.branch_popover = Some(view);
+                let subscription = cx.subscribe(&view, |this, _, _, _| {
+                    this.branch_popover.take();
+                });
+                self.branch_popover = Some((view, subscription));
             }
         }
 
@@ -491,8 +480,8 @@ impl CollabTitlebarItem {
     }
 
     pub fn toggle_project_menu(&mut self, _: &ToggleProjectMenu, cx: &mut ViewContext<Self>) {
-        let workspace = self.workspace.clone();
         if self.project_popover.take().is_none() {
+            let workspace = self.workspace.clone();
             cx.spawn(|this, mut cx| async move {
                 let workspaces = WORKSPACE_DB
                     .recent_workspaces_on_disk()
@@ -506,16 +495,13 @@ impl CollabTitlebarItem {
                 this.update(&mut cx, move |this, cx| {
                     let view = RecentProjects::open_popover(workspace, workspaces, cx);
 
-                    cx.subscribe(&view.picker, |this, _, _: &DismissEvent, cx| {
-                        this.project_popover = None;
-                        cx.notify();
-                    })
-                    .detach();
                     let focus_handle = view.focus_handle(cx);
                     cx.focus(&focus_handle);
-                    // todo!()
-                    //this.branch_popover.take();
-                    this.project_popover = Some(view);
+                    this.branch_popover.take();
+                    let subscription = cx.subscribe(&view, |this, _, _, _| {
+                        this.project_popover.take();
+                    });
+                    this.project_popover = Some((view, subscription));
                     cx.notify();
                 })
                 .log_err();

crates/recent_projects2/src/recent_projects.rs 🔗

@@ -3,8 +3,8 @@ mod projects;
 
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
-    AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, Result, Task, View,
-    ViewContext, WeakView,
+    AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, Result, Subscription,
+    Task, View, ViewContext, WeakView,
 };
 use highlighted_workspace_location::HighlightedWorkspaceLocation;
 use ordered_float::OrderedFloat;
@@ -23,17 +23,22 @@ pub fn init(cx: &mut AppContext) {
     cx.observe_new_views(RecentProjects::register).detach();
 }
 
-#[derive(Clone)]
 pub struct RecentProjects {
     pub picker: View<Picker<RecentProjectsDelegate>>,
+    rem_width: f32,
+    _subscription: Subscription,
 }
 
 impl ModalView for RecentProjects {}
 
 impl RecentProjects {
-    fn new(delegate: RecentProjectsDelegate, cx: &mut WindowContext<'_>) -> Self {
+    fn new(delegate: RecentProjectsDelegate, rem_width: f32, cx: &mut ViewContext<Self>) -> Self {
+        let picker = cx.build_view(|cx| Picker::new(delegate, cx));
+        let _subscription = cx.subscribe(&picker, |_, _, _, cx| cx.emit(DismissEvent));
         Self {
-            picker: cx.build_view(|cx| Picker::new(delegate, cx)),
+            picker,
+            rem_width,
+            _subscription,
         }
     }
 
@@ -76,9 +81,7 @@ impl RecentProjects {
                         let delegate =
                             RecentProjectsDelegate::new(weak_workspace, workspace_locations, true);
 
-                        let modal = RecentProjects::new(delegate, cx);
-                        cx.subscribe(&modal.picker, |_, _, _, cx| cx.emit(DismissEvent))
-                            .detach();
+                        let modal = RecentProjects::new(delegate, 34., cx);
                         modal
                     });
                 } else {
@@ -94,11 +97,14 @@ impl RecentProjects {
         workspace: WeakView<Workspace>,
         workspaces: Vec<WorkspaceLocation>,
         cx: &mut WindowContext<'_>,
-    ) -> Self {
-        Self::new(
-            RecentProjectsDelegate::new(workspace, workspaces, false),
-            cx,
-        )
+    ) -> View<Self> {
+        cx.build_view(|cx| {
+            Self::new(
+                RecentProjectsDelegate::new(workspace, workspaces, false),
+                20.,
+                cx,
+            )
+        })
     }
 }
 
@@ -113,8 +119,15 @@ impl FocusableView for RecentProjects {
 impl Render for RecentProjects {
     type Element = Div;
 
-    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
-        v_stack().w(rems(34.)).child(self.picker.clone())
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+        v_stack()
+            .w(rems(self.rem_width))
+            .child(self.picker.clone())
+            .on_mouse_down_out(cx.listener(|this, _, cx| {
+                this.picker.update(cx, |this, cx| {
+                    this.cancel(&Default::default(), cx);
+                })
+            }))
     }
 }
 

crates/vcs_menu2/src/lib.rs 🔗

@@ -3,8 +3,8 @@ use fs::repository::Branch;
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
     actions, rems, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView,
-    ParentElement, Render, SharedString, Styled, Task, View, ViewContext, VisualContext,
-    WindowContext,
+    InteractiveElement, ParentElement, Render, SharedString, Styled, Subscription, Task, View,
+    ViewContext, VisualContext, WindowContext,
 };
 use picker::{Picker, PickerDelegate};
 use std::sync::Arc;
@@ -18,31 +18,61 @@ pub fn init(cx: &mut AppContext) {
     // todo!() po
     cx.observe_new_views(|workspace: &mut Workspace, _| {
         workspace.register_action(|workspace, action, cx| {
-            ModalBranchList::toggle_modal(workspace, action, cx).log_err();
+            BranchList::toggle_modal(workspace, action, cx).log_err();
         });
     })
     .detach();
 }
-pub type BranchList = Picker<BranchListDelegate>;
 
-pub struct ModalBranchList {
+pub struct BranchList {
     pub picker: View<Picker<BranchListDelegate>>,
+    rem_width: f32,
+    _subscription: Subscription,
 }
 
-impl ModalView for ModalBranchList {}
-impl EventEmitter<DismissEvent> for ModalBranchList {}
+impl BranchList {
+    fn new(delegate: BranchListDelegate, rem_width: f32, cx: &mut ViewContext<Self>) -> Self {
+        let picker = cx.build_view(|cx| Picker::new(delegate, cx));
+        let _subscription = cx.subscribe(&picker, |_, _, _, cx| cx.emit(DismissEvent));
+        Self {
+            picker,
+            rem_width,
+            _subscription,
+        }
+    }
+    fn toggle_modal(
+        workspace: &mut Workspace,
+        _: &OpenRecent,
+        cx: &mut ViewContext<Workspace>,
+    ) -> Result<()> {
+        // Modal branch picker has a longer trailoff than a popover one.
+        let delegate = BranchListDelegate::new(workspace, cx.view().clone(), 70, cx)?;
+        workspace.toggle_modal(cx, |cx| BranchList::new(delegate, 34., cx));
 
-impl FocusableView for ModalBranchList {
+        Ok(())
+    }
+}
+impl ModalView for BranchList {}
+impl EventEmitter<DismissEvent> for BranchList {}
+
+impl FocusableView for BranchList {
     fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
         self.picker.focus_handle(cx)
     }
 }
 
-impl Render for ModalBranchList {
+impl Render for BranchList {
     type Element = Div;
 
-    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
-        v_stack().w(rems(34.)).child(self.picker.clone())
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+        v_stack()
+            .w(rems(self.rem_width))
+            .child(self.picker.clone())
+            .on_mouse_down_out(cx.listener(|this, _, cx| {
+                this.picker.update(cx, |this, cx| {
+                    this.cancel(&Default::default(), cx);
+                })
+            }))
     }
 }
 
@@ -53,29 +83,7 @@ pub fn build_branch_list(
     let delegate = workspace.update(cx, |workspace, cx| {
         BranchListDelegate::new(workspace, cx.view().clone(), 29, cx)
     })?;
-
-    Ok(cx.build_view(|cx| Picker::new(delegate, cx)))
-}
-
-impl ModalBranchList {
-    fn toggle_modal(
-        workspace: &mut Workspace,
-        _: &OpenRecent,
-        cx: &mut ViewContext<Workspace>,
-    ) -> Result<()> {
-        // Modal branch picker has a longer trailoff than a popover one.
-        let delegate = BranchListDelegate::new(workspace, cx.view().clone(), 70, cx)?;
-        workspace.toggle_modal(cx, |cx| {
-            let modal = ModalBranchList {
-                picker: cx.build_view(|cx| Picker::new(delegate, cx)),
-            };
-            cx.subscribe(&modal.picker, |_, _, _, cx| cx.emit(DismissEvent))
-                .detach();
-            modal
-        });
-
-        Ok(())
-    }
+    Ok(cx.build_view(move |cx| BranchList::new(delegate, 20., cx)))
 }
 
 pub struct BranchListDelegate {
@@ -116,7 +124,7 @@ impl BranchListDelegate {
         })
     }
 
-    fn display_error_toast(&self, message: String, cx: &mut ViewContext<BranchList>) {
+    fn display_error_toast(&self, message: String, cx: &mut WindowContext<'_>) {
         const GIT_CHECKOUT_FAILURE_ID: usize = 2048;
         self.workspace.update(cx, |model, ctx| {
             model.show_toast(Toast::new(GIT_CHECKOUT_FAILURE_ID, message), ctx)