dock.rs

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