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}