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 let style = &cx.global::<Settings>().theme.workspace.sidebar;
193 ChildView::new(active_item.to_any())
194 .contained()
195 .with_style(style.container)
196 .with_resize_handle::<ResizeHandleTag, _>(
197 self.sidebar_side as usize,
198 self.sidebar_side.to_resizable_side(),
199 4.,
200 style.initial_size,
201 cx,
202 )
203 .boxed()
204 } else {
205 Empty::new().boxed()
206 }
207 }
208}
209
210impl SidebarButtons {
211 pub fn new(sidebar: ViewHandle<Sidebar>, cx: &mut ViewContext<Self>) -> Self {
212 cx.observe(&sidebar, |_, _, cx| cx.notify()).detach();
213 Self { sidebar }
214 }
215}
216
217impl Entity for SidebarButtons {
218 type Event = ();
219}
220
221impl View for SidebarButtons {
222 fn ui_name() -> &'static str {
223 "SidebarToggleButton"
224 }
225
226 fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
227 let theme = &cx.global::<Settings>().theme;
228 let tooltip_style = theme.tooltip.clone();
229 let theme = &theme.workspace.status_bar.sidebar_buttons;
230 let sidebar = self.sidebar.read(cx);
231 let item_style = theme.item;
232 let badge_style = theme.badge;
233 let active_ix = sidebar.active_item_ix;
234 let is_open = sidebar.is_open;
235 let sidebar_side = sidebar.sidebar_side;
236 let group_style = match sidebar_side {
237 SidebarSide::Left => theme.group_left,
238 SidebarSide::Right => theme.group_right,
239 };
240
241 #[allow(clippy::needless_collect)]
242 let items = sidebar
243 .items
244 .iter()
245 .map(|item| (item.icon_path, item.tooltip.clone(), item.view.clone()))
246 .collect::<Vec<_>>();
247
248 Flex::row()
249 .with_children(items.into_iter().enumerate().map(
250 |(ix, (icon_path, tooltip, item_view))| {
251 let action = ToggleSidebarItem {
252 sidebar_side,
253 item_index: ix,
254 };
255 MouseEventHandler::<Self>::new(ix, cx, move |state, cx| {
256 let is_active = is_open && ix == active_ix;
257 let style = item_style.style_for(state, is_active);
258 Stack::new()
259 .with_child(Svg::new(icon_path).with_color(style.icon_color).boxed())
260 .with_children(if !is_active && item_view.should_show_badge(cx) {
261 Some(
262 Empty::new()
263 .collapsed()
264 .contained()
265 .with_style(badge_style)
266 .aligned()
267 .bottom()
268 .right()
269 .boxed(),
270 )
271 } else {
272 None
273 })
274 .constrained()
275 .with_width(style.icon_size)
276 .with_height(style.icon_size)
277 .contained()
278 .with_style(style.container)
279 .boxed()
280 })
281 .with_cursor_style(CursorStyle::PointingHand)
282 .on_click(MouseButton::Left, {
283 let action = action.clone();
284 move |_, cx| cx.dispatch_action(action.clone())
285 })
286 .with_tooltip::<Self, _>(
287 ix,
288 tooltip,
289 Some(Box::new(action)),
290 tooltip_style.clone(),
291 cx,
292 )
293 .boxed()
294 },
295 ))
296 .contained()
297 .with_style(group_style)
298 .boxed()
299 }
300}
301
302impl StatusItemView for SidebarButtons {
303 fn set_active_pane_item(
304 &mut self,
305 _: Option<&dyn crate::ItemHandle>,
306 _: &mut ViewContext<Self>,
307 ) {
308 }
309}