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