Skip to main content

freya_components/
tabs.rs

1use dioxus::prelude::*;
2use freya_core::platform::CursorIcon;
3use freya_elements::{
4    self as dioxus_elements,
5    KeyboardEvent,
6};
7use freya_hooks::{
8    use_activable_route,
9    use_applied_theme,
10    use_focus,
11    use_platform,
12    BottomTabTheme,
13    BottomTabThemeWith,
14    TabTheme,
15    TabThemeWith,
16};
17
18/// Horizontal container for Tabs. Use in combination with [`Tab`]
19#[allow(non_snake_case)]
20#[component]
21pub fn Tabsbar(children: Element) -> Element {
22    rsx!(
23        rect {
24            direction: "horizontal",
25            {children}
26        }
27    )
28}
29
30/// Current status of the Tab.
31#[derive(Debug, Default, PartialEq, Clone, Copy)]
32pub enum TabStatus {
33    /// Default state.
34    #[default]
35    Idle,
36    /// Mouse is hovering the Tab.
37    Hovering,
38}
39
40///  Clickable Tab. Usually used in combination with [`Tabsbar`], [`crate::Link`] and [`crate::ActivableRoute`].
41///
42/// # Styling
43/// Inherits the [`TabTheme`](freya_hooks::TabTheme) theme.
44///
45/// # Example
46///
47/// ```rust
48/// # use freya::prelude::*;
49/// # use dioxus_router::prelude::{Routable, Router};
50/// # #[allow(non_snake_case)]
51/// # fn PageNotFound() -> Element { VNode::empty() }
52/// # #[allow(non_snake_case)]
53/// # fn Settings() -> Element { VNode::empty() }
54/// # #[derive(Routable, Clone, PartialEq)]
55/// # #[rustfmt::skip]
56/// # pub enum Route {
57/// #     #[layout(Bar)]
58/// #       #[route("/")]
59/// #       Settings,
60/// #     #[end_layout]
61/// #     #[route("/..route")]
62/// #     PageNotFound { },
63/// # }
64/// fn app() -> Element {
65///     rsx!(
66///         Tabsbar {
67///             Tab {
68///                 label {
69///                     "Home"
70///                 }
71///             }
72///             Link {
73///                 to: Route::Settings,
74///                 Tab {
75///                     label {
76///                         "Settings"
77///                     }
78///                 }
79///             }
80///         }
81///     )
82/// }
83/// # use freya_testing::prelude::*;
84/// # #[component]
85/// # fn Bar() -> Element {
86/// #   rsx!(
87/// #       Preview {
88/// #          Tabsbar {
89/// #              Tab {
90/// #                  label {
91/// #                      "Home"
92/// #                  }
93/// #              }
94/// #              ActivableRoute {
95/// #                  route: Route::Settings,
96/// #                  Tab {
97/// #                      label {
98/// #                          "Settings"
99/// #                      }
100/// #                  }
101/// #              }
102/// #          }
103/// #       }
104/// #   )
105/// # }
106/// # launch_doc(|| {
107/// #   rsx!(Router::<Route> {})
108/// # }, (250., 250.).into(), "./images/gallery_tab.png");
109/// ```
110///
111/// # Preview
112/// ![Tab Preview][tab]
113#[cfg_attr(feature = "docs",
114    doc = embed_doc_image::embed_image!("tab", "images/gallery_tab.png")
115)]
116#[component]
117pub fn Tab(
118    children: Element,
119    theme: Option<TabThemeWith>,
120    /// Optionally handle the `onclick` event in the SidebarItem.
121    onpress: Option<EventHandler<()>>,
122) -> Element {
123    let focus = use_focus();
124    let mut status = use_signal(TabStatus::default);
125    let platform = use_platform();
126    let is_active = use_activable_route();
127
128    let a11y_id = focus.attribute();
129
130    let TabTheme {
131        background,
132        hover_background,
133        border_fill,
134        focus_border_fill,
135        padding,
136        width,
137        height,
138        font_theme,
139    } = use_applied_theme!(&theme, tab);
140
141    use_drop(move || {
142        if *status.read() == TabStatus::Hovering {
143            platform.set_cursor(CursorIcon::default());
144        }
145    });
146
147    let onclick = move |_| {
148        if let Some(onpress) = &onpress {
149            onpress.call(());
150        }
151    };
152
153    let onmouseenter = move |_| {
154        platform.set_cursor(CursorIcon::Pointer);
155        status.set(TabStatus::Hovering);
156    };
157
158    let onmouseleave = move |_| {
159        platform.set_cursor(CursorIcon::default());
160        status.set(TabStatus::default());
161    };
162
163    let background = match *status.read() {
164        TabStatus::Hovering => hover_background,
165        TabStatus::Idle => background,
166    };
167    let border = if focus.is_focused_with_keyboard() || is_active {
168        focus_border_fill
169    } else {
170        border_fill
171    };
172
173    rsx!(
174        rect {
175            onclick,
176            onmouseenter,
177            onmouseleave,
178            a11y_id,
179            width: "{width}",
180            height: "{height}",
181            overflow: "clip",
182            a11y_role:"tab",
183            color: "{font_theme.color}",
184            background: "{background}",
185            content: "fit",
186            rect {
187                padding: "{padding}",
188                main_align: "center",
189                cross_align: "center",
190                {children}
191            }
192            rect {
193                height: "2",
194                width: "fill-min",
195                background: "{border}"
196            }
197        }
198    )
199}
200
201///  Clickable BottomTab. Same thing as Tab but designed to be placed in the bottom of your app,
202///  usually used in combination with [`Tabsbar`], [`crate::Link`] and [`crate::ActivableRoute`].
203///
204/// # Styling
205/// Inherits the [`BottomTabTheme`](freya_hooks::BottomTabTheme) theme.
206///
207/// # Example
208///
209/// ```rust
210/// # use freya::prelude::*;
211/// # use dioxus_router::prelude::{Routable, Router};
212/// # #[allow(non_snake_case)]
213/// # fn PageNotFound() -> Element { VNode::empty() }
214/// # #[allow(non_snake_case)]
215/// # fn Settings() -> Element { VNode::empty() }
216/// # #[derive(Routable, Clone, PartialEq)]
217/// # #[rustfmt::skip]
218/// # pub enum Route {
219/// #     #[layout(Bar)]
220/// #       #[route("/")]
221/// #       Settings,
222/// #     #[end_layout]
223/// #     #[route("/..route")]
224/// #     PageNotFound { },
225/// # }
226/// fn app() -> Element {
227///     rsx!(
228///         Tabsbar {
229///             BottomTab {
230///                 label {
231///                     "Home"
232///                 }
233///             }
234///             Link {
235///                 to: Route::Settings,
236///                 BottomTab {
237///                     label {
238///                         "Settings"
239///                     }
240///                 }
241///             }
242///         }
243///     )
244/// }
245/// # use freya_testing::prelude::*;
246/// # #[component]
247/// # fn Bar() -> Element {
248/// #   rsx!(
249/// #       Preview {
250/// #          Tabsbar {
251/// #              BottomTab {
252/// #                  label {
253/// #                      "Home"
254/// #                  }
255/// #              }
256/// #              ActivableRoute {
257/// #                  route: Route::Settings,
258/// #                  BottomTab {
259/// #                      label {
260/// #                          "Settings"
261/// #                      }
262/// #                  }
263/// #              }
264/// #          }
265/// #       }
266/// #   )
267/// # }
268/// # launch_doc(|| {
269/// #   rsx!(Router::<Route> {})
270/// # }, (250., 250.).into(), "./images/gallery_bottom_tab.png");
271/// ```
272///
273/// # Preview
274/// ![Bottom Tab Preview][bottom_tab]
275#[cfg_attr(feature = "docs",
276    doc = embed_doc_image::embed_image!("bottom_tab", "images/gallery_bottom_tab.png")
277)]
278#[component]
279pub fn BottomTab(
280    children: Element,
281    theme: Option<BottomTabThemeWith>,
282    /// Optionally handle the `onclick` event in the SidebarItem.
283    onpress: Option<EventHandler<()>>,
284) -> Element {
285    let focus = use_focus();
286    let mut status = use_signal(TabStatus::default);
287    let platform = use_platform();
288    let is_active = use_activable_route();
289
290    let a11y_id = focus.attribute();
291
292    let BottomTabTheme {
293        background,
294        hover_background,
295        padding,
296        width,
297        height,
298        font_theme,
299    } = use_applied_theme!(&theme, bottom_tab);
300
301    use_drop(move || {
302        if *status.read() == TabStatus::Hovering {
303            platform.set_cursor(CursorIcon::default());
304        }
305    });
306
307    let onclick = move |_| {
308        if let Some(onpress) = &onpress {
309            onpress.call(());
310        }
311    };
312
313    let onmouseenter = move |_| {
314        platform.set_cursor(CursorIcon::Pointer);
315        status.set(TabStatus::Hovering);
316    };
317
318    let onmouseleave = move |_| {
319        platform.set_cursor(CursorIcon::default());
320        status.set(TabStatus::default());
321    };
322
323    let onkeydown = move |ev: KeyboardEvent| {
324        if focus.validate_keydown(&ev) {
325            if let Some(onpress) = &onpress {
326                onpress.call(());
327            }
328        }
329    };
330
331    let background = match *status.read() {
332        _ if focus.is_focused_with_keyboard() || is_active => hover_background,
333        TabStatus::Hovering => hover_background,
334        TabStatus::Idle => background,
335    };
336
337    rsx!(
338        rect {
339            onclick,
340            onmouseenter,
341            onmouseleave,
342            onkeydown,
343            a11y_id,
344            width: "{width}",
345            height: "{height}",
346            overflow: "clip",
347            a11y_role:"tab",
348            color: "{font_theme.color}",
349            background: "{background}",
350            padding: "{padding}",
351            main_align: "center",
352            cross_align: "center",
353            corner_radius: "99",
354            text_height: "disable-least-ascent",
355            {children}
356        }
357    )
358}