dock.rs

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