dock.rs

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