From e93d55472570b85097f33024a126e1e1fb2d824b Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 16 Apr 2024 18:03:54 -0400 Subject: [PATCH] Add Editor Controls Menu to Tool Bar (#10655) This PR adds an editor controls menu to the tool bar. This menu will be used to contain controls that toggle visual features in the editor, like toggling inlay hints, showing git status or blame, hiding the gutter, hiding or showing elements in the tool bar, etc. For the moment, this consolidates the new Inline Git Blame toggle and the old Inlay Hints toggle. In the future it will contain additional controls. Before: ![CleanShot - 2024-04-16 at 16 38 53@2x](https://github.com/zed-industries/zed/assets/1714999/249e353f-786a-4391-8d49-66dd61feff8a) After: ![CleanShot - 2024-04-16 at 16 38 43@2x](https://github.com/zed-industries/zed/assets/1714999/5b3cf4d5-855a-4475-ac05-8474b6c94b7b) --- Release Notes: - Added an editor controls menu to the tool bar. This will contain visual, editor-specific options like toggling inlay hints, showing git status or blame, etc. - Removed the top level inlay hint toggle from the tool bar due to the above change. - Added the ability to toggle inline git blame from the new editor controls menu. --------- Co-authored-by: Marshall Bowers <1486634+maxdeviant@users.noreply.github.com> --- assets/icons/sliders.svg | 3 + crates/editor/src/editor.rs | 4 + .../quick_action_bar/src/quick_action_bar.rs | 120 ++++++++++++++---- crates/ui/src/components/context_menu.rs | 43 ++++++- crates/ui/src/components/icon.rs | 2 + 5 files changed, 141 insertions(+), 31 deletions(-) create mode 100644 assets/icons/sliders.svg diff --git a/assets/icons/sliders.svg b/assets/icons/sliders.svg new file mode 100644 index 0000000000..33e50b08d7 --- /dev/null +++ b/assets/icons/sliders.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 851c6b558b..7beabfba48 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -8889,6 +8889,10 @@ impl Editor { cx.notify(); } + pub fn git_blame_inline_enabled(&self) -> bool { + self.git_blame_inline_enabled + } + fn start_git_blame(&mut self, user_triggered: bool, cx: &mut ViewContext) { if let Some(project) = self.project.as_ref() { let Some(buffer) = self.buffer().read(cx).as_singleton() else { diff --git a/crates/quick_action_bar/src/quick_action_bar.rs b/crates/quick_action_bar/src/quick_action_bar.rs index a6fe46ec66..1967ff10ea 100644 --- a/crates/quick_action_bar/src/quick_action_bar.rs +++ b/crates/quick_action_bar/src/quick_action_bar.rs @@ -3,18 +3,21 @@ use assistant::{AssistantPanel, InlineAssist}; use editor::{Editor, EditorSettings}; use gpui::{ - Action, ClickEvent, ElementId, EventEmitter, InteractiveElement, ParentElement, Render, Styled, - Subscription, View, ViewContext, WeakView, + anchored, deferred, Action, AnchorCorner, ClickEvent, DismissEvent, ElementId, EventEmitter, + InteractiveElement, ParentElement, Render, Styled, Subscription, View, ViewContext, WeakView, }; use search::{buffer_search, BufferSearchBar}; use settings::{Settings, SettingsStore}; -use ui::{prelude::*, ButtonSize, ButtonStyle, IconButton, IconName, IconSize, Tooltip}; +use ui::{ + prelude::*, ButtonSize, ButtonStyle, ContextMenu, IconButton, IconName, IconSize, Tooltip, +}; use workspace::{ item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, }; pub struct QuickActionBar { buffer_search_bar: View, + toggle_settings_menu: Option>, active_item: Option>, _inlay_hints_enabled_subscription: Option, workspace: WeakView, @@ -29,6 +32,7 @@ impl QuickActionBar { ) -> Self { let mut this = Self { buffer_search_bar, + toggle_settings_menu: None, active_item: None, _inlay_hints_enabled_subscription: None, workspace: workspace.weak_handle(), @@ -63,6 +67,17 @@ impl QuickActionBar { ToolbarItemLocation::Hidden } } + + fn render_menu_overlay(menu: &View) -> Div { + div().absolute().bottom_0().right_0().size_0().child( + deferred( + anchored() + .anchor(AnchorCorner::TopRight) + .child(menu.clone()), + ) + .with_priority(1), + ) + } } impl Render for QuickActionBar { @@ -70,22 +85,6 @@ impl Render for QuickActionBar { let Some(editor) = self.active_editor() else { return div().id("empty quick action bar"); }; - let inlay_hints_button = Some(QuickActionBarButton::new( - "toggle inlay hints", - IconName::InlayHint, - editor.read(cx).inlay_hints_enabled(), - Box::new(editor::actions::ToggleInlayHints), - "Toggle Inlay Hints", - { - let editor = editor.clone(); - move |_, cx| { - editor.update(cx, |editor, cx| { - editor.toggle_inlay_hints(&editor::actions::ToggleInlayHints, cx); - }); - } - }, - )) - .filter(|_| editor.read(cx).supports_inlay_hints(cx)); let search_button = Some(QuickActionBarButton::new( "toggle buffer search", @@ -122,14 +121,85 @@ impl Render for QuickActionBar { }, ); + let editor_settings_dropdown = + IconButton::new("toggle_editor_settings_icon", IconName::Sliders) + .size(ButtonSize::Compact) + .icon_size(IconSize::Small) + .style(ButtonStyle::Subtle) + .selected(self.toggle_settings_menu.is_some()) + .on_click({ + let editor = editor.clone(); + cx.listener(move |quick_action_bar, _, cx| { + let inlay_hints_enabled = editor.read(cx).inlay_hints_enabled(); + let supports_inlay_hints = editor.read(cx).supports_inlay_hints(cx); + let git_blame_inline_enabled = editor.read(cx).git_blame_inline_enabled(); + + let menu = ContextMenu::build(cx, |mut menu, _| { + if supports_inlay_hints { + menu = menu.toggleable_entry( + "Show Inlay Hints", + inlay_hints_enabled, + Some(editor::actions::ToggleInlayHints.boxed_clone()), + { + let editor = editor.clone(); + move |cx| { + editor.update(cx, |editor, cx| { + editor.toggle_inlay_hints( + &editor::actions::ToggleInlayHints, + cx, + ); + }); + } + }, + ); + } + + menu = menu.toggleable_entry( + "Show Git Blame", + git_blame_inline_enabled, + Some(editor::actions::ToggleGitBlameInline.boxed_clone()), + { + let editor = editor.clone(); + move |cx| { + editor.update(cx, |editor, cx| { + editor.toggle_git_blame_inline( + &editor::actions::ToggleGitBlameInline, + cx, + ) + }); + } + }, + ); + + menu + }); + cx.subscribe(&menu, |quick_action_bar, _, _: &DismissEvent, _cx| { + quick_action_bar.toggle_settings_menu = None; + }) + .detach(); + quick_action_bar.toggle_settings_menu = Some(menu); + }) + }) + .tooltip(|cx| Tooltip::text("Editor Controls", cx)); + h_flex() .id("quick action bar") - .gap_2() - .children(inlay_hints_button) - .children(search_button) - .when(AssistantSettings::get_global(cx).button, |bar| { - bar.child(assistant_button) - }) + .gap_3() + .child( + h_flex() + .gap_1p5() + .children(search_button) + .when(AssistantSettings::get_global(cx).button, |bar| { + bar.child(assistant_button) + }), + ) + .child(editor_settings_dropdown) + .when_some( + self.toggle_settings_menu.as_ref(), + |el, toggle_settings_menu| { + el.child(Self::render_menu_overlay(toggle_settings_menu)) + }, + ) } } diff --git a/crates/ui/src/components/context_menu.rs b/crates/ui/src/components/context_menu.rs index 57b934f305..e4c42fe5eb 100644 --- a/crates/ui/src/components/context_menu.rs +++ b/crates/ui/src/components/context_menu.rs @@ -13,6 +13,7 @@ enum ContextMenuItem { Separator, Header(SharedString), Entry { + toggled: Option, label: SharedString, icon: Option, handler: Rc, @@ -92,6 +93,24 @@ impl ContextMenu { handler: impl Fn(&mut WindowContext) + 'static, ) -> Self { self.items.push(ContextMenuItem::Entry { + toggled: None, + label: label.into(), + handler: Rc::new(handler), + icon: None, + action, + }); + self + } + + pub fn toggleable_entry( + mut self, + label: impl Into, + toggled: bool, + action: Option>, + handler: impl Fn(&mut WindowContext) + 'static, + ) -> Self { + self.items.push(ContextMenuItem::Entry { + toggled: Some(toggled), label: label.into(), handler: Rc::new(handler), icon: None, @@ -114,6 +133,7 @@ impl ContextMenu { pub fn action(mut self, label: impl Into, action: Box) -> Self { self.items.push(ContextMenuItem::Entry { + toggled: None, label: label.into(), action: Some(action.boxed_clone()), handler: Rc::new(move |cx| cx.dispatch_action(action.boxed_clone())), @@ -124,6 +144,7 @@ impl ContextMenu { pub fn link(mut self, label: impl Into, action: Box) -> Self { self.items.push(ContextMenuItem::Entry { + toggled: None, label: label.into(), action: Some(action.boxed_clone()), handler: Rc::new(move |cx| cx.dispatch_action(action.boxed_clone())), @@ -279,6 +300,7 @@ impl Render for ContextMenu { .inset(true) .into_any_element(), ContextMenuItem::Entry { + toggled, label, handler, icon, @@ -300,13 +322,14 @@ impl Render for ContextMenu { ListItem::new(ix) .inset(true) .selected(Some(ix) == self.selected_index) - .on_click(move |_, cx| { - handler(cx); - menu.update(cx, |menu, cx| { - menu.clicked = true; - cx.emit(DismissEvent); + .when_some(*toggled, |list_item, toggled| { + list_item.start_slot(if toggled { + v_flex().flex_none().child( + Icon::new(IconName::Check).color(Color::Accent), + ) + } else { + v_flex().flex_none().size(IconSize::default().rems()) }) - .ok(); }) .child( h_flex() @@ -328,6 +351,14 @@ impl Render for ContextMenu { .map(|binding| div().ml_1().child(binding)) })), ) + .on_click(move |_, cx| { + handler(cx); + menu.update(cx, |menu, cx| { + menu.clicked = true; + cx.emit(DismissEvent); + }) + .ok(); + }) .into_any_element() } ContextMenuItem::CustomEntry { diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index 5992f88438..cd7c53d67c 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -105,6 +105,7 @@ pub enum IconName { Return, ReplyArrowRight, Settings, + Sliders, Screen, SelectAll, Server, @@ -204,6 +205,7 @@ impl IconName { IconName::Return => "icons/return.svg", IconName::ReplyArrowRight => "icons/reply_arrow_right.svg", IconName::Settings => "icons/file_icons/settings.svg", + IconName::Sliders => "icons/sliders.svg", IconName::Screen => "icons/desktop.svg", IconName::SelectAll => "icons/select_all.svg", IconName::Server => "icons/server.svg",