freya_components/
sidebar.rs

1use freya_core::prelude::*;
2use torin::size::Size;
3
4use crate::{
5    activable_route_context::use_activable_route,
6    get_theme,
7    scrollviews::ScrollView,
8    theming::component_themes::{
9        SideBarItemTheme,
10        SideBarItemThemePartial,
11        SideBarTheme,
12        SideBarThemePartial,
13    },
14};
15
16/// Sidebar layout component with a bar on one side and content on the other.
17///
18/// # Example
19///
20/// ```rust
21/// # use freya::prelude::*;
22/// fn app() -> impl IntoElement {
23///     SideBar::new()
24///         .bar(
25///             rect()
26///                 .child(SideBarItem::new().child("Home"))
27///                 .child(SideBarItem::new().child("Settings")),
28///         )
29///         .content(rect().expanded().center().child("Main content"))
30/// }
31/// # use freya_testing::prelude::*;
32/// # launch_doc(|| {
33/// #   rect().center().expanded().child(
34/// #       app()
35/// #   )
36/// # }, "./images/gallery_sidebar.png")
37/// # .with_hook(|t| { t.move_cursor((20., 20.)); t.sync_and_update(); })
38/// # .with_scale_factor(0.75)
39/// # .render();
40/// ```
41///
42/// # Preview
43/// ![Sidebar Preview][sidebar]
44#[cfg_attr(feature = "docs",
45    doc = embed_doc_image::embed_image!("sidebar", "images/gallery_sidebar.png"),
46)]
47#[derive(PartialEq)]
48pub struct SideBar {
49    /// Theme override.
50    pub(crate) theme: Option<SideBarThemePartial>,
51    /// This is what is rendered next to the sidebar.
52    content: Option<Element>,
53    /// This is what is rendered in the sidebar.
54    bar: Option<Element>,
55    /// Width of the sidebar.
56    width: Size,
57}
58
59impl Default for SideBar {
60    fn default() -> Self {
61        Self::new()
62    }
63}
64
65impl SideBar {
66    pub fn new() -> Self {
67        Self {
68            theme: None,
69            content: None,
70            bar: None,
71            width: Size::px(180.),
72        }
73    }
74
75    pub fn content(mut self, content: impl Into<Element>) -> Self {
76        self.content = Some(content.into());
77        self
78    }
79
80    pub fn bar(mut self, bar: impl Into<Element>) -> Self {
81        self.bar = Some(bar.into());
82        self
83    }
84
85    pub fn width(mut self, width: impl Into<Size>) -> Self {
86        self.width = width.into();
87        self
88    }
89}
90
91impl Component for SideBar {
92    fn render(&self) -> impl IntoElement {
93        let SideBarTheme {
94            spacing,
95            padding,
96            background,
97            color,
98        } = get_theme!(&self.theme, sidebar);
99
100        rect()
101            .horizontal()
102            .width(Size::fill())
103            .height(Size::fill())
104            .color(color)
105            .child(
106                rect()
107                    .overflow(Overflow::Clip)
108                    .width(self.width.clone())
109                    .height(Size::fill())
110                    .background(background)
111                    .child(
112                        ScrollView::new()
113                            .width(self.width.clone())
114                            .spacing(spacing)
115                            .child(rect().padding(padding).maybe_child(self.bar.clone())),
116                    ),
117            )
118            .child(
119                rect()
120                    .overflow(Overflow::Clip)
121                    .expanded()
122                    .maybe_child(self.content.clone()),
123            )
124    }
125}
126
127#[derive(Debug, Default, PartialEq, Clone, Copy)]
128pub enum ButtonStatus {
129    /// Default state.
130    #[default]
131    Idle,
132    /// Mouse is hovering the button.
133    Hovering,
134}
135#[derive(PartialEq)]
136pub struct SideBarItem {
137    /// Theme override.
138    pub(crate) theme: Option<SideBarItemThemePartial>,
139    /// Inner child for the [SideBarItem].
140    children: Vec<Element>,
141    /// Optionally handle the `on_press` event in the [SideBarItem].
142    on_press: Option<EventHandler<Event<PressEventData>>>,
143    /// Optionally specify a custom `overflow` attribute for this component. Defaults to [OverflowMode::Clip].
144    overflow: Overflow,
145    key: DiffKey,
146}
147
148impl KeyExt for SideBarItem {
149    fn write_key(&mut self) -> &mut DiffKey {
150        &mut self.key
151    }
152}
153
154impl Default for SideBarItem {
155    fn default() -> Self {
156        Self::new()
157    }
158}
159
160impl ChildrenExt for SideBarItem {
161    fn get_children(&mut self) -> &mut Vec<Element> {
162        &mut self.children
163    }
164}
165
166impl SideBarItem {
167    pub fn new() -> Self {
168        Self {
169            theme: None,
170            children: Vec::new(),
171            on_press: None,
172            overflow: Overflow::Clip,
173            key: DiffKey::None,
174        }
175    }
176
177    pub fn on_press(mut self, on_press: impl Into<EventHandler<Event<PressEventData>>>) -> Self {
178        self.on_press = Some(on_press.into());
179        self
180    }
181
182    pub fn overflow(mut self, overflow: impl Into<Overflow>) -> Self {
183        self.overflow = overflow.into();
184        self
185    }
186}
187
188impl Component for SideBarItem {
189    fn render(&self) -> impl IntoElement {
190        let SideBarItemTheme {
191            margin,
192            hover_background,
193            active_background,
194            background,
195            corner_radius,
196            padding,
197            color,
198        } = get_theme!(&self.theme, sidebar_item);
199        let mut status = use_state(ButtonStatus::default);
200        let is_active = use_activable_route();
201
202        use_drop(move || {
203            if status() == ButtonStatus::Hovering {
204                Cursor::set(CursorIcon::default());
205            }
206        });
207
208        let on_pointer_enter = move |_| {
209            status.set(ButtonStatus::Hovering);
210            Cursor::set(CursorIcon::Pointer);
211        };
212
213        let on_pointer_leave = move |_| {
214            status.set(ButtonStatus::default());
215            Cursor::set(CursorIcon::default());
216        };
217
218        let background = match *status.read() {
219            _ if is_active => active_background,
220            ButtonStatus::Hovering => hover_background,
221            ButtonStatus::Idle => background,
222        };
223
224        rect()
225            .a11y_focusable(true)
226            .a11y_role(AccessibilityRole::Link)
227            .map(self.on_press.clone(), |rect, on_press| {
228                rect.on_press(on_press)
229            })
230            .on_pointer_enter(on_pointer_enter)
231            .on_pointer_leave(on_pointer_leave)
232            .overflow(self.overflow)
233            .width(Size::fill())
234            .margin(margin)
235            .padding(padding)
236            .color(color)
237            .background(background)
238            .corner_radius(corner_radius)
239            .children(self.children.clone())
240    }
241
242    fn render_key(&self) -> DiffKey {
243        self.key.clone().or(self.default_key())
244    }
245}