quick_action_bar.rs

  1use assistant::assistant_settings::AssistantSettings;
  2use assistant::{AssistantPanel, InlineAssist};
  3use editor::{Editor, EditorSettings};
  4
  5use gpui::{
  6    anchored, deferred, Action, AnchorCorner, ClickEvent, DismissEvent, ElementId, EventEmitter,
  7    InteractiveElement, ParentElement, Render, Styled, Subscription, View, ViewContext, WeakView,
  8};
  9use search::{buffer_search, BufferSearchBar};
 10use settings::{Settings, SettingsStore};
 11use ui::{
 12    prelude::*, ButtonSize, ButtonStyle, ContextMenu, IconButton, IconName, IconSize, Tooltip,
 13};
 14use workspace::{
 15    item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
 16};
 17
 18pub struct QuickActionBar {
 19    buffer_search_bar: View<BufferSearchBar>,
 20    toggle_settings_menu: Option<View<ContextMenu>>,
 21    active_item: Option<Box<dyn ItemHandle>>,
 22    _inlay_hints_enabled_subscription: Option<Subscription>,
 23    workspace: WeakView<Workspace>,
 24    show: bool,
 25}
 26
 27impl QuickActionBar {
 28    pub fn new(
 29        buffer_search_bar: View<BufferSearchBar>,
 30        workspace: &Workspace,
 31        cx: &mut ViewContext<Self>,
 32    ) -> Self {
 33        let mut this = Self {
 34            buffer_search_bar,
 35            toggle_settings_menu: None,
 36            active_item: None,
 37            _inlay_hints_enabled_subscription: None,
 38            workspace: workspace.weak_handle(),
 39            show: true,
 40        };
 41        this.apply_settings(cx);
 42        cx.observe_global::<SettingsStore>(|this, cx| this.apply_settings(cx))
 43            .detach();
 44        this
 45    }
 46
 47    fn active_editor(&self) -> Option<View<Editor>> {
 48        self.active_item
 49            .as_ref()
 50            .and_then(|item| item.downcast::<Editor>())
 51    }
 52
 53    fn apply_settings(&mut self, cx: &mut ViewContext<Self>) {
 54        let new_show = EditorSettings::get_global(cx).toolbar.quick_actions;
 55        if new_show != self.show {
 56            self.show = new_show;
 57            cx.emit(ToolbarItemEvent::ChangeLocation(
 58                self.get_toolbar_item_location(),
 59            ));
 60        }
 61    }
 62
 63    fn get_toolbar_item_location(&self) -> ToolbarItemLocation {
 64        if self.show && self.active_editor().is_some() {
 65            ToolbarItemLocation::PrimaryRight
 66        } else {
 67            ToolbarItemLocation::Hidden
 68        }
 69    }
 70
 71    fn render_menu_overlay(menu: &View<ContextMenu>) -> Div {
 72        div().absolute().bottom_0().right_0().size_0().child(
 73            deferred(
 74                anchored()
 75                    .anchor(AnchorCorner::TopRight)
 76                    .child(menu.clone()),
 77            )
 78            .with_priority(1),
 79        )
 80    }
 81}
 82
 83impl Render for QuickActionBar {
 84    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
 85        let Some(editor) = self.active_editor() else {
 86            return div().id("empty quick action bar");
 87        };
 88
 89        let search_button = Some(QuickActionBarButton::new(
 90            "toggle buffer search",
 91            IconName::MagnifyingGlass,
 92            !self.buffer_search_bar.read(cx).is_dismissed(),
 93            Box::new(buffer_search::Deploy::find()),
 94            "Buffer Search",
 95            {
 96                let buffer_search_bar = self.buffer_search_bar.clone();
 97                move |_, cx| {
 98                    buffer_search_bar.update(cx, |search_bar, cx| {
 99                        search_bar.toggle(&buffer_search::Deploy::find(), cx)
100                    });
101                }
102            },
103        ))
104        .filter(|_| editor.is_singleton(cx));
105
106        let assistant_button = QuickActionBarButton::new(
107            "toggle inline assistant",
108            IconName::MagicWand,
109            false,
110            Box::new(InlineAssist),
111            "Inline Assist",
112            {
113                let workspace = self.workspace.clone();
114                move |_, cx| {
115                    if let Some(workspace) = workspace.upgrade() {
116                        workspace.update(cx, |workspace, cx| {
117                            AssistantPanel::inline_assist(workspace, &InlineAssist, cx);
118                        });
119                    }
120                }
121            },
122        );
123
124        let editor_settings_dropdown =
125            IconButton::new("toggle_editor_settings_icon", IconName::Sliders)
126                .size(ButtonSize::Compact)
127                .icon_size(IconSize::Small)
128                .style(ButtonStyle::Subtle)
129                .selected(self.toggle_settings_menu.is_some())
130                .on_click({
131                    let editor = editor.clone();
132                    cx.listener(move |quick_action_bar, _, cx| {
133                        let inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
134                        let supports_inlay_hints = editor.read(cx).supports_inlay_hints(cx);
135                        let git_blame_inline_enabled = editor.read(cx).git_blame_inline_enabled();
136
137                        let menu = ContextMenu::build(cx, |mut menu, _| {
138                            if supports_inlay_hints {
139                                menu = menu.toggleable_entry(
140                                    "Show Inlay Hints",
141                                    inlay_hints_enabled,
142                                    Some(editor::actions::ToggleInlayHints.boxed_clone()),
143                                    {
144                                        let editor = editor.clone();
145                                        move |cx| {
146                                            editor.update(cx, |editor, cx| {
147                                                editor.toggle_inlay_hints(
148                                                    &editor::actions::ToggleInlayHints,
149                                                    cx,
150                                                );
151                                            });
152                                        }
153                                    },
154                                );
155                            }
156
157                            menu = menu.toggleable_entry(
158                                "Show Git Blame Inline",
159                                git_blame_inline_enabled,
160                                Some(editor::actions::ToggleGitBlameInline.boxed_clone()),
161                                {
162                                    let editor = editor.clone();
163                                    move |cx| {
164                                        editor.update(cx, |editor, cx| {
165                                            editor.toggle_git_blame_inline(
166                                                &editor::actions::ToggleGitBlameInline,
167                                                cx,
168                                            )
169                                        });
170                                    }
171                                },
172                            );
173
174                            menu
175                        });
176                        cx.subscribe(&menu, |quick_action_bar, _, _: &DismissEvent, _cx| {
177                            quick_action_bar.toggle_settings_menu = None;
178                        })
179                        .detach();
180                        quick_action_bar.toggle_settings_menu = Some(menu);
181                    })
182                })
183                .when(self.toggle_settings_menu.is_none(), |this| {
184                    this.tooltip(|cx| Tooltip::text("Editor Controls", cx))
185                });
186
187        h_flex()
188            .id("quick action bar")
189            .gap_3()
190            .child(
191                h_flex()
192                    .gap_1p5()
193                    .children(search_button)
194                    .when(AssistantSettings::get_global(cx).button, |bar| {
195                        bar.child(assistant_button)
196                    }),
197            )
198            .child(editor_settings_dropdown)
199            .when_some(
200                self.toggle_settings_menu.as_ref(),
201                |el, toggle_settings_menu| {
202                    el.child(Self::render_menu_overlay(toggle_settings_menu))
203                },
204            )
205    }
206}
207
208impl EventEmitter<ToolbarItemEvent> for QuickActionBar {}
209
210#[derive(IntoElement)]
211struct QuickActionBarButton {
212    id: ElementId,
213    icon: IconName,
214    toggled: bool,
215    action: Box<dyn Action>,
216    tooltip: SharedString,
217    on_click: Box<dyn Fn(&ClickEvent, &mut WindowContext)>,
218}
219
220impl QuickActionBarButton {
221    fn new(
222        id: impl Into<ElementId>,
223        icon: IconName,
224        toggled: bool,
225        action: Box<dyn Action>,
226        tooltip: impl Into<SharedString>,
227        on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
228    ) -> Self {
229        Self {
230            id: id.into(),
231            icon,
232            toggled,
233            action,
234            tooltip: tooltip.into(),
235            on_click: Box::new(on_click),
236        }
237    }
238}
239
240impl RenderOnce for QuickActionBarButton {
241    fn render(self, _: &mut WindowContext) -> impl IntoElement {
242        let tooltip = self.tooltip.clone();
243        let action = self.action.boxed_clone();
244
245        IconButton::new(self.id.clone(), self.icon)
246            .size(ButtonSize::Compact)
247            .icon_size(IconSize::Small)
248            .style(ButtonStyle::Subtle)
249            .selected(self.toggled)
250            .tooltip(move |cx| Tooltip::for_action(tooltip.clone(), &*action, cx))
251            .on_click(move |event, cx| (self.on_click)(event, cx))
252    }
253}
254
255impl ToolbarItemView for QuickActionBar {
256    fn set_active_pane_item(
257        &mut self,
258        active_pane_item: Option<&dyn ItemHandle>,
259        cx: &mut ViewContext<Self>,
260    ) -> ToolbarItemLocation {
261        self.active_item = active_pane_item.map(ItemHandle::boxed_clone);
262        if let Some(active_item) = active_pane_item {
263            self._inlay_hints_enabled_subscription.take();
264
265            if let Some(editor) = active_item.downcast::<Editor>() {
266                let mut inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
267                let mut supports_inlay_hints = editor.read(cx).supports_inlay_hints(cx);
268                self._inlay_hints_enabled_subscription =
269                    Some(cx.observe(&editor, move |_, editor, cx| {
270                        let editor = editor.read(cx);
271                        let new_inlay_hints_enabled = editor.inlay_hints_enabled();
272                        let new_supports_inlay_hints = editor.supports_inlay_hints(cx);
273                        let should_notify = inlay_hints_enabled != new_inlay_hints_enabled
274                            || supports_inlay_hints != new_supports_inlay_hints;
275                        inlay_hints_enabled = new_inlay_hints_enabled;
276                        supports_inlay_hints = new_supports_inlay_hints;
277                        if should_notify {
278                            cx.notify()
279                        }
280                    }));
281            }
282        }
283        self.get_toolbar_item_location()
284    }
285}