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