1use crate::{status_bar::StatusItemView, Axis, Workspace};
2use gpui::{
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 // todo!()
245 // pub fn set_panel_zoomed(&mut self, panel: &AnyView, zoomed: bool, cx: &mut ViewContext<Self>) {
246 // for entry in &mut self.panel_entries {
247 // if entry.panel.as_any() == panel {
248 // if zoomed != entry.panel.is_zoomed(cx) {
249 // entry.panel.set_zoomed(zoomed, cx);
250 // }
251 // } else if entry.panel.is_zoomed(cx) {
252 // entry.panel.set_zoomed(false, cx);
253 // }
254 // }
255
256 // cx.notify();
257 // }
258
259 pub fn zoom_out(&mut self, cx: &mut ViewContext<Self>) {
260 for entry in &mut self.panel_entries {
261 if entry.panel.is_zoomed(cx) {
262 entry.panel.set_zoomed(false, cx);
263 }
264 }
265 }
266
267 pub(crate) fn add_panel<T: Panel>(&mut self, panel: View<T>, cx: &mut ViewContext<Self>) {
268 let subscriptions = [
269 cx.observe(&panel, |_, _, cx| cx.notify()),
270 cx.subscribe(&panel, |this, panel, event, cx| {
271 if T::should_activate_on_event(event) {
272 if let Some(ix) = this
273 .panel_entries
274 .iter()
275 .position(|entry| entry.panel.id() == panel.id())
276 {
277 this.set_open(true, cx);
278 this.activate_panel(ix, cx);
279 // todo!()
280 // cx.focus(&panel);
281 }
282 } else if T::should_close_on_event(event)
283 && this.visible_panel().map_or(false, |p| p.id() == panel.id())
284 {
285 this.set_open(false, cx);
286 }
287 }),
288 ];
289
290 // todo!()
291 // let dock_view_id = cx.view_id();
292 self.panel_entries.push(PanelEntry {
293 panel: Arc::new(panel),
294 // todo!()
295 // context_menu: cx.add_view(|cx| {
296 // let mut menu = ContextMenu::new(dock_view_id, cx);
297 // menu.set_position_mode(OverlayPositionMode::Local);
298 // menu
299 // }),
300 _subscriptions: subscriptions,
301 });
302 cx.notify()
303 }
304
305 pub fn remove_panel<T: Panel>(&mut self, panel: &View<T>, cx: &mut ViewContext<Self>) {
306 if let Some(panel_ix) = self
307 .panel_entries
308 .iter()
309 .position(|entry| entry.panel.id() == panel.id())
310 {
311 if panel_ix == self.active_panel_index {
312 self.active_panel_index = 0;
313 self.set_open(false, cx);
314 } else if panel_ix < self.active_panel_index {
315 self.active_panel_index -= 1;
316 }
317 self.panel_entries.remove(panel_ix);
318 cx.notify();
319 }
320 }
321
322 pub fn panels_len(&self) -> usize {
323 self.panel_entries.len()
324 }
325
326 pub fn activate_panel(&mut self, panel_ix: usize, cx: &mut ViewContext<Self>) {
327 if panel_ix != self.active_panel_index {
328 if let Some(active_panel) = self.panel_entries.get(self.active_panel_index) {
329 active_panel.panel.set_active(false, cx);
330 }
331
332 self.active_panel_index = panel_ix;
333 if let Some(active_panel) = self.panel_entries.get(self.active_panel_index) {
334 active_panel.panel.set_active(true, cx);
335 }
336
337 cx.notify();
338 }
339 }
340
341 pub fn visible_panel(&self) -> Option<&Arc<dyn PanelHandle>> {
342 let entry = self.visible_entry()?;
343 Some(&entry.panel)
344 }
345
346 pub fn active_panel(&self) -> Option<&Arc<dyn PanelHandle>> {
347 Some(&self.panel_entries.get(self.active_panel_index)?.panel)
348 }
349
350 fn visible_entry(&self) -> Option<&PanelEntry> {
351 if self.is_open {
352 self.panel_entries.get(self.active_panel_index)
353 } else {
354 None
355 }
356 }
357
358 pub fn zoomed_panel(&self, cx: &WindowContext) -> Option<Arc<dyn PanelHandle>> {
359 let entry = self.visible_entry()?;
360 if entry.panel.is_zoomed(cx) {
361 Some(entry.panel.clone())
362 } else {
363 None
364 }
365 }
366
367 pub fn panel_size(&self, panel: &dyn PanelHandle, cx: &WindowContext) -> Option<f32> {
368 self.panel_entries
369 .iter()
370 .find(|entry| entry.panel.id() == panel.id())
371 .map(|entry| entry.panel.size(cx))
372 }
373
374 pub fn active_panel_size(&self, cx: &WindowContext) -> Option<f32> {
375 if self.is_open {
376 self.panel_entries
377 .get(self.active_panel_index)
378 .map(|entry| entry.panel.size(cx))
379 } else {
380 None
381 }
382 }
383
384 pub fn resize_active_panel(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
385 if let Some(entry) = self.panel_entries.get_mut(self.active_panel_index) {
386 entry.panel.set_size(size, cx);
387 cx.notify();
388 }
389 }
390
391 // pub fn render_placeholder(&self, cx: &WindowContext) -> AnyElement<Workspace> {
392 // todo!()
393 // if let Some(active_entry) = self.visible_entry() {
394 // Empty::new()
395 // .into_any()
396 // .contained()
397 // .with_style(self.style(cx))
398 // .resizable::<WorkspaceBounds>(
399 // self.position.to_resize_handle_side(),
400 // active_entry.panel.size(cx),
401 // |_, _, _| {},
402 // )
403 // .into_any()
404 // } else {
405 // Empty::new().into_any()
406 // }
407 // }
408}
409
410impl Render for Dock {
411 type Element = Div<Self>;
412
413 fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
414 todo!()
415 }
416}
417
418// todo!()
419// impl 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::<WorkspaceBounds>(
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: View<Dock>,
455 workspace: WeakView<Workspace>,
456 cx: &mut ViewContext<Self>,
457 ) -> Self {
458 cx.observe(&dock, |_, _, cx| cx.notify()).detach();
459 Self { dock, workspace }
460 }
461}
462
463impl EventEmitter for PanelButtons {
464 type Event = ();
465}
466
467// impl Render for PanelButtons {
468// type Element = ();
469
470// fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
471// todo!("")
472// }
473
474// fn ui_name() -> &'static str {
475// "PanelButtons"
476// }
477
478// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
479// let theme = &settings::get::<ThemeSettings>(cx).theme;
480// let tooltip_style = theme.tooltip.clone();
481// let theme = &theme.workspace.status_bar.panel_buttons;
482// let button_style = theme.button.clone();
483// let dock = self.dock.read(cx);
484// let active_ix = dock.active_panel_index;
485// let is_open = dock.is_open;
486// let dock_position = dock.position;
487// let group_style = match dock_position {
488// DockPosition::Left => theme.group_left,
489// DockPosition::Bottom => theme.group_bottom,
490// DockPosition::Right => theme.group_right,
491// };
492// let menu_corner = match dock_position {
493// DockPosition::Left => AnchorCorner::BottomLeft,
494// DockPosition::Bottom | DockPosition::Right => AnchorCorner::BottomRight,
495// };
496
497// let panels = dock
498// .panel_entries
499// .iter()
500// .map(|item| (item.panel.clone(), item.context_menu.clone()))
501// .collect::<Vec<_>>();
502// Flex::row()
503// .with_children(panels.into_iter().enumerate().filter_map(
504// |(panel_ix, (view, context_menu))| {
505// let icon_path = view.icon_path(cx)?;
506// let is_active = is_open && panel_ix == active_ix;
507// let (tooltip, tooltip_action) = if is_active {
508// (
509// format!("Close {} dock", dock_position.to_label()),
510// Some(match dock_position {
511// DockPosition::Left => crate::ToggleLeftDock.boxed_clone(),
512// DockPosition::Bottom => crate::ToggleBottomDock.boxed_clone(),
513// DockPosition::Right => crate::ToggleRightDock.boxed_clone(),
514// }),
515// )
516// } else {
517// view.icon_tooltip(cx)
518// };
519// Some(
520// Stack::new()
521// .with_child(
522// MouseEventHandler::new::<Self, _>(panel_ix, cx, |state, cx| {
523// let style = button_style.in_state(is_active);
524
525// let style = style.style_for(state);
526// Flex::row()
527// .with_child(
528// Svg::new(icon_path)
529// .with_color(style.icon_color)
530// .constrained()
531// .with_width(style.icon_size)
532// .aligned(),
533// )
534// .with_children(if let Some(label) = view.icon_label(cx) {
535// Some(
536// Label::new(label, style.label.text.clone())
537// .contained()
538// .with_style(style.label.container)
539// .aligned(),
540// )
541// } else {
542// None
543// })
544// .constrained()
545// .with_height(style.icon_size)
546// .contained()
547// .with_style(style.container)
548// })
549// .with_cursor_style(CursorStyle::PointingHand)
550// .on_click(MouseButton::Left, {
551// let tooltip_action =
552// tooltip_action.as_ref().map(|action| action.boxed_clone());
553// move |_, this, cx| {
554// if let Some(tooltip_action) = &tooltip_action {
555// let window = cx.window();
556// let view_id = this.workspace.id();
557// let tooltip_action = tooltip_action.boxed_clone();
558// cx.spawn(|_, mut cx| async move {
559// window.dispatch_action(
560// view_id,
561// &*tooltip_action,
562// &mut cx,
563// );
564// })
565// .detach();
566// }
567// }
568// })
569// .on_click(MouseButton::Right, {
570// let view = view.clone();
571// let menu = context_menu.clone();
572// move |_, _, cx| {
573// const POSITIONS: [DockPosition; 3] = [
574// DockPosition::Left,
575// DockPosition::Right,
576// DockPosition::Bottom,
577// ];
578
579// menu.update(cx, |menu, cx| {
580// let items = POSITIONS
581// .into_iter()
582// .filter(|position| {
583// *position != dock_position
584// && view.position_is_valid(*position, cx)
585// })
586// .map(|position| {
587// let view = view.clone();
588// ContextMenuItem::handler(
589// format!("Dock {}", position.to_label()),
590// move |cx| view.set_position(position, cx),
591// )
592// })
593// .collect();
594// menu.show(Default::default(), menu_corner, items, cx);
595// })
596// }
597// })
598// .with_tooltip::<Self>(
599// panel_ix,
600// tooltip,
601// tooltip_action,
602// tooltip_style.clone(),
603// cx,
604// ),
605// )
606// .with_child(ChildView::new(&context_menu, cx)),
607// )
608// },
609// ))
610// .contained()
611// .with_style(group_style)
612// .into_any()
613// }
614// }
615
616impl Render for PanelButtons {
617 type Element = Div<Self>;
618
619 fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
620 // todo!()
621 let dock = self.dock.read(cx);
622 div().children(
623 dock.panel_entries
624 .iter()
625 .map(|panel| panel.panel.persistent_name(cx)),
626 )
627 }
628}
629
630impl StatusItemView for PanelButtons {
631 fn set_active_pane_item(
632 &mut self,
633 _active_pane_item: Option<&dyn crate::ItemHandle>,
634 _cx: &mut ViewContext<Self>,
635 ) {
636 // todo!(This is empty in the old `workspace::dock`)
637 }
638}
639
640#[cfg(any(test, feature = "test-support"))]
641pub mod test {
642 use super::*;
643 use gpui::{div, Div, ViewContext, WindowContext};
644
645 #[derive(Debug)]
646 pub enum TestPanelEvent {
647 PositionChanged,
648 Activated,
649 Closed,
650 ZoomIn,
651 ZoomOut,
652 Focus,
653 }
654
655 pub struct TestPanel {
656 pub position: DockPosition,
657 pub zoomed: bool,
658 pub active: bool,
659 pub has_focus: bool,
660 pub size: f32,
661 }
662
663 impl EventEmitter for TestPanel {
664 type Event = TestPanelEvent;
665 }
666
667 impl TestPanel {
668 pub fn new(position: DockPosition) -> Self {
669 Self {
670 position,
671 zoomed: false,
672 active: false,
673 has_focus: false,
674 size: 300.,
675 }
676 }
677 }
678
679 impl Render for TestPanel {
680 type Element = Div<Self>;
681
682 fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
683 div()
684 }
685 }
686
687 impl Panel for TestPanel {
688 fn persistent_name(&self) -> &'static str {
689 "TestPanel"
690 }
691
692 fn position(&self, _: &gpui::WindowContext) -> super::DockPosition {
693 self.position
694 }
695
696 fn position_is_valid(&self, _: super::DockPosition) -> bool {
697 true
698 }
699
700 fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
701 self.position = position;
702 cx.emit(TestPanelEvent::PositionChanged);
703 }
704
705 fn size(&self, _: &WindowContext) -> f32 {
706 self.size
707 }
708
709 fn set_size(&mut self, size: Option<f32>, _: &mut ViewContext<Self>) {
710 self.size = size.unwrap_or(300.);
711 }
712
713 fn icon_path(&self, _: &WindowContext) -> Option<&'static str> {
714 Some("icons/test_panel.svg")
715 }
716
717 fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {
718 ("Test Panel".into(), None)
719 }
720
721 fn should_change_position_on_event(event: &Self::Event) -> bool {
722 matches!(event, TestPanelEvent::PositionChanged)
723 }
724
725 fn should_zoom_in_on_event(event: &Self::Event) -> bool {
726 matches!(event, TestPanelEvent::ZoomIn)
727 }
728
729 fn should_zoom_out_on_event(event: &Self::Event) -> bool {
730 matches!(event, TestPanelEvent::ZoomOut)
731 }
732
733 fn is_zoomed(&self, _: &WindowContext) -> bool {
734 self.zoomed
735 }
736
737 fn set_zoomed(&mut self, zoomed: bool, _cx: &mut ViewContext<Self>) {
738 self.zoomed = zoomed;
739 }
740
741 fn set_active(&mut self, active: bool, _cx: &mut ViewContext<Self>) {
742 self.active = active;
743 }
744
745 fn should_activate_on_event(event: &Self::Event) -> bool {
746 matches!(event, TestPanelEvent::Activated)
747 }
748
749 fn should_close_on_event(event: &Self::Event) -> bool {
750 matches!(event, TestPanelEvent::Closed)
751 }
752
753 fn has_focus(&self, _cx: &WindowContext) -> bool {
754 self.has_focus
755 }
756
757 fn is_focus_event(event: &Self::Event) -> bool {
758 matches!(event, TestPanelEvent::Focus)
759 }
760 }
761}