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