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