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",