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