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)]
 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                Pane::add_item(workspace, &pane, item_to_add, true, true, None, cx);
156            }
157        } else if let Some(last_active_center_pane) = workspace.last_active_center_pane.clone() {
158            cx.focus(last_active_center_pane);
159        }
160        cx.emit(crate::Event::DockAnchorChanged);
161        cx.notify();
162    }
163
164    pub fn hide(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
165        Self::set_dock_position(workspace, workspace.dock.position.hide(), cx);
166    }
167
168    pub fn show(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
169        Self::set_dock_position(workspace, workspace.dock.position.show(), cx);
170    }
171
172    pub fn hide_on_sidebar_shown(
173        workspace: &mut Workspace,
174        sidebar_side: SidebarSide,
175        cx: &mut ViewContext<Workspace>,
176    ) {
177        if (sidebar_side == SidebarSide::Right && workspace.dock.is_anchored_at(DockAnchor::Right))
178            || workspace.dock.is_anchored_at(DockAnchor::Expanded)
179        {
180            Self::hide(workspace, cx);
181        }
182    }
183
184    fn toggle(workspace: &mut Workspace, _: &ToggleDock, cx: &mut ViewContext<Workspace>) {
185        Self::set_dock_position(workspace, workspace.dock.position.toggle(), cx);
186    }
187
188    fn activate_or_hide_dock(
189        workspace: &mut Workspace,
190        _: &ActivateOrHideDock,
191        cx: &mut ViewContext<Workspace>,
192    ) {
193        let dock_pane = workspace.dock_pane().clone();
194        if dock_pane.read(cx).is_active() {
195            Self::hide(workspace, cx);
196        } else {
197            Self::show(workspace, cx);
198            cx.focus(dock_pane);
199        }
200    }
201
202    fn move_dock(
203        workspace: &mut Workspace,
204        &MoveDock(new_anchor): &MoveDock,
205        cx: &mut ViewContext<Workspace>,
206    ) {
207        Self::set_dock_position(workspace, DockPosition::Shown(new_anchor), cx);
208    }
209
210    pub fn render(
211        &self,
212        theme: &Theme,
213        anchor: DockAnchor,
214        cx: &mut RenderContext<Workspace>,
215    ) -> Option<ElementBox> {
216        let style = &theme.workspace.dock;
217
218        self.position
219            .is_visible()
220            .then(|| self.position.anchor())
221            .filter(|current_anchor| *current_anchor == anchor)
222            .map(|anchor| match anchor {
223                DockAnchor::Bottom | DockAnchor::Right => {
224                    let mut panel_style = style.panel.clone();
225                    let (resize_side, initial_size) = if anchor == DockAnchor::Bottom {
226                        panel_style.margin = Margin {
227                            top: panel_style.margin.top,
228                            ..Default::default()
229                        };
230
231                        (Side::Top, style.initial_size_bottom)
232                    } else {
233                        panel_style.margin = Margin {
234                            left: panel_style.margin.left,
235                            ..Default::default()
236                        };
237                        (Side::Left, style.initial_size_right)
238                    };
239
240                    enum DockResizeHandle {}
241
242                    let resizable = Container::new(ChildView::new(self.pane.clone()).boxed())
243                        .with_style(panel_style)
244                        .with_resize_handle::<DockResizeHandle, _>(
245                            resize_side as usize,
246                            resize_side,
247                            4.,
248                            self.panel_sizes
249                                .get(&anchor)
250                                .copied()
251                                .unwrap_or(initial_size),
252                            cx,
253                        );
254
255                    let size = resizable.current_size();
256                    let workspace = cx.handle();
257                    cx.defer(move |cx| {
258                        if let Some(workspace) = workspace.upgrade(cx) {
259                            workspace.update(cx, |workspace, _| {
260                                workspace.dock.panel_sizes.insert(anchor, size);
261                            })
262                        }
263                    });
264
265                    resizable.flex(style.flex, false).boxed()
266                }
267                DockAnchor::Expanded => {
268                    enum ExpandedDockWash {}
269                    enum ExpandedDockPane {}
270                    Container::new(
271                        MouseEventHandler::<ExpandedDockWash>::new(0, cx, |_state, cx| {
272                            MouseEventHandler::<ExpandedDockPane>::new(0, cx, |_state, _cx| {
273                                ChildView::new(self.pane.clone()).boxed()
274                            })
275                            .capture_all()
276                            .contained()
277                            .with_style(style.maximized)
278                            .boxed()
279                        })
280                        .capture_all()
281                        .on_down(MouseButton::Left, |_, cx| {
282                            cx.dispatch_action(ToggleDock);
283                        })
284                        .with_cursor_style(CursorStyle::Arrow)
285                        .boxed(),
286                    )
287                    .with_background_color(style.wash_color)
288                    .boxed()
289                }
290            })
291    }
292}
293
294pub struct ToggleDockButton {
295    workspace: WeakViewHandle<Workspace>,
296}
297
298impl ToggleDockButton {
299    pub fn new(workspace: ViewHandle<Workspace>, cx: &mut ViewContext<Self>) -> Self {
300        // When dock moves, redraw so that the icon and toggle status matches.
301        cx.subscribe(&workspace, |_, _, _, cx| cx.notify()).detach();
302
303        Self {
304            workspace: workspace.downgrade(),
305        }
306    }
307}
308
309impl Entity for ToggleDockButton {
310    type Event = ();
311}
312
313impl View for ToggleDockButton {
314    fn ui_name() -> &'static str {
315        "Dock Toggle"
316    }
317
318    fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
319        let workspace = self.workspace.upgrade(cx);
320
321        if workspace.is_none() {
322            return Empty::new().boxed();
323        }
324
325        let dock_position = workspace.unwrap().read(cx).dock.position;
326
327        let theme = cx.global::<Settings>().theme.clone();
328        MouseEventHandler::<Self>::new(0, cx, {
329            let theme = theme.clone();
330            move |state, _| {
331                let style = theme
332                    .workspace
333                    .status_bar
334                    .sidebar_buttons
335                    .item
336                    .style_for(state, dock_position.is_visible());
337
338                Svg::new(icon_for_dock_anchor(dock_position.anchor()))
339                    .with_color(style.icon_color)
340                    .constrained()
341                    .with_width(style.icon_size)
342                    .with_height(style.icon_size)
343                    .contained()
344                    .with_style(style.container)
345                    .boxed()
346            }
347        })
348        .with_cursor_style(CursorStyle::PointingHand)
349        .on_click(MouseButton::Left, |_, cx| {
350            cx.dispatch_action(ToggleDock);
351        })
352        .with_tooltip::<Self, _>(
353            0,
354            "Toggle Dock".to_string(),
355            Some(Box::new(ToggleDock)),
356            theme.tooltip.clone(),
357            cx,
358        )
359        .boxed()
360    }
361}
362
363impl StatusItemView for ToggleDockButton {
364    fn set_active_pane_item(
365        &mut self,
366        _active_pane_item: Option<&dyn crate::ItemHandle>,
367        _cx: &mut ViewContext<Self>,
368    ) {
369        //Not applicable
370    }
371}
372
373#[cfg(test)]
374mod tests {
375    use super::*;
376    use gpui::{TestAppContext, ViewContext};
377    use project::{FakeFs, Project};
378    use settings::Settings;
379
380    use crate::{tests::TestItem, ItemHandle, Workspace};
381
382    pub fn default_item_factory(
383        _workspace: &mut Workspace,
384        cx: &mut ViewContext<Workspace>,
385    ) -> Box<dyn ItemHandle> {
386        Box::new(cx.add_view(|_| TestItem::new()))
387    }
388
389    #[gpui::test]
390    async fn test_dock_hides_when_pane_empty(cx: &mut TestAppContext) {
391        cx.foreground().forbid_parking();
392
393        Settings::test_async(cx);
394        let fs = FakeFs::new(cx.background());
395
396        let project = Project::test(fs, [], cx).await;
397        let (_, workspace) = cx.add_window(|cx| Workspace::new(project, default_item_factory, cx));
398
399        // Open dock
400        workspace.update(cx, |workspace, cx| {
401            Dock::show(workspace, cx);
402        });
403
404        // Ensure dock has an item in it
405        let dock_item_handle = workspace.read_with(cx, |workspace, cx| {
406            let dock = workspace.dock_pane().read(cx);
407            dock.items()
408                .next()
409                .expect("Dock should have an item in it")
410                .clone()
411        });
412
413        // Close item
414        let close_task = workspace.update(cx, |workspace, cx| {
415            Pane::close_item(
416                workspace,
417                workspace.dock_pane().clone(),
418                dock_item_handle.id(),
419                cx,
420            )
421        });
422        close_task.await.expect("Dock item closed successfully");
423
424        // Ensure dock closes
425        workspace.read_with(cx, |workspace, cx| {
426            assert!(workspace.dock.visible_pane().is_some())
427        });
428
429        // Open again
430        workspace.update(cx, |workspace, cx| {
431            Dock::show(workspace, cx);
432        });
433
434        // Ensure dock has item in it
435        workspace.read_with(cx, |workspace, cx| {
436            let dock = workspace.dock_pane().read(cx);
437            dock.items().next().expect("Dock should have an item in it");
438        });
439    }
440
441    #[gpui::test]
442    async fn test_dock_panel_collisions(cx: &mut TestAppContext) {
443        // Open dock expanded
444        // Open left panel
445        // Ensure dock closes
446        // Open dock to the right
447        // Open left panel
448        // Ensure dock is left open
449        // Open right panel
450        // Ensure dock closes
451        // Open dock bottom
452        // Open left panel
453        // Open right panel
454        // Ensure dock still open
455    }
456
457    #[gpui::test]
458    async fn test_focusing_panes_shows_and_hides_dock(cx: &mut TestAppContext) {
459        // Open item in center pane
460        // Open dock expanded
461        // Focus new item
462        // Ensure the dock gets hidden
463        // Open dock to the right
464        // Focus new item
465        // Ensure dock stays shown but inactive
466        // Add item to dock and hide it
467        // Focus the added item
468        // Ensure the dock is open
469    }
470}