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}