dock.rs

  1use collections::HashMap;
  2use gpui::{
  3    actions,
  4    elements::{ChildView, Container, Empty, Margin, MouseEventHandler, Side, Svg},
  5    impl_internal_actions, CursorStyle, Element, ElementBox, Entity, MouseButton,
  6    MutableAppContext, RenderContext, View, ViewContext, ViewHandle, WeakViewHandle,
  7};
  8use serde::Deserialize;
  9use settings::{DockAnchor, Settings};
 10use theme::Theme;
 11
 12use crate::{sidebar::SidebarSide, ItemHandle, Pane, StatusItemView, Workspace};
 13
 14#[derive(PartialEq, Clone, Deserialize)]
 15pub struct MoveDock(pub DockAnchor);
 16
 17#[derive(PartialEq, Clone)]
 18pub struct AddDefaultItemToDock;
 19
 20actions!(workspace, [ToggleDock, ActivateOrHideDock]);
 21impl_internal_actions!(workspace, [MoveDock, AddDefaultItemToDock]);
 22
 23pub fn init(cx: &mut MutableAppContext) {
 24    cx.add_action(Dock::toggle);
 25    cx.add_action(Dock::activate_or_hide_dock);
 26    cx.add_action(Dock::move_dock);
 27}
 28
 29#[derive(Copy, Clone, PartialEq, Eq, Debug)]
 30pub enum DockPosition {
 31    Shown(DockAnchor),
 32    Hidden(DockAnchor),
 33}
 34
 35impl Default for DockPosition {
 36    fn default() -> Self {
 37        DockPosition::Hidden(Default::default())
 38    }
 39}
 40
 41pub fn icon_for_dock_anchor(anchor: DockAnchor) -> &'static str {
 42    match anchor {
 43        DockAnchor::Right => "icons/dock_right_12.svg",
 44        DockAnchor::Bottom => "icons/dock_bottom_12.svg",
 45        DockAnchor::Expanded => "icons/dock_modal_12.svg",
 46    }
 47}
 48
 49impl DockPosition {
 50    fn is_visible(&self) -> bool {
 51        match self {
 52            DockPosition::Shown(_) => true,
 53            DockPosition::Hidden(_) => false,
 54        }
 55    }
 56
 57    fn anchor(&self) -> DockAnchor {
 58        match self {
 59            DockPosition::Shown(anchor) | DockPosition::Hidden(anchor) => *anchor,
 60        }
 61    }
 62
 63    fn toggle(self) -> Self {
 64        match self {
 65            DockPosition::Shown(anchor) => DockPosition::Hidden(anchor),
 66            DockPosition::Hidden(anchor) => DockPosition::Shown(anchor),
 67        }
 68    }
 69
 70    fn hide(self) -> Self {
 71        match self {
 72            DockPosition::Shown(anchor) => DockPosition::Hidden(anchor),
 73            DockPosition::Hidden(_) => self,
 74        }
 75    }
 76
 77    fn show(self) -> Self {
 78        match self {
 79            DockPosition::Hidden(anchor) => DockPosition::Shown(anchor),
 80            DockPosition::Shown(_) => self,
 81        }
 82    }
 83}
 84
 85pub type DefaultItemFactory =
 86    fn(&mut Workspace, &mut ViewContext<Workspace>) -> Box<dyn ItemHandle>;
 87
 88pub struct Dock {
 89    position: DockPosition,
 90    panel_sizes: HashMap<DockAnchor, f32>,
 91    pane: ViewHandle<Pane>,
 92    default_item_factory: DefaultItemFactory,
 93}
 94
 95impl Dock {
 96    pub fn new(cx: &mut ViewContext<Workspace>, default_item_factory: DefaultItemFactory) -> Self {
 97        let anchor = cx.global::<Settings>().default_dock_anchor;
 98        let pane = cx.add_view(|cx| Pane::new(Some(anchor), cx));
 99        pane.update(cx, |pane, cx| {
100            pane.set_active(false, cx);
101        });
102        let pane_id = pane.id();
103        cx.subscribe(&pane, move |workspace, _, event, cx| {
104            workspace.handle_pane_event(pane_id, event, cx);
105        })
106        .detach();
107
108        Self {
109            pane,
110            panel_sizes: Default::default(),
111            position: DockPosition::Hidden(anchor),
112            default_item_factory,
113        }
114    }
115
116    pub fn pane(&self) -> &ViewHandle<Pane> {
117        &self.pane
118    }
119
120    pub fn visible_pane(&self) -> Option<&ViewHandle<Pane>> {
121        self.position.is_visible().then(|| self.pane())
122    }
123
124    pub fn is_anchored_at(&self, anchor: DockAnchor) -> bool {
125        self.position.is_visible() && self.position.anchor() == anchor
126    }
127
128    fn set_dock_position(
129        workspace: &mut Workspace,
130        new_position: DockPosition,
131        cx: &mut ViewContext<Workspace>,
132    ) {
133        if workspace.dock.position == new_position {
134            return;
135        }
136
137        workspace.dock.position = new_position;
138        // Tell the pane about the new anchor position
139        workspace.dock.pane.update(cx, |pane, cx| {
140            pane.set_docked(Some(new_position.anchor()), cx)
141        });
142
143        if workspace.dock.position.is_visible() {
144            // Close the right sidebar if the dock is on the right side and the right sidebar is open
145            if workspace.dock.position.anchor() == DockAnchor::Right {
146                if workspace.right_sidebar().read(cx).is_open() {
147                    workspace.toggle_sidebar(SidebarSide::Right, cx);
148                }
149            }
150
151            // Ensure that the pane has at least one item or construct a default item to put in it
152            let pane = workspace.dock.pane.clone();
153            if pane.read(cx).items().next().is_none() {
154                let item_to_add = (workspace.dock.default_item_factory)(workspace, cx);
155                // Adding the item focuses the pane by default
156                Pane::add_item(workspace, &pane, item_to_add, true, true, None, cx);
157            } else {
158                cx.focus(pane);
159            }
160        } else if let Some(last_active_center_pane) = workspace.last_active_center_pane.clone() {
161            cx.focus(last_active_center_pane);
162        }
163        cx.emit(crate::Event::DockAnchorChanged);
164        cx.notify();
165    }
166
167    pub fn hide(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
168        Self::set_dock_position(workspace, workspace.dock.position.hide(), cx);
169    }
170
171    pub fn show(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
172        Self::set_dock_position(workspace, workspace.dock.position.show(), cx);
173    }
174
175    pub fn hide_on_sidebar_shown(
176        workspace: &mut Workspace,
177        sidebar_side: SidebarSide,
178        cx: &mut ViewContext<Workspace>,
179    ) {
180        if (sidebar_side == SidebarSide::Right && workspace.dock.is_anchored_at(DockAnchor::Right))
181            || workspace.dock.is_anchored_at(DockAnchor::Expanded)
182        {
183            Self::hide(workspace, cx);
184        }
185    }
186
187    fn toggle(workspace: &mut Workspace, _: &ToggleDock, cx: &mut ViewContext<Workspace>) {
188        Self::set_dock_position(workspace, workspace.dock.position.toggle(), cx);
189    }
190
191    fn activate_or_hide_dock(
192        workspace: &mut Workspace,
193        _: &ActivateOrHideDock,
194        cx: &mut ViewContext<Workspace>,
195    ) {
196        let dock_pane = workspace.dock_pane().clone();
197        if dock_pane.read(cx).is_active() {
198            Self::hide(workspace, cx);
199        } else {
200            Self::show(workspace, cx);
201            cx.focus(dock_pane);
202        }
203    }
204
205    fn move_dock(
206        workspace: &mut Workspace,
207        &MoveDock(new_anchor): &MoveDock,
208        cx: &mut ViewContext<Workspace>,
209    ) {
210        Self::set_dock_position(workspace, DockPosition::Shown(new_anchor), cx);
211    }
212
213    pub fn render(
214        &self,
215        theme: &Theme,
216        anchor: DockAnchor,
217        cx: &mut RenderContext<Workspace>,
218    ) -> Option<ElementBox> {
219        let style = &theme.workspace.dock;
220
221        self.position
222            .is_visible()
223            .then(|| self.position.anchor())
224            .filter(|current_anchor| *current_anchor == anchor)
225            .map(|anchor| match anchor {
226                DockAnchor::Bottom | DockAnchor::Right => {
227                    let mut panel_style = style.panel.clone();
228                    let (resize_side, initial_size) = if anchor == DockAnchor::Bottom {
229                        panel_style.margin = Margin {
230                            top: panel_style.margin.top,
231                            ..Default::default()
232                        };
233
234                        (Side::Top, style.initial_size_bottom)
235                    } else {
236                        panel_style.margin = Margin {
237                            left: panel_style.margin.left,
238                            ..Default::default()
239                        };
240                        (Side::Left, style.initial_size_right)
241                    };
242
243                    enum DockResizeHandle {}
244
245                    let resizable = Container::new(ChildView::new(self.pane.clone()).boxed())
246                        .with_style(panel_style)
247                        .with_resize_handle::<DockResizeHandle, _>(
248                            resize_side as usize,
249                            resize_side,
250                            4.,
251                            self.panel_sizes
252                                .get(&anchor)
253                                .copied()
254                                .unwrap_or(initial_size),
255                            cx,
256                        );
257
258                    let size = resizable.current_size();
259                    let workspace = cx.handle();
260                    cx.defer(move |cx| {
261                        if let Some(workspace) = workspace.upgrade(cx) {
262                            workspace.update(cx, |workspace, _| {
263                                workspace.dock.panel_sizes.insert(anchor, size);
264                            })
265                        }
266                    });
267
268                    resizable.flex(style.flex, false).boxed()
269                }
270                DockAnchor::Expanded => {
271                    enum ExpandedDockWash {}
272                    enum ExpandedDockPane {}
273                    Container::new(
274                        MouseEventHandler::<ExpandedDockWash>::new(0, cx, |_state, cx| {
275                            MouseEventHandler::<ExpandedDockPane>::new(0, cx, |_state, _cx| {
276                                ChildView::new(self.pane.clone()).boxed()
277                            })
278                            .capture_all()
279                            .contained()
280                            .with_style(style.maximized)
281                            .boxed()
282                        })
283                        .capture_all()
284                        .on_down(MouseButton::Left, |_, cx| {
285                            cx.dispatch_action(ToggleDock);
286                        })
287                        .with_cursor_style(CursorStyle::Arrow)
288                        .boxed(),
289                    )
290                    .with_background_color(style.wash_color)
291                    .boxed()
292                }
293            })
294    }
295}
296
297pub struct ToggleDockButton {
298    workspace: WeakViewHandle<Workspace>,
299}
300
301impl ToggleDockButton {
302    pub fn new(workspace: ViewHandle<Workspace>, cx: &mut ViewContext<Self>) -> Self {
303        // When dock moves, redraw so that the icon and toggle status matches.
304        cx.subscribe(&workspace, |_, _, _, cx| cx.notify()).detach();
305
306        Self {
307            workspace: workspace.downgrade(),
308        }
309    }
310}
311
312impl Entity for ToggleDockButton {
313    type Event = ();
314}
315
316impl View for ToggleDockButton {
317    fn ui_name() -> &'static str {
318        "Dock Toggle"
319    }
320
321    fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
322        let workspace = self.workspace.upgrade(cx);
323
324        if workspace.is_none() {
325            return Empty::new().boxed();
326        }
327
328        let dock_position = workspace.unwrap().read(cx).dock.position;
329
330        let theme = cx.global::<Settings>().theme.clone();
331        MouseEventHandler::<Self>::new(0, cx, {
332            let theme = theme.clone();
333            move |state, _| {
334                let style = theme
335                    .workspace
336                    .status_bar
337                    .sidebar_buttons
338                    .item
339                    .style_for(state, dock_position.is_visible());
340
341                Svg::new(icon_for_dock_anchor(dock_position.anchor()))
342                    .with_color(style.icon_color)
343                    .constrained()
344                    .with_width(style.icon_size)
345                    .with_height(style.icon_size)
346                    .contained()
347                    .with_style(style.container)
348                    .boxed()
349            }
350        })
351        .with_cursor_style(CursorStyle::PointingHand)
352        .on_click(MouseButton::Left, |_, cx| {
353            cx.dispatch_action(ToggleDock);
354        })
355        .with_tooltip::<Self, _>(
356            0,
357            "Toggle Dock".to_string(),
358            Some(Box::new(ToggleDock)),
359            theme.tooltip.clone(),
360            cx,
361        )
362        .boxed()
363    }
364}
365
366impl StatusItemView for ToggleDockButton {
367    fn set_active_pane_item(
368        &mut self,
369        _active_pane_item: Option<&dyn crate::ItemHandle>,
370        _cx: &mut ViewContext<Self>,
371    ) {
372        //Not applicable
373    }
374}
375
376#[cfg(test)]
377mod tests {
378    use std::ops::{Deref, DerefMut};
379
380    use gpui::{AppContext, TestAppContext, UpdateView, ViewContext};
381    use project::{FakeFs, Project};
382    use settings::Settings;
383
384    use super::*;
385    use crate::{sidebar::Sidebar, tests::TestItem, ItemHandle, Workspace};
386
387    pub fn default_item_factory(
388        _workspace: &mut Workspace,
389        cx: &mut ViewContext<Workspace>,
390    ) -> Box<dyn ItemHandle> {
391        Box::new(cx.add_view(|_| TestItem::new()))
392    }
393
394    #[gpui::test]
395    async fn test_dock_hides_when_pane_empty(cx: &mut TestAppContext) {
396        let mut cx = DockTestContext::new(cx).await;
397
398        // Closing the last item in the dock hides the dock
399        cx.move_dock(DockAnchor::Right);
400        let old_items = cx.dock_items();
401        assert!(!old_items.is_empty());
402        cx.close_dock_items().await;
403        cx.assert_dock_position(DockPosition::Hidden(DockAnchor::Right));
404
405        // Reopening the dock adds a new item
406        cx.move_dock(DockAnchor::Right);
407        let new_items = cx.dock_items();
408        assert!(!new_items.is_empty());
409        assert!(new_items
410            .into_iter()
411            .all(|new_item| !old_items.contains(&new_item)));
412    }
413
414    #[gpui::test]
415    async fn test_dock_panel_collisions(cx: &mut TestAppContext) {
416        let mut cx = DockTestContext::new(cx).await;
417
418        // Dock closes when expanded for either panel
419        cx.move_dock(DockAnchor::Expanded);
420        cx.open_sidebar(SidebarSide::Left);
421        cx.assert_dock_position(DockPosition::Hidden(DockAnchor::Expanded));
422        cx.close_sidebar(SidebarSide::Left);
423        cx.move_dock(DockAnchor::Expanded);
424        cx.open_sidebar(SidebarSide::Right);
425        cx.assert_dock_position(DockPosition::Hidden(DockAnchor::Expanded));
426
427        // Dock closes in the right position if the right sidebar is opened
428        cx.move_dock(DockAnchor::Right);
429        cx.open_sidebar(SidebarSide::Left);
430        cx.assert_dock_position(DockPosition::Shown(DockAnchor::Right));
431        cx.open_sidebar(SidebarSide::Right);
432        cx.assert_dock_position(DockPosition::Hidden(DockAnchor::Right));
433        cx.close_sidebar(SidebarSide::Right);
434
435        // Dock in bottom position ignores sidebars
436        cx.move_dock(DockAnchor::Bottom);
437        cx.open_sidebar(SidebarSide::Left);
438        cx.open_sidebar(SidebarSide::Right);
439        cx.assert_dock_position(DockPosition::Shown(DockAnchor::Bottom));
440
441        // Opening the dock in the right position closes the right sidebar
442        cx.move_dock(DockAnchor::Right);
443        cx.assert_sidebar_closed(SidebarSide::Right);
444    }
445
446    #[gpui::test]
447    async fn test_focusing_panes_shows_and_hides_dock(cx: &mut TestAppContext) {
448        let mut cx = DockTestContext::new(cx).await;
449
450        // Focusing an item not in the dock when expanded hides the dock
451        let center_item = cx.add_item_to_center_pane();
452        cx.move_dock(DockAnchor::Expanded);
453        let dock_item = cx
454            .dock_items()
455            .get(0)
456            .cloned()
457            .expect("Dock should have an item at this point");
458        center_item.update(&mut cx, |_, cx| cx.focus_self());
459        cx.assert_dock_position(DockPosition::Hidden(DockAnchor::Expanded));
460
461        // Focusing an item not in the dock when not expanded, leaves the dock open but inactive
462        cx.move_dock(DockAnchor::Right);
463        center_item.update(&mut cx, |_, cx| cx.focus_self());
464        cx.assert_dock_position(DockPosition::Shown(DockAnchor::Right));
465        cx.assert_dock_pane_inactive();
466        cx.assert_workspace_pane_active();
467
468        // Focusing an item in the dock activates it's pane
469        dock_item.update(&mut cx, |_, cx| cx.focus_self());
470        cx.assert_dock_position(DockPosition::Shown(DockAnchor::Right));
471        cx.assert_dock_pane_active();
472        cx.assert_workspace_pane_inactive();
473    }
474
475    #[gpui::test]
476    async fn test_toggle_dock_focus(cx: &mut TestAppContext) {
477        let cx = DockTestContext::new(cx).await;
478
479        cx.move_dock(DockAnchor::Right);
480        cx.assert_dock_pane_active();
481        cx.toggle_dock();
482        cx.move_dock(DockAnchor::Right);
483        cx.assert_dock_pane_active();
484    }
485
486    struct DockTestContext<'a> {
487        pub cx: &'a mut TestAppContext,
488        pub window_id: usize,
489        pub workspace: ViewHandle<Workspace>,
490    }
491
492    impl<'a> DockTestContext<'a> {
493        pub async fn new(cx: &'a mut TestAppContext) -> DockTestContext<'a> {
494            Settings::test_async(cx);
495            let fs = FakeFs::new(cx.background());
496
497            cx.update(|cx| init(cx));
498            let project = Project::test(fs, [], cx).await;
499            let (window_id, workspace) =
500                cx.add_window(|cx| Workspace::new(project, default_item_factory, cx));
501
502            workspace.update(cx, |workspace, cx| {
503                let left_panel = cx.add_view(|_| TestItem::new());
504                workspace.left_sidebar().update(cx, |sidebar, cx| {
505                    sidebar.add_item(
506                        "icons/folder_tree_16.svg",
507                        "Left Test Panel".to_string(),
508                        left_panel.clone(),
509                        cx,
510                    );
511                });
512
513                let right_panel = cx.add_view(|_| TestItem::new());
514                workspace.right_sidebar().update(cx, |sidebar, cx| {
515                    sidebar.add_item(
516                        "icons/folder_tree_16.svg",
517                        "Right Test Panel".to_string(),
518                        right_panel.clone(),
519                        cx,
520                    );
521                });
522            });
523
524            Self {
525                cx,
526                window_id,
527                workspace,
528            }
529        }
530
531        pub fn workspace<F, T>(&self, read: F) -> T
532        where
533            F: FnOnce(&Workspace, &AppContext) -> T,
534        {
535            self.workspace.read_with(self.cx, read)
536        }
537
538        pub fn update_workspace<F, T>(&mut self, update: F) -> T
539        where
540            F: FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
541        {
542            self.workspace.update(self.cx, update)
543        }
544
545        pub fn sidebar<F, T>(&self, sidebar_side: SidebarSide, read: F) -> T
546        where
547            F: FnOnce(&Sidebar, &AppContext) -> T,
548        {
549            self.workspace(|workspace, cx| {
550                let sidebar = match sidebar_side {
551                    SidebarSide::Left => workspace.left_sidebar(),
552                    SidebarSide::Right => workspace.right_sidebar(),
553                }
554                .read(cx);
555
556                read(sidebar, cx)
557            })
558        }
559
560        pub fn center_pane_handle(&self) -> ViewHandle<Pane> {
561            self.workspace(|workspace, _| {
562                workspace
563                    .last_active_center_pane
564                    .clone()
565                    .unwrap_or_else(|| workspace.center.panes()[0].clone())
566            })
567        }
568
569        pub fn add_item_to_center_pane(&mut self) -> ViewHandle<TestItem> {
570            self.update_workspace(|workspace, cx| {
571                let item = cx.add_view(|_| TestItem::new());
572                let pane = workspace
573                    .last_active_center_pane
574                    .clone()
575                    .unwrap_or_else(|| workspace.center.panes()[0].clone());
576                Pane::add_item(
577                    workspace,
578                    &pane,
579                    Box::new(item.clone()),
580                    true,
581                    true,
582                    None,
583                    cx,
584                );
585                item
586            })
587        }
588
589        pub fn dock_pane<F, T>(&self, read: F) -> T
590        where
591            F: FnOnce(&Pane, &AppContext) -> T,
592        {
593            self.workspace(|workspace, cx| {
594                let dock_pane = workspace.dock_pane().read(cx);
595                read(dock_pane, cx)
596            })
597        }
598
599        pub fn move_dock(&self, anchor: DockAnchor) {
600            self.cx.dispatch_action(self.window_id, MoveDock(anchor));
601        }
602
603        pub fn toggle_dock(&self) {
604            self.cx.dispatch_action(self.window_id, ToggleDock);
605        }
606
607        pub fn open_sidebar(&mut self, sidebar_side: SidebarSide) {
608            if !self.sidebar(sidebar_side, |sidebar, _| sidebar.is_open()) {
609                self.update_workspace(|workspace, cx| workspace.toggle_sidebar(sidebar_side, cx));
610            }
611        }
612
613        pub fn close_sidebar(&mut self, sidebar_side: SidebarSide) {
614            if self.sidebar(sidebar_side, |sidebar, _| sidebar.is_open()) {
615                self.update_workspace(|workspace, cx| workspace.toggle_sidebar(sidebar_side, cx));
616            }
617        }
618
619        pub fn dock_items(&self) -> Vec<ViewHandle<TestItem>> {
620            self.dock_pane(|pane, cx| {
621                pane.items()
622                    .map(|item| {
623                        item.act_as::<TestItem>(cx)
624                            .expect("Dock Test Context uses TestItems in the dock")
625                    })
626                    .collect()
627            })
628        }
629
630        pub async fn close_dock_items(&mut self) {
631            self.update_workspace(|workspace, cx| {
632                Pane::close_items(workspace, workspace.dock_pane().clone(), cx, |_| true)
633            })
634            .await
635            .expect("Could not close dock items")
636        }
637
638        pub fn assert_dock_position(&self, expected_position: DockPosition) {
639            self.workspace(|workspace, _| assert_eq!(workspace.dock.position, expected_position));
640        }
641
642        pub fn assert_sidebar_closed(&self, sidebar_side: SidebarSide) {
643            assert!(!self.sidebar(sidebar_side, |sidebar, _| sidebar.is_open()));
644        }
645
646        pub fn assert_workspace_pane_active(&self) {
647            assert!(self
648                .center_pane_handle()
649                .read_with(self.cx, |pane, _| pane.is_active()));
650        }
651
652        pub fn assert_workspace_pane_inactive(&self) {
653            assert!(!self
654                .center_pane_handle()
655                .read_with(self.cx, |pane, _| pane.is_active()));
656        }
657
658        pub fn assert_dock_pane_active(&self) {
659            assert!(self.dock_pane(|pane, _| pane.is_active()))
660        }
661
662        pub fn assert_dock_pane_inactive(&self) {
663            assert!(!self.dock_pane(|pane, _| pane.is_active()))
664        }
665    }
666
667    impl<'a> Deref for DockTestContext<'a> {
668        type Target = gpui::TestAppContext;
669
670        fn deref(&self) -> &Self::Target {
671            self.cx
672        }
673    }
674
675    impl<'a> DerefMut for DockTestContext<'a> {
676        fn deref_mut(&mut self) -> &mut Self::Target {
677            &mut self.cx
678        }
679    }
680
681    impl<'a> UpdateView for DockTestContext<'a> {
682        fn update_view<T, S>(
683            &mut self,
684            handle: &ViewHandle<T>,
685            update: &mut dyn FnMut(&mut T, &mut ViewContext<T>) -> S,
686        ) -> S
687        where
688            T: View,
689        {
690            handle.update(self.cx, update)
691        }
692    }
693}