dock.rs

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