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}