Pane context menus & focus shenanigans

Julia and Conrad Irwin created

Co-Authored-By: Conrad Irwin <conrad@zed.dev>

Change summary

crates/workspace2/src/pane.rs       | 227 ++++++++++++++++++++++++------
crates/workspace2/src/toolbar.rs    |   1 
crates/workspace2/src/workspace2.rs |  81 -----------
3 files changed, 176 insertions(+), 133 deletions(-)

Detailed changes

crates/workspace2/src/pane.rs 🔗

@@ -2,14 +2,15 @@ use crate::{
     item::{Item, ItemHandle, ItemSettings, WeakItemHandle},
     toolbar::Toolbar,
     workspace_settings::{AutosaveSetting, WorkspaceSettings},
-    SplitDirection, Workspace,
+    NewCenterTerminal, NewFile, NewSearch, SplitDirection, ToggleZoom, Workspace,
 };
 use anyhow::Result;
 use collections::{HashMap, HashSet, VecDeque};
 use gpui::{
-    actions, prelude::*, Action, AnyWeakView, AppContext, AsyncWindowContext, Div, EntityId,
-    EventEmitter, FocusHandle, Focusable, FocusableView, Model, Pixels, Point, PromptLevel, Render,
-    Task, View, ViewContext, VisualContext, WeakView, WindowContext,
+    actions, overlay, prelude::*, Action, AnchorCorner, AnyWeakView, AppContext,
+    AsyncWindowContext, DismissEvent, Div, EntityId, EventEmitter, FocusHandle, Focusable,
+    FocusableView, Model, Pixels, Point, PromptLevel, Render, Task, View, ViewContext,
+    VisualContext, WeakView, WindowContext,
 };
 use parking_lot::Mutex;
 use project2::{Project, ProjectEntryId, ProjectPath};
@@ -25,8 +26,8 @@ use std::{
     },
 };
 
-use ui::v_stack;
-use ui::{prelude::*, Color, Icon, IconButton, IconElement, Tooltip};
+use ui::{menu_handle, prelude::*, Color, Icon, IconButton, IconElement, Tooltip};
+use ui::{v_stack, ContextMenu};
 use util::truncate_and_remove_front;
 
 #[derive(PartialEq, Clone, Copy, Deserialize, Debug)]
@@ -50,7 +51,7 @@ pub enum SaveIntent {
 
 //todo!("Do we need the default bound on actions? Decide soon")
 // #[register_action]
-#[derive(Clone, Deserialize, PartialEq, Debug)]
+#[derive(Action, Clone, Deserialize, PartialEq, Debug)]
 pub struct ActivateItem(pub usize);
 
 // #[derive(Clone, PartialEq)]
@@ -158,7 +159,9 @@ pub struct Pane {
     autoscroll: bool,
     nav_history: NavHistory,
     toolbar: View<Toolbar>,
-    //     tab_bar_context_menu: TabBarContextMenu,
+    tab_bar_focus_handle: FocusHandle,
+    new_item_menu: Option<View<ContextMenu>>,
+    split_item_menu: Option<View<ContextMenu>>,
     //     tab_context_menu: ViewHandle<ContextMenu>,
     workspace: WeakView<Workspace>,
     project: Model<Project>,
@@ -323,6 +326,9 @@ impl Pane {
                 next_timestamp,
             }))),
             toolbar: cx.build_view(|_| Toolbar::new()),
+            tab_bar_focus_handle: cx.focus_handle(),
+            new_item_menu: None,
+            split_item_menu: None,
             // tab_bar_context_menu: TabBarContextMenu {
             //     kind: TabBarContextMenuKind::New,
             //     handle: context_menu,
@@ -397,6 +403,7 @@ impl Pane {
     }
 
     pub fn has_focus(&self, cx: &WindowContext) -> bool {
+        // todo!(); // inline this manually
         self.focus_handle.contains_focused(cx)
     }
 
@@ -422,11 +429,11 @@ impl Pane {
                 }
 
                 active_item.focus_handle(cx).focus(cx);
-            // todo!() Do this once we have tab bar context menu
-            // } else if !self.tab_bar_context_menu.handle.is_focused() {
-            } else if let Some(focused) = cx.focused() {
-                self.last_focused_view_by_item
-                    .insert(active_item.item_id(), focused);
+            } else if !self.tab_bar_focus_handle.contains_focused(cx) {
+                if let Some(focused) = cx.focused() {
+                    self.last_focused_view_by_item
+                        .insert(active_item.item_id(), focused);
+                }
             }
         }
     }
