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