dock.rs

  1use crate::{StatusItemView, Workspace};
  2use context_menu::{ContextMenu, ContextMenuItem};
  3use gpui::{
  4    elements::*, impl_actions, platform::CursorStyle, platform::MouseButton, AnyViewHandle,
  5    AppContext, Entity, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
  6};
  7use serde::Deserialize;
  8use settings::Settings;
  9use std::rc::Rc;
 10
 11pub trait Panel: View {
 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 icon_path(&self) -> &'static str;
 16    fn icon_tooltip(&self) -> String;
 17    fn icon_label(&self, _: &AppContext) -> Option<String> {
 18        None
 19    }
 20    fn should_change_position_on_event(_: &Self::Event) -> bool;
 21    fn should_activate_on_event(&self, _: &Self::Event, _: &AppContext) -> bool;
 22    fn should_close_on_event(&self, _: &Self::Event, _: &AppContext) -> bool;
 23}
 24
 25pub trait PanelHandle {
 26    fn id(&self) -> usize;
 27    fn position(&self, cx: &WindowContext) -> DockPosition;
 28    fn position_is_valid(&self, position: DockPosition, cx: &WindowContext) -> bool;
 29    fn set_position(&self, position: DockPosition, cx: &mut WindowContext);
 30    fn icon_path(&self, cx: &WindowContext) -> &'static str;
 31    fn icon_tooltip(&self, cx: &WindowContext) -> String;
 32    fn icon_label(&self, cx: &WindowContext) -> Option<String>;
 33    fn is_focused(&self, cx: &WindowContext) -> bool;
 34    fn as_any(&self) -> &AnyViewHandle;
 35}
 36
 37impl<T> PanelHandle for ViewHandle<T>
 38where
 39    T: Panel,
 40{
 41    fn id(&self) -> usize {
 42        self.id()
 43    }
 44
 45    fn position(&self, cx: &WindowContext) -> DockPosition {
 46        self.read(cx).position(cx)
 47    }
 48
 49    fn position_is_valid(&self, position: DockPosition, cx: &WindowContext) -> bool {
 50        self.read(cx).position_is_valid(position)
 51    }
 52
 53    fn set_position(&self, position: DockPosition, cx: &mut WindowContext) {
 54        self.update(cx, |this, cx| this.set_position(position, cx))
 55    }
 56
 57    fn icon_path(&self, cx: &WindowContext) -> &'static str {
 58        self.read(cx).icon_path()
 59    }
 60
 61    fn icon_tooltip(&self, cx: &WindowContext) -> String {
 62        self.read(cx).icon_tooltip()
 63    }
 64
 65    fn icon_label(&self, cx: &WindowContext) -> Option<String> {
 66        self.read(cx).icon_label(cx)
 67    }
 68
 69    fn is_focused(&self, cx: &WindowContext) -> bool {
 70        ViewHandle::is_focused(self, cx)
 71    }
 72
 73    fn as_any(&self) -> &AnyViewHandle {
 74        self
 75    }
 76}
 77
 78impl From<&dyn PanelHandle> for AnyViewHandle {
 79    fn from(val: &dyn PanelHandle) -> Self {
 80        val.as_any().clone()
 81    }
 82}
 83
 84pub enum Event {
 85    Close,
 86}
 87
 88pub struct Dock {
 89    position: DockPosition,
 90    panels: Vec<PanelEntry>,
 91    is_open: bool,
 92    active_item_ix: usize,
 93}
 94
 95#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
 96pub enum DockPosition {
 97    Left,
 98    Bottom,
 99    Right,
100}
101
102impl From<settings::DockPosition> for DockPosition {
103    fn from(value: settings::DockPosition) -> Self {
104        match value {
105            settings::DockPosition::Left => Self::Left,
106            settings::DockPosition::Bottom => Self::Bottom,
107            settings::DockPosition::Right => Self::Right,
108        }
109    }
110}
111
112impl From<DockPosition> for settings::DockPosition {
113    fn from(value: DockPosition) -> settings::DockPosition {
114        match value {
115            DockPosition::Left => settings::DockPosition::Left,
116            DockPosition::Bottom => settings::DockPosition::Bottom,
117            DockPosition::Right => settings::DockPosition::Right,
118        }
119    }
120}
121
122impl DockPosition {
123    fn to_label(&self) -> &'static str {
124        match self {
125            Self::Left => "left",
126            Self::Bottom => "bottom",
127            Self::Right => "right",
128        }
129    }
130
131    fn to_resizable_side(self) -> Side {
132        match self {
133            Self::Left => Side::Right,
134            Self::Bottom => Side::Top,
135            Self::Right => Side::Left,
136        }
137    }
138}
139
140struct PanelEntry {
141    panel: Rc<dyn PanelHandle>,
142    context_menu: ViewHandle<ContextMenu>,
143    _subscriptions: [Subscription; 2],
144}
145
146pub struct PanelButtons {
147    dock: ViewHandle<Dock>,
148    workspace: WeakViewHandle<Workspace>,
149}
150
151#[derive(Clone, Debug, Deserialize, PartialEq)]
152pub struct TogglePanel {
153    pub dock_position: DockPosition,
154    pub item_index: usize,
155}
156
157impl_actions!(workspace, [TogglePanel]);
158
159impl Dock {
160    pub fn new(position: DockPosition) -> Self {
161        Self {
162            position,
163            panels: Default::default(),
164            active_item_ix: 0,
165            is_open: false,
166        }
167    }
168
169    pub fn is_open(&self) -> bool {
170        self.is_open
171    }
172
173    pub fn active_item_ix(&self) -> usize {
174        self.active_item_ix
175    }
176
177    pub fn set_open(&mut self, open: bool, cx: &mut ViewContext<Self>) {
178        if open != self.is_open {
179            self.is_open = open;
180            cx.notify();
181        }
182    }
183
184    pub fn toggle_open(&mut self, cx: &mut ViewContext<Self>) {
185        if self.is_open {}
186        self.is_open = !self.is_open;
187        cx.notify();
188    }
189
190    pub fn add_panel<T: Panel>(&mut self, panel: ViewHandle<T>, cx: &mut ViewContext<Self>) {
191        let subscriptions = [
192            cx.observe(&panel, |_, _, cx| cx.notify()),
193            cx.subscribe(&panel, |this, view, event, cx| {
194                if view.read(cx).should_activate_on_event(event, cx) {
195                    if let Some(ix) = this
196                        .panels
197                        .iter()
198                        .position(|item| item.panel.id() == view.id())
199                    {
200                        this.activate_item(ix, cx);
201                    }
202                } else if view.read(cx).should_close_on_event(event, cx) {
203                    cx.emit(Event::Close);
204                }
205            }),
206        ];
207
208        let dock_view_id = cx.view_id();
209        self.panels.push(PanelEntry {
210            panel: Rc::new(panel),
211            context_menu: cx.add_view(|cx| {
212                let mut menu = ContextMenu::new(dock_view_id, cx);
213                menu.set_position_mode(OverlayPositionMode::Local);
214                menu
215            }),
216            _subscriptions: subscriptions,
217        });
218        cx.notify()
219    }
220
221    pub fn remove_panel<T: Panel>(&mut self, panel: &ViewHandle<T>, cx: &mut ViewContext<Self>) {
222        if let Some(panel_ix) = self
223            .panels
224            .iter()
225            .position(|item| item.panel.id() == panel.id())
226        {
227            if panel_ix == self.active_item_ix {
228                self.active_item_ix = 0;
229                cx.emit(Event::Close);
230            }
231            self.panels.remove(panel_ix);
232            cx.notify();
233        }
234    }
235
236    pub fn panels_len(&self) -> usize {
237        self.panels.len()
238    }
239
240    pub fn activate_item(&mut self, item_ix: usize, cx: &mut ViewContext<Self>) {
241        self.active_item_ix = item_ix;
242        cx.notify();
243    }
244
245    pub fn toggle_item(&mut self, item_ix: usize, cx: &mut ViewContext<Self>) {
246        if self.active_item_ix == item_ix {
247            self.is_open = false;
248        } else {
249            self.active_item_ix = item_ix;
250        }
251        cx.notify();
252    }
253
254    pub fn active_item(&self) -> Option<&Rc<dyn PanelHandle>> {
255        if self.is_open {
256            self.panels.get(self.active_item_ix).map(|item| &item.panel)
257        } else {
258            None
259        }
260    }
261}
262
263impl Entity for Dock {
264    type Event = Event;
265}
266
267impl View for Dock {
268    fn ui_name() -> &'static str {
269        "Dock"
270    }
271
272    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
273        if let Some(active_item) = self.active_item() {
274            enum ResizeHandleTag {}
275            let style = &cx.global::<Settings>().theme.workspace.dock;
276            ChildView::new(active_item.as_any(), cx)
277                .contained()
278                .with_style(style.container)
279                .with_resize_handle::<ResizeHandleTag>(
280                    self.position as usize,
281                    self.position.to_resizable_side(),
282                    4.,
283                    style.initial_size,
284                    cx,
285                )
286                .into_any()
287        } else {
288            Empty::new().into_any()
289        }
290    }
291}
292
293impl PanelButtons {
294    pub fn new(
295        dock: ViewHandle<Dock>,
296        workspace: WeakViewHandle<Workspace>,
297        cx: &mut ViewContext<Self>,
298    ) -> Self {
299        cx.observe(&dock, |_, _, cx| cx.notify()).detach();
300        Self { dock, workspace }
301    }
302}
303
304impl Entity for PanelButtons {
305    type Event = ();
306}
307
308impl View for PanelButtons {
309    fn ui_name() -> &'static str {
310        "PanelButtons"
311    }
312
313    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
314        let theme = &cx.global::<Settings>().theme;
315        let tooltip_style = theme.tooltip.clone();
316        let theme = &theme.workspace.status_bar.panel_buttons;
317        let item_style = theme.button.clone();
318        let dock = self.dock.read(cx);
319        let active_ix = dock.active_item_ix;
320        let is_open = dock.is_open;
321        let dock_position = dock.position;
322        let group_style = match dock_position {
323            DockPosition::Left => theme.group_left,
324            DockPosition::Bottom => theme.group_bottom,
325            DockPosition::Right => theme.group_right,
326        };
327        let menu_corner = match dock_position {
328            DockPosition::Left => AnchorCorner::BottomLeft,
329            DockPosition::Bottom | DockPosition::Right => AnchorCorner::BottomRight,
330        };
331
332        let items = dock
333            .panels
334            .iter()
335            .map(|item| (item.panel.clone(), item.context_menu.clone()))
336            .collect::<Vec<_>>();
337        Flex::row()
338            .with_children(
339                items
340                    .into_iter()
341                    .enumerate()
342                    .map(|(ix, (view, context_menu))| {
343                        let action = TogglePanel {
344                            dock_position,
345                            item_index: ix,
346                        };
347
348                        Stack::new()
349                            .with_child(
350                                MouseEventHandler::<Self, _>::new(ix, cx, |state, cx| {
351                                    let is_active = is_open && ix == active_ix;
352                                    let style = item_style.style_for(state, is_active);
353                                    Flex::row()
354                                        .with_child(
355                                            Svg::new(view.icon_path(cx))
356                                                .with_color(style.icon_color)
357                                                .constrained()
358                                                .with_width(style.icon_size)
359                                                .aligned(),
360                                        )
361                                        .with_children(if let Some(label) = view.icon_label(cx) {
362                                            Some(
363                                                Label::new(label, style.label.text.clone())
364                                                    .contained()
365                                                    .with_style(style.label.container)
366                                                    .aligned(),
367                                            )
368                                        } else {
369                                            None
370                                        })
371                                        .constrained()
372                                        .with_height(style.icon_size)
373                                        .contained()
374                                        .with_style(style.container)
375                                })
376                                .with_cursor_style(CursorStyle::PointingHand)
377                                .on_click(MouseButton::Left, {
378                                    let action = action.clone();
379                                    move |_, this, cx| {
380                                        if let Some(workspace) = this.workspace.upgrade(cx) {
381                                            let action = action.clone();
382                                            cx.window_context().defer(move |cx| {
383                                                workspace.update(cx, |workspace, cx| {
384                                                    workspace.toggle_panel(&action, cx)
385                                                });
386                                            });
387                                        }
388                                    }
389                                })
390                                .on_click(MouseButton::Right, {
391                                    let view = view.clone();
392                                    let menu = context_menu.clone();
393                                    move |_, _, cx| {
394                                        const POSITIONS: [DockPosition; 3] = [
395                                            DockPosition::Left,
396                                            DockPosition::Right,
397                                            DockPosition::Bottom,
398                                        ];
399
400                                        menu.update(cx, |menu, cx| {
401                                            let items = POSITIONS
402                                                .into_iter()
403                                                .filter(|position| {
404                                                    *position != dock_position
405                                                        && view.position_is_valid(*position, cx)
406                                                })
407                                                .map(|position| {
408                                                    let view = view.clone();
409                                                    ContextMenuItem::handler(
410                                                        format!("Dock {}", position.to_label()),
411                                                        move |cx| view.set_position(position, cx),
412                                                    )
413                                                })
414                                                .collect();
415                                            menu.show(Default::default(), menu_corner, items, cx);
416                                        })
417                                    }
418                                })
419                                .with_tooltip::<Self>(
420                                    ix,
421                                    view.icon_tooltip(cx),
422                                    Some(Box::new(action)),
423                                    tooltip_style.clone(),
424                                    cx,
425                                ),
426                            )
427                            .with_child(ChildView::new(&context_menu, cx))
428                    }),
429            )
430            .contained()
431            .with_style(group_style)
432            .into_any()
433    }
434}
435
436impl StatusItemView for PanelButtons {
437    fn set_active_pane_item(
438        &mut self,
439        _: Option<&dyn crate::ItemHandle>,
440        _: &mut ViewContext<Self>,
441    ) {
442    }
443}