Skip to main content

freya_components/
sidebar.rs

1use freya_core::prelude::*;
2use torin::{
3    gaps::Gaps,
4    size::Size,
5};
6
7use crate::{
8    activable_route_context::use_activable_route,
9    define_theme,
10    get_theme,
11};
12
13define_theme! {
14    %[component]
15    pub SideBarItem {
16        %[fields]
17        color: Color,
18        background: Color,
19        hover_background: Color,
20        active_background: Color,
21        focus_border_fill: Color,
22        corner_radius: CornerRadius,
23        margin: Gaps,
24        padding: Gaps,
25    }
26}
27
28#[derive(Debug, Default, PartialEq, Clone, Copy)]
29pub enum SideBarItemStatus {
30    /// Default state.
31    #[default]
32    Idle,
33    /// User is hovering the sidebar item.
34    Hovering,
35}
36
37/// Button designed for sidebars.
38///
39/// # Example
40///
41/// ```rust
42/// # use freya::prelude::*;
43/// fn app() -> impl IntoElement {
44///     rect()
45///         .horizontal()
46///         .child(
47///             rect()
48///                 .theme_background()
49///                 .padding(8.)
50///                 .width(Size::px(150.))
51///                 .height(Size::fill())
52///                 .child(SideBarItem::new().child("Home"))
53///                 .child(SideBarItem::new().child("Settings")),
54///         )
55///         .child(rect().expanded().center().child("Main content"))
56/// }
57/// # use freya_testing::prelude::*;
58/// # launch_doc(|| {
59/// #   rect().center().expanded().child(
60/// #       app()
61/// #   )
62/// # }, "./images/gallery_sidebar.png")
63/// # .with_hook(|t| { t.move_cursor((20., 20.)); t.sync_and_update(); })
64/// # .with_scale_factor(0.75)
65/// # .render();
66/// ```
67///
68/// # Preview
69/// ![SideBarItem Preview][SideBarItem]
70
71#[derive(Clone, PartialEq)]
72pub struct SideBarItem {
73    /// Theme override.
74    pub(crate) theme: Option<SideBarItemThemePartial>,
75    /// Inner child for the [SideBarItem].
76    children: Vec<Element>,
77    /// Optionally handle the `on_press` event in the [SideBarItem].
78    on_press: Option<EventHandler<Event<PressEventData>>>,
79    /// Optionally specify a custom `overflow` attribute for this component. Defaults to [OverflowMode::Clip].
80    overflow: Overflow,
81    key: DiffKey,
82}
83
84impl KeyExt for SideBarItem {
85    fn write_key(&mut self) -> &mut DiffKey {
86        &mut self.key
87    }
88}
89
90impl Default for SideBarItem {
91    fn default() -> Self {
92        Self::new()
93    }
94}
95
96impl ChildrenExt for SideBarItem {
97    fn get_children(&mut self) -> &mut Vec<Element> {
98        &mut self.children
99    }
100}
101
102impl SideBarItem {
103    pub fn new() -> Self {
104        Self {
105            theme: None,
106            children: Vec::new(),
107            on_press: None,
108            overflow: Overflow::Clip,
109            key: DiffKey::None,
110        }
111    }
112
113    pub fn on_press(mut self, on_press: impl Into<EventHandler<Event<PressEventData>>>) -> Self {
114        self.on_press = Some(on_press.into());
115        self
116    }
117
118    pub fn overflow(mut self, overflow: impl Into<Overflow>) -> Self {
119        self.overflow = overflow.into();
120        self
121    }
122
123    /// Get the theme override for this component.
124    pub fn get_theme(&self) -> Option<&SideBarItemThemePartial> {
125        self.theme.as_ref()
126    }
127
128    /// Set a theme override for this component.
129    pub fn theme(mut self, theme: SideBarItemThemePartial) -> Self {
130        self.theme = Some(theme);
131        self
132    }
133}
134
135impl Component for SideBarItem {
136    fn render(&self) -> impl IntoElement {
137        let SideBarItemTheme {
138            margin,
139            hover_background,
140            active_background,
141            background,
142            focus_border_fill,
143            corner_radius,
144            padding,
145            color,
146        } = get_theme!(&self.theme, SideBarItemThemePreference, "sidebar_item");
147        let mut status = use_state(SideBarItemStatus::default);
148        let is_active = use_activable_route();
149        let a11y_id = use_a11y();
150        let focus = use_focus(a11y_id);
151
152        let on_pointer_enter = move |_| {
153            status.set(SideBarItemStatus::Hovering);
154        };
155
156        let on_pointer_leave = move |_| {
157            status.set(SideBarItemStatus::default());
158        };
159
160        let background = match *status.read() {
161            _ if is_active => active_background,
162            SideBarItemStatus::Hovering => hover_background,
163            SideBarItemStatus::Idle => background,
164        };
165
166        let border = (focus() == Focus::Keyboard).then(|| {
167            Border::new()
168                .fill(focus_border_fill)
169                .width(2.)
170                .alignment(BorderAlignment::Inner)
171        });
172
173        let on_press = self.on_press.clone();
174        rect()
175            .a11y_id(a11y_id)
176            .a11y_focusable(true)
177            .a11y_role(AccessibilityRole::Link)
178            .on_press(move |e: Event<PressEventData>| {
179                if let Some(handler) = &on_press {
180                    handler.call(e);
181                }
182            })
183            .on_pointer_enter(on_pointer_enter)
184            .on_pointer_leave(on_pointer_leave)
185            .overflow(self.overflow)
186            .width(Size::fill())
187            .margin(margin)
188            .padding(padding)
189            .color(color)
190            .background(background)
191            .corner_radius(corner_radius)
192            .map(border, |rect, border| rect.border(border))
193            .children(self.children.clone())
194    }
195
196    fn render_key(&self) -> DiffKey {
197        self.key.clone().or(self.default_key())
198    }
199}