1use crate::{StatusItemView, Workspace};
2use context_menu::{ContextMenu, ContextMenuItem};
3use gpui::{
4 elements::*, platform::CursorStyle, platform::MouseButton, Action, AnyViewHandle, AppContext,
5 Axis, Entity, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
6};
7use serde::Deserialize;
8use std::rc::Rc;
9use theme::ThemeSettings;
10
11pub trait Panel: View {
12 fn position(&self, cx: &WindowContext) -> DockPosition;
13 fn position_is_valid(&self, position: DockPosition) -> bool;
14 fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>);
15 fn size(&self, cx: &WindowContext) -> f32;
16 fn set_size(&mut self, size: f32, cx: &mut ViewContext<Self>);
17 fn icon_path(&self) -> &'static str;
18 fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>);
19 fn icon_label(&self, _: &WindowContext) -> Option<String> {
20 None
21 }
22 fn should_change_position_on_event(_: &Self::Event) -> bool;
23 fn should_zoom_in_on_event(_: &Self::Event) -> bool {
24 false
25 }
26 fn should_zoom_out_on_event(_: &Self::Event) -> bool {
27 false
28 }
29 fn is_zoomed(&self, _cx: &WindowContext) -> bool {
30 false
31 }
32 fn set_zoomed(&mut self, _zoomed: bool, _cx: &mut ViewContext<Self>) {
33
34 }
35 fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {
36
37 }
38 fn should_activate_on_event(_: &Self::Event) -> bool {
39 false
40 }
41 fn should_close_on_event(_: &Self::Event) -> bool {
42 false
43 }
44 fn has_focus(&self, cx: &WindowContext) -> bool;
45 fn is_focus_event(_: &Self::Event) -> bool;
46}
47
48pub trait PanelHandle {
49 fn id(&self) -> usize;
50 fn position(&self, cx: &WindowContext) -> DockPosition;
51 fn position_is_valid(&self, position: DockPosition, cx: &WindowContext) -> bool;
52 fn set_position(&self, position: DockPosition, cx: &mut WindowContext);
53 fn is_zoomed(&self, cx: &WindowContext) -> bool;
54 fn set_zoomed(&self, zoomed: bool, cx: &mut WindowContext);
55 fn set_active(&self, active: bool, cx: &mut WindowContext);
56 fn size(&self, cx: &WindowContext) -> f32;
57 fn set_size(&self, size: f32, cx: &mut WindowContext);
58 fn icon_path(&self, cx: &WindowContext) -> &'static str;
59 fn icon_tooltip(&self, cx: &WindowContext) -> (String, Option<Box<dyn Action>>);
60 fn icon_label(&self, cx: &WindowContext) -> Option<String>;
61 fn has_focus(&self, cx: &WindowContext) -> bool;
62 fn as_any(&self) -> &AnyViewHandle;
63}
64
65impl<T> PanelHandle for ViewHandle<T>
66where
67 T: Panel,
68{
69 fn id(&self) -> usize {
70 self.id()
71 }
72
73 fn position(&self, cx: &WindowContext) -> DockPosition {
74 self.read(cx).position(cx)
75 }
76
77 fn position_is_valid(&self, position: DockPosition, cx: &WindowContext) -> bool {
78 self.read(cx).position_is_valid(position)
79 }
80
81 fn set_position(&self, position: DockPosition, cx: &mut WindowContext) {
82 self.update(cx, |this, cx| this.set_position(position, cx))
83 }
84
85 fn size(&self, cx: &WindowContext) -> f32 {
86 self.read(cx).size(cx)
87 }
88
89 fn set_size(&self, size: f32, cx: &mut WindowContext) {
90 self.update(cx, |this, cx| this.set_size(size, cx))
91 }
92
93 fn is_zoomed(&self, cx: &WindowContext) -> bool {
94 self.read(cx).is_zoomed(cx)
95 }
96
97 fn set_zoomed(&self, zoomed: bool, cx: &mut WindowContext) {
98 self.update(cx, |this, cx| this.set_zoomed(zoomed, cx))
99 }
100
101 fn set_active(&self, active: bool, cx: &mut WindowContext) {
102 self.update(cx, |this, cx| this.set_active(active, cx))
103 }
104
105 fn icon_path(&self, cx: &WindowContext) -> &'static str {
106 self.read(cx).icon_path()
107 }
108
109 fn icon_tooltip(&self, cx: &WindowContext) -> (String, Option<Box<dyn Action>>) {
110 self.read(cx).icon_tooltip()
111 }
112
113 fn icon_label(&self, cx: &WindowContext) -> Option<String> {
114 self.read(cx).icon_label(cx)
115 }
116
117 fn has_focus(&self, cx: &WindowContext) -> bool {
118 self.read(cx).has_focus(cx)
119 }
120
121 fn as_any(&self) -> &AnyViewHandle {
122 self
123 }
124}
125
126impl From<&dyn PanelHandle> for AnyViewHandle {
127 fn from(val: &dyn PanelHandle) -> Self {
128 val.as_any().clone()
129 }
130}
131
132pub struct Dock {
133 position: DockPosition,
134 panel_entries: Vec<PanelEntry>,
135 is_open: bool,
136 active_panel_index: usize,
137}
138
139#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
140pub enum DockPosition {
141 Left,
142 Bottom,
143 Right,
144}
145
146impl DockPosition {
147 fn to_label(&self) -> &'static str {
148 match self {
149 Self::Left => "left",
150 Self::Bottom => "bottom",
151 Self::Right => "right",
152 }
153 }
154
155 fn to_resize_handle_side(self) -> HandleSide {
156 match self {
157 Self::Left => HandleSide::Right,
158 Self::Bottom => HandleSide::Top,
159 Self::Right => HandleSide::Left,
160 }
161 }
162
163 pub fn axis(&self) -> Axis {
164 match self {
165 Self::Left | Self::Right => Axis::Horizontal,
166 Self::Bottom => Axis::Vertical,
167 }
168 }
169}
170
171struct PanelEntry {
172 panel: Rc<dyn PanelHandle>,
173 context_menu: ViewHandle<ContextMenu>,
174 _subscriptions: [Subscription; 2],
175}
176
177pub struct PanelButtons {
178 dock: ViewHandle<Dock>,
179 workspace: WeakViewHandle<Workspace>,
180}
181
182impl Dock {
183 pub fn new(position: DockPosition) -> Self {
184 Self {
185 position,
186 panel_entries: Default::default(),
187 active_panel_index: 0,
188 is_open: false,
189 }
190 }
191
192 pub fn position(&self) -> DockPosition {
193 self.position
194 }
195
196 pub fn is_open(&self) -> bool {
197 self.is_open
198 }
199
200 pub fn has_focus(&self, cx: &WindowContext) -> bool {
201 self.visible_panel()
202 .map_or(false, |panel| panel.has_focus(cx))
203 }
204
205 pub fn panel<T: Panel>(&self) -> Option<ViewHandle<T>> {
206 self.panel_entries
207 .iter()
208 .find_map(|entry| entry.panel.as_any().clone().downcast())
209 }
210
211 pub fn panel_index_for_type<T: Panel>(&self) -> Option<usize> {
212 self.panel_entries
213 .iter()
214 .position(|entry| entry.panel.as_any().is::<T>())
215 }
216
217 pub fn panel_index_for_ui_name(&self, ui_name: &str, cx: &AppContext) -> Option<usize> {
218 self.panel_entries.iter().position(|entry| {
219 let panel = entry.panel.as_any();
220 cx.view_ui_name(panel.window_id(), panel.id()) == Some(ui_name)
221 })
222 }
223
224 pub fn active_panel_index(&self) -> usize {
225 self.active_panel_index
226 }
227
228 pub(crate) fn set_open(&mut self, open: bool, cx: &mut ViewContext<Self>) {
229 if open != self.is_open {
230 self.is_open = open;
231 if let Some(active_panel) = self.panel_entries.get(self.active_panel_index) {
232 active_panel.panel.set_active(open, cx);
233 }
234
235 cx.notify();
236 }
237 }
238
239 pub fn set_panel_zoomed(
240 &mut self,
241 panel: &AnyViewHandle,
242 zoomed: bool,
243 cx: &mut ViewContext<Self>,
244 ) {
245 for entry in &mut self.panel_entries {
246 if entry.panel.as_any() == panel {
247 if zoomed != entry.panel.is_zoomed(cx) {
248 entry.panel.set_zoomed(zoomed, cx);
249 }
250 } else if entry.panel.is_zoomed(cx) {
251 entry.panel.set_zoomed(false, cx);
252 }
253 }
254
255 cx.notify();
256 }
257
258 pub fn zoom_out(&mut self, cx: &mut ViewContext<Self>) {
259 for entry in &mut self.panel_entries {
260 if entry.panel.is_zoomed(cx) {
261 entry.panel.set_zoomed(false, cx);
262 }
263 }
264 }
265
266 pub(crate) fn add_panel<T: Panel>(&mut self, panel: ViewHandle<T>, cx: &mut ViewContext<Self>) {
267 let subscriptions = [
268 cx.observe(&panel, |_, _, cx| cx.notify()),
269 cx.subscribe(&panel, |this, panel, event, cx| {
270 if T::should_activate_on_event(event) {
271 if let Some(ix) = this
272 .panel_entries
273 .iter()
274 .position(|entry| entry.panel.id() == panel.id())
275 {
276 this.set_open(true, cx);
277 this.activate_panel(ix, cx);
278 cx.focus(&panel);
279 }
280 } else if T::should_close_on_event(event)
281 && this.visible_panel().map_or(false, |p| p.id() == panel.id())
282 {
283 this.set_open(false, cx);
284 }
285 }),
286 ];
287
288 let dock_view_id = cx.view_id();
289 self.panel_entries.push(PanelEntry {
290 panel: Rc::new(panel),
291 context_menu: cx.add_view(|cx| {
292 let mut menu = ContextMenu::new(dock_view_id, cx);
293 menu.set_position_mode(OverlayPositionMode::Local);
294 menu
295 }),
296 _subscriptions: subscriptions,
297 });
298 cx.notify()
299 }
300
301 pub fn remove_panel<T: Panel>(&mut self, panel: &ViewHandle<T>, cx: &mut ViewContext<Self>) {
302 if let Some(panel_ix) = self
303 .panel_entries
304 .iter()
305 .position(|entry| entry.panel.id() == panel.id())
306 {
307 if panel_ix == self.active_panel_index {
308 self.active_panel_index = 0;
309 self.set_open(false, cx);
310 } else if panel_ix < self.active_panel_index {
311 self.active_panel_index -= 1;
312 }
313 self.panel_entries.remove(panel_ix);
314 cx.notify();
315 }
316 }
317
318 pub fn panels_len(&self) -> usize {
319 self.panel_entries.len()
320 }
321
322 pub fn activate_panel(&mut self, panel_ix: usize, cx: &mut ViewContext<Self>) {
323 if panel_ix != self.active_panel_index {
324 if let Some(active_panel) = self.panel_entries.get(self.active_panel_index) {
325 active_panel.panel.set_active(false, cx);
326 }
327
328 self.active_panel_index = panel_ix;
329 if let Some(active_panel) = self.panel_entries.get(self.active_panel_index) {
330 active_panel.panel.set_active(true, cx);
331 }
332
333 cx.notify();
334 }
335 }
336
337 pub fn visible_panel(&self) -> Option<&Rc<dyn PanelHandle>> {
338 let entry = self.visible_entry()?;
339 Some(&entry.panel)
340 }
341
342 pub fn active_panel(&self) -> Option<&Rc<dyn PanelHandle>> {
343 Some(&self.panel_entries.get(self.active_panel_index)?.panel)
344 }
345
346 fn visible_entry(&self) -> Option<&PanelEntry> {
347 if self.is_open {
348 self.panel_entries.get(self.active_panel_index)
349 } else {
350 None
351 }
352 }
353
354 pub fn zoomed_panel(&self, cx: &WindowContext) -> Option<Rc<dyn PanelHandle>> {
355 let entry = self.visible_entry()?;
356 if entry.panel.is_zoomed(cx) {
357 Some(entry.panel.clone())
358 } else {
359 None
360 }
361 }
362
363 pub fn panel_size(&self, panel: &dyn PanelHandle, cx: &WindowContext) -> Option<f32> {
364 self.panel_entries
365 .iter()
366 .find(|entry| entry.panel.id() == panel.id())
367 .map(|entry| entry.panel.size(cx))
368 }
369
370 pub fn active_panel_size(&self, cx: &WindowContext) -> Option<f32> {
371 if self.is_open {
372 self.panel_entries
373 .get(self.active_panel_index)
374 .map(|entry| entry.panel.size(cx))
375 } else {
376 None
377 }
378 }
379
380 pub fn resize_active_panel(&mut self, size: f32, cx: &mut ViewContext<Self>) {
381 if let Some(entry) = self.panel_entries.get_mut(self.active_panel_index) {
382 entry.panel.set_size(size, cx);
383 cx.notify();
384 }
385 }
386
387 pub fn render_placeholder(&self, cx: &WindowContext) -> AnyElement<Workspace> {
388 if let Some(active_entry) = self.visible_entry() {
389 Empty::new()
390 .into_any()
391 .contained()
392 .with_style(self.style(cx))
393 .resizable(
394 self.position.to_resize_handle_side(),
395 active_entry.panel.size(cx),
396 |_, _, _| {},
397 )
398 .into_any()
399 } else {
400 Empty::new().into_any()
401 }
402 }
403
404 fn style(&self, cx: &WindowContext) -> ContainerStyle {
405 let theme = &settings::get::<ThemeSettings>(cx).theme;
406 let style = match self.position {
407 DockPosition::Left => theme.workspace.dock.left,
408 DockPosition::Bottom => theme.workspace.dock.bottom,
409 DockPosition::Right => theme.workspace.dock.right,
410 };
411 style
412 }
413}
414
415impl Entity for Dock {
416 type Event = ();
417}
418
419impl View for Dock {
420 fn ui_name() -> &'static str {
421 "Dock"
422 }
423
424 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
425 if let Some(active_entry) = self.visible_entry() {
426 let style = self.style(cx);
427 ChildView::new(active_entry.panel.as_any(), cx)
428 .contained()
429 .with_style(style)
430 .resizable(
431 self.position.to_resize_handle_side(),
432 active_entry.panel.size(cx),
433 |dock: &mut Self, size, cx| dock.resize_active_panel(size, cx),
434 )
435 .into_any()
436 } else {
437 Empty::new().into_any()
438 }
439 }
440
441 fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
442 if cx.is_self_focused() {
443 if let Some(active_entry) = self.visible_entry() {
444 cx.focus(active_entry.panel.as_any());
445 } else {
446 cx.focus_parent();
447 }
448 }
449 }
450}
451
452impl PanelButtons {
453 pub fn new(
454 dock: ViewHandle<Dock>,
455 workspace: WeakViewHandle<Workspace>,
456 cx: &mut ViewContext<Self>,
457 ) -> Self {
458 cx.observe(&dock, |_, _, cx| cx.notify()).detach();
459 Self { dock, workspace }
460 }
461}
462
463impl Entity for PanelButtons {
464 type Event = ();
465}
466
467impl View for PanelButtons {
468 fn ui_name() -> &'static str {
469 "PanelButtons"
470 }
471
472 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
473 let theme = &settings::get::<ThemeSettings>(cx).theme;
474 let tooltip_style = theme.tooltip.clone();
475 let theme = &theme.workspace.status_bar.panel_buttons;
476 let button_style = theme.button.clone();
477 let dock = self.dock.read(cx);
478 let active_ix = dock.active_panel_index;
479 let is_open = dock.is_open;
480 let dock_position = dock.position;
481 let group_style = match dock_position {
482 DockPosition::Left => theme.group_left,
483 DockPosition::Bottom => theme.group_bottom,
484 DockPosition::Right => theme.group_right,
485 };
486 let menu_corner = match dock_position {
487 DockPosition::Left => AnchorCorner::BottomLeft,
488 DockPosition::Bottom | DockPosition::Right => AnchorCorner::BottomRight,
489 };
490
491 let panels = dock
492 .panel_entries
493 .iter()
494 .map(|item| (item.panel.clone(), item.context_menu.clone()))
495 .collect::<Vec<_>>();
496 Flex::row()
497 .with_children(panels.into_iter().enumerate().map(
498 |(panel_ix, (view, context_menu))| {
499 let is_active = is_open && panel_ix == active_ix;
500 let (tooltip, tooltip_action) = if is_active {
501 (
502 format!("Close {} dock", dock_position.to_label()),
503 Some(match dock_position {
504 DockPosition::Left => crate::ToggleLeftDock.boxed_clone(),
505 DockPosition::Bottom => crate::ToggleBottomDock.boxed_clone(),
506 DockPosition::Right => crate::ToggleRightDock.boxed_clone(),
507 }),
508 )
509 } else {
510 view.icon_tooltip(cx)
511 };
512 Stack::new()
513 .with_child(
514 MouseEventHandler::<Self, _>::new(panel_ix, cx, |state, cx| {
515 let style = button_style.in_state(is_active);
516
517 let style = style.style_for(state);
518 Flex::row()
519 .with_child(
520 Svg::new(view.icon_path(cx))
521 .with_color(style.icon_color)
522 .constrained()
523 .with_width(style.icon_size)
524 .aligned(),
525 )
526 .with_children(if let Some(label) = view.icon_label(cx) {
527 Some(
528 Label::new(label, style.label.text.clone())
529 .contained()
530 .with_style(style.label.container)
531 .aligned(),
532 )
533 } else {
534 None
535 })
536 .constrained()
537 .with_height(style.icon_size)
538 .contained()
539 .with_style(style.container)
540 })
541 .with_cursor_style(CursorStyle::PointingHand)
542 .on_click(MouseButton::Left, {
543 let tooltip_action =
544 tooltip_action.as_ref().map(|action| action.boxed_clone());
545 move |_, this, cx| {
546 if let Some(tooltip_action) = &tooltip_action {
547 let window_id = cx.window_id();
548 let view_id = this.workspace.id();
549 let tooltip_action = tooltip_action.boxed_clone();
550 cx.spawn(|_, mut cx| async move {
551 cx.dispatch_action(
552 window_id,
553 view_id,
554 &*tooltip_action,
555 )
556 .ok();
557 })
558 .detach();
559 }
560 }
561 })
562 .on_click(MouseButton::Right, {
563 let view = view.clone();
564 let menu = context_menu.clone();
565 move |_, _, cx| {
566 const POSITIONS: [DockPosition; 3] = [
567 DockPosition::Left,
568 DockPosition::Right,
569 DockPosition::Bottom,
570 ];
571
572 menu.update(cx, |menu, cx| {
573 let items = POSITIONS
574 .into_iter()
575 .filter(|position| {
576 *position != dock_position
577 && view.position_is_valid(*position, cx)
578 })
579 .map(|position| {
580 let view = view.clone();
581 ContextMenuItem::handler(
582 format!("Dock {}", position.to_label()),
583 move |cx| view.set_position(position, cx),
584 )
585 })
586 .collect();
587 menu.show(Default::default(), menu_corner, items, cx);
588 })
589 }
590 })
591 .with_tooltip::<Self>(
592 panel_ix,
593 tooltip,
594 tooltip_action,
595 tooltip_style.clone(),
596 cx,
597 ),
598 )
599 .with_child(ChildView::new(&context_menu, cx))
600 },
601 ))
602 .contained()
603 .with_style(group_style)
604 .into_any()
605 }
606}
607
608impl StatusItemView for PanelButtons {
609 fn set_active_pane_item(
610 &mut self,
611 _: Option<&dyn crate::ItemHandle>,
612 _: &mut ViewContext<Self>,
613 ) {
614 }
615}
616
617#[cfg(any(test, feature = "test-support"))]
618pub mod test {
619 use super::*;
620 use gpui::{ViewContext, WindowContext};
621
622 #[derive(Debug)]
623 pub enum TestPanelEvent {
624 PositionChanged,
625 Activated,
626 Closed,
627 ZoomIn,
628 ZoomOut,
629 Focus,
630 }
631
632 pub struct TestPanel {
633 pub position: DockPosition,
634 pub zoomed: bool,
635 pub active: bool,
636 pub has_focus: bool,
637 pub size: f32,
638 }
639
640 impl TestPanel {
641 pub fn new(position: DockPosition) -> Self {
642 Self {
643 position,
644 zoomed: false,
645 active: false,
646 has_focus: false,
647 size: 300.,
648 }
649 }
650 }
651
652 impl Entity for TestPanel {
653 type Event = TestPanelEvent;
654 }
655
656 impl View for TestPanel {
657 fn ui_name() -> &'static str {
658 "TestPanel"
659 }
660
661 fn render(&mut self, _: &mut ViewContext<'_, '_, Self>) -> AnyElement<Self> {
662 Empty::new().into_any()
663 }
664
665 fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
666 self.has_focus = true;
667 cx.emit(TestPanelEvent::Focus);
668 }
669
670 fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
671 self.has_focus = false;
672 }
673 }
674
675 impl Panel for TestPanel {
676 fn position(&self, _: &gpui::WindowContext) -> super::DockPosition {
677 self.position
678 }
679
680 fn position_is_valid(&self, _: super::DockPosition) -> bool {
681 true
682 }
683
684 fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
685 self.position = position;
686 cx.emit(TestPanelEvent::PositionChanged);
687 }
688
689 fn is_zoomed(&self, _: &WindowContext) -> bool {
690 self.zoomed
691 }
692
693 fn set_zoomed(&mut self, zoomed: bool, _cx: &mut ViewContext<Self>) {
694 self.zoomed = zoomed;
695 }
696
697 fn set_active(&mut self, active: bool, _cx: &mut ViewContext<Self>) {
698 self.active = active;
699 }
700
701 fn size(&self, _: &WindowContext) -> f32 {
702 self.size
703 }
704
705 fn set_size(&mut self, size: f32, _: &mut ViewContext<Self>) {
706 self.size = size;
707 }
708
709 fn icon_path(&self) -> &'static str {
710 "icons/test_panel.svg"
711 }
712
713 fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {
714 ("Test Panel".into(), None)
715 }
716
717 fn should_change_position_on_event(event: &Self::Event) -> bool {
718 matches!(event, TestPanelEvent::PositionChanged)
719 }
720
721 fn should_zoom_in_on_event(event: &Self::Event) -> bool {
722 matches!(event, TestPanelEvent::ZoomIn)
723 }
724
725 fn should_zoom_out_on_event(event: &Self::Event) -> bool {
726 matches!(event, TestPanelEvent::ZoomOut)
727 }
728
729 fn should_activate_on_event(event: &Self::Event) -> bool {
730 matches!(event, TestPanelEvent::Activated)
731 }
732
733 fn should_close_on_event(event: &Self::Event) -> bool {
734 matches!(event, TestPanelEvent::Closed)
735 }
736
737 fn has_focus(&self, _cx: &WindowContext) -> bool {
738 self.has_focus
739 }
740
741 fn is_focus_event(event: &Self::Event) -> bool {
742 matches!(event, TestPanelEvent::Focus)
743 }
744 }
745}