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