1use crate::StatusItemView;
2use gpui::{
3 elements::*, impl_actions, platform::CursorStyle, AnyViewHandle, AppContext, Entity,
4 MouseButton, RenderContext, Subscription, View, ViewContext, ViewHandle,
5};
6use serde::Deserialize;
7use settings::Settings;
8use std::rc::Rc;
9
10pub trait SidebarItem: View {
11 fn should_activate_item_on_event(&self, _: &Self::Event, _: &AppContext) -> bool {
12 false
13 }
14 fn should_show_badge(&self, cx: &AppContext) -> bool;
15 fn contains_focused_view(&self, _: &AppContext) -> bool {
16 false
17 }
18}
19
20pub trait SidebarItemHandle {
21 fn id(&self) -> usize;
22 fn should_show_badge(&self, cx: &AppContext) -> bool;
23 fn is_focused(&self, cx: &AppContext) -> bool;
24 fn to_any(&self) -> AnyViewHandle;
25}
26
27impl<T> SidebarItemHandle for ViewHandle<T>
28where
29 T: SidebarItem,
30{
31 fn id(&self) -> usize {
32 self.id()
33 }
34
35 fn should_show_badge(&self, cx: &AppContext) -> bool {
36 self.read(cx).should_show_badge(cx)
37 }
38
39 fn is_focused(&self, cx: &AppContext) -> bool {
40 ViewHandle::is_focused(self, cx) || self.read(cx).contains_focused_view(cx)
41 }
42
43 fn to_any(&self) -> AnyViewHandle {
44 self.into()
45 }
46}
47
48impl From<&dyn SidebarItemHandle> for AnyViewHandle {
49 fn from(val: &dyn SidebarItemHandle) -> Self {
50 val.to_any()
51 }
52}
53
54pub struct Sidebar {
55 sidebar_side: SidebarSide,
56 items: Vec<Item>,
57 is_open: bool,
58 active_item_ix: usize,
59}
60
61#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
62pub enum SidebarSide {
63 Left,
64 Right,
65}
66
67impl SidebarSide {
68 fn to_resizable_side(self) -> Side {
69 match self {
70 Self::Left => Side::Right,
71 Self::Right => Side::Left,
72 }
73 }
74}
75
76struct Item {
77 icon_path: &'static str,
78 tooltip: String,
79 view: Rc<dyn SidebarItemHandle>,
80 _subscriptions: [Subscription; 2],
81}
82
83pub struct SidebarButtons {
84 sidebar: ViewHandle<Sidebar>,
85}
86
87#[derive(Clone, Debug, Deserialize, PartialEq)]
88pub struct ToggleSidebarItem {
89 pub sidebar_side: SidebarSide,
90 pub item_index: usize,
91}
92
93impl_actions!(workspace, [ToggleSidebarItem]);
94
95impl Sidebar {
96 pub fn new(sidebar_side: SidebarSide) -> Self {
97 Self {
98 sidebar_side,
99 items: Default::default(),
100 active_item_ix: 0,
101 is_open: false,
102 }
103 }
104
105 pub fn is_open(&self) -> bool {
106 self.is_open
107 }
108
109 pub fn active_item_ix(&self) -> usize {
110 self.active_item_ix
111 }
112
113 pub fn set_open(&mut self, open: bool, cx: &mut ViewContext<Self>) {
114 if open != self.is_open {
115 self.is_open = open;
116 cx.notify();
117 }
118 }
119
120 pub fn toggle_open(&mut self, cx: &mut ViewContext<Self>) {
121 if self.is_open {}
122 self.is_open = !self.is_open;
123 cx.notify();
124 }
125
126 pub fn add_item<T: SidebarItem>(
127 &mut self,
128 icon_path: &'static str,
129 tooltip: String,
130 view: ViewHandle<T>,
131 cx: &mut ViewContext<Self>,
132 ) {
133 let subscriptions = [
134 cx.observe(&view, |_, _, cx| cx.notify()),
135 cx.subscribe(&view, |this, view, event, cx| {
136 if view.read(cx).should_activate_item_on_event(event, cx) {
137 if let Some(ix) = this
138 .items
139 .iter()
140 .position(|item| item.view.id() == view.id())
141 {
142 this.activate_item(ix, cx);
143 }
144 }
145 }),
146 ];
147 cx.reparent(&view);
148 self.items.push(Item {
149 icon_path,
150 tooltip,
151 view: Rc::new(view),
152 _subscriptions: subscriptions,
153 });
154 cx.notify()
155 }
156
157 pub fn activate_item(&mut self, item_ix: usize, cx: &mut ViewContext<Self>) {
158 self.active_item_ix = item_ix;
159 cx.notify();
160 }
161
162 pub fn toggle_item(&mut self, item_ix: usize, cx: &mut ViewContext<Self>) {
163 if self.active_item_ix == item_ix {
164 self.is_open = false;
165 } else {
166 self.active_item_ix = item_ix;
167 }
168 cx.notify();
169 }
170
171 pub fn active_item(&self) -> Option<&Rc<dyn SidebarItemHandle>> {
172 if self.is_open {
173 self.items.get(self.active_item_ix).map(|item| &item.view)
174 } else {
175 None
176 }
177 }
178}
179
180impl Entity for Sidebar {
181 type Event = ();
182}
183
184impl View for Sidebar {
185 fn ui_name() -> &'static str {
186 "Sidebar"
187 }
188
189 fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
190 if let Some(active_item) = self.active_item() {
191 enum ResizeHandleTag {}
192 ChildView::new(active_item.to_any())
193 .with_resize_handle::<ResizeHandleTag, _>(
194 self.sidebar_side as usize,
195 self.sidebar_side.to_resizable_side(),
196 // TODO: Expose both of these constants in the theme
197 4.,
198 260.,
199 cx,
200 )
201 .boxed()
202 } else {
203 Empty::new().boxed()
204 }
205 }
206}
207
208impl SidebarButtons {
209 pub fn new(sidebar: ViewHandle<Sidebar>, cx: &mut ViewContext<Self>) -> Self {
210 cx.observe(&sidebar, |_, _, cx| cx.notify()).detach();
211 Self { sidebar }
212 }
213}
214
215impl Entity for SidebarButtons {
216 type Event = ();
217}
218
219impl View for SidebarButtons {
220 fn ui_name() -> &'static str {
221 "SidebarToggleButton"
222 }
223
224 fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
225 let theme = &cx.global::<Settings>().theme;
226 let tooltip_style = theme.tooltip.clone();
227 let theme = &theme.workspace.status_bar.sidebar_buttons;
228 let sidebar = self.sidebar.read(cx);
229 let item_style = theme.item;
230 let badge_style = theme.badge;
231 let active_ix = sidebar.active_item_ix;
232 let is_open = sidebar.is_open;
233 let sidebar_side = sidebar.sidebar_side;
234 let group_style = match sidebar_side {
235 SidebarSide::Left => theme.group_left,
236 SidebarSide::Right => theme.group_right,
237 };
238
239 #[allow(clippy::needless_collect)]
240 let items = sidebar
241 .items
242 .iter()
243 .map(|item| (item.icon_path, item.tooltip.clone(), item.view.clone()))
244 .collect::<Vec<_>>();
245
246 Flex::row()
247 .with_children(items.into_iter().enumerate().map(
248 |(ix, (icon_path, tooltip, item_view))| {
249 let action = ToggleSidebarItem {
250 sidebar_side,
251 item_index: ix,
252 };
253 MouseEventHandler::<Self>::new(ix, cx, move |state, cx| {
254 let is_active = is_open && ix == active_ix;
255 let style = item_style.style_for(state, is_active);
256 Stack::new()
257 .with_child(Svg::new(icon_path).with_color(style.icon_color).boxed())
258 .with_children(if !is_active && item_view.should_show_badge(cx) {
259 Some(
260 Empty::new()
261 .collapsed()
262 .contained()
263 .with_style(badge_style)
264 .aligned()
265 .bottom()
266 .right()
267 .boxed(),
268 )
269 } else {
270 None
271 })
272 .constrained()
273 .with_width(style.icon_size)
274 .with_height(style.icon_size)
275 .contained()
276 .with_style(style.container)
277 .boxed()
278 })
279 .with_cursor_style(CursorStyle::PointingHand)
280 .on_click(MouseButton::Left, {
281 let action = action.clone();
282 move |_, cx| cx.dispatch_action(action.clone())
283 })
284 .with_tooltip::<Self, _>(
285 ix,
286 tooltip,
287 Some(Box::new(action)),
288 tooltip_style.clone(),
289 cx,
290 )
291 .boxed()
292 },
293 ))
294 .contained()
295 .with_style(group_style)
296 .boxed()
297 }
298}
299
300impl StatusItemView for SidebarButtons {
301 fn set_active_pane_item(
302 &mut self,
303 _: Option<&dyn crate::ItemHandle>,
304 _: &mut ViewContext<Self>,
305 ) {
306 }
307}