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}