1use crate::{
2 button::{Button, ButtonVariants as _},
3 h_flex, v_flex, ActiveTheme as _, Collapsible, Icon, IconName, Sizable as _, StyledExt,
4};
5use gpui::{
6 div, percentage, prelude::FluentBuilder as _, AnyElement, App, ClickEvent, ElementId,
7 InteractiveElement as _, IntoElement, ParentElement as _, RenderOnce, SharedString,
8 StatefulInteractiveElement as _, StyleRefinement, Styled, Window,
9};
10use std::rc::Rc;
11
12#[derive(IntoElement)]
14pub struct SidebarMenu {
15 style: StyleRefinement,
16 collapsed: bool,
17 items: Vec<SidebarMenuItem>,
18}
19
20impl SidebarMenu {
21 pub fn new() -> Self {
23 Self {
24 style: StyleRefinement::default(),
25 items: Vec::new(),
26 collapsed: false,
27 }
28 }
29
30 pub fn child(mut self, child: impl Into<SidebarMenuItem>) -> Self {
34 self.items.push(child.into());
35 self
36 }
37
38 pub fn children(
40 mut self,
41 children: impl IntoIterator<Item = impl Into<SidebarMenuItem>>,
42 ) -> Self {
43 self.items = children.into_iter().map(Into::into).collect();
44 self
45 }
46}
47
48impl Collapsible for SidebarMenu {
49 fn is_collapsed(&self) -> bool {
50 self.collapsed
51 }
52
53 fn collapsed(mut self, collapsed: bool) -> Self {
54 self.collapsed = collapsed;
55 self
56 }
57}
58
59impl Styled for SidebarMenu {
60 fn style(&mut self) -> &mut StyleRefinement {
61 &mut self.style
62 }
63}
64
65impl RenderOnce for SidebarMenu {
66 fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
67 v_flex().gap_2().refine_style(&self.style).children(
68 self.items
69 .into_iter()
70 .enumerate()
71 .map(|(ix, item)| item.id(ix).collapsed(self.collapsed)),
72 )
73 }
74}
75
76#[derive(IntoElement)]
78pub struct SidebarMenuItem {
79 id: ElementId,
80 icon: Option<Icon>,
81 label: SharedString,
82 handler: Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>,
83 active: bool,
84 default_open: bool,
85 click_to_open: bool,
86 collapsed: bool,
87 children: Vec<Self>,
88 suffix: Option<AnyElement>,
89 disabled: bool,
90}
91
92impl SidebarMenuItem {
93 pub fn new(label: impl Into<SharedString>) -> Self {
95 Self {
96 id: ElementId::Integer(0),
97 icon: None,
98 label: label.into(),
99 handler: Rc::new(|_, _, _| {}),
100 active: false,
101 collapsed: false,
102 default_open: false,
103 click_to_open: false,
104 children: Vec::new(),
105 suffix: None,
106 disabled: false,
107 }
108 }
109
110 pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
112 self.icon = Some(icon.into());
113 self
114 }
115
116 pub fn active(mut self, active: bool) -> Self {
118 self.active = active;
119 self
120 }
121
122 pub fn on_click(
124 mut self,
125 handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
126 ) -> Self {
127 self.handler = Rc::new(handler);
128 self
129 }
130
131 pub fn collapsed(mut self, collapsed: bool) -> Self {
133 self.collapsed = collapsed;
134 self
135 }
136
137 pub fn default_open(mut self, open: bool) -> Self {
141 self.default_open = open;
142 self
143 }
144
145 pub fn click_to_open(mut self, click_to_open: bool) -> Self {
151 self.click_to_open = click_to_open;
152 self
153 }
154
155 pub fn children(mut self, children: impl IntoIterator<Item = impl Into<Self>>) -> Self {
156 self.children = children.into_iter().map(Into::into).collect();
157 self
158 }
159
160 pub fn suffix(mut self, suffix: impl IntoElement) -> Self {
162 self.suffix = Some(suffix.into_any_element());
163 self
164 }
165
166 pub fn disable(mut self, disable: bool) -> Self {
168 self.disabled = disable;
169 self
170 }
171
172 fn id(mut self, id: impl Into<ElementId>) -> Self {
174 self.id = id.into();
175 self
176 }
177
178 fn is_submenu(&self) -> bool {
179 self.children.len() > 0
180 }
181}
182
183impl RenderOnce for SidebarMenuItem {
184 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
185 let click_to_open = self.click_to_open;
186 let default_open = self.default_open;
187 let open_state = window.use_keyed_state(self.id.clone(), cx, |_, _| default_open);
188
189 let handler = self.handler.clone();
190 let is_collapsed = self.collapsed;
191 let is_active = self.active;
192 let is_hoverable = !is_active && !self.disabled;
193 let is_disabled = self.disabled;
194 let is_submenu = self.is_submenu();
195 let is_open = is_submenu && !is_collapsed && *open_state.read(cx);
196
197 div()
198 .id(self.id.clone())
199 .w_full()
200 .child(
201 h_flex()
202 .size_full()
203 .id("item")
204 .overflow_x_hidden()
205 .flex_shrink_0()
206 .p_2()
207 .gap_x_2()
208 .rounded(cx.theme().radius)
209 .text_sm()
210 .when(is_hoverable, |this| {
211 this.hover(|this| {
212 this.bg(cx.theme().sidebar_accent.opacity(0.8))
213 .text_color(cx.theme().sidebar_accent_foreground)
214 })
215 })
216 .when(is_active, |this| {
217 this.font_medium()
218 .bg(cx.theme().sidebar_accent)
219 .text_color(cx.theme().sidebar_accent_foreground)
220 })
221 .when_some(self.icon.clone(), |this, icon| this.child(icon))
222 .when(is_collapsed, |this| {
223 this.justify_center().when(is_active, |this| {
224 this.bg(cx.theme().sidebar_accent)
225 .text_color(cx.theme().sidebar_accent_foreground)
226 })
227 })
228 .when(!is_collapsed, |this| {
229 this.h_7()
230 .child(
231 h_flex()
232 .flex_1()
233 .gap_x_2()
234 .justify_between()
235 .overflow_x_hidden()
236 .child(
237 h_flex()
238 .flex_1()
239 .overflow_x_hidden()
240 .child(self.label.clone()),
241 )
242 .when_some(self.suffix, |this, suffix| this.child(suffix)),
243 )
244 .when(is_submenu, |this| {
245 this.child(
246 Button::new("caret")
247 .xsmall()
248 .ghost()
249 .icon(
250 Icon::new(IconName::ChevronRight)
251 .size_4()
252 .when(is_open, |this| {
253 this.rotate(percentage(90. / 360.))
254 }),
255 )
256 .on_click({
257 let open_state = open_state.clone();
258 move |_, _, cx| {
259 cx.stop_propagation();
261 open_state.update(cx, |is_open, cx| {
262 *is_open = !*is_open;
263 cx.notify();
264 })
265 }
266 }),
267 )
268 })
269 })
270 .when(is_disabled, |this| {
271 this.text_color(cx.theme().muted_foreground)
272 })
273 .when(!is_disabled, |this| {
274 this.on_click({
275 let open_state = open_state.clone();
276 move |ev, window, cx| {
277 if click_to_open {
278 open_state.update(cx, |is_open, cx| {
279 *is_open = true;
280 cx.notify();
281 });
282 }
283
284 handler(ev, window, cx)
285 }
286 })
287 }),
288 )
289 .when(is_open, |this| {
290 this.child(
291 v_flex()
292 .id("submenu")
293 .border_l_1()
294 .border_color(cx.theme().sidebar_border)
295 .gap_1()
296 .ml_3p5()
297 .pl_2p5()
298 .py_0p5()
299 .children(
300 self.children
301 .into_iter()
302 .enumerate()
303 .map(|(ix, item)| item.id(ix)),
304 ),
305 )
306 })
307 }
308}