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
410// todo!()
411// impl View for Dock {
412// fn ui_name() -> &'static str {
413// "Dock"
414// }
415
416// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
417// if let Some(active_entry) = self.visible_entry() {
418// let style = self.style(cx);
419// ChildView::new(active_entry.panel.as_any(), cx)
420// .contained()
421// .with_style(style)
422// .resizable::<WorkspaceBounds>(
423// self.position.to_resize_handle_side(),
424// active_entry.panel.size(cx),
425// |dock: &mut Self, size, cx| dock.resize_active_panel(size, cx),
426// )
427// .into_any()
428// } else {
429// Empty::new().into_any()
430// }
431// }
432
433// fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
434// if cx.is_self_focused() {
435// if let Some(active_entry) = self.visible_entry() {
436// cx.focus(active_entry.panel.as_any());
437// } else {
438// cx.focus_parent();
439// }
440// }
441// }
442// }
443
444impl PanelButtons {
445 pub fn new(
446 dock: View<Dock>,
447 workspace: WeakView<Workspace>,
448 cx: &mut ViewContext<Self>,
449 ) -> Self {
450 cx.observe(&dock, |_, _, cx| cx.notify()).detach();
451 Self { dock, workspace }
452 }
453}
454
455impl EventEmitter for PanelButtons {
456 type Event = ();
457}
458
459// impl Render for PanelButtons {
460// type Element = ();
461
462// fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
463// todo!("")
464// }
465
466// fn ui_name() -> &'static str {
467// "PanelButtons"
468// }
469
470// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
471// let theme = &settings::get::<ThemeSettings>(cx).theme;
472// let tooltip_style = theme.tooltip.clone();
473// let theme = &theme.workspace.status_bar.panel_buttons;
474// let button_style = theme.button.clone();
475// let dock = self.dock.read(cx);
476// let active_ix = dock.active_panel_index;
477// let is_open = dock.is_open;
478// let dock_position = dock.position;
479// let group_style = match dock_position {
480// DockPosition::Left => theme.group_left,
481// DockPosition::Bottom => theme.group_bottom,
482// DockPosition::Right => theme.group_right,
483// };
484// let menu_corner = match dock_position {
485// DockPosition::Left => AnchorCorner::BottomLeft,
486// DockPosition::Bottom | DockPosition::Right => AnchorCorner::BottomRight,
487// };
488
489// let panels = dock
490// .panel_entries
491// .iter()
492// .map(|item| (item.panel.clone(), item.context_menu.clone()))
493// .collect::<Vec<_>>();
494// Flex::row()
495// .with_children(panels.into_iter().enumerate().filter_map(
496// |(panel_ix, (view, context_menu))| {
497// let icon_path = view.icon_path(cx)?;
498// let is_active = is_open && panel_ix == active_ix;
499// let (tooltip, tooltip_action) = if is_active {
500// (
501// format!("Close {} dock", dock_position.to_label()),
502// Some(match dock_position {
503// DockPosition::Left => crate::ToggleLeftDock.boxed_clone(),
504// DockPosition::Bottom => crate::ToggleBottomDock.boxed_clone(),
505// DockPosition::Right => crate::ToggleRightDock.boxed_clone(),
506// }),
507// )
508// } else {
509// view.icon_tooltip(cx)
510// };
511// Some(
512// Stack::new()
513// .with_child(
514// MouseEventHandler::new::<Self, _>(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(icon_path)
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 = cx.window();
548// let view_id = this.workspace.id();
549// let tooltip_action = tooltip_action.boxed_clone();
550// cx.spawn(|_, mut cx| async move {
551// window.dispatch_action(
552// view_id,
553// &*tooltip_action,
554// &mut cx,
555// );
556// })
557// .detach();
558// }
559// }
560// })
561// .on_click(MouseButton::Right, {
562// let view = view.clone();
563// let menu = context_menu.clone();
564// move |_, _, cx| {
565// const POSITIONS: [DockPosition; 3] = [
566// DockPosition::Left,
567// DockPosition::Right,
568// DockPosition::Bottom,
569// ];
570
571// menu.update(cx, |menu, cx| {
572// let items = POSITIONS
573// .into_iter()
574// .filter(|position| {
575// *position != dock_position
576// && view.position_is_valid(*position, cx)
577// })
578// .map(|position| {
579// let view = view.clone();
580// ContextMenuItem::handler(
581// format!("Dock {}", position.to_label()),
582// move |cx| view.set_position(position, cx),
583// )
584// })
585// .collect();
586// menu.show(Default::default(), menu_corner, items, cx);
587// })
588// }
589// })
590// .with_tooltip::<Self>(
591// panel_ix,
592// tooltip,
593// tooltip_action,
594// tooltip_style.clone(),
595// cx,
596// ),
597// )
598// .with_child(ChildView::new(&context_menu, cx)),
599// )
600// },
601// ))
602// .contained()
603// .with_style(group_style)
604// .into_any()
605// }
606// }
607
608impl Render for PanelButtons {
609 type Element = Div<Self>;
610
611 fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
612 // todo!()
613 let dock = self.dock.read(cx);
614 div().children(
615 dock.panel_entries
616 .iter()
617 .map(|panel| panel.panel.persistent_name(cx)),
618 )
619 }
620}
621
622impl StatusItemView for PanelButtons {
623 fn set_active_pane_item(
624 &mut self,
625 _active_pane_item: Option<&dyn crate::ItemHandle>,
626 _cx: &mut ViewContext<Self>,
627 ) {
628 // todo!(This is empty in the old `workspace::dock`)
629 }
630}
631
632#[cfg(any(test, feature = "test-support"))]
633pub mod test {
634 use super::*;
635 use gpui::{div, Div, ViewContext, WindowContext};
636
637 #[derive(Debug)]
638 pub enum TestPanelEvent {
639 PositionChanged,
640 Activated,
641 Closed,
642 ZoomIn,
643 ZoomOut,
644 Focus,
645 }
646
647 pub struct TestPanel {
648 pub position: DockPosition,
649 pub zoomed: bool,
650 pub active: bool,
651 pub has_focus: bool,
652 pub size: f32,
653 }
654
655 impl EventEmitter for TestPanel {
656 type Event = TestPanelEvent;
657 }
658
659 impl TestPanel {
660 pub fn new(position: DockPosition) -> Self {
661 Self {
662 position,
663 zoomed: false,
664 active: false,
665 has_focus: false,
666 size: 300.,
667 }
668 }
669 }
670
671 impl Render for TestPanel {
672 type Element = Div<Self>;
673
674 fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
675 div()
676 }
677 }
678
679 impl Panel for TestPanel {
680 fn persistent_name(&self) -> &'static str {
681 "TestPanel"
682 }
683
684 fn position(&self, _: &gpui::WindowContext) -> super::DockPosition {
685 self.position
686 }
687
688 fn position_is_valid(&self, _: super::DockPosition) -> bool {
689 true
690 }
691
692 fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
693 self.position = position;
694 cx.emit(TestPanelEvent::PositionChanged);
695 }
696
697 fn size(&self, _: &WindowContext) -> f32 {
698 self.size
699 }
700
701 fn set_size(&mut self, size: Option<f32>, _: &mut ViewContext<Self>) {
702 self.size = size.unwrap_or(300.);
703 }
704
705 fn icon_path(&self, _: &WindowContext) -> Option<&'static str> {
706 Some("icons/test_panel.svg")
707 }
708
709 fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {
710 ("Test Panel".into(), None)
711 }
712
713 fn should_change_position_on_event(event: &Self::Event) -> bool {
714 matches!(event, TestPanelEvent::PositionChanged)
715 }
716
717 fn should_zoom_in_on_event(event: &Self::Event) -> bool {
718 matches!(event, TestPanelEvent::ZoomIn)
719 }
720
721 fn should_zoom_out_on_event(event: &Self::Event) -> bool {
722 matches!(event, TestPanelEvent::ZoomOut)
723 }
724
725 fn is_zoomed(&self, _: &WindowContext) -> bool {
726 self.zoomed
727 }
728
729 fn set_zoomed(&mut self, zoomed: bool, _cx: &mut ViewContext<Self>) {
730 self.zoomed = zoomed;
731 }
732
733 fn set_active(&mut self, active: bool, _cx: &mut ViewContext<Self>) {
734 self.active = active;
735 }
736
737 fn should_activate_on_event(event: &Self::Event) -> bool {
738 matches!(event, TestPanelEvent::Activated)
739 }
740
741 fn should_close_on_event(event: &Self::Event) -> bool {
742 matches!(event, TestPanelEvent::Closed)
743 }
744
745 fn has_focus(&self, _cx: &WindowContext) -> bool {
746 self.has_focus
747 }
748
749 fn is_focus_event(event: &Self::Event) -> bool {
750 matches!(event, TestPanelEvent::Focus)
751 }
752 }
753}