sidebar.rs

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