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