dock.rs

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