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