sidebar.rs

  1use crate::StatusItemView;
  2use gpui::{
  3    elements::*, impl_actions, platform::CursorStyle, AnyViewHandle, AppContext, Entity,
  4    RenderContext, Subscription, View, ViewContext, ViewHandle,
  5};
  6use serde::Deserialize;
  7use settings::Settings;
  8use std::{cell::RefCell, rc::Rc};
  9use theme::Theme;
 10
 11pub trait SidebarItem: View {
 12    fn should_show_badge(&self, cx: &AppContext) -> bool;
 13}
 14
 15pub trait SidebarItemHandle {
 16    fn should_show_badge(&self, cx: &AppContext) -> bool;
 17    fn to_any(&self) -> AnyViewHandle;
 18}
 19
 20impl<T> SidebarItemHandle for ViewHandle<T>
 21where
 22    T: SidebarItem,
 23{
 24    fn should_show_badge(&self, cx: &AppContext) -> bool {
 25        self.read(cx).should_show_badge(cx)
 26    }
 27
 28    fn to_any(&self) -> AnyViewHandle {
 29        self.into()
 30    }
 31}
 32
 33impl Into<AnyViewHandle> for &dyn SidebarItemHandle {
 34    fn into(self) -> AnyViewHandle {
 35        self.to_any()
 36    }
 37}
 38
 39pub struct Sidebar {
 40    side: Side,
 41    items: Vec<Item>,
 42    active_item_ix: Option<usize>,
 43    actual_width: Rc<RefCell<f32>>,
 44    custom_width: Rc<RefCell<f32>>,
 45}
 46
 47#[derive(Clone, Copy, Debug, Deserialize)]
 48pub enum Side {
 49    Left,
 50    Right,
 51}
 52
 53struct Item {
 54    icon_path: &'static str,
 55    view: Rc<dyn SidebarItemHandle>,
 56    _observation: Subscription,
 57}
 58
 59pub struct SidebarButtons {
 60    sidebar: ViewHandle<Sidebar>,
 61}
 62
 63#[derive(Clone, Debug, Deserialize)]
 64pub struct ToggleSidebarItem {
 65    pub side: Side,
 66    pub item_index: usize,
 67}
 68
 69#[derive(Clone, Debug, Deserialize)]
 70pub struct ToggleSidebarItemFocus {
 71    pub side: Side,
 72    pub item_index: usize,
 73}
 74
 75impl_actions!(workspace, [ToggleSidebarItem, ToggleSidebarItemFocus]);
 76
 77impl Sidebar {
 78    pub fn new(side: Side) -> Self {
 79        Self {
 80            side,
 81            items: Default::default(),
 82            active_item_ix: None,
 83            actual_width: Rc::new(RefCell::new(260.)),
 84            custom_width: Rc::new(RefCell::new(260.)),
 85        }
 86    }
 87
 88    pub fn add_item<T: SidebarItem>(
 89        &mut self,
 90        icon_path: &'static str,
 91        view: ViewHandle<T>,
 92        cx: &mut ViewContext<Self>,
 93    ) {
 94        let subscription = cx.observe(&view, |_, _, cx| cx.notify());
 95        self.items.push(Item {
 96            icon_path,
 97            view: Rc::new(view),
 98            _observation: subscription,
 99        });
100        cx.notify()
101    }
102
103    pub fn activate_item(&mut self, item_ix: usize, cx: &mut ViewContext<Self>) {
104        self.active_item_ix = Some(item_ix);
105        cx.notify();
106    }
107
108    pub fn toggle_item(&mut self, item_ix: usize, cx: &mut ViewContext<Self>) {
109        if self.active_item_ix == Some(item_ix) {
110            self.active_item_ix = None;
111        } else {
112            self.active_item_ix = Some(item_ix);
113        }
114        cx.notify();
115    }
116
117    pub fn active_item(&self) -> Option<&dyn SidebarItemHandle> {
118        self.active_item_ix
119            .and_then(|ix| self.items.get(ix))
120            .map(|item| item.view.as_ref())
121    }
122
123    fn render_resize_handle(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
124        let actual_width = self.actual_width.clone();
125        let custom_width = self.custom_width.clone();
126        let side = self.side;
127        MouseEventHandler::new::<Self, _, _>(side as usize, cx, |_, _| {
128            Empty::new()
129                .contained()
130                .with_style(theme.workspace.sidebar_resize_handle)
131                .boxed()
132        })
133        .with_padding(Padding {
134            left: 4.,
135            right: 4.,
136            ..Default::default()
137        })
138        .with_cursor_style(CursorStyle::ResizeLeftRight)
139        .on_drag(move |delta, cx| {
140            let prev_width = *actual_width.borrow();
141            *custom_width.borrow_mut() = 0f32
142                .max(match side {
143                    Side::Left => prev_width + delta.x(),
144                    Side::Right => prev_width - delta.x(),
145                })
146                .round();
147
148            cx.notify();
149        })
150        .boxed()
151    }
152}
153
154impl Entity for Sidebar {
155    type Event = ();
156}
157
158impl View for Sidebar {
159    fn ui_name() -> &'static str {
160        "Sidebar"
161    }
162
163    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
164        let theme = cx.global::<Settings>().theme.clone();
165        if let Some(active_item) = self.active_item() {
166            let mut container = Flex::row();
167            if matches!(self.side, Side::Right) {
168                container.add_child(self.render_resize_handle(&theme, cx));
169            }
170
171            container.add_child(
172                Hook::new(
173                    ChildView::new(active_item)
174                        .constrained()
175                        .with_max_width(*self.custom_width.borrow())
176                        .boxed(),
177                )
178                .on_after_layout({
179                    let actual_width = self.actual_width.clone();
180                    move |size, _| *actual_width.borrow_mut() = size.x()
181                })
182                .flex(1., false)
183                .boxed(),
184            );
185            if matches!(self.side, Side::Left) {
186                container.add_child(self.render_resize_handle(&theme, cx));
187            }
188            container.boxed()
189        } else {
190            Empty::new().boxed()
191        }
192    }
193}
194
195impl SidebarButtons {
196    pub fn new(sidebar: ViewHandle<Sidebar>, cx: &mut ViewContext<Self>) -> Self {
197        cx.observe(&sidebar, |_, _, cx| cx.notify()).detach();
198        Self { sidebar }
199    }
200}
201
202impl Entity for SidebarButtons {
203    type Event = ();
204}
205
206impl View for SidebarButtons {
207    fn ui_name() -> &'static str {
208        "SidebarToggleButton"
209    }
210
211    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
212        let theme = &cx
213            .global::<Settings>()
214            .theme
215            .workspace
216            .status_bar
217            .sidebar_buttons;
218        let sidebar = self.sidebar.read(cx);
219        let item_style = theme.item;
220        let badge_style = theme.badge;
221        let active_ix = sidebar.active_item_ix;
222        let side = sidebar.side;
223        let group_style = match side {
224            Side::Left => theme.group_left,
225            Side::Right => theme.group_right,
226        };
227        let items = sidebar
228            .items
229            .iter()
230            .map(|item| (item.icon_path, item.view.clone()))
231            .collect::<Vec<_>>();
232        Flex::row()
233            .with_children(
234                items
235                    .into_iter()
236                    .enumerate()
237                    .map(|(ix, (icon_path, item_view))| {
238                        MouseEventHandler::new::<Self, _, _>(ix, cx, move |state, cx| {
239                            let is_active = Some(ix) == active_ix;
240                            let style = item_style.style_for(state, is_active);
241                            Stack::new()
242                                .with_child(
243                                    Svg::new(icon_path).with_color(style.icon_color).boxed(),
244                                )
245                                .with_children(if !is_active && item_view.should_show_badge(cx) {
246                                    Some(
247                                        Empty::new()
248                                            .collapsed()
249                                            .contained()
250                                            .with_style(badge_style)
251                                            .aligned()
252                                            .bottom()
253                                            .right()
254                                            .boxed(),
255                                    )
256                                } else {
257                                    None
258                                })
259                                .constrained()
260                                .with_width(style.icon_size)
261                                .with_height(style.icon_size)
262                                .contained()
263                                .with_style(style.container)
264                                .boxed()
265                        })
266                        .with_cursor_style(CursorStyle::PointingHand)
267                        .on_click(move |_, cx| {
268                            cx.dispatch_action(ToggleSidebarItem {
269                                side,
270                                item_index: ix,
271                            })
272                        })
273                        .boxed()
274                    }),
275            )
276            .contained()
277            .with_style(group_style)
278            .boxed()
279    }
280}
281
282impl StatusItemView for SidebarButtons {
283    fn set_active_pane_item(
284        &mut self,
285        _: Option<&dyn crate::ItemHandle>,
286        _: &mut ViewContext<Self>,
287    ) {
288    }
289}