1use dioxus::prelude::*;
2use freya_core::platform::CursorIcon;
3use freya_elements::{
4 self as dioxus_elements,
5 events::{
6 KeyboardEvent,
7 PointerEvent,
8 PointerType,
9 },
10 MouseButton,
11 TouchPhase,
12};
13use freya_hooks::{
14 use_applied_theme,
15 use_focus,
16 use_platform,
17 ButtonTheme,
18 ButtonThemeWith,
19};
20
21#[derive(Props, Clone, PartialEq)]
23pub struct ButtonProps {
24 pub theme: Option<ButtonThemeWith>,
26 pub children: Element,
28 pub onpress: Option<EventHandler<PressEvent>>,
30 pub onclick: Option<EventHandler<()>>,
32}
33
34#[cfg_attr(feature = "docs",
66 doc = embed_doc_image::embed_image!("button", "images/gallery_button.png")
67)]
68#[allow(non_snake_case)]
69pub fn Button(props: ButtonProps) -> Element {
70 let theme = use_applied_theme!(&props.theme, button);
71 ButtonBase(BaseButtonProps {
72 theme,
73 children: props.children,
74 onpress: props.onpress,
75 onclick: props.onclick,
76 })
77}
78
79#[cfg_attr(feature = "docs",
111 doc = embed_doc_image::embed_image!("filled_button", "images/gallery_filled_button.png")
112)]
113#[allow(non_snake_case)]
114pub fn FilledButton(props: ButtonProps) -> Element {
115 let theme = use_applied_theme!(&props.theme, filled_button);
116 ButtonBase(BaseButtonProps {
117 theme,
118 children: props.children,
119 onpress: props.onpress,
120 onclick: props.onclick,
121 })
122}
123
124#[cfg_attr(feature = "docs",
156 doc = embed_doc_image::embed_image!("outline_button", "images/gallery_outline_button.png")
157)]
158#[allow(non_snake_case)]
159pub fn OutlineButton(props: ButtonProps) -> Element {
160 let theme = use_applied_theme!(&props.theme, outline_button);
161 ButtonBase(BaseButtonProps {
162 theme,
163 children: props.children,
164 onpress: props.onpress,
165 onclick: props.onclick,
166 })
167}
168
169pub enum PressEvent {
170 Pointer(PointerEvent),
171 Key(KeyboardEvent),
172}
173
174impl PressEvent {
175 pub fn stop_propagation(&self) {
176 match &self {
177 Self::Pointer(ev) => ev.stop_propagation(),
178 Self::Key(ev) => ev.stop_propagation(),
179 }
180 }
181}
182
183#[derive(Props, Clone, PartialEq)]
185pub struct BaseButtonProps {
186 pub theme: ButtonTheme,
188 pub children: Element,
190 pub onpress: Option<EventHandler<PressEvent>>,
194 pub onclick: Option<EventHandler<()>>,
196}
197
198#[derive(Debug, Default, PartialEq, Clone, Copy)]
200pub enum ButtonStatus {
201 #[default]
203 Idle,
204 Hovering,
206}
207
208#[allow(non_snake_case)]
209pub fn ButtonBase(
210 BaseButtonProps {
211 onpress,
212 children,
213 theme,
214 onclick,
215 }: BaseButtonProps,
216) -> Element {
217 let mut focus = use_focus();
218 let mut status = use_signal(ButtonStatus::default);
219 let platform = use_platform();
220
221 let a11y_id = focus.attribute();
222
223 let ButtonTheme {
224 background,
225 hover_background,
226 border_fill,
227 focus_border_fill,
228 padding,
229 margin,
230 corner_radius,
231 width,
232 height,
233 font_theme,
234 shadow,
235 } = theme;
236
237 let onpointerup = {
238 to_owned![onpress, onclick];
239 move |ev: PointerEvent| {
240 focus.request_focus();
241 if let Some(onpress) = &onpress {
242 let is_valid = match ev.data.pointer_type {
243 PointerType::Mouse {
244 trigger_button: Some(MouseButton::Left),
245 } => true,
246 PointerType::Touch { phase, .. } => phase == TouchPhase::Ended,
247 _ => false,
248 };
249 if is_valid {
250 onpress.call(PressEvent::Pointer(ev))
251 }
252 } else if let Some(onclick) = onclick {
253 if let PointerType::Mouse {
254 trigger_button: Some(MouseButton::Left),
255 ..
256 } = ev.data.pointer_type
257 {
258 onclick.call(())
259 }
260 }
261 }
262 };
263
264 use_drop(move || {
265 if *status.read() == ButtonStatus::Hovering {
266 platform.set_cursor(CursorIcon::default());
267 }
268 });
269
270 let onmouseenter = move |_| {
271 platform.set_cursor(CursorIcon::Pointer);
272 status.set(ButtonStatus::Hovering);
273 };
274
275 let onmouseleave = move |_| {
276 platform.set_cursor(CursorIcon::default());
277 status.set(ButtonStatus::default());
278 };
279
280 let onkeydown = move |ev: KeyboardEvent| {
281 if focus.validate_keydown(&ev) {
282 if let Some(onpress) = &onpress {
283 onpress.call(PressEvent::Key(ev))
284 }
285 }
286 };
287
288 let background = match *status.read() {
289 ButtonStatus::Hovering => hover_background,
290 ButtonStatus::Idle => background,
291 };
292 let border = if focus.is_focused_with_keyboard() {
293 format!("2 inner {focus_border_fill}")
294 } else {
295 format!("1 inner {border_fill}")
296 };
297
298 rsx!(
299 rect {
300 onpointerup,
301 onmouseenter,
302 onmouseleave,
303 onkeydown,
304 a11y_id,
305 width: "{width}",
306 height: "{height}",
307 padding: "{padding}",
308 margin: "{margin}",
309 overflow: "clip",
310 a11y_role:"button",
311 color: "{font_theme.color}",
312 shadow: "{shadow}",
313 border,
314 corner_radius: "{corner_radius}",
315 background: "{background}",
316 text_height: "disable-least-ascent",
317 main_align: "center",
318 cross_align: "center",
319 {&children}
320 }
321 )
322}
323
324#[cfg(test)]
325mod test {
326 use freya::prelude::*;
327 use freya_testing::prelude::*;
328
329 #[tokio::test]
330 pub async fn button() {
331 fn button_app() -> Element {
332 let mut state = use_signal(|| false);
333
334 rsx!(
335 Button {
336 onpress: move |_| state.toggle(),
337 label {
338 "{state}"
339 }
340 }
341 )
342 }
343
344 let mut utils = launch_test(button_app);
345 let root = utils.root();
346 let label = root.get(0).get(0);
347 utils.wait_for_update().await;
348
349 assert_eq!(label.get(0).text(), Some("false"));
350
351 utils.click_cursor((15.0, 15.0)).await;
352
353 assert_eq!(label.get(0).text(), Some("true"));
354
355 utils.push_event(TestEvent::Touch {
356 name: EventName::TouchStart,
357 location: (15.0, 15.0).into(),
358 finger_id: 1,
359 phase: TouchPhase::Started,
360 force: None,
361 });
362 utils.wait_for_update().await;
363
364 utils.push_event(TestEvent::Touch {
365 name: EventName::TouchEnd,
366 location: (15.0, 15.0).into(),
367 finger_id: 1,
368 phase: TouchPhase::Ended,
369 force: None,
370 });
371 utils.wait_for_update().await;
372
373 assert_eq!(label.get(0).text(), Some("false"));
374 }
375}