sidebar.rs

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