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}