1use crate::{
2 ActiveTheme as _, Collapsible, Icon, IconName, Sizable as _, StyledExt,
3 button::{Button, ButtonVariants as _},
4 h_flex,
5 menu::{ContextMenuExt, PopupMenu},
6 sidebar::SidebarItem,
7 v_flex,
8};
9use rgpui::{
10 AnyElement, App, ClickEvent, ElementId, InteractiveElement as _, IntoElement,
11 ParentElement as _, SharedString, StatefulInteractiveElement as _, StyleRefinement, Styled,
12 Window, div, percentage, prelude::FluentBuilder,
13};
14use std::rc::Rc;
15
16#[derive(Clone)]
18pub struct SidebarMenu {
19 style: StyleRefinement,
20 collapsed: bool,
21 items: Vec<SidebarMenuItem>,
22}
23
24impl SidebarMenu {
25 pub fn new() -> Self {
27 Self {
28 style: StyleRefinement::default(),
29 items: Vec::new(),
30 collapsed: false,
31 }
32 }
33
34 pub fn child(mut self, child: impl Into<SidebarMenuItem>) -> Self {
38 self.items.push(child.into());
39 self
40 }
41
42 pub fn children(
44 mut self,
45 children: impl IntoIterator<Item = impl Into<SidebarMenuItem>>,
46 ) -> Self {
47 self.items = children.into_iter().map(Into::into).collect();
48 self
49 }
50}
51
52impl Collapsible for SidebarMenu {
53 fn is_collapsed(&self) -> bool {
54 self.collapsed
55 }
56
57 fn collapsed(mut self, collapsed: bool) -> Self {
58 self.collapsed = collapsed;
59 self
60 }
61}
62
63impl SidebarItem for SidebarMenu {
64 fn render(
65 self,
66 id: impl Into<ElementId>,
67 window: &mut Window,
68 cx: &mut App,
69 ) -> impl IntoElement {
70 let id = id.into();
71
72 v_flex()
73 .gap_2()
74 .refine_style(&self.style)
75 .children(self.items.into_iter().enumerate().map(|(ix, item)| {
76 let id = SharedString::from(format!("{}-{}", id, ix));
77 item.collapsed(self.collapsed)
78 .render(id, window, cx)
79 .into_any_element()
80 }))
81 }
82}
83
84impl Styled for SidebarMenu {
85 fn style(&mut self) -> &mut StyleRefinement {
86 &mut self.style
87 }
88}
89
90#[derive(Clone)]
92pub struct SidebarMenuItem {
93 icon: Option<Icon>,
94 label: SharedString,
95 handler: Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>,
96 active: bool,
97 default_open: bool,
98 click_to_open: bool,
99 collapsed: bool,
100 click_to_toggle: bool,
101 children: Vec<Self>,
102 suffix: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyElement + 'static>>,
103 disabled: bool,
104 context_menu: Option<Rc<dyn Fn(PopupMenu, &mut Window, &mut App) -> PopupMenu + 'static>>,
105}
106
107impl SidebarMenuItem {
108 pub fn new(label: impl Into<SharedString>) -> Self {
110 Self {
111 icon: None,
112 label: label.into(),
113 handler: Rc::new(|_, _, _| {}),
114 active: false,
115 collapsed: false,
116 default_open: false,
117 click_to_open: false,
118 click_to_toggle: false,
119 children: Vec::new(),
120 suffix: None,
121 disabled: false,
122 context_menu: None,
123 }
124 }
125
126 pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
128 self.icon = Some(icon.into());
129 self
130 }
131
132 pub fn active(mut self, active: bool) -> Self {
134 self.active = active;
135 self
136 }
137
138 pub fn on_click(
140 mut self,
141 handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
142 ) -> Self {
143 self.handler = Rc::new(handler);
144 self
145 }
146
147 pub fn collapsed(mut self, collapsed: bool) -> Self {
149 self.collapsed = collapsed;
150 self
151 }
152
153 pub fn default_open(mut self, open: bool) -> Self {
157 self.default_open = open;
158 self
159 }
160
161 pub fn click_to_open(mut self, click_to_open: bool) -> Self {
167 self.click_to_open = click_to_open;
168 self
169 }
170
171 pub fn click_to_toggle(mut self, click_to_toggle: bool) -> Self {
177 self.click_to_toggle = click_to_toggle;
178 self
179 }
180
181 pub fn children(mut self, children: impl IntoIterator<Item = impl Into<Self>>) -> Self {
182 self.children = children.into_iter().map(Into::into).collect();
183 self
184 }
185
186 pub fn suffix<F, E>(mut self, builder: F) -> Self
188 where
189 F: Fn(&mut Window, &mut App) -> E + 'static,
190 E: IntoElement,
191 {
192 self.suffix = Some(Rc::new(move |window, cx| {
193 builder(window, cx).into_any_element()
194 }));
195 self
196 }
197
198 pub fn disable(mut self, disable: bool) -> Self {
200 self.disabled = disable;
201 self
202 }
203
204 fn is_submenu(&self) -> bool {
205 self.children.len() > 0
206 }
207
208 pub fn context_menu(
210 mut self,
211 f: impl Fn(PopupMenu, &mut Window, &mut App) -> PopupMenu + 'static,
212 ) -> Self {
213 self.context_menu = Some(Rc::new(f));
214 self
215 }
216}
217
218impl FluentBuilder for SidebarMenuItem {}
219
220impl Collapsible for SidebarMenuItem {
221 fn is_collapsed(&self) -> bool {
222 self.collapsed
223 }
224
225 fn collapsed(mut self, collapsed: bool) -> Self {
226 self.collapsed = collapsed;
227 self
228 }
229}
230
231impl SidebarItem for SidebarMenuItem {
232 fn render(
233 self,
234 id: impl Into<ElementId>,
235 window: &mut Window,
236 cx: &mut App,
237 ) -> impl IntoElement {
238 let click_to_open = self.click_to_open;
239 let click_to_toggle = self.click_to_toggle;
240 let default_open = self.default_open;
241 let id = id.into();
242 let is_submenu = self.is_submenu();
243 let open_state = if is_submenu {
244 Some(window.use_keyed_state(id.clone(), cx, |_, _| default_open))
245 } else {
246 None
247 };
248 let handler = self.handler.clone();
249 let is_collapsed = self.collapsed;
250 let is_active = self.active;
251 let is_hoverable = !is_active && !self.disabled;
252 let is_disabled = self.disabled;
253 let is_open = open_state
254 .as_ref()
255 .map_or(false, |s| !is_collapsed && *s.read(cx));
256
257 div()
258 .id(id.clone())
259 .w_full()
260 .child(
261 h_flex()
262 .size_full()
263 .id("item")
264 .overflow_x_hidden()
265 .flex_shrink_0()
266 .p_2()
267 .gap_x_2()
268 .rounded(cx.theme().radius)
269 .text_sm()
270 .when(is_hoverable, |this| {
271 this.hover(|this| {
272 this.bg(cx.theme().sidebar_accent.opacity(0.8))
273 .text_color(cx.theme().sidebar_accent_foreground)
274 })
275 })
276 .when(is_active, |this| {
277 this.font_medium()
278 .bg(cx.theme().sidebar_accent)
279 .text_color(cx.theme().sidebar_accent_foreground)
280 })
281 .when_some(self.icon.clone(), |this, icon| this.child(icon))
282 .when(is_collapsed, |this| {
283 this.justify_center().when(is_active, |this| {
284 this.bg(cx.theme().sidebar_accent)
285 .text_color(cx.theme().sidebar_accent_foreground)
286 })
287 })
288 .when(!is_collapsed, |this| {
289 this.h_7()
290 .child(
291 h_flex()
292 .flex_1()
293 .gap_x_2()
294 .justify_between()
295 .overflow_x_hidden()
296 .child(
297 h_flex()
298 .flex_1()
299 .overflow_x_hidden()
300 .child(self.label.clone()),
301 )
302 .when_some(self.suffix.clone(), |this, suffix| {
303 this.child(suffix(window, cx).into_any_element())
304 }),
305 )
306 .when_some(open_state.clone(), |this, open_state| {
307 this.child(
308 Button::new("caret")
309 .xsmall()
310 .ghost()
311 .icon(
312 Icon::new(IconName::ChevronRight)
313 .size_4()
314 .when(is_open, |this| {
315 this.rotate(percentage(90. / 360.))
316 }),
317 )
318 .on_click({
319 move |_, _, cx| {
320 cx.stop_propagation();
322 open_state.update(cx, |is_open, cx| {
323 *is_open = !*is_open;
324 cx.notify();
325 })
326 }
327 }),
328 )
329 })
330 })
331 .when(is_disabled, |this| {
332 this.text_color(cx.theme().muted_foreground)
333 })
334 .when(!is_disabled, |this| {
335 this.on_click({
336 let open_state = open_state.clone();
337 move |ev, window, cx| {
338 if click_to_open {
339 if let Some(ref s) = open_state {
340 s.update(cx, |is_open: &mut bool, cx| {
341 *is_open = true;
342 cx.notify();
343 });
344 }
345 } else if click_to_toggle {
346 if let Some(ref s) = open_state {
347 s.update(cx, |is_open: &mut bool, cx| {
348 *is_open = !*is_open;
349 cx.notify();
350 });
351 }
352 }
353 handler(ev, window, cx)
354 }
355 })
356 })
357 .map(|this| {
358 if let Some(context_menu) = self.context_menu {
359 this.context_menu(move |menu, window, cx| {
360 context_menu(menu, window, cx)
361 })
362 .into_any_element()
363 } else {
364 this.into_any_element()
365 }
366 }),
367 )
368 .when(is_open, |this| {
369 this.child(
370 v_flex()
371 .id("submenu")
372 .border_l_1()
373 .border_color(cx.theme().sidebar_border)
374 .gap_1()
375 .ml_3p5()
376 .pl_2p5()
377 .py_0p5()
378 .children(self.children.into_iter().enumerate().map(|(ix, item)| {
379 let id = format!("{}-{}", id, ix);
380 item.render(id, window, cx).into_any_element()
381 })),
382 )
383 })
384 }
385}