1use std::rc::Rc;
2
3use iced::alignment::{Horizontal, Vertical};
4use iced::border::Border;
5use iced::widget::{button as iced_button, column, container, row, text};
6use iced::{Background, Color, Element, Length};
7
8use crate::button::{ButtonProps, ButtonSize, ButtonVariant, button};
9use crate::theme::Theme;
10
11#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
12pub enum SidebarSide {
13 #[default]
14 Left,
15 Right,
16}
17
18pub struct SidebarProviderProps {
19 pub open: bool,
20 pub default_open: bool,
21 pub expanded_width: f32,
22 pub collapsed_width: f32,
23 pub animate: bool,
24}
25
26impl SidebarProviderProps {
27 pub fn new(open: bool) -> Self {
28 let style = crate::theme::ThemeStyles::default().sidebar;
29 Self {
30 open,
31 default_open: true,
32 expanded_width: style.expanded_width,
33 collapsed_width: style.collapsed_width,
34 animate: true,
35 }
36 }
37
38 pub fn default_open(mut self, default_open: bool) -> Self {
39 self.default_open = default_open;
40 self
41 }
42
43 pub fn expanded_width(mut self, width: f32) -> Self {
44 self.expanded_width = width;
45 self
46 }
47
48 pub fn collapsed_width(mut self, width: f32) -> Self {
49 self.collapsed_width = width;
50 self
51 }
52
53 pub fn animate(mut self, animate: bool) -> Self {
54 self.animate = animate;
55 self
56 }
57}
58
59pub struct SidebarContext<'a, Message> {
60 pub open: bool,
61 pub expanded_width: f32,
62 pub collapsed_width: f32,
63 pub animate: bool,
64 on_open_change: Option<Rc<dyn Fn(bool) -> Message + 'a>>,
65}
66
67impl<'a, Message: Clone> SidebarContext<'a, Message> {
68 pub fn is_collapsed(&self) -> bool {
69 !self.open
70 }
71
72 pub fn set_open_message(&self, open: bool) -> Option<Message> {
73 self.on_open_change.as_ref().map(|f| f(open))
74 }
75
76 pub fn toggle_message(&self) -> Option<Message> {
77 self.on_open_change.as_ref().map(|f| f(!self.open))
78 }
79}
80
81#[derive(Clone, Copy, Debug)]
82pub struct SidebarProps {
83 pub side: SidebarSide,
84 pub padding: f32,
85 pub border: bool,
86}
87
88impl SidebarProps {
89 pub fn new() -> Self {
90 Self {
91 side: SidebarSide::Left,
92 padding: 0.0,
93 border: true,
94 }
95 }
96
97 pub fn side(mut self, side: SidebarSide) -> Self {
98 self.side = side;
99 self
100 }
101
102 pub fn padding(mut self, padding: f32) -> Self {
103 self.padding = padding;
104 self
105 }
106
107 pub fn border(mut self, border: bool) -> Self {
108 self.border = border;
109 self
110 }
111}
112
113impl Default for SidebarProps {
114 fn default() -> Self {
115 Self::new()
116 }
117}
118
119pub fn sidebar_provider<'a, Message: Clone + 'a, F>(
120 props: SidebarProviderProps,
121 on_open_change: Option<F>,
122 add_contents: impl FnOnce(&SidebarContext<'a, Message>) -> Element<'a, Message>,
123) -> Element<'a, Message>
124where
125 F: Fn(bool) -> Message + 'a,
126{
127 let on_open_change = on_open_change.map(|f| Rc::new(f) as Rc<dyn Fn(bool) -> Message + 'a>);
128 let ctx = SidebarContext {
129 open: props.open,
130 expanded_width: props.expanded_width,
131 collapsed_width: props.collapsed_width,
132 animate: props.animate,
133 on_open_change,
134 };
135
136 add_contents(&ctx)
137}
138
139pub fn sidebar<'a, Message: Clone + 'a>(
140 ctx: &SidebarContext<'_, Message>,
141 props: SidebarProps,
142 theme: &Theme,
143 add_contents: impl FnOnce(&SidebarContext<'_, Message>) -> Element<'a, Message>,
144) -> Element<'a, Message> {
145 let width = if ctx.open {
146 ctx.expanded_width
147 } else {
148 ctx.collapsed_width
149 };
150
151 let palette = theme.palette;
152 let border = Border {
153 radius: theme.radius.md.into(),
154 width: if props.border { 1.0 } else { 0.0 },
155 color: palette.sidebar_border,
156 };
157 let theme = theme.clone();
158
159 container(add_contents(ctx))
160 .width(Length::Fixed(width))
161 .height(Length::Fill)
162 .padding(props.padding)
163 .style(move |_t| iced::widget::container::Style {
164 background: Some(Background::Color(theme.palette.sidebar)),
165 text_color: Some(theme.palette.sidebar_foreground),
166 border,
167 ..Default::default()
168 })
169 .into()
170}
171
172pub fn sidebar_trigger<'a, Message: Clone + 'a>(
173 label: impl Into<String>,
174 ctx: &SidebarContext<'_, Message>,
175 theme: &Theme,
176) -> Element<'a, Message> {
177 button(
178 label.into(),
179 ctx.toggle_message(),
180 ButtonProps::new()
181 .variant(ButtonVariant::Ghost)
182 .size(ButtonSize::Size1),
183 theme,
184 )
185 .into()
186}
187
188pub fn sidebar_header<'a, Message: Clone + 'a>(
189 ctx: &SidebarContext<'_, Message>,
190 content: impl Into<Element<'a, Message>>,
191) -> Element<'a, Message> {
192 sidebar_section(
193 ctx,
194 crate::theme::ThemeStyles::default()
195 .sidebar
196 .header_footer_padding,
197 content,
198 )
199}
200
201pub fn sidebar_content<'a, Message: Clone + 'a>(
202 ctx: &SidebarContext<'_, Message>,
203 content: impl Into<Element<'a, Message>>,
204) -> Element<'a, Message> {
205 sidebar_section(
206 ctx,
207 crate::theme::ThemeStyles::default().sidebar.content_padding,
208 content,
209 )
210}
211
212pub fn sidebar_footer<'a, Message: Clone + 'a>(
213 ctx: &SidebarContext<'_, Message>,
214 content: impl Into<Element<'a, Message>>,
215) -> Element<'a, Message> {
216 sidebar_section(
217 ctx,
218 crate::theme::ThemeStyles::default()
219 .sidebar
220 .header_footer_padding,
221 content,
222 )
223}
224
225fn sidebar_section<'a, Message: Clone + 'a>(
226 _ctx: &SidebarContext<'_, Message>,
227 padding: f32,
228 content: impl Into<Element<'a, Message>>,
229) -> Element<'a, Message> {
230 container(content).padding(padding).into()
231}
232
233#[derive(Clone, Copy, Debug)]
234pub struct SidebarGroupProps {
235 pub spacing: f32,
236}
237
238impl SidebarGroupProps {
239 pub fn new() -> Self {
240 Self { spacing: 8.0 }
241 }
242
243 pub fn spacing(mut self, spacing: f32) -> Self {
244 self.spacing = spacing;
245 self
246 }
247}
248
249impl Default for SidebarGroupProps {
250 fn default() -> Self {
251 Self::new()
252 }
253}
254
255pub fn sidebar_group<'a, Message: Clone + 'a>(
256 _ctx: &SidebarContext<'_, Message>,
257 props: SidebarGroupProps,
258 content: impl Into<Vec<Element<'a, Message>>>,
259) -> Element<'a, Message> {
260 column(content.into()).spacing(props.spacing).into()
261}
262
263#[derive(Clone, Debug)]
264pub struct SidebarGroupLabelProps {
265 pub text: String,
266 pub show_when_collapsed: bool,
267}
268
269impl SidebarGroupLabelProps {
270 pub fn new(text: impl Into<String>) -> Self {
271 Self {
272 text: text.into(),
273 show_when_collapsed: false,
274 }
275 }
276
277 pub fn show_when_collapsed(mut self, show: bool) -> Self {
278 self.show_when_collapsed = show;
279 self
280 }
281}
282
283pub fn sidebar_group_label<'a, Message: Clone + 'a>(
284 props: SidebarGroupLabelProps,
285 ctx: &SidebarContext<'_, Message>,
286 theme: &Theme,
287) -> Element<'a, Message> {
288 if ctx.is_collapsed() && !props.show_when_collapsed {
289 return container(text("")).into();
290 }
291
292 let color = apply_opacity(theme.palette.sidebar_foreground, 0.6);
293
294 text(props.text)
295 .size(11u32)
296 .style(move |_t| iced::widget::text::Style { color: Some(color) })
297 .into()
298}
299
300pub fn sidebar_group_content<'a, Message: Clone + 'a>(
301 content: impl Into<Vec<Element<'a, Message>>>,
302) -> Element<'a, Message> {
303 column(content.into())
304 .spacing(crate::theme::ThemeStyles::default().sidebar.menu_spacing)
305 .into()
306}
307
308pub fn sidebar_menu<'a, Message: Clone + 'a>(
309 content: impl Into<Vec<Element<'a, Message>>>,
310) -> Element<'a, Message> {
311 column(content.into())
312 .spacing(crate::theme::ThemeStyles::default().sidebar.menu_spacing)
313 .into()
314}
315
316pub fn sidebar_menu_item<'a, Message: Clone + 'a>(
317 content: impl Into<Vec<Element<'a, Message>>>,
318) -> Element<'a, Message> {
319 row(content.into()).spacing(0).into()
320}
321
322#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
323pub enum SidebarMenuButtonSize {
324 Sm,
325 #[default]
326 Md,
327 Lg,
328}
329
330impl SidebarMenuButtonSize {
331 fn height(self) -> f32 {
332 match self {
333 SidebarMenuButtonSize::Sm => 28.0,
334 SidebarMenuButtonSize::Md => 32.0,
335 SidebarMenuButtonSize::Lg => 40.0,
336 }
337 }
338
339 fn padding(self) -> [f32; 2] {
340 match self {
341 SidebarMenuButtonSize::Sm => [6.0, 10.0],
342 SidebarMenuButtonSize::Md => [8.0, 12.0],
343 SidebarMenuButtonSize::Lg => [10.0, 12.0],
344 }
345 }
346
347 fn text_size(self) -> u32 {
348 match self {
349 SidebarMenuButtonSize::Sm => 12,
350 SidebarMenuButtonSize::Md => 13,
351 SidebarMenuButtonSize::Lg => 14,
352 }
353 }
354}
355
356#[derive(Clone, Debug)]
357pub struct SidebarMenuButtonProps {
358 pub label: String,
359 pub size: SidebarMenuButtonSize,
360 pub active: bool,
361 pub disabled: bool,
362 pub show_label_when_collapsed: bool,
363}
364
365impl SidebarMenuButtonProps {
366 pub fn new(label: impl Into<String>) -> Self {
367 Self {
368 label: label.into(),
369 size: SidebarMenuButtonSize::Md,
370 active: false,
371 disabled: false,
372 show_label_when_collapsed: true,
373 }
374 }
375
376 pub fn size(mut self, size: SidebarMenuButtonSize) -> Self {
377 self.size = size;
378 self
379 }
380
381 pub fn active(mut self, active: bool) -> Self {
382 self.active = active;
383 self
384 }
385
386 pub fn disabled(mut self, disabled: bool) -> Self {
387 self.disabled = disabled;
388 self
389 }
390
391 pub fn show_label_when_collapsed(mut self, show: bool) -> Self {
392 self.show_label_when_collapsed = show;
393 self
394 }
395}
396
397pub fn sidebar_menu_button<'a, Message: Clone + 'a>(
398 props: SidebarMenuButtonProps,
399 on_press: Option<Message>,
400 ctx: &SidebarContext<'_, Message>,
401 theme: &Theme,
402) -> Element<'a, Message> {
403 let collapsed = ctx.is_collapsed();
404 let mut label = props.label.clone();
405 if collapsed && !props.show_label_when_collapsed {
406 label.truncate(1);
407 }
408
409 let mut content = container(text(label).size(props.size.text_size()))
410 .height(Length::Fixed(props.size.height()))
411 .width(Length::Fill)
412 .align_y(Vertical::Center);
413
414 if collapsed && !props.show_label_when_collapsed {
415 content = content.align_x(Horizontal::Center).padding(0);
416 } else {
417 content = content
418 .align_x(Horizontal::Left)
419 .padding(props.size.padding());
420 }
421
422 let mut button = iced_button(content);
423
424 if let Some(msg) = on_press
425 && !props.disabled
426 {
427 button = button.on_press(msg);
428 }
429
430 let theme = theme.clone();
431 let style_props = props.clone();
432 button = button.style(move |_t, status| {
433 sidebar_menu_button_style(&theme, &style_props, status, collapsed)
434 });
435 button.into()
436}
437
438fn sidebar_menu_button_style(
439 theme: &Theme,
440 props: &SidebarMenuButtonProps,
441 status: iced_button::Status,
442 _collapsed: bool,
443) -> iced_button::Style {
444 let palette = theme.palette;
445 let hovered = matches!(status, iced_button::Status::Hovered);
446 let pressed = matches!(status, iced_button::Status::Pressed);
447
448 let mut background = Color::TRANSPARENT;
449 if props.active || hovered || pressed {
450 background = palette.sidebar_accent;
451 }
452
453 let mut text_color = palette.sidebar_foreground;
454 if props.active || hovered || pressed {
455 text_color = palette.sidebar_accent_foreground;
456 }
457
458 if props.disabled {
459 text_color = palette.sidebar_foreground;
460 background = Color::TRANSPARENT;
461 }
462
463 iced_button::Style {
464 background: Some(Background::Color(background)),
465 text_color,
466 border: Border {
467 radius: theme.radius.sm.into(),
468 width: 0.0,
469 color: Color::TRANSPARENT,
470 },
471 shadow: Default::default(),
472 snap: true,
473 }
474}
475
476fn apply_opacity(color: Color, opacity: f32) -> Color {
477 Color {
478 a: color.a * opacity,
479 ..color
480 }
481}