sidebar.rs

  1use super::Workspace;
  2use gpui::{elements::*, impl_actions, platform::CursorStyle, AnyViewHandle, RenderContext};
  3use serde::Deserialize;
  4use std::{cell::RefCell, rc::Rc};
  5use theme::Theme;
  6
  7pub struct Sidebar {
  8    side: Side,
  9    items: Vec<Item>,
 10    active_item_ix: Option<usize>,
 11    actual_width: Rc<RefCell<f32>>,
 12    custom_width: Rc<RefCell<f32>>,
 13}
 14
 15#[derive(Clone, Copy, Deserialize)]
 16pub enum Side {
 17    Left,
 18    Right,
 19}
 20
 21struct Item {
 22    icon_path: &'static str,
 23    view: AnyViewHandle,
 24}
 25
 26#[derive(Clone, Deserialize)]
 27pub struct ToggleSidebarItem(pub SidebarItemId);
 28
 29#[derive(Clone, Deserialize)]
 30pub struct ToggleSidebarItemFocus(pub SidebarItemId);
 31
 32impl_actions!(workspace, [ToggleSidebarItem, ToggleSidebarItemFocus]);
 33
 34#[derive(Clone, Deserialize)]
 35pub struct SidebarItemId {
 36    pub side: Side,
 37    pub item_index: usize,
 38}
 39
 40impl Sidebar {
 41    pub fn new(side: Side) -> Self {
 42        Self {
 43            side,
 44            items: Default::default(),
 45            active_item_ix: None,
 46            actual_width: Rc::new(RefCell::new(260.)),
 47            custom_width: Rc::new(RefCell::new(260.)),
 48        }
 49    }
 50
 51    pub fn add_item(&mut self, icon_path: &'static str, view: AnyViewHandle) {
 52        self.items.push(Item { icon_path, view });
 53    }
 54
 55    pub fn activate_item(&mut self, item_ix: usize) {
 56        self.active_item_ix = Some(item_ix);
 57    }
 58
 59    pub fn toggle_item(&mut self, item_ix: usize) {
 60        if self.active_item_ix == Some(item_ix) {
 61            self.active_item_ix = None;
 62        } else {
 63            self.active_item_ix = Some(item_ix);
 64        }
 65    }
 66
 67    pub fn active_item(&self) -> Option<&AnyViewHandle> {
 68        self.active_item_ix
 69            .and_then(|ix| self.items.get(ix))
 70            .map(|item| &item.view)
 71    }
 72
 73    fn theme<'a>(&self, theme: &'a Theme) -> &'a theme::Sidebar {
 74        match self.side {
 75            Side::Left => &theme.workspace.left_sidebar,
 76            Side::Right => &theme.workspace.right_sidebar,
 77        }
 78    }
 79
 80    pub fn render(&self, theme: &Theme, cx: &mut RenderContext<Workspace>) -> ElementBox {
 81        let side = self.side;
 82        let theme = self.theme(theme);
 83
 84        ConstrainedBox::new(
 85            Container::new(
 86                Flex::column()
 87                    .with_children(self.items.iter().enumerate().map(|(item_index, item)| {
 88                        let theme = if Some(item_index) == self.active_item_ix {
 89                            &theme.active_item
 90                        } else {
 91                            &theme.item
 92                        };
 93                        enum SidebarButton {}
 94                        MouseEventHandler::new::<SidebarButton, _, _>(item.view.id(), cx, |_, _| {
 95                            ConstrainedBox::new(
 96                                Align::new(
 97                                    ConstrainedBox::new(
 98                                        Svg::new(item.icon_path)
 99                                            .with_color(theme.icon_color)
100                                            .boxed(),
101                                    )
102                                    .with_height(theme.icon_size)
103                                    .boxed(),
104                                )
105                                .boxed(),
106                            )
107                            .with_height(theme.height)
108                            .boxed()
109                        })
110                        .with_cursor_style(CursorStyle::PointingHand)
111                        .on_mouse_down(move |cx| {
112                            cx.dispatch_action(ToggleSidebarItem(SidebarItemId {
113                                side,
114                                item_index,
115                            }))
116                        })
117                        .boxed()
118                    }))
119                    .boxed(),
120            )
121            .with_style(theme.container)
122            .boxed(),
123        )
124        .with_width(theme.width)
125        .boxed()
126    }
127
128    pub fn render_active_item(
129        &self,
130        theme: &Theme,
131        cx: &mut RenderContext<Workspace>,
132    ) -> Option<ElementBox> {
133        if let Some(active_item) = self.active_item() {
134            let mut container = Flex::row();
135            if matches!(self.side, Side::Right) {
136                container.add_child(self.render_resize_handle(theme, cx));
137            }
138
139            container.add_child(
140                Hook::new(
141                    ConstrainedBox::new(ChildView::new(active_item).boxed())
142                        .with_max_width(*self.custom_width.borrow())
143                        .boxed(),
144                )
145                .on_after_layout({
146                    let actual_width = self.actual_width.clone();
147                    move |size, _| *actual_width.borrow_mut() = size.x()
148                })
149                .flex(1., false)
150                .boxed(),
151            );
152            if matches!(self.side, Side::Left) {
153                container.add_child(self.render_resize_handle(theme, cx));
154            }
155            Some(container.boxed())
156        } else {
157            None
158        }
159    }
160
161    fn render_resize_handle(&self, theme: &Theme, cx: &mut RenderContext<Workspace>) -> ElementBox {
162        let actual_width = self.actual_width.clone();
163        let custom_width = self.custom_width.clone();
164        let side = self.side;
165        MouseEventHandler::new::<Self, _, _>(side as usize, cx, |_, _| {
166            Container::new(Empty::new().boxed())
167                .with_style(self.theme(theme).resize_handle)
168                .boxed()
169        })
170        .with_padding(Padding {
171            left: 4.,
172            right: 4.,
173            ..Default::default()
174        })
175        .with_cursor_style(CursorStyle::ResizeLeftRight)
176        .on_drag(move |delta, cx| {
177            let prev_width = *actual_width.borrow();
178            match side {
179                Side::Left => *custom_width.borrow_mut() = 0f32.max(prev_width + delta.x()),
180                Side::Right => *custom_width.borrow_mut() = 0f32.max(prev_width - delta.x()),
181            }
182
183            cx.notify();
184        })
185        .boxed()
186    }
187}