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