dock.rs

  1use crate::{StatusItemView, Workspace};
  2use context_menu::{ContextMenu, ContextMenuItem};
  3use gpui::{
  4    elements::*, platform::CursorStyle, platform::MouseButton, Action, AnyViewHandle, AppContext,
  5    Axis, Entity, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
  6};
  7use serde::Deserialize;
  8use std::rc::Rc;
  9use theme::ThemeSettings;
 10
 11pub trait Panel: View {
 12    fn position(&self, cx: &WindowContext) -> DockPosition;
 13    fn position_is_valid(&self, position: DockPosition) -> bool;
 14    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>);
 15    fn size(&self, cx: &WindowContext) -> f32;
 16    fn set_size(&mut self, size: f32, cx: &mut ViewContext<Self>);
 17    fn icon_path(&self) -> &'static str;
 18    fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>);
 19    fn icon_label(&self, _: &WindowContext) -> Option<String> {
 20        None
 21    }
 22    fn should_change_position_on_event(_: &Self::Event) -> bool;
 23    fn should_zoom_in_on_event(_: &Self::Event) -> bool {
 24        false
 25    }
 26    fn should_zoom_out_on_event(_: &Self::Event) -> bool {
 27        false
 28    }
 29    fn is_zoomed(&self, _cx: &WindowContext) -> bool {
 30        false
 31    }
 32    fn set_zoomed(&mut self, _zoomed: bool, _cx: &mut ViewContext<Self>) {
 33
 34    }
 35    fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {
 36
 37    }
 38    fn should_activate_on_event(_: &Self::Event) -> bool {
 39        false
 40    }
 41    fn should_close_on_event(_: &Self::Event) -> bool {
 42        false
 43    }
 44    fn has_focus(&self, cx: &WindowContext) -> bool;
 45    fn is_focus_event(_: &Self::Event) -> bool;
 46}
 47
 48pub trait PanelHandle {
 49    fn id(&self) -> usize;
 50    fn position(&self, cx: &WindowContext) -> DockPosition;
 51    fn position_is_valid(&self, position: DockPosition, cx: &WindowContext) -> bool;
 52    fn set_position(&self, position: DockPosition, cx: &mut WindowContext);
 53    fn is_zoomed(&self, cx: &WindowContext) -> bool;
 54    fn set_zoomed(&self, zoomed: bool, cx: &mut WindowContext);
 55    fn set_active(&self, active: bool, cx: &mut WindowContext);
 56    fn size(&self, cx: &WindowContext) -> f32;
 57    fn set_size(&self, size: f32, cx: &mut WindowContext);
 58    fn icon_path(&self, cx: &WindowContext) -> &'static str;
 59    fn icon_tooltip(&self, cx: &WindowContext) -> (String, Option<Box<dyn Action>>);
 60    fn icon_label(&self, cx: &WindowContext) -> Option<String>;
 61    fn has_focus(&self, cx: &WindowContext) -> bool;
 62    fn as_any(&self) -> &AnyViewHandle;
 63}
 64
 65impl<T> PanelHandle for ViewHandle<T>
 66where
 67    T: Panel,
 68{
 69    fn id(&self) -> usize {
 70        self.id()
 71    }
 72
 73    fn position(&self, cx: &WindowContext) -> DockPosition {
 74        self.read(cx).position(cx)
 75    }
 76
 77    fn position_is_valid(&self, position: DockPosition, cx: &WindowContext) -> bool {
 78        self.read(cx).position_is_valid(position)
 79    }
 80
 81    fn set_position(&self, position: DockPosition, cx: &mut WindowContext) {
 82        self.update(cx, |this, cx| this.set_position(position, cx))
 83    }
 84
 85    fn size(&self, cx: &WindowContext) -> f32 {
 86        self.read(cx).size(cx)
 87    }
 88
 89    fn set_size(&self, size: f32, cx: &mut WindowContext) {
 90        self.update(cx, |this, cx| this.set_size(size, cx))
 91    }
 92
 93    fn is_zoomed(&self, cx: &WindowContext) -> bool {
 94        self.read(cx).is_zoomed(cx)
 95    }
 96
 97    fn set_zoomed(&self, zoomed: bool, cx: &mut WindowContext) {
 98        self.update(cx, |this, cx| this.set_zoomed(zoomed, cx))
 99    }
100
101    fn set_active(&self, active: bool, cx: &mut WindowContext) {
102        self.update(cx, |this, cx| this.set_active(active, cx))
103    }
104
105    fn icon_path(&self, cx: &WindowContext) -> &'static str {
106        self.read(cx).icon_path()
107    }
108
109    fn icon_tooltip(&self, cx: &WindowContext) -> (String, Option<Box<dyn Action>>) {
110        self.read(cx).icon_tooltip()
111    }
112
113    fn icon_label(&self, cx: &WindowContext) -> Option<String> {
114        self.read(cx).icon_label(cx)
115    }
116
117    fn has_focus(&self, cx: &WindowContext) -> bool {
118        self.read(cx).has_focus(cx)
119    }
120
121    fn as_any(&self) -> &AnyViewHandle {
122        self
123    }
124}
125
126impl From<&dyn PanelHandle> for AnyViewHandle {
127    fn from(val: &dyn PanelHandle) -> Self {
128        val.as_any().clone()
129    }
130}
131
132pub struct Dock {
133    position: DockPosition,
134    panel_entries: Vec<PanelEntry>,
135    is_open: bool,
136    active_panel_index: usize,
137}
138
139#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
140pub enum DockPosition {
141    Left,
142    Bottom,
143    Right,
144}
145
146impl DockPosition {
147    fn to_label(&self) -> &'static str {
148        match self {
149            Self::Left => "left",
150            Self::Bottom => "bottom",
151            Self::Right => "right",
152        }
153    }
154
155    fn to_resize_handle_side(self) -> HandleSide {
156        match self {
157            Self::Left => HandleSide::Right,
158            Self::Bottom => HandleSide::Top,
159            Self::Right => HandleSide::Left,
160        }
161    }
162
163    pub fn axis(&self) -> Axis {
164        match self {
165            Self::Left | Self::Right => Axis::Horizontal,
166            Self::Bottom => Axis::Vertical,
167        }
168    }
169}
170
171struct PanelEntry {
172    panel: Rc<dyn PanelHandle>,
173    context_menu: ViewHandle<ContextMenu>,
174    _subscriptions: [Subscription; 2],
175}
176
177pub struct PanelButtons {
178    dock: ViewHandle<Dock>,
179    workspace: WeakViewHandle<Workspace>,
180}
181
182impl Dock {
183    pub fn new(position: DockPosition) -> Self {
184        Self {
185            position,
186            panel_entries: Default::default(),
187            active_panel_index: 0,
188            is_open: false,
189        }
190    }
191
192    pub fn position(&self) -> DockPosition {
193        self.position
194    }
195
196    pub fn is_open(&self) -> bool {
197        self.is_open
198    }
199
200    pub fn has_focus(&self, cx: &WindowContext) -> bool {
201        self.visible_panel()
202            .map_or(false, |panel| panel.has_focus(cx))
203    }
204
205    pub fn panel<T: Panel>(&self) -> Option<ViewHandle<T>> {
206        self.panel_entries
207            .iter()
208            .find_map(|entry| entry.panel.as_any().clone().downcast())
209    }
210
211    pub fn panel_index_for_type<T: Panel>(&self) -> Option<usize> {
212        self.panel_entries
213            .iter()
214            .position(|entry| entry.panel.as_any().is::<T>())
215    }
216
217    pub fn panel_index_for_ui_name(&self, ui_name: &str, cx: &AppContext) -> Option<usize> {
218        self.panel_entries.iter().position(|entry| {
219            let panel = entry.panel.as_any();
220            cx.view_ui_name(panel.window_id(), panel.id()) == Some(ui_name)
221        })
222    }
223
224    pub fn active_panel_index(&self) -> usize {
225        self.active_panel_index
226    }
227
228    pub(crate) fn set_open(&mut self, open: bool, cx: &mut ViewContext<Self>) {
229        if open != self.is_open {
230            self.is_open = open;
231            if let Some(active_panel) = self.panel_entries.get(self.active_panel_index) {
232                active_panel.panel.set_active(open, cx);
233            }
234
235            cx.notify();
236        }
237    }
238
239    pub fn set_panel_zoomed(
240        &mut self,
241        panel: &AnyViewHandle,
242        zoomed: bool,
243        cx: &mut ViewContext<Self>,
244    ) {
245        for entry in &mut self.panel_entries {
246            if entry.panel.as_any() == panel {
247                if zoomed != entry.panel.is_zoomed(cx) {
248                    entry.panel.set_zoomed(zoomed, cx);
249                }
250            } else if entry.panel.is_zoomed(cx) {
251                entry.panel.set_zoomed(false, cx);
252            }
253        }
254
255        cx.notify();
256    }
257
258    pub fn zoom_out(&mut self, cx: &mut ViewContext<Self>) {
259        for entry in &mut self.panel_entries {
260            if entry.panel.is_zoomed(cx) {
261                entry.panel.set_zoomed(false, cx);
262            }
263        }
264    }
265
266    pub(crate) fn add_panel<T: Panel>(&mut self, panel: ViewHandle<T>, cx: &mut ViewContext<Self>) {
267        let subscriptions = [
268            cx.observe(&panel, |_, _, cx| cx.notify()),
269            cx.subscribe(&panel, |this, panel, event, cx| {
270                if T::should_activate_on_event(event) {
271                    if let Some(ix) = this
272                        .panel_entries
273                        .iter()
274                        .position(|entry| entry.panel.id() == panel.id())
275                    {
276                        this.set_open(true, cx);
277                        this.activate_panel(ix, cx);
278                        cx.focus(&panel);
279                    }
280                } else if T::should_close_on_event(event)
281                    && this.visible_panel().map_or(false, |p| p.id() == panel.id())
282                {
283                    this.set_open(false, cx);
284                }
285            }),
286        ];
287
288        let dock_view_id = cx.view_id();
289        self.panel_entries.push(PanelEntry {
290            panel: Rc::new(panel),
291            context_menu: cx.add_view(|cx| {
292                let mut menu = ContextMenu::new(dock_view_id, cx);
293                menu.set_position_mode(OverlayPositionMode::Local);
294                menu
295            }),
296            _subscriptions: subscriptions,
297        });
298        cx.notify()
299    }
300
301    pub fn remove_panel<T: Panel>(&mut self, panel: &ViewHandle<T>, cx: &mut ViewContext<Self>) {
302        if let Some(panel_ix) = self
303            .panel_entries
304            .iter()
305            .position(|entry| entry.panel.id() == panel.id())
306        {
307            if panel_ix == self.active_panel_index {
308                self.active_panel_index = 0;
309                self.set_open(false, cx);
310            } else if panel_ix < self.active_panel_index {
311                self.active_panel_index -= 1;
312            }
313            self.panel_entries.remove(panel_ix);
314            cx.notify();
315        }
316    }
317
318    pub fn panels_len(&self) -> usize {
319        self.panel_entries.len()
320    }
321
322    pub fn activate_panel(&mut self, panel_ix: usize, cx: &mut ViewContext<Self>) {
323        if panel_ix != self.active_panel_index {
324            if let Some(active_panel) = self.panel_entries.get(self.active_panel_index) {
325                active_panel.panel.set_active(false, cx);
326            }
327
328            self.active_panel_index = panel_ix;
329            if let Some(active_panel) = self.panel_entries.get(self.active_panel_index) {
330                active_panel.panel.set_active(true, cx);
331            }
332
333            cx.notify();
334        }
335    }
336
337    pub fn visible_panel(&self) -> Option<&Rc<dyn PanelHandle>> {
338        let entry = self.visible_entry()?;
339        Some(&entry.panel)
340    }
341
342    pub fn active_panel(&self) -> Option<&Rc<dyn PanelHandle>> {
343        Some(&self.panel_entries.get(self.active_panel_index)?.panel)
344    }
345
346    fn visible_entry(&self) -> Option<&PanelEntry> {
347        if self.is_open {
348            self.panel_entries.get(self.active_panel_index)
349        } else {
350            None
351        }
352    }
353
354    pub fn zoomed_panel(&self, cx: &WindowContext) -> Option<Rc<dyn PanelHandle>> {
355        let entry = self.visible_entry()?;
356        if entry.panel.is_zoomed(cx) {
357            Some(entry.panel.clone())
358        } else {
359            None
360        }
361    }
362
363    pub fn panel_size(&self, panel: &dyn PanelHandle, cx: &WindowContext) -> Option<f32> {
364        self.panel_entries
365            .iter()
366            .find(|entry| entry.panel.id() == panel.id())
367            .map(|entry| entry.panel.size(cx))
368    }
369
370    pub fn active_panel_size(&self, cx: &WindowContext) -> Option<f32> {
371        if self.is_open {
372            self.panel_entries
373                .get(self.active_panel_index)
374                .map(|entry| entry.panel.size(cx))
375        } else {
376            None
377        }
378    }
379
380    pub fn resize_active_panel(&mut self, size: f32, cx: &mut ViewContext<Self>) {
381        if let Some(entry) = self.panel_entries.get_mut(self.active_panel_index) {
382            entry.panel.set_size(size, cx);
383            cx.notify();
384        }
385    }
386
387    pub fn render_placeholder(&self, cx: &WindowContext) -> AnyElement<Workspace> {
388        if let Some(active_entry) = self.visible_entry() {
389            Empty::new()
390                .into_any()
391                .contained()
392                .with_style(self.style(cx))
393                .resizable(
394                    self.position.to_resize_handle_side(),
395                    active_entry.panel.size(cx),
396                    |_, _, _| {},
397                )
398                .into_any()
399        } else {
400            Empty::new().into_any()
401        }
402    }
403
404    fn style(&self, cx: &WindowContext) -> ContainerStyle {
405        let theme = &settings::get::<ThemeSettings>(cx).theme;
406        let style = match self.position {
407            DockPosition::Left => theme.workspace.dock.left,
408            DockPosition::Bottom => theme.workspace.dock.bottom,
409            DockPosition::Right => theme.workspace.dock.right,
410        };
411        style
412    }
413}
414
415impl Entity for Dock {
416    type Event = ();
417}
418
419impl View for Dock {
420    fn ui_name() -> &'static str {
421        "Dock"
422    }
423
424    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
425        if let Some(active_entry) = self.visible_entry() {
426            let style = self.style(cx);
427            ChildView::new(active_entry.panel.as_any(), cx)
428                .contained()
429                .with_style(style)
430                .resizable(
431                    self.position.to_resize_handle_side(),
432                    active_entry.panel.size(cx),
433                    |dock: &mut Self, size, cx| dock.resize_active_panel(size, cx),
434                )
435                .into_any()
436        } else {
437            Empty::new().into_any()
438        }
439    }
440
441    fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
442        if cx.is_self_focused() {
443            if let Some(active_entry) = self.visible_entry() {
444                cx.focus(active_entry.panel.as_any());
445            } else {
446                cx.focus_parent();
447            }
448        }
449    }
450}
451
452impl PanelButtons {
453    pub fn new(
454        dock: ViewHandle<Dock>,
455        workspace: WeakViewHandle<Workspace>,
456        cx: &mut ViewContext<Self>,
457    ) -> Self {
458        cx.observe(&dock, |_, _, cx| cx.notify()).detach();
459        Self { dock, workspace }
460    }
461}
462
463impl Entity for PanelButtons {
464    type Event = ();
465}
466
467impl View for PanelButtons {
468    fn ui_name() -> &'static str {
469        "PanelButtons"
470    }
471
472    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
473        let theme = &settings::get::<ThemeSettings>(cx).theme;
474        let tooltip_style = theme.tooltip.clone();
475        let theme = &theme.workspace.status_bar.panel_buttons;
476        let button_style = theme.button.clone();
477        let dock = self.dock.read(cx);
478        let active_ix = dock.active_panel_index;
479        let is_open = dock.is_open;
480        let dock_position = dock.position;
481        let group_style = match dock_position {
482            DockPosition::Left => theme.group_left,
483            DockPosition::Bottom => theme.group_bottom,
484            DockPosition::Right => theme.group_right,
485        };
486        let menu_corner = match dock_position {
487            DockPosition::Left => AnchorCorner::BottomLeft,
488            DockPosition::Bottom | DockPosition::Right => AnchorCorner::BottomRight,
489        };
490
491        let panels = dock
492            .panel_entries
493            .iter()
494            .map(|item| (item.panel.clone(), item.context_menu.clone()))
495            .collect::<Vec<_>>();
496        Flex::row()
497            .with_children(panels.into_iter().enumerate().map(
498                |(panel_ix, (view, context_menu))| {
499                    let is_active = is_open && panel_ix == active_ix;
500                    let (tooltip, tooltip_action) = if is_active {
501                        (
502                            format!("Close {} dock", dock_position.to_label()),
503                            Some(match dock_position {
504                                DockPosition::Left => crate::ToggleLeftDock.boxed_clone(),
505                                DockPosition::Bottom => crate::ToggleBottomDock.boxed_clone(),
506                                DockPosition::Right => crate::ToggleRightDock.boxed_clone(),
507                            }),
508                        )
509                    } else {
510                        view.icon_tooltip(cx)
511                    };
512                    Stack::new()
513                        .with_child(
514                            MouseEventHandler::<Self, _>::new(panel_ix, cx, |state, cx| {
515                                let style = button_style.in_state(is_active);
516
517                                let style = style.style_for(state);
518                                Flex::row()
519                                    .with_child(
520                                        Svg::new(view.icon_path(cx))
521                                            .with_color(style.icon_color)
522                                            .constrained()
523                                            .with_width(style.icon_size)
524                                            .aligned(),
525                                    )
526                                    .with_children(if let Some(label) = view.icon_label(cx) {
527                                        Some(
528                                            Label::new(label, style.label.text.clone())
529                                                .contained()
530                                                .with_style(style.label.container)
531                                                .aligned(),
532                                        )
533                                    } else {
534                                        None
535                                    })
536                                    .constrained()
537                                    .with_height(style.icon_size)
538                                    .contained()
539                                    .with_style(style.container)
540                            })
541                            .with_cursor_style(CursorStyle::PointingHand)
542                            .on_click(MouseButton::Left, {
543                                let tooltip_action =
544                                    tooltip_action.as_ref().map(|action| action.boxed_clone());
545                                move |_, this, cx| {
546                                    if let Some(tooltip_action) = &tooltip_action {
547                                        let window_id = cx.window_id();
548                                        let view_id = this.workspace.id();
549                                        let tooltip_action = tooltip_action.boxed_clone();
550                                        cx.spawn(|_, mut cx| async move {
551                                            cx.dispatch_action(
552                                                window_id,
553                                                view_id,
554                                                &*tooltip_action,
555                                            )
556                                            .ok();
557                                        })
558                                        .detach();
559                                    }
560                                }
561                            })
562                            .on_click(MouseButton::Right, {
563                                let view = view.clone();
564                                let menu = context_menu.clone();
565                                move |_, _, cx| {
566                                    const POSITIONS: [DockPosition; 3] = [
567                                        DockPosition::Left,
568                                        DockPosition::Right,
569                                        DockPosition::Bottom,
570                                    ];
571
572                                    menu.update(cx, |menu, cx| {
573                                        let items = POSITIONS
574                                            .into_iter()
575                                            .filter(|position| {
576                                                *position != dock_position
577                                                    && view.position_is_valid(*position, cx)
578                                            })
579                                            .map(|position| {
580                                                let view = view.clone();
581                                                ContextMenuItem::handler(
582                                                    format!("Dock {}", position.to_label()),
583                                                    move |cx| view.set_position(position, cx),
584                                                )
585                                            })
586                                            .collect();
587                                        menu.show(Default::default(), menu_corner, items, cx);
588                                    })
589                                }
590                            })
591                            .with_tooltip::<Self>(
592                                panel_ix,
593                                tooltip,
594                                tooltip_action,
595                                tooltip_style.clone(),
596                                cx,
597                            ),
598                        )
599                        .with_child(ChildView::new(&context_menu, cx))
600                },
601            ))
602            .contained()
603            .with_style(group_style)
604            .into_any()
605    }
606}
607
608impl StatusItemView for PanelButtons {
609    fn set_active_pane_item(
610        &mut self,
611        _: Option<&dyn crate::ItemHandle>,
612        _: &mut ViewContext<Self>,
613    ) {
614    }
615}
616
617#[cfg(any(test, feature = "test-support"))]
618pub mod test {
619    use super::*;
620    use gpui::{ViewContext, WindowContext};
621
622    #[derive(Debug)]
623    pub enum TestPanelEvent {
624        PositionChanged,
625        Activated,
626        Closed,
627        ZoomIn,
628        ZoomOut,
629        Focus,
630    }
631
632    pub struct TestPanel {
633        pub position: DockPosition,
634        pub zoomed: bool,
635        pub active: bool,
636        pub has_focus: bool,
637        pub size: f32,
638    }
639
640    impl TestPanel {
641        pub fn new(position: DockPosition) -> Self {
642            Self {
643                position,
644                zoomed: false,
645                active: false,
646                has_focus: false,
647                size: 300.,
648            }
649        }
650    }
651
652    impl Entity for TestPanel {
653        type Event = TestPanelEvent;
654    }
655
656    impl View for TestPanel {
657        fn ui_name() -> &'static str {
658            "TestPanel"
659        }
660
661        fn render(&mut self, _: &mut ViewContext<'_, '_, Self>) -> AnyElement<Self> {
662            Empty::new().into_any()
663        }
664
665        fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
666            self.has_focus = true;
667            cx.emit(TestPanelEvent::Focus);
668        }
669
670        fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
671            self.has_focus = false;
672        }
673    }
674
675    impl Panel for TestPanel {
676        fn position(&self, _: &gpui::WindowContext) -> super::DockPosition {
677            self.position
678        }
679
680        fn position_is_valid(&self, _: super::DockPosition) -> bool {
681            true
682        }
683
684        fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
685            self.position = position;
686            cx.emit(TestPanelEvent::PositionChanged);
687        }
688
689        fn is_zoomed(&self, _: &WindowContext) -> bool {
690            self.zoomed
691        }
692
693        fn set_zoomed(&mut self, zoomed: bool, _cx: &mut ViewContext<Self>) {
694            self.zoomed = zoomed;
695        }
696
697        fn set_active(&mut self, active: bool, _cx: &mut ViewContext<Self>) {
698            self.active = active;
699        }
700
701        fn size(&self, _: &WindowContext) -> f32 {
702            self.size
703        }
704
705        fn set_size(&mut self, size: f32, _: &mut ViewContext<Self>) {
706            self.size = size;
707        }
708
709        fn icon_path(&self) -> &'static str {
710            "icons/test_panel.svg"
711        }
712
713        fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {
714            ("Test Panel".into(), None)
715        }
716
717        fn should_change_position_on_event(event: &Self::Event) -> bool {
718            matches!(event, TestPanelEvent::PositionChanged)
719        }
720
721        fn should_zoom_in_on_event(event: &Self::Event) -> bool {
722            matches!(event, TestPanelEvent::ZoomIn)
723        }
724
725        fn should_zoom_out_on_event(event: &Self::Event) -> bool {
726            matches!(event, TestPanelEvent::ZoomOut)
727        }
728
729        fn should_activate_on_event(event: &Self::Event) -> bool {
730            matches!(event, TestPanelEvent::Activated)
731        }
732
733        fn should_close_on_event(event: &Self::Event) -> bool {
734            matches!(event, TestPanelEvent::Closed)
735        }
736
737        fn has_focus(&self, _cx: &WindowContext) -> bool {
738            self.has_focus
739        }
740
741        fn is_focus_event(event: &Self::Event) -> bool {
742            matches!(event, TestPanelEvent::Focus)
743        }
744    }
745}