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    fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
428        if cx.is_self_focused() {
429            if let Some(active_entry) = self.active_entry() {
430                cx.focus(active_entry.panel.as_any());
431            } else {
432                cx.focus_parent();
433            }
434        }
435    }
436}
437
438impl PanelButtons {
439    pub fn new(
440        dock: ViewHandle<Dock>,
441        workspace: WeakViewHandle<Workspace>,
442        cx: &mut ViewContext<Self>,
443    ) -> Self {
444        cx.observe(&dock, |_, _, cx| cx.notify()).detach();
445        Self { dock, workspace }
446    }
447}
448
449impl Entity for PanelButtons {
450    type Event = ();
451}
452
453impl View for PanelButtons {
454    fn ui_name() -> &'static str {
455        "PanelButtons"
456    }
457
458    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
459        let theme = &settings::get::<ThemeSettings>(cx).theme;
460        let tooltip_style = theme.tooltip.clone();
461        let theme = &theme.workspace.status_bar.panel_buttons;
462        let button_style = theme.button.clone();
463        let dock = self.dock.read(cx);
464        let active_ix = dock.active_panel_index;
465        let is_open = dock.is_open;
466        let dock_position = dock.position;
467        let group_style = match dock_position {
468            DockPosition::Left => theme.group_left,
469            DockPosition::Bottom => theme.group_bottom,
470            DockPosition::Right => theme.group_right,
471        };
472        let menu_corner = match dock_position {
473            DockPosition::Left => AnchorCorner::BottomLeft,
474            DockPosition::Bottom | DockPosition::Right => AnchorCorner::BottomRight,
475        };
476
477        let panels = dock
478            .panel_entries
479            .iter()
480            .map(|item| (item.panel.clone(), item.context_menu.clone()))
481            .collect::<Vec<_>>();
482        Flex::row()
483            .with_children(
484                panels
485                    .into_iter()
486                    .enumerate()
487                    .map(|(ix, (view, context_menu))| {
488                        let action = TogglePanel {
489                            dock_position,
490                            panel_index: ix,
491                        };
492
493                        Stack::new()
494                            .with_child(
495                                MouseEventHandler::<Self, _>::new(ix, cx, |state, cx| {
496                                    let is_active = is_open && ix == active_ix;
497                                    let style = button_style.style_for(state, is_active);
498                                    Flex::row()
499                                        .with_child(
500                                            Svg::new(view.icon_path(cx))
501                                                .with_color(style.icon_color)
502                                                .constrained()
503                                                .with_width(style.icon_size)
504                                                .aligned(),
505                                        )
506                                        .with_children(if let Some(label) = view.icon_label(cx) {
507                                            Some(
508                                                Label::new(label, style.label.text.clone())
509                                                    .contained()
510                                                    .with_style(style.label.container)
511                                                    .aligned(),
512                                            )
513                                        } else {
514                                            None
515                                        })
516                                        .constrained()
517                                        .with_height(style.icon_size)
518                                        .contained()
519                                        .with_style(style.container)
520                                })
521                                .with_cursor_style(CursorStyle::PointingHand)
522                                .on_click(MouseButton::Left, {
523                                    let action = action.clone();
524                                    move |_, this, cx| {
525                                        if let Some(workspace) = this.workspace.upgrade(cx) {
526                                            let action = action.clone();
527                                            cx.window_context().defer(move |cx| {
528                                                workspace.update(cx, |workspace, cx| {
529                                                    workspace.toggle_panel(&action, cx)
530                                                });
531                                            });
532                                        }
533                                    }
534                                })
535                                .on_click(MouseButton::Right, {
536                                    let view = view.clone();
537                                    let menu = context_menu.clone();
538                                    move |_, _, cx| {
539                                        const POSITIONS: [DockPosition; 3] = [
540                                            DockPosition::Left,
541                                            DockPosition::Right,
542                                            DockPosition::Bottom,
543                                        ];
544
545                                        menu.update(cx, |menu, cx| {
546                                            let items = POSITIONS
547                                                .into_iter()
548                                                .filter(|position| {
549                                                    *position != dock_position
550                                                        && view.position_is_valid(*position, cx)
551                                                })
552                                                .map(|position| {
553                                                    let view = view.clone();
554                                                    ContextMenuItem::handler(
555                                                        format!("Dock {}", position.to_label()),
556                                                        move |cx| view.set_position(position, cx),
557                                                    )
558                                                })
559                                                .collect();
560                                            menu.show(Default::default(), menu_corner, items, cx);
561                                        })
562                                    }
563                                })
564                                .with_tooltip::<Self>(
565                                    ix,
566                                    view.icon_tooltip(cx),
567                                    Some(Box::new(action)),
568                                    tooltip_style.clone(),
569                                    cx,
570                                ),
571                            )
572                            .with_child(ChildView::new(&context_menu, cx))
573                    }),
574            )
575            .contained()
576            .with_style(group_style)
577            .into_any()
578    }
579}
580
581impl StatusItemView for PanelButtons {
582    fn set_active_pane_item(
583        &mut self,
584        _: Option<&dyn crate::ItemHandle>,
585        _: &mut ViewContext<Self>,
586    ) {
587    }
588}
589
590#[cfg(test)]
591pub(crate) mod test {
592    use super::*;
593    use gpui::{ViewContext, WindowContext};
594
595    pub enum TestPanelEvent {
596        PositionChanged,
597        Activated,
598        Closed,
599        ZoomIn,
600        ZoomOut,
601        Focus,
602    }
603
604    pub struct TestPanel {
605        pub position: DockPosition,
606        pub zoomed: bool,
607        pub active: bool,
608        pub has_focus: bool,
609        pub size: f32,
610    }
611
612    impl TestPanel {
613        pub fn new(position: DockPosition) -> Self {
614            Self {
615                position,
616                zoomed: false,
617                active: false,
618                has_focus: false,
619                size: 300.,
620            }
621        }
622    }
623
624    impl Entity for TestPanel {
625        type Event = TestPanelEvent;
626    }
627
628    impl View for TestPanel {
629        fn ui_name() -> &'static str {
630            "TestPanel"
631        }
632
633        fn render(&mut self, _: &mut ViewContext<'_, '_, Self>) -> AnyElement<Self> {
634            Empty::new().into_any()
635        }
636
637        fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
638            self.has_focus = true;
639            cx.emit(TestPanelEvent::Focus);
640        }
641
642        fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
643            self.has_focus = false;
644        }
645    }
646
647    impl Panel for TestPanel {
648        fn position(&self, _: &gpui::WindowContext) -> super::DockPosition {
649            self.position
650        }
651
652        fn position_is_valid(&self, _: super::DockPosition) -> bool {
653            true
654        }
655
656        fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
657            self.position = position;
658            cx.emit(TestPanelEvent::PositionChanged);
659        }
660
661        fn is_zoomed(&self, _: &WindowContext) -> bool {
662            self.zoomed
663        }
664
665        fn set_zoomed(&mut self, zoomed: bool, _cx: &mut ViewContext<Self>) {
666            self.zoomed = zoomed;
667        }
668
669        fn set_active(&mut self, active: bool, _cx: &mut ViewContext<Self>) {
670            self.active = active;
671        }
672
673        fn size(&self, _: &WindowContext) -> f32 {
674            self.size
675        }
676
677        fn set_size(&mut self, size: f32, _: &mut ViewContext<Self>) {
678            self.size = size;
679        }
680
681        fn icon_path(&self) -> &'static str {
682            "icons/test_panel.svg"
683        }
684
685        fn icon_tooltip(&self) -> String {
686            "Test Panel".into()
687        }
688
689        fn should_change_position_on_event(event: &Self::Event) -> bool {
690            matches!(event, TestPanelEvent::PositionChanged)
691        }
692
693        fn should_zoom_in_on_event(event: &Self::Event) -> bool {
694            matches!(event, TestPanelEvent::ZoomIn)
695        }
696
697        fn should_zoom_out_on_event(event: &Self::Event) -> bool {
698            matches!(event, TestPanelEvent::ZoomOut)
699        }
700
701        fn should_activate_on_event(event: &Self::Event) -> bool {
702            matches!(event, TestPanelEvent::Activated)
703        }
704
705        fn should_close_on_event(event: &Self::Event) -> bool {
706            matches!(event, TestPanelEvent::Closed)
707        }
708
709        fn has_focus(&self, _cx: &WindowContext) -> bool {
710            self.has_focus
711        }
712
713        fn is_focus_event(event: &Self::Event) -> bool {
714            matches!(event, TestPanelEvent::Focus)
715        }
716    }
717}