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 std::rc::Rc;
  8
  9pub trait SidebarItem: View {
 10    fn should_activate_item_on_event(&self, _: &Self::Event, _: &AppContext) -> bool {
 11        false
 12    }
 13    fn should_show_badge(&self, _: &AppContext) -> bool {
 14        false
 15    }
 16    fn contains_focused_view(&self, _: &AppContext) -> bool {
 17        false
 18    }
 19}
 20
 21pub trait SidebarItemHandle {
 22    fn id(&self) -> usize;
 23    fn should_show_badge(&self, cx: &WindowContext) -> bool;
 24    fn is_focused(&self, cx: &WindowContext) -> bool;
 25    fn as_any(&self) -> &AnyViewHandle;
 26}
 27
 28impl<T> SidebarItemHandle for ViewHandle<T>
 29where
 30    T: SidebarItem,
 31{
 32    fn id(&self) -> usize {
 33        self.id()
 34    }
 35
 36    fn should_show_badge(&self, cx: &WindowContext) -> bool {
 37        self.read(cx).should_show_badge(cx)
 38    }
 39
 40    fn is_focused(&self, cx: &WindowContext) -> bool {
 41        ViewHandle::is_focused(self, cx) || self.read(cx).contains_focused_view(cx)
 42    }
 43
 44    fn as_any(&self) -> &AnyViewHandle {
 45        self
 46    }
 47}
 48
 49impl From<&dyn SidebarItemHandle> for AnyViewHandle {
 50    fn from(val: &dyn SidebarItemHandle) -> Self {
 51        val.as_any().clone()
 52    }
 53}
 54
 55pub struct Sidebar {
 56    sidebar_side: SidebarSide,
 57    items: Vec<Item>,
 58    is_open: bool,
 59    active_item_ix: usize,
 60}
 61
 62#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
 63pub enum SidebarSide {
 64    Left,
 65    Right,
 66}
 67
 68impl SidebarSide {
 69    fn to_resizable_side(self) -> Side {
 70        match self {
 71            Self::Left => Side::Right,
 72            Self::Right => Side::Left,
 73        }
 74    }
 75}
 76
 77struct Item {
 78    icon_path: &'static str,
 79    tooltip: String,
 80    view: Rc<dyn SidebarItemHandle>,
 81    _subscriptions: [Subscription; 2],
 82}
 83
 84pub struct SidebarButtons {
 85    sidebar: ViewHandle<Sidebar>,
 86    workspace: WeakViewHandle<Workspace>,
 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
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 = &theme::current(cx).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(
214        sidebar: ViewHandle<Sidebar>,
215        workspace: WeakViewHandle<Workspace>,
216        cx: &mut ViewContext<Self>,
217    ) -> Self {
218        cx.observe(&sidebar, |_, _, cx| cx.notify()).detach();
219        Self { sidebar, workspace }
220    }
221}
222
223impl Entity for SidebarButtons {
224    type Event = ();
225}
226
227impl View for SidebarButtons {
228    fn ui_name() -> &'static str {
229        "SidebarToggleButton"
230    }
231
232    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
233        let theme = &theme::current(cx);
234        let tooltip_style = theme.tooltip.clone();
235        let theme = &theme.workspace.status_bar.sidebar_buttons;
236        let sidebar = self.sidebar.read(cx);
237        let item_style = theme.item.clone();
238        let badge_style = theme.badge;
239        let active_ix = sidebar.active_item_ix;
240        let is_open = sidebar.is_open;
241        let sidebar_side = sidebar.sidebar_side;
242        let group_style = match sidebar_side {
243            SidebarSide::Left => theme.group_left,
244            SidebarSide::Right => theme.group_right,
245        };
246
247        #[allow(clippy::needless_collect)]
248        let items = sidebar
249            .items
250            .iter()
251            .map(|item| (item.icon_path, item.tooltip.clone(), item.view.clone()))
252            .collect::<Vec<_>>();
253
254        Flex::row()
255            .with_children(items.into_iter().enumerate().map(
256                |(ix, (icon_path, tooltip, item_view))| {
257                    let action = ToggleSidebarItem {
258                        sidebar_side,
259                        item_index: ix,
260                    };
261                    MouseEventHandler::<Self, _>::new(ix, cx, |state, cx| {
262                        let is_active = is_open && ix == active_ix;
263                        let style = item_style.style_for(state, is_active);
264                        Stack::new()
265                            .with_child(Svg::new(icon_path).with_color(style.icon_color))
266                            .with_children(if !is_active && item_view.should_show_badge(cx) {
267                                Some(
268                                    Empty::new()
269                                        .collapsed()
270                                        .contained()
271                                        .with_style(badge_style)
272                                        .aligned()
273                                        .bottom()
274                                        .right(),
275                                )
276                            } else {
277                                None
278                            })
279                            .constrained()
280                            .with_width(style.icon_size)
281                            .with_height(style.icon_size)
282                            .contained()
283                            .with_style(style.container)
284                    })
285                    .with_cursor_style(CursorStyle::PointingHand)
286                    .on_click(MouseButton::Left, {
287                        let action = action.clone();
288                        move |_, this, cx| {
289                            if let Some(workspace) = this.workspace.upgrade(cx) {
290                                let action = action.clone();
291                                cx.window_context().defer(move |cx| {
292                                    workspace.update(cx, |workspace, cx| {
293                                        workspace.toggle_sidebar_item(&action, cx)
294                                    });
295                                });
296                            }
297                        }
298                    })
299                    .with_tooltip::<Self>(
300                        ix,
301                        tooltip,
302                        Some(Box::new(action)),
303                        tooltip_style.clone(),
304                        cx,
305                    )
306                },
307            ))
308            .contained()
309            .with_style(group_style)
310            .into_any()
311    }
312}
313
314impl StatusItemView for SidebarButtons {
315    fn set_active_pane_item(
316        &mut self,
317        _: Option<&dyn crate::ItemHandle>,
318        _: &mut ViewContext<Self>,
319    ) {
320    }
321}