From bcf449d3fecbadcb41228e47039b5a53626d9d7b Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 22 Nov 2023 15:19:10 -0800 Subject: [PATCH 1/5] Add a basic context menu to the project panel --- crates/project_panel2/src/project_panel.rs | 175 ++++++++++++--------- crates/ui2/src/components/context_menu.rs | 8 +- 2 files changed, 103 insertions(+), 80 deletions(-) diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs index d4c63e75bf68057bffa8f616f47fa8488e28bedf..2ee2e3ab1fe52ad2e4e234ca100def9f96205340 100644 --- a/crates/project_panel2/src/project_panel.rs +++ b/crates/project_panel2/src/project_panel.rs @@ -8,10 +8,11 @@ use file_associations::FileAssociations; use anyhow::{anyhow, Result}; use gpui::{ - actions, div, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext, + actions, div, overlay, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext, ClipboardItem, Div, EventEmitter, FocusHandle, Focusable, FocusableView, InteractiveElement, - Model, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, Stateful, Styled, - Task, UniformListScrollHandle, View, ViewContext, VisualContext as _, WeakView, WindowContext, + Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, + Stateful, Styled, Subscription, Task, UniformListScrollHandle, View, ViewContext, + VisualContext as _, WeakView, WindowContext, }; use menu::{Confirm, SelectNext, SelectPrev}; use project::{ @@ -29,7 +30,7 @@ use std::{ sync::Arc, }; use theme::ActiveTheme as _; -use ui::{v_stack, IconElement, Label, ListItem}; +use ui::{v_stack, ContextMenu, IconElement, Label, ListItem}; use unicase::UniCase; use util::{maybe, ResultExt, TryFutureExt}; use workspace::{ @@ -49,6 +50,7 @@ pub struct ProjectPanel { last_worktree_root_id: Option, expanded_dir_ids: HashMap>, selection: Option, + context_menu: Option<(View, Point, Subscription)>, edit_state: Option, filename_editor: View, clipboard_entry: Option, @@ -231,6 +233,7 @@ impl ProjectPanel { expanded_dir_ids: Default::default(), selection: None, edit_state: None, + context_menu: None, filename_editor, clipboard_entry: None, // context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)), @@ -366,80 +369,92 @@ impl ProjectPanel { fn deploy_context_menu( &mut self, - _position: Point, - _entry_id: ProjectEntryId, - _cx: &mut ViewContext, + position: Point, + entry_id: ProjectEntryId, + cx: &mut ViewContext, ) { - // todo!() - // let project = self.project.read(cx); + let this = cx.view().clone(); + let project = self.project.read(cx); - // let worktree_id = if let Some(id) = project.worktree_id_for_entry(entry_id, cx) { - // id - // } else { - // return; - // }; + let worktree_id = if let Some(id) = project.worktree_id_for_entry(entry_id, cx) { + id + } else { + return; + }; - // self.selection = Some(Selection { - // worktree_id, - // entry_id, - // }); - - // let mut menu_entries = Vec::new(); - // if let Some((worktree, entry)) = self.selected_entry(cx) { - // let is_root = Some(entry) == worktree.root_entry(); - // if !project.is_remote() { - // menu_entries.push(ContextMenuItem::action( - // "Add Folder to Project", - // workspace::AddFolderToProject, - // )); - // if is_root { - // let project = self.project.clone(); - // menu_entries.push(ContextMenuItem::handler("Remove from Project", move |cx| { - // project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx)); - // })); - // } - // } - // menu_entries.push(ContextMenuItem::action("New File", NewFile)); - // menu_entries.push(ContextMenuItem::action("New Folder", NewDirectory)); - // menu_entries.push(ContextMenuItem::Separator); - // menu_entries.push(ContextMenuItem::action("Cut", Cut)); - // menu_entries.push(ContextMenuItem::action("Copy", Copy)); - // if let Some(clipboard_entry) = self.clipboard_entry { - // if clipboard_entry.worktree_id() == worktree.id() { - // menu_entries.push(ContextMenuItem::action("Paste", Paste)); - // } - // } - // menu_entries.push(ContextMenuItem::Separator); - // menu_entries.push(ContextMenuItem::action("Copy Path", CopyPath)); - // menu_entries.push(ContextMenuItem::action( - // "Copy Relative Path", - // CopyRelativePath, - // )); - - // if entry.is_dir() { - // menu_entries.push(ContextMenuItem::Separator); - // } - // menu_entries.push(ContextMenuItem::action("Reveal in Finder", RevealInFinder)); - // if entry.is_dir() { - // menu_entries.push(ContextMenuItem::action("Open in Terminal", OpenInTerminal)); - // menu_entries.push(ContextMenuItem::action( - // "Search Inside", - // NewSearchInDirectory, - // )); - // } - - // menu_entries.push(ContextMenuItem::Separator); - // menu_entries.push(ContextMenuItem::action("Rename", Rename)); - // if !is_root { - // menu_entries.push(ContextMenuItem::action("Delete", Delete)); - // } - // } - - // // self.context_menu.update(cx, |menu, cx| { - // // menu.show(position, AnchorCorner::TopLeft, menu_entries, cx); - // // }); - - // cx.notify(); + self.selection = Some(Selection { + worktree_id, + entry_id, + }); + + if let Some((worktree, entry)) = self.selected_entry(cx) { + let is_root = Some(entry) == worktree.root_entry(); + let is_dir = entry.is_dir(); + let worktree_id = worktree.id(); + let is_local = project.is_local(); + + let context_menu = ContextMenu::build(cx, |mut menu, cx| { + if is_local { + menu = menu.action( + "Add Folder to Project", + Box::new(workspace::AddFolderToProject), + ); + if is_root { + menu = menu.entry( + "Remove from Project", + cx.listener_for(&this, move |this, _, cx| { + this.project.update(cx, |project, cx| { + project.remove_worktree(worktree_id, cx) + }); + }), + ); + } + } + + menu = menu + .action("New File", Box::new(NewFile)) + .action("New Folder", Box::new(NewDirectory)) + .separator() + .action("Cut", Box::new(Cut)) + .action("Copy", Box::new(Copy)); + + if let Some(clipboard_entry) = self.clipboard_entry { + if clipboard_entry.worktree_id() == worktree_id { + menu = menu.action("Paste", Box::new(Paste)); + } + } + + menu = menu + .separator() + .action("Copy Path", Box::new(CopyPath)) + .action("Copy Relative Path", Box::new(CopyRelativePath)) + .separator() + .action("Reveal in Finder", Box::new(RevealInFinder)); + + if is_dir { + menu = menu + .action("Open in Terminal", Box::new(OpenInTerminal)) + .action("Search Inside", Box::new(NewSearchInDirectory)) + } + + menu = menu.separator().action("Rename", Box::new(Rename)); + + if !is_root { + menu = menu.action("Delete", Box::new(Delete)); + } + + menu + }); + + cx.focus_view(&context_menu); + let subscription = cx.on_blur(&context_menu.focus_handle(cx), |this, cx| { + this.context_menu.take(); + cx.notify(); + }); + self.context_menu = Some((context_menu, position, subscription)); + } + + cx.notify(); } fn expand_selected_entry(&mut self, _: &ExpandSelectedEntry, cx: &mut ViewContext) { @@ -1379,6 +1394,9 @@ impl ProjectPanel { .ml_1(), ) .on_click(cx.listener(move |this, event: &gpui::ClickEvent, cx| { + if event.down.button == MouseButton::Right { + return; + } if !show_editor { if kind.is_dir() { this.toggle_expanded(entry_id, cx); @@ -1415,6 +1433,7 @@ impl Render for ProjectPanel { div() .id("project-panel") .size_full() + .relative() .key_context("ProjectPanel") .on_action(cx.listener(Self::select_next)) .on_action(cx.listener(Self::select_prev)) @@ -1458,6 +1477,12 @@ impl Render for ProjectPanel { .size_full() .track_scroll(self.list.clone()), ) + .children(self.context_menu.as_ref().map(|(menu, position, _)| { + overlay() + .position(*position) + .anchor(gpui::AnchorCorner::BottomLeft) + .child(menu.clone()) + })) } else { v_stack() .id("empty-project_panel") diff --git a/crates/ui2/src/components/context_menu.rs b/crates/ui2/src/components/context_menu.rs index 8cd519f62916dd351f2056100acb8d396801f8c0..3510256c0ce50b2c73d8b091a06e433be42f8ff7 100644 --- a/crates/ui2/src/components/context_menu.rs +++ b/crates/ui2/src/components/context_menu.rs @@ -1,13 +1,10 @@ -use std::cell::RefCell; -use std::rc::Rc; - -use crate::{prelude::*, v_stack, Label, List}; -use crate::{ListItem, ListSeparator, ListSubHeader}; +use crate::{prelude::*, v_stack, Label, List, ListItem, ListSeparator, ListSubHeader}; use gpui::{ overlay, px, Action, AnchorCorner, AnyElement, AppContext, Bounds, ClickEvent, DismissEvent, DispatchPhase, Div, EventEmitter, FocusHandle, FocusableView, IntoElement, LayoutId, ManagedView, MouseButton, MouseDownEvent, Pixels, Point, Render, View, VisualContext, }; +use std::{cell::RefCell, rc::Rc}; pub enum ContextMenuItem { Separator, @@ -177,6 +174,7 @@ pub struct MenuHandleState { child_element: Option, menu_element: Option, } + impl Element for MenuHandle { type State = MenuHandleState; From 6bf7ad71ebb3a52dc3e3039a59c231c0633c1f24 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 28 Nov 2023 16:07:42 -0800 Subject: [PATCH 2/5] Show action key bindings in context menus --- crates/project_panel2/src/project_panel.rs | 28 ++++++++-------- crates/terminal_view2/src/terminal_view.rs | 9 +++-- crates/ui2/src/components/context_menu.rs | 39 ++++++++++++++++------ 3 files changed, 49 insertions(+), 27 deletions(-) diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs index 2ee2e3ab1fe52ad2e4e234ca100def9f96205340..891355ea1f90c2ba16ecf2a9dec5e40ed25bed3f 100644 --- a/crates/project_panel2/src/project_panel.rs +++ b/crates/project_panel2/src/project_panel.rs @@ -398,6 +398,7 @@ impl ProjectPanel { menu = menu.action( "Add Folder to Project", Box::new(workspace::AddFolderToProject), + cx, ); if is_root { menu = menu.entry( @@ -412,35 +413,35 @@ impl ProjectPanel { } menu = menu - .action("New File", Box::new(NewFile)) - .action("New Folder", Box::new(NewDirectory)) + .action("New File", Box::new(NewFile), cx) + .action("New Folder", Box::new(NewDirectory), cx) .separator() - .action("Cut", Box::new(Cut)) - .action("Copy", Box::new(Copy)); + .action("Cut", Box::new(Cut), cx) + .action("Copy", Box::new(Copy), cx); if let Some(clipboard_entry) = self.clipboard_entry { if clipboard_entry.worktree_id() == worktree_id { - menu = menu.action("Paste", Box::new(Paste)); + menu = menu.action("Paste", Box::new(Paste), cx); } } menu = menu .separator() - .action("Copy Path", Box::new(CopyPath)) - .action("Copy Relative Path", Box::new(CopyRelativePath)) + .action("Copy Path", Box::new(CopyPath), cx) + .action("Copy Relative Path", Box::new(CopyRelativePath), cx) .separator() - .action("Reveal in Finder", Box::new(RevealInFinder)); + .action("Reveal in Finder", Box::new(RevealInFinder), cx); if is_dir { menu = menu - .action("Open in Terminal", Box::new(OpenInTerminal)) - .action("Search Inside", Box::new(NewSearchInDirectory)) + .action("Open in Terminal", Box::new(OpenInTerminal), cx) + .action("Search Inside", Box::new(NewSearchInDirectory), cx) } - menu = menu.separator().action("Rename", Box::new(Rename)); + menu = menu.separator().action("Rename", Box::new(Rename), cx); if !is_root { - menu = menu.action("Delete", Box::new(Delete)); + menu = menu.action("Delete", Box::new(Delete), cx); } menu @@ -658,7 +659,6 @@ impl ProjectPanel { } fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { - dbg!("odd"); self.edit_state = None; self.update_visible_entries(None, cx); cx.focus(&self.focus_handle); @@ -1385,7 +1385,7 @@ impl ProjectPanel { }) .child( if let (Some(editor), true) = (Some(&self.filename_editor), show_editor) { - div().w_full().child(editor.clone()) + div().h_full().w_full().child(editor.clone()) } else { div() .text_color(filename_text_color) diff --git a/crates/terminal_view2/src/terminal_view.rs b/crates/terminal_view2/src/terminal_view.rs index 9f3ed313880989631c25b7cb7f9fe1460c9224c3..b007d58c34bcb2163f42bd2b88e1979a18152f56 100644 --- a/crates/terminal_view2/src/terminal_view.rs +++ b/crates/terminal_view2/src/terminal_view.rs @@ -298,9 +298,12 @@ impl TerminalView { position: gpui::Point, cx: &mut ViewContext, ) { - self.context_menu = Some(ContextMenu::build(cx, |menu, _| { - menu.action("Clear", Box::new(Clear)) - .action("Close", Box::new(CloseActiveItem { save_intent: None })) + self.context_menu = Some(ContextMenu::build(cx, |menu, cx| { + menu.action("Clear", Box::new(Clear), cx).action( + "Close", + Box::new(CloseActiveItem { save_intent: None }), + cx, + ) })); dbg!(&position); // todo!() diff --git a/crates/ui2/src/components/context_menu.rs b/crates/ui2/src/components/context_menu.rs index 3510256c0ce50b2c73d8b091a06e433be42f8ff7..b5ef8cf160839674c3a3e452975ab45fee4d1996 100644 --- a/crates/ui2/src/components/context_menu.rs +++ b/crates/ui2/src/components/context_menu.rs @@ -1,4 +1,4 @@ -use crate::{prelude::*, v_stack, Label, List, ListItem, ListSeparator, ListSubHeader}; +use crate::{prelude::*, v_stack, KeyBinding, Label, List, ListItem, ListSeparator, ListSubHeader}; use gpui::{ overlay, px, Action, AnchorCorner, AnyElement, AppContext, Bounds, ClickEvent, DismissEvent, DispatchPhase, Div, EventEmitter, FocusHandle, FocusableView, IntoElement, LayoutId, @@ -9,7 +9,11 @@ use std::{cell::RefCell, rc::Rc}; pub enum ContextMenuItem { Separator, Header(SharedString), - Entry(SharedString, Rc), + Entry { + label: SharedString, + click_handler: Rc, + key_binding: Option, + }, } pub struct ContextMenu { @@ -57,16 +61,26 @@ impl ContextMenu { label: impl Into, on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static, ) -> Self { - self.items - .push(ContextMenuItem::Entry(label.into(), Rc::new(on_click))); + self.items.push(ContextMenuItem::Entry { + label: label.into(), + click_handler: Rc::new(on_click), + key_binding: None, + }); self } - pub fn action(self, label: impl Into, action: Box) -> Self { - // todo: add the keybindings to the list entry - self.entry(label.into(), move |_, cx| { - cx.dispatch_action(action.boxed_clone()) - }) + pub fn action( + mut self, + label: impl Into, + action: Box, + cx: &mut WindowContext, + ) -> Self { + self.items.push(ContextMenuItem::Entry { + label: label.into(), + key_binding: KeyBinding::for_action(&*action, cx), + click_handler: Rc::new(move |_, cx| cx.dispatch_action(action.boxed_clone())), + }); + self } pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { @@ -106,12 +120,17 @@ impl Render for ContextMenu { ContextMenuItem::Header(header) => { ListSubHeader::new(header.clone()).into_any_element() } - ContextMenuItem::Entry(entry, callback) => { + ContextMenuItem::Entry { + label: entry, + click_handler: callback, + key_binding, + } => { let callback = callback.clone(); let dismiss = cx.listener(|_, _, cx| cx.emit(DismissEvent::Dismiss)); ListItem::new(entry.clone()) .child(Label::new(entry.clone())) + .children(key_binding.clone()) .on_click(move |event, cx| { callback(event, cx); dismiss(event, cx) From 77acba9e4c5982d680ed8fda5aeb0025b0850587 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 28 Nov 2023 17:23:49 -0800 Subject: [PATCH 3/5] Right-align key bindings in context menus Remove extra div in `List` that prevented list items from filling the available space. Co-authored-by: Marshall --- crates/ui2/src/components/context_menu.rs | 17 ++++++++++++++--- crates/ui2/src/components/list.rs | 12 +++++------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/crates/ui2/src/components/context_menu.rs b/crates/ui2/src/components/context_menu.rs index b5ef8cf160839674c3a3e452975ab45fee4d1996..29d8af54a7baf7886f847a1238525569c6d86989 100644 --- a/crates/ui2/src/components/context_menu.rs +++ b/crates/ui2/src/components/context_menu.rs @@ -1,4 +1,6 @@ -use crate::{prelude::*, v_stack, KeyBinding, Label, List, ListItem, ListSeparator, ListSubHeader}; +use crate::{ + h_stack, prelude::*, v_stack, KeyBinding, Label, List, ListItem, ListSeparator, ListSubHeader, +}; use gpui::{ overlay, px, Action, AnchorCorner, AnyElement, AppContext, Bounds, ClickEvent, DismissEvent, DispatchPhase, Div, EventEmitter, FocusHandle, FocusableView, IntoElement, LayoutId, @@ -129,8 +131,17 @@ impl Render for ContextMenu { let dismiss = cx.listener(|_, _, cx| cx.emit(DismissEvent::Dismiss)); ListItem::new(entry.clone()) - .child(Label::new(entry.clone())) - .children(key_binding.clone()) + .child( + h_stack() + .w_full() + .justify_between() + .child(Label::new(entry.clone())) + .children( + key_binding + .clone() + .map(|binding| div().ml_1().child(binding)), + ), + ) .on_click(move |event, cx| { callback(event, cx); dismiss(event, cx) diff --git a/crates/ui2/src/components/list.rs b/crates/ui2/src/components/list.rs index 749de951d9a3055050e18198956eb71c75684cfe..fd801b7393c88b72a83edf1cf86670dc6173bf3b 100644 --- a/crates/ui2/src/components/list.rs +++ b/crates/ui2/src/components/list.rs @@ -362,17 +362,15 @@ impl RenderOnce for List { type Rendered = Div; fn render(self, _cx: &mut WindowContext) -> Self::Rendered { - let list_content = match (self.children.is_empty(), self.toggle) { - (false, _) => div().children(self.children), - (true, Toggle::Toggled(false)) => div(), - (true, _) => div().child(Label::new(self.empty_message.clone()).color(Color::Muted)), - }; - v_stack() .w_full() .py_1() .children(self.header.map(|header| header)) - .child(list_content) + .map(|this| match (self.children.is_empty(), self.toggle) { + (false, _) => this.children(self.children), + (true, Toggle::Toggled(false)) => this, + (true, _) => this.child(Label::new(self.empty_message.clone()).color(Color::Muted)), + }) } } From ac342291189799d2565637326733a75ba1df4506 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 29 Nov 2023 09:39:20 -0800 Subject: [PATCH 4/5] Add keyboard control over context menus Co-authored-by: Conrad --- crates/gpui2/src/window.rs | 12 +- crates/project_panel2/src/project_panel.rs | 18 +- crates/ui2/src/components/context_menu.rs | 156 ++++++++++++------ .../src/components/stories/context_menu.rs | 4 +- crates/workspace2/src/dock.rs | 2 +- 5 files changed, 126 insertions(+), 66 deletions(-) diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 6f342f70654653808d6b9797c898682ba4c532e8..862b2f5643e3b43e811588524c5c9a525192bce8 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1482,13 +1482,15 @@ impl<'a> WindowContext<'a> { } } - pub fn constructor_for( + pub fn handler_for( &self, view: &View, - f: impl Fn(&mut V, &mut ViewContext) -> R + 'static, - ) -> impl Fn(&mut WindowContext) -> R + 'static { - let view = view.clone(); - move |cx: &mut WindowContext| view.update(cx, |view, cx| f(view, cx)) + f: impl Fn(&mut V, &mut ViewContext) + 'static, + ) -> impl Fn(&mut WindowContext) { + let view = view.downgrade(); + move |cx: &mut WindowContext| { + view.update(cx, |view, cx| f(view, cx)).ok(); + } } //========== ELEMENT RELATED FUNCTIONS =========== diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs index 891355ea1f90c2ba16ecf2a9dec5e40ed25bed3f..88b9a9c28c9b9caeb95e801da9950d7612b4a4c2 100644 --- a/crates/project_panel2/src/project_panel.rs +++ b/crates/project_panel2/src/project_panel.rs @@ -9,10 +9,10 @@ use file_associations::FileAssociations; use anyhow::{anyhow, Result}; use gpui::{ actions, div, overlay, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext, - ClipboardItem, Div, EventEmitter, FocusHandle, Focusable, FocusableView, InteractiveElement, - Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, - Stateful, Styled, Subscription, Task, UniformListScrollHandle, View, ViewContext, - VisualContext as _, WeakView, WindowContext, + ClipboardItem, DismissEvent, Div, EventEmitter, FocusHandle, Focusable, FocusableView, + InteractiveElement, Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, + PromptLevel, Render, Stateful, Styled, Subscription, Task, UniformListScrollHandle, View, + ViewContext, VisualContext as _, WeakView, WindowContext, }; use menu::{Confirm, SelectNext, SelectPrev}; use project::{ @@ -403,7 +403,7 @@ impl ProjectPanel { if is_root { menu = menu.entry( "Remove from Project", - cx.listener_for(&this, move |this, _, cx| { + cx.handler_for(&this, move |this, cx| { this.project.update(cx, |project, cx| { project.remove_worktree(worktree_id, cx) }); @@ -448,9 +448,11 @@ impl ProjectPanel { }); cx.focus_view(&context_menu); - let subscription = cx.on_blur(&context_menu.focus_handle(cx), |this, cx| { - this.context_menu.take(); - cx.notify(); + let subscription = cx.subscribe(&context_menu, |this, _, event, cx| match event { + DismissEvent::Dismiss => { + this.context_menu.take(); + cx.notify(); + } }); self.context_menu = Some((context_menu, position, subscription)); } diff --git a/crates/ui2/src/components/context_menu.rs b/crates/ui2/src/components/context_menu.rs index 29d8af54a7baf7886f847a1238525569c6d86989..b446bc5f3d1803805a2ab4b7d3ba3d4999da00e9 100644 --- a/crates/ui2/src/components/context_menu.rs +++ b/crates/ui2/src/components/context_menu.rs @@ -2,10 +2,11 @@ use crate::{ h_stack, prelude::*, v_stack, KeyBinding, Label, List, ListItem, ListSeparator, ListSubHeader, }; use gpui::{ - overlay, px, Action, AnchorCorner, AnyElement, AppContext, Bounds, ClickEvent, DismissEvent, - DispatchPhase, Div, EventEmitter, FocusHandle, FocusableView, IntoElement, LayoutId, - ManagedView, MouseButton, MouseDownEvent, Pixels, Point, Render, View, VisualContext, + overlay, px, Action, AnchorCorner, AnyElement, AppContext, Bounds, DismissEvent, DispatchPhase, + Div, EventEmitter, FocusHandle, FocusableView, IntoElement, LayoutId, ManagedView, MouseButton, + MouseDownEvent, Pixels, Point, Render, View, VisualContext, }; +use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev}; use std::{cell::RefCell, rc::Rc}; pub enum ContextMenuItem { @@ -13,7 +14,7 @@ pub enum ContextMenuItem { Header(SharedString), Entry { label: SharedString, - click_handler: Rc, + handler: Rc, key_binding: Option, }, } @@ -21,6 +22,7 @@ pub enum ContextMenuItem { pub struct ContextMenu { items: Vec, focus_handle: FocusHandle, + selected_index: Option, } impl FocusableView for ContextMenu { @@ -42,6 +44,7 @@ impl ContextMenu { Self { items: Default::default(), focus_handle: cx.focus_handle(), + selected_index: None, }, cx, ) @@ -61,11 +64,11 @@ impl ContextMenu { pub fn entry( mut self, label: impl Into, - on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static, + on_click: impl Fn(&mut WindowContext) + 'static, ) -> Self { self.items.push(ContextMenuItem::Entry { label: label.into(), - click_handler: Rc::new(on_click), + handler: Rc::new(on_click), key_binding: None, }); self @@ -80,19 +83,72 @@ impl ContextMenu { self.items.push(ContextMenuItem::Entry { label: label.into(), key_binding: KeyBinding::for_action(&*action, cx), - click_handler: Rc::new(move |_, cx| cx.dispatch_action(action.boxed_clone())), + handler: Rc::new(move |cx| cx.dispatch_action(action.boxed_clone())), }); self } pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { - // todo!() + if let Some(ContextMenuItem::Entry { handler, .. }) = + self.selected_index.and_then(|ix| self.items.get(ix)) + { + (handler)(cx) + } cx.emit(DismissEvent::Dismiss); } pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { cx.emit(DismissEvent::Dismiss); } + + fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext) { + self.selected_index = self.items.iter().position(|item| item.is_selectable()); + cx.notify(); + } + + fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext) { + for (ix, item) in self.items.iter().enumerate().rev() { + if item.is_selectable() { + self.selected_index = Some(ix); + cx.notify(); + break; + } + } + } + + fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { + if let Some(ix) = self.selected_index { + for (ix, item) in self.items.iter().enumerate().skip(ix + 1) { + if item.is_selectable() { + self.selected_index = Some(ix); + cx.notify(); + break; + } + } + } else { + self.select_first(&Default::default(), cx); + } + } + + pub fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { + if let Some(ix) = self.selected_index { + for (ix, item) in self.items.iter().enumerate().take(ix).rev() { + if item.is_selectable() { + self.selected_index = Some(ix); + cx.notify(); + break; + } + } + } else { + self.select_last(&Default::default(), cx); + } + } +} + +impl ContextMenuItem { + fn is_selectable(&self) -> bool { + matches!(self, Self::Entry { .. }) + } } impl Render for ContextMenu { @@ -103,52 +159,52 @@ impl Render for ContextMenu { v_stack() .min_w(px(200.)) .track_focus(&self.focus_handle) - .on_mouse_down_out( - cx.listener(|this: &mut Self, _, cx| this.cancel(&Default::default(), cx)), - ) - // .on_action(ContextMenu::select_first) - // .on_action(ContextMenu::select_last) - // .on_action(ContextMenu::select_next) - // .on_action(ContextMenu::select_prev) + .on_mouse_down_out(cx.listener(|this, _, cx| this.cancel(&Default::default(), cx))) + .key_context("menu") + .on_action(cx.listener(ContextMenu::select_first)) + .on_action(cx.listener(ContextMenu::select_last)) + .on_action(cx.listener(ContextMenu::select_next)) + .on_action(cx.listener(ContextMenu::select_prev)) .on_action(cx.listener(ContextMenu::confirm)) .on_action(cx.listener(ContextMenu::cancel)) .flex_none() - // .bg(cx.theme().colors().elevated_surface_background) - // .border() - // .border_color(cx.theme().colors().border) .child( - List::new().children(self.items.iter().map(|item| match item { - ContextMenuItem::Separator => ListSeparator::new().into_any_element(), - ContextMenuItem::Header(header) => { - ListSubHeader::new(header.clone()).into_any_element() - } - ContextMenuItem::Entry { - label: entry, - click_handler: callback, - key_binding, - } => { - let callback = callback.clone(); - let dismiss = cx.listener(|_, _, cx| cx.emit(DismissEvent::Dismiss)); - - ListItem::new(entry.clone()) - .child( - h_stack() - .w_full() - .justify_between() - .child(Label::new(entry.clone())) - .children( - key_binding - .clone() - .map(|binding| div().ml_1().child(binding)), - ), - ) - .on_click(move |event, cx| { - callback(event, cx); - dismiss(event, cx) - }) - .into_any_element() - } - })), + List::new().children(self.items.iter().enumerate().map( + |(ix, item)| match item { + ContextMenuItem::Separator => ListSeparator::new().into_any_element(), + ContextMenuItem::Header(header) => { + ListSubHeader::new(header.clone()).into_any_element() + } + ContextMenuItem::Entry { + label: entry, + handler: callback, + key_binding, + } => { + let callback = callback.clone(); + let dismiss = + cx.listener(|_, _, cx| cx.emit(DismissEvent::Dismiss)); + + ListItem::new(entry.clone()) + .child( + h_stack() + .w_full() + .justify_between() + .child(Label::new(entry.clone())) + .children( + key_binding + .clone() + .map(|binding| div().ml_1().child(binding)), + ), + ) + .selected(Some(ix) == self.selected_index) + .on_click(move |event, cx| { + callback(cx); + dismiss(event, cx) + }) + .into_any_element() + } + }, + )), ), ) } diff --git a/crates/ui2/src/components/stories/context_menu.rs b/crates/ui2/src/components/stories/context_menu.rs index 9a8b7efbe666bb95317c98299b4ad72d0a1d23cd..d5fb94df4f0f413dd117e3bc2321e5251c6dffda 100644 --- a/crates/ui2/src/components/stories/context_menu.rs +++ b/crates/ui2/src/components/stories/context_menu.rs @@ -10,11 +10,11 @@ fn build_menu(cx: &mut WindowContext, header: impl Into) -> View Date: Wed, 29 Nov 2023 09:42:54 -0800 Subject: [PATCH 5/5] Make DismissEvent a unit struct Co-authored-by: Conrad --- crates/auto_update2/src/update_notification.rs | 2 +- .../collab_ui2/src/collab_panel/contact_finder.rs | 2 +- crates/command_palette2/src/command_palette.rs | 2 +- crates/file_finder2/src/file_finder.rs | 6 ++---- crates/go_to_line2/src/go_to_line.rs | 6 +++--- crates/gpui2/src/app/async_context.rs | 5 ++--- crates/gpui2/src/app/test_context.rs | 2 +- crates/gpui2/src/window.rs | 8 +++----- crates/project_panel2/src/project_panel.rs | 8 +++----- crates/ui2/src/components/context_menu.rs | 15 ++++++--------- crates/workspace2/src/notifications.rs | 8 +++----- 11 files changed, 26 insertions(+), 38 deletions(-) diff --git a/crates/auto_update2/src/update_notification.rs b/crates/auto_update2/src/update_notification.rs index d15d82e112e612143ef2aaff509965899818e95a..4a2efcf8076bb882a67c1f25b6300dd4cd48c1d1 100644 --- a/crates/auto_update2/src/update_notification.rs +++ b/crates/auto_update2/src/update_notification.rs @@ -51,6 +51,6 @@ impl UpdateNotification { } pub fn dismiss(&mut self, cx: &mut ViewContext) { - cx.emit(DismissEvent::Dismiss); + cx.emit(DismissEvent); } } diff --git a/crates/collab_ui2/src/collab_panel/contact_finder.rs b/crates/collab_ui2/src/collab_panel/contact_finder.rs index 48453ada72fe6acf3ebfa3f514af0ad03d300035..3701b070d923d822c643eb866674825f41af7fe7 100644 --- a/crates/collab_ui2/src/collab_panel/contact_finder.rs +++ b/crates/collab_ui2/src/collab_panel/contact_finder.rs @@ -161,7 +161,7 @@ impl PickerDelegate for ContactFinderDelegate { fn dismissed(&mut self, cx: &mut ViewContext>) { //cx.emit(PickerEvent::Dismiss); self.parent - .update(cx, |_, cx| cx.emit(DismissEvent::Dismiss)) + .update(cx, |_, cx| cx.emit(DismissEvent)) .log_err(); } diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index 076a2625a19f050c504418990501dd3a0d240814..cf39b4f29b0952de1859fa22e5aa68a87e46b1a2 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -267,7 +267,7 @@ impl PickerDelegate for CommandPaletteDelegate { fn dismissed(&mut self, cx: &mut ViewContext>) { self.command_palette - .update(cx, |_, cx| cx.emit(DismissEvent::Dismiss)) + .update(cx, |_, cx| cx.emit(DismissEvent)) .log_err(); } diff --git a/crates/file_finder2/src/file_finder.rs b/crates/file_finder2/src/file_finder.rs index 2f7b26dfb55c9a548eea0ab4fbc88a5544f29921..a24da580ebea9db1467f5936930558821fbd5220 100644 --- a/crates/file_finder2/src/file_finder.rs +++ b/crates/file_finder2/src/file_finder.rs @@ -687,9 +687,7 @@ impl PickerDelegate for FileFinderDelegate { .log_err(); } } - finder - .update(&mut cx, |_, cx| cx.emit(DismissEvent::Dismiss)) - .ok()?; + finder.update(&mut cx, |_, cx| cx.emit(DismissEvent)).ok()?; Some(()) }) @@ -700,7 +698,7 @@ impl PickerDelegate for FileFinderDelegate { fn dismissed(&mut self, cx: &mut ViewContext>) { self.file_finder - .update(cx, |_, cx| cx.emit(DismissEvent::Dismiss)) + .update(cx, |_, cx| cx.emit(DismissEvent)) .log_err(); } diff --git a/crates/go_to_line2/src/go_to_line.rs b/crates/go_to_line2/src/go_to_line.rs index c734281d22d731b0c55d617c8cf7f58e60077a45..5ad95c1f6ea6cf0fb49e185a044d8be7eaf383fe 100644 --- a/crates/go_to_line2/src/go_to_line.rs +++ b/crates/go_to_line2/src/go_to_line.rs @@ -90,7 +90,7 @@ impl GoToLine { ) { match event { // todo!() this isn't working... - editor::EditorEvent::Blurred => cx.emit(DismissEvent::Dismiss), + editor::EditorEvent::Blurred => cx.emit(DismissEvent), editor::EditorEvent::BufferEdited { .. } => self.highlight_current_line(cx), _ => {} } @@ -125,7 +125,7 @@ impl GoToLine { } fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { - cx.emit(DismissEvent::Dismiss); + cx.emit(DismissEvent); } fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { @@ -142,7 +142,7 @@ impl GoToLine { self.prev_scroll_position.take(); } - cx.emit(DismissEvent::Dismiss); + cx.emit(DismissEvent); } } diff --git a/crates/gpui2/src/app/async_context.rs b/crates/gpui2/src/app/async_context.rs index 11420bee69d2c8acd9cb179df5e68bcd7c3e9e5b..92ccc118f12826076621629a470dfda5c6a545d1 100644 --- a/crates/gpui2/src/app/async_context.rs +++ b/crates/gpui2/src/app/async_context.rs @@ -325,8 +325,7 @@ impl VisualContext for AsyncWindowContext { where V: crate::ManagedView, { - self.window.update(self, |_, cx| { - view.update(cx, |_, cx| cx.emit(DismissEvent::Dismiss)) - }) + self.window + .update(self, |_, cx| view.update(cx, |_, cx| cx.emit(DismissEvent))) } } diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index 71bc8e3d8172713e1f2c0ab7d7f5ffb33fe4171e..9637720a67e78ba6f735534f924b4028d8d8c75f 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/crates/gpui2/src/app/test_context.rs @@ -611,7 +611,7 @@ impl<'a> VisualContext for VisualTestContext<'a> { { self.window .update(self.cx, |_, cx| { - view.update(cx, |_, cx| cx.emit(crate::DismissEvent::Dismiss)) + view.update(cx, |_, cx| cx.emit(crate::DismissEvent)) }) .unwrap() } diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 862b2f5643e3b43e811588524c5c9a525192bce8..f8326e9df83ed82ce2b3c829a4d08467376bd844 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -197,9 +197,7 @@ pub trait ManagedView: FocusableView + EventEmitter {} impl> ManagedView for M {} -pub enum DismissEvent { - Dismiss, -} +pub struct DismissEvent; // Holds the state for a specific window. pub struct Window { @@ -1701,7 +1699,7 @@ impl VisualContext for WindowContext<'_> { where V: ManagedView, { - self.update_view(view, |_, cx| cx.emit(DismissEvent::Dismiss)) + self.update_view(view, |_, cx| cx.emit(DismissEvent)) } } @@ -2405,7 +2403,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { where V: ManagedView, { - self.defer(|_, cx| cx.emit(DismissEvent::Dismiss)) + self.defer(|_, cx| cx.emit(DismissEvent)) } pub fn listener( diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs index 88b9a9c28c9b9caeb95e801da9950d7612b4a4c2..594c2d8e034b79b31a99d3a76baa56bcb5a40adf 100644 --- a/crates/project_panel2/src/project_panel.rs +++ b/crates/project_panel2/src/project_panel.rs @@ -448,11 +448,9 @@ impl ProjectPanel { }); cx.focus_view(&context_menu); - let subscription = cx.subscribe(&context_menu, |this, _, event, cx| match event { - DismissEvent::Dismiss => { - this.context_menu.take(); - cx.notify(); - } + let subscription = cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| { + this.context_menu.take(); + cx.notify(); }); self.context_menu = Some((context_menu, position, subscription)); } diff --git a/crates/ui2/src/components/context_menu.rs b/crates/ui2/src/components/context_menu.rs index b446bc5f3d1803805a2ab4b7d3ba3d4999da00e9..3772fb1bd2681ee635f3cc896568739062280c19 100644 --- a/crates/ui2/src/components/context_menu.rs +++ b/crates/ui2/src/components/context_menu.rs @@ -94,11 +94,11 @@ impl ContextMenu { { (handler)(cx) } - cx.emit(DismissEvent::Dismiss); + cx.emit(DismissEvent); } pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { - cx.emit(DismissEvent::Dismiss); + cx.emit(DismissEvent); } fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext) { @@ -181,8 +181,7 @@ impl Render for ContextMenu { key_binding, } => { let callback = callback.clone(); - let dismiss = - cx.listener(|_, _, cx| cx.emit(DismissEvent::Dismiss)); + let dismiss = cx.listener(|_, _, cx| cx.emit(DismissEvent)); ListItem::new(entry.clone()) .child( @@ -348,11 +347,9 @@ impl Element for MenuHandle { let new_menu = (builder)(cx); let menu2 = menu.clone(); - cx.subscribe(&new_menu, move |_modal, e, cx| match e { - &DismissEvent::Dismiss => { - *menu2.borrow_mut() = None; - cx.notify(); - } + cx.subscribe(&new_menu, move |_modal, _: &DismissEvent, cx| { + *menu2.borrow_mut() = None; + cx.notify(); }) .detach(); cx.focus_view(&new_menu); diff --git a/crates/workspace2/src/notifications.rs b/crates/workspace2/src/notifications.rs index def13c518e17308ef91df1b5cc940c71beae6e77..eacf3d288a84fce1cb9526a9191786e3e4223f0d 100644 --- a/crates/workspace2/src/notifications.rs +++ b/crates/workspace2/src/notifications.rs @@ -106,10 +106,8 @@ impl Workspace { let notification = build_notification(cx); cx.subscribe( ¬ification, - move |this, handle, event: &DismissEvent, cx| match event { - DismissEvent::Dismiss => { - this.dismiss_notification_internal(type_id, id, cx); - } + move |this, handle, event: &DismissEvent, cx| { + this.dismiss_notification_internal(type_id, id, cx); }, ) .detach(); @@ -260,7 +258,7 @@ pub mod simple_message_notification { } pub fn dismiss(&mut self, cx: &mut ViewContext) { - cx.emit(DismissEvent::Dismiss); + cx.emit(DismissEvent); } }