freya_components/
tabs.rs

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