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