@@ -673,21 +680,16 @@ impl Pane {
             .position(|i| i.item_id() == item.item_id())
     }
 
-    // pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext<Self>) {
-    //     // Potentially warn the user of the new keybinding
-    //     let workspace_handle = self.workspace().clone();
-    //     cx.spawn(|_, mut cx| async move { notify_of_new_dock(&workspace_handle, &mut cx) })
-    //         .detach();
-
-    //     if self.zoomed {
-    //         cx.emit(Event::ZoomOut);
-    //     } else if !self.items.is_empty() {
-    //         if !self.has_focus {
-    //             cx.focus_self();
-    //         }
-    //         cx.emit(Event::ZoomIn);
-    //     }
-    // }
+    pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext<Self>) {
+        if self.zoomed {
+            cx.emit(Event::ZoomOut);
+        } else if !self.items.is_empty() {
+            if !self.focus_handle.contains_focused(cx) {
+                cx.focus_self();
+            }
+            cx.emit(Event::ZoomIn);
+        }
+    }
 
     pub fn activate_item(
         &mut self,
@@ -1424,7 +1426,7 @@ impl Pane {
         let close_right = ItemSettings::get_global(cx).close_position.right();
         let is_active = ix == self.active_item_index;
 
-        div()
+        let tab = div()
             .group("")
             .id(ix)
             .cursor_pointer()
@@ -1498,13 +1500,41 @@ impl Pane {
                     .children((!close_right).then(|| close_icon()))
                     .child(label)
                     .children(close_right.then(|| close_icon())),
-            )
+            );
+
+        menu_handle(ix).child(|_| tab).menu(|cx| {
+            ContextMenu::build(cx, |menu, cx| {
+                menu.action(
+                    "Close Active Item",
+                    CloseActiveItem { save_intent: None }.boxed_clone(),
+                    cx,
+                )
+                .action("Close Inactive Items", CloseInactiveItems.boxed_clone(), cx)
+                .action("Close Clean Items", CloseCleanItems.boxed_clone(), cx)
+                .action(
+                    "Close Items To The Left",
+                    CloseItemsToTheLeft.boxed_clone(),
+                    cx,
+                )
+                .action(
+                    "Close Items To The Right",
+                    CloseItemsToTheRight.boxed_clone(),
+                    cx,
+                )
+                .action(
+                    "Close All Items",
+                    CloseAllItems { save_intent: None }.boxed_clone(),
+                    cx,
+                )
+            })
+        })
     }
 
     fn render_tab_bar(&mut self, cx: &mut ViewContext<'_, Pane>) -> impl IntoElement {
         div()
             .group("tab_bar")
             .id("tab_bar")
+            .track_focus(&self.tab_bar_focus_handle)
             .w_full()
             .flex()
             .bg(cx.theme().colors().tab_bar_background)
@@ -1563,20 +1593,87 @@ impl Pane {
                             .gap_px()
                             .child(
                                 div()
+                                    .bg(gpui::blue())
                                     .border()
                                     .border_color(gpui::red())
-                                    .child(IconButton::new("plus", Icon::Plus)),
+                                    .child(IconButton::new("plus", Icon::Plus).on_click(
+                                        cx.listener(|this, _, cx| {
+                                            let menu = ContextMenu::build(cx, |menu, cx| {
+                                                menu.action("New File", NewFile.boxed_clone(), cx)
+                                                    .action(
+                                                        "New Terminal",
+                                                        NewCenterTerminal.boxed_clone(),
+                                                        cx,
+                                                    )
+                                                    .action(
+                                                        "New Search",
+                                                        NewSearch.boxed_clone(),
+                                                        cx,
+                                                    )
+                                            });
+                                            cx.subscribe(
+                                                &menu,
+                                                |this, _, event: &DismissEvent, cx| {
+                                                    this.focus(cx);
+                                                    this.new_item_menu = None;
+                                                },
+                                            )
+                                            .detach();
+                                            this.new_item_menu = Some(menu);
+                                        }),
+                                    ))
+                                    .when_some(self.new_item_menu.as_ref(), |el, new_item_menu| {
+                                        el.child(Self::render_menu_overlay(new_item_menu))
+                                    }),
                             )
                             .child(
                                 div()
                                     .border()
                                     .border_color(gpui::red())
-                                    .child(IconButton::new("split", Icon::Split)),
+                                    .child(IconButton::new("split", Icon::Split).on_click(
+                                        cx.listener(|this, _, cx| {
+                                            let menu = ContextMenu::build(cx, |menu, cx| {
+                                                menu.action(
+                                                    "Split Right",
+                                                    SplitRight.boxed_clone(),
+                                                    cx,
+                                                )
+                                                .action("Split Left", SplitLeft.boxed_clone(), cx)
+                                                .action("Split Up", SplitUp.boxed_clone(), cx)
+                                                .action("Split Down", SplitDown.boxed_clone(), cx)
+                                            });
+                                            cx.subscribe(
+                                                &menu,
+                                                |this, _, event: &DismissEvent, cx| {
+                                                    this.focus(cx);
+                                                    this.split_item_menu = None;
+                                                },
+                                            )
+                                            .detach();
+                                            this.split_item_menu = Some(menu);
+                                        }),
+                                    ))
+                                    .when_some(
+                                        self.split_item_menu.as_ref(),
+                                        |el, split_item_menu| {
+                                            el.child(Self::render_menu_overlay(split_item_menu))
+                                        },
+                                    ),
                             ),
                     ),
             )
     }
 
