1use crate::{h_flex, v_flex, ActiveTheme as _, Collapsible, Icon, IconName, StyledExt};
2use gpui::{
3 div, percentage, prelude::FluentBuilder as _, AnyElement, App, ClickEvent, ElementId,
4 InteractiveElement as _, IntoElement, ParentElement as _, RenderOnce, SharedString,
5 StatefulInteractiveElement as _, Styled as _, Window,
6};
7use std::rc::Rc;
8
9#[derive(IntoElement)]
11pub struct SidebarMenu {
12 collapsed: bool,
13 items: Vec<SidebarMenuItem>,
14}
15
16impl SidebarMenu {
17 pub fn new() -> Self {
19 Self {
20 items: Vec::new(),
21 collapsed: false,
22 }
23 }
24
25 pub fn child(mut self, child: impl Into<SidebarMenuItem>) -> Self {
29 self.items.push(child.into());
30 self
31 }
32
33 pub fn children(
35 mut self,
36 children: impl IntoIterator<Item = impl Into<SidebarMenuItem>>,
37 ) -> Self {
38 self.items = children.into_iter().map(Into::into).collect();
39 self
40 }
41}
42
43impl Collapsible for SidebarMenu {
44 fn is_collapsed(&self) -> bool {
45 self.collapsed
46 }
47
48 fn collapsed(mut self, collapsed: bool) -> Self {
49 self.collapsed = collapsed;
50 self
51 }
52}
53
54impl RenderOnce for SidebarMenu {
55 fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
56 v_flex().gap_2().children(
57 self.items
58 .into_iter()
59 .enumerate()
60 .map(|(ix, item)| item.id(ix).collapsed(self.collapsed)),
61 )
62 }
63}
64
65#[derive(IntoElement)]
67pub struct SidebarMenuItem {
68 id: ElementId,
69 icon: Option<Icon>,
70 label: SharedString,
71 handler: Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>,
72 active: bool,
73 collapsed: bool,
74 children: Vec<Self>,
75 suffix: Option<AnyElement>,
76}
77
78impl SidebarMenuItem {
79 pub fn new(label: impl Into<SharedString>) -> Self {
81 Self {
82 id: ElementId::Integer(0),
83 icon: None,
84 label: label.into(),
85 handler: Rc::new(|_, _, _| {}),
86 active: false,
87 collapsed: false,
88 children: Vec::new(),
89 suffix: None,
90 }
91 }
92
93 pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
95 self.icon = Some(icon.into());
96 self
97 }
98
99 pub fn active(mut self, active: bool) -> Self {
101 self.active = active;
102 self
103 }
104
105 pub fn on_click(
107 mut self,
108 handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
109 ) -> Self {
110 self.handler = Rc::new(handler);
111 self
112 }
113
114 pub fn collapsed(mut self, collapsed: bool) -> Self {
116 self.collapsed = collapsed;
117 self
118 }
119
120 pub fn children(mut self, children: impl IntoIterator<Item = impl Into<Self>>) -> Self {
121 self.children = children.into_iter().map(Into::into).collect();
122 self
123 }
124
125 pub fn suffix(mut self, suffix: impl IntoElement) -> Self {
127 self.suffix = Some(suffix.into_any_element());
128 self
129 }
130
131 fn id(mut self, id: impl Into<ElementId>) -> Self {
133 self.id = id.into();
134 self
135 }
136
137 fn is_submenu(&self) -> bool {
138 self.children.len() > 0
139 }
140
141 fn is_open(&self) -> bool {
142 if self.is_submenu() {
143 self.active
144 } else {
145 false
146 }
147 }
148}
149
150impl RenderOnce for SidebarMenuItem {
151 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
152 let handler = self.handler.clone();
153 let is_collapsed = self.collapsed;
154 let is_active = self.active;
155 let is_open = self.is_open();
156 let is_submenu = self.is_submenu();
157
158 div()
159 .id(self.id.clone())
160 .w_full()
161 .child(
162 h_flex()
163 .size_full()
164 .id("item")
165 .overflow_x_hidden()
166 .flex_shrink_0()
167 .p_2()
168 .gap_x_2()
169 .rounded(cx.theme().radius)
170 .text_sm()
171 .hover(|this| {
172 if is_active {
173 return this;
174 }
175
176 this.bg(cx.theme().sidebar_accent.opacity(0.8))
177 .text_color(cx.theme().sidebar_accent_foreground)
178 })
179 .when(is_active && !is_submenu, |this| {
180 this.font_medium()
181 .bg(cx.theme().sidebar_accent)
182 .text_color(cx.theme().sidebar_accent_foreground)
183 })
184 .when_some(self.icon.clone(), |this, icon| this.child(icon))
185 .when(is_collapsed, |this| {
186 this.justify_center().when(is_active, |this| {
187 this.bg(cx.theme().sidebar_accent)
188 .text_color(cx.theme().sidebar_accent_foreground)
189 })
190 })
191 .when(!is_collapsed, |this| {
192 this.h_7()
193 .child(
194 h_flex()
195 .flex_1()
196 .gap_x_2()
197 .justify_between()
198 .overflow_x_hidden()
199 .child(
200 h_flex()
201 .flex_1()
202 .overflow_x_hidden()
203 .child(self.label.clone()),
204 )
205 .when_some(self.suffix, |this, suffix| this.child(suffix)),
206 )
207 .when(is_submenu, |this| {
208 this.child(
209 Icon::new(IconName::ChevronRight)
210 .size_4()
211 .when(is_open, |this| this.rotate(percentage(90. / 360.))),
212 )
213 })
214 })
215 .on_click(move |ev, window, cx| handler(ev, window, cx)),
216 )
217 .when(is_submenu && is_open && !is_collapsed, |this| {
218 this.child(
219 v_flex()
220 .id("submenu")
221 .border_l_1()
222 .border_color(cx.theme().sidebar_border)
223 .gap_1()
224 .ml_3p5()
225 .pl_2p5()
226 .py_0p5()
227 .children(
228 self.children
229 .into_iter()
230 .enumerate()
231 .map(|(ix, item)| item.id(ix)),
232 ),
233 )
234 })
235 }
236}