+    fn render_menu_overlay(menu: &View<ContextMenu>) -> Div {
+        div()
+            .absolute()
+            .z_index(1)
+            .bottom_0()
+            .right_0()
+            .size_0()
+            .child(overlay().anchor(AnchorCorner::TopRight).child(menu.clone()))
+    }
+
     //     fn render_tabs(&mut self, cx: &mut ViewContext<Self>) -> impl Element<Self> {
     //         let theme = theme::current(cx).clone();
 
@@ -2004,25 +2101,53 @@ impl Render for Pane {
             .on_action(cx.listener(|pane: &mut Pane, _: &SplitDown, cx| {
                 pane.split(SplitDirection::Down, cx)
             }))
-            //     cx.add_action(Pane::toggle_zoom);
-            //     cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
-            //         pane.activate_item(action.0, true, true, cx);
-            //     });
-            //     cx.add_action(|pane: &mut Pane, _: &ActivateLastItem, cx| {
-            //         pane.activate_item(pane.items.len() - 1, true, true, cx);
-            //     });
-            //     cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
-            //         pane.activate_prev_item(true, cx);
-            //     });
-            //     cx.add_action(|pane: &mut Pane, _: &ActivateNextItem, cx| {
-            //         pane.activate_next_item(true, cx);
-            //     });
-            //     cx.add_async_action(Pane::close_active_item);
-            //     cx.add_async_action(Pane::close_inactive_items);
-            //     cx.add_async_action(Pane::close_clean_items);
-            //     cx.add_async_action(Pane::close_items_to_the_left);
-            //     cx.add_async_action(Pane::close_items_to_the_right);
-            //     cx.add_async_action(Pane::close_all_items);
+            .on_action(cx.listener(Pane::toggle_zoom))
+            .on_action(cx.listener(|pane: &mut Pane, action: &ActivateItem, cx| {
+                pane.activate_item(action.0, true, true, cx);
+            }))
+            .on_action(cx.listener(|pane: &mut Pane, _: &ActivateLastItem, cx| {
+                pane.activate_item(pane.items.len() - 1, true, true, cx);
+            }))
+            .on_action(cx.listener(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
+                pane.activate_prev_item(true, cx);
+            }))
+            .on_action(cx.listener(|pane: &mut Pane, _: &ActivateNextItem, cx| {
+                pane.activate_next_item(true, cx);
+            }))
+            .on_action(
+                cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
+                    pane.close_active_item(action, cx)
+                        .map(|task| task.detach_and_log_err(cx));
+                }),
+            )
+            .on_action(
+                cx.listener(|pane: &mut Self, action: &CloseInactiveItems, cx| {
+                    pane.close_inactive_items(action, cx)
+                        .map(|task| task.detach_and_log_err(cx));
+                }),
+            )
+            .on_action(
+                cx.listener(|pane: &mut Self, action: &CloseCleanItems, cx| {
+                    pane.close_clean_items(action, cx)
+                        .map(|task| task.detach_and_log_err(cx));
+                }),
+            )
+            .on_action(
+                cx.listener(|pane: &mut Self, action: &CloseItemsToTheLeft, cx| {
+                    pane.close_items_to_the_left(action, cx)
+                        .map(|task| task.detach_and_log_err(cx));
+                }),
+            )
+            .on_action(
+                cx.listener(|pane: &mut Self, action: &CloseItemsToTheRight, cx| {
+                    pane.close_items_to_the_right(action, cx)
+                        .map(|task| task.detach_and_log_err(cx));
+                }),
+            )
+            .on_action(cx.listener(|pane: &mut Self, action: &CloseAllItems, cx| {
+                pane.close_all_items(action, cx)
+                    .map(|task| task.detach_and_log_err(cx));
+            }))
             .size_full()
             .on_action(
                 cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {

crates/workspace2/src/toolbar.rs 🔗

@@ -290,7 +290,6 @@ impl<T: ToolbarItemView> ToolbarItemViewHandle for View<T> {
     }
 
     fn focus_changed(&mut self, pane_focused: bool, cx: &mut WindowContext) {
-        println!("focus changed, pane_focused: {pane_focused}");
         self.update(cx, |this, cx| {
             this.pane_focus_update(pane_focused, cx);
             cx.notify();

crates/workspace2/src/workspace2.rs 🔗

@@ -3587,87 +3587,6 @@ fn open_items(
     })
 }
 
-// todo!()
-// fn notify_of_new_dock(workspace: &WeakView<Workspace>, cx: &mut AsyncAppContext) {
-//     const NEW_PANEL_BLOG_POST: &str = "https://zed.dev/blog/new-panel-system";
-//     const NEW_DOCK_HINT_KEY: &str = "show_new_dock_key";
-//     const MESSAGE_ID: usize = 2;
-
-//     if workspace
-//         .read_with(cx, |workspace, cx| {
-//             workspace.has_shown_notification_once::<MessageNotification>(MESSAGE_ID, cx)
-//         })
-//         .unwrap_or(false)
-//     {
-//         return;
-//     }
-
-//     if db::kvp::KEY_VALUE_STORE
-//         .read_kvp(NEW_DOCK_HINT_KEY)
-//         .ok()
-//         .flatten()
-//         .is_some()
-//     {
-//         if !workspace
-//             .read_with(cx, |workspace, cx| {
-//                 workspace.has_shown_notification_once::<MessageNotification>(MESSAGE_ID, cx)
-//             })
-//             .unwrap_or(false)
-//         {
-//             cx.update(|cx| {
-//                 cx.update_global::<NotificationTracker, _, _>(|tracker, _| {
-//                     let entry = tracker
-//                         .entry(TypeId::of::<MessageNotification>())
-//                         .or_default();
-//                     if !entry.contains(&MESSAGE_ID) {
-//                         entry.push(MESSAGE_ID);
-//                     }
-//                 });
-//             });
-//         }
-
-//         return;
-//     }
-
-//     cx.spawn(|_| async move {
-//         db::kvp::KEY_VALUE_STORE
-//             .write_kvp(NEW_DOCK_HINT_KEY.to_string(), "seen".to_string())
-//             .await
-//             .ok();
-//     })
-//     .detach();
-
-//     workspace
-//         .update(cx, |workspace, cx| {
-//             workspace.show_notification_once(2, cx, |cx| {
-//                 cx.build_view(|_| {
-//                     MessageNotification::new_element(|text, _| {
-//                         Text::new(
-//                             "Looking for the dock? Try ctrl-`!\nshift-escape now zooms your pane.",
-//                             text,
-//                         )
-//                         .with_custom_runs(vec![26..32, 34..46], |_, bounds, cx| {
-//                             let code_span_background_color = settings::get::<ThemeSettings>(cx)
-//                                 .theme
-//                                 .editor
-//                                 .document_highlight_read_background;
-
-//                             cx.scene().push_quad(gpui::Quad {
-//                                 bounds,
-//                                 background: Some(code_span_background_color),
-//                                 border: Default::default(),
-//                                 corner_radii: (2.0).into(),
-//                             })
-//                         })
-//                         .into_any()
-//                     })
-//                     .with_click_message("Read more about the new panel system")
-//                     .on_click(|cx| cx.platform().open_url(NEW_PANEL_BLOG_POST))
-//                 })
-//             })
-//         })
-//         .ok();
-
 fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncAppContext) {
     const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";