1use crate::core::border::{self, Border};
20use crate::core::keyboard;
21use crate::core::keyboard::key;
22use crate::core::layout;
23use crate::core::mouse;
24use crate::core::overlay;
25use crate::core::renderer;
26use crate::core::theme::palette;
27use crate::core::touch;
28use crate::core::widget::Operation;
29use crate::core::widget::operation::accessible::{Accessible, Role};
30use crate::core::widget::operation::focusable::{self, Focusable};
31use crate::core::widget::tree::{self, Tree};
32use crate::core::window;
33use crate::core::{
34 Background, Color, Element, Event, Layout, Length, Padding, Rectangle, Shadow, Shell, Size,
35 Theme, Vector, Widget,
36};
37
38pub struct Button<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
76where
77 Renderer: crate::core::Renderer,
78 Theme: Catalog,
79{
80 content: Element<'a, Message, Theme, Renderer>,
81 on_press: Option<OnPress<'a, Message>>,
82 width: Length,
83 height: Length,
84 padding: Padding,
85 clip: bool,
86 class: Theme::Class<'a>,
87 status: Option<Status>,
88}
89
90enum OnPress<'a, Message> {
91 Direct(Message),
92 Closure(Box<dyn Fn() -> Message + 'a>),
93}
94
95impl<Message: Clone> OnPress<'_, Message> {
96 fn get(&self) -> Message {
97 match self {
98 OnPress::Direct(message) => message.clone(),
99 OnPress::Closure(f) => f(),
100 }
101 }
102}
103
104impl<'a, Message, Theme, Renderer> Button<'a, Message, Theme, Renderer>
105where
106 Renderer: crate::core::Renderer,
107 Theme: Catalog,
108{
109 pub fn new(content: impl Into<Element<'a, Message, Theme, Renderer>>) -> Self {
111 let content = content.into();
112 let size = content.as_widget().size_hint();
113
114 Button {
115 content,
116 on_press: None,
117 width: size.width.fluid(),
118 height: size.height.fluid(),
119 padding: DEFAULT_PADDING,
120 clip: false,
121 class: Theme::default(),
122 status: None,
123 }
124 }
125
126 pub fn width(mut self, width: impl Into<Length>) -> Self {
128 self.width = width.into();
129 self
130 }
131
132 pub fn height(mut self, height: impl Into<Length>) -> Self {
134 self.height = height.into();
135 self
136 }
137
138 pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
140 self.padding = padding.into();
141 self
142 }
143
144 pub fn on_press(mut self, on_press: Message) -> Self {
148 self.on_press = Some(OnPress::Direct(on_press));
149 self
150 }
151
152 pub fn on_press_with(mut self, on_press: impl Fn() -> Message + 'a) -> Self {
161 self.on_press = Some(OnPress::Closure(Box::new(on_press)));
162 self
163 }
164
165 pub fn on_press_maybe(mut self, on_press: Option<Message>) -> Self {
170 self.on_press = on_press.map(OnPress::Direct);
171 self
172 }
173
174 pub fn clip(mut self, clip: bool) -> Self {
177 self.clip = clip;
178 self
179 }
180
181 #[must_use]
183 pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
184 where
185 Theme::Class<'a>: From<StyleFn<'a, Theme>>,
186 {
187 self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
188 self
189 }
190
191 #[cfg(feature = "advanced")]
193 #[must_use]
194 pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
195 self.class = class.into();
196 self
197 }
198}
199
200#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
201struct State {
202 is_pressed: bool,
203 is_focused: bool,
204 focus_visible: bool,
205}
206
207impl focusable::Focusable for State {
208 fn is_focused(&self) -> bool {
209 self.is_focused
210 }
211
212 fn focus(&mut self) {
213 self.is_focused = true;
214 self.focus_visible = true;
215 }
216
217 fn unfocus(&mut self) {
218 self.is_focused = false;
219 self.focus_visible = false;
220 }
221}
222
223impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
224 for Button<'a, Message, Theme, Renderer>
225where
226 Message: 'a + Clone,
227 Renderer: 'a + crate::core::Renderer,
228 Theme: Catalog,
229{
230 fn tag(&self) -> tree::Tag {
231 tree::Tag::of::<State>()
232 }
233
234 fn state(&self) -> tree::State {
235 tree::State::new(State::default())
236 }
237
238 fn children(&self) -> Vec<Tree> {
239 vec![Tree::new(&self.content)]
240 }
241
242 fn diff(&self, tree: &mut Tree) {
243 tree.diff_children(std::slice::from_ref(&self.content));
244 }
245
246 fn size(&self) -> Size<Length> {
247 Size {
248 width: self.width,
249 height: self.height,
250 }
251 }
252
253 fn layout(
254 &mut self,
255 tree: &mut Tree,
256 renderer: &Renderer,
257 limits: &layout::Limits,
258 ) -> layout::Node {
259 layout::padded(limits, self.width, self.height, self.padding, |limits| {
260 self.content
261 .as_widget_mut()
262 .layout(&mut tree.children[0], renderer, limits)
263 })
264 }
265
266 fn operate(
267 &mut self,
268 tree: &mut Tree,
269 layout: Layout<'_>,
270 renderer: &Renderer,
271 operation: &mut dyn Operation,
272 ) {
273 let state = tree.state.downcast_mut::<State>();
274
275 operation.accessible(
276 None,
277 layout.bounds(),
278 &Accessible {
279 role: Role::Button,
280 disabled: self.on_press.is_none(),
281 ..Accessible::default()
282 },
283 );
284
285 if self.on_press.is_some() {
286 operation.focusable(None, layout.bounds(), state);
287 } else {
288 state.unfocus();
289 }
290
291 operation.container(None, layout.bounds());
292 operation.traverse(&mut |operation| {
293 self.content.as_widget_mut().operate(
294 &mut tree.children[0],
295 layout.children().next().unwrap(),
296 renderer,
297 operation,
298 );
299 });
300 }
301
302 fn update(
303 &mut self,
304 tree: &mut Tree,
305 event: &Event,
306 layout: Layout<'_>,
307 cursor: mouse::Cursor,
308 renderer: &Renderer,
309 shell: &mut Shell<'_, Message>,
310 viewport: &Rectangle,
311 ) {
312 self.content.as_widget_mut().update(
313 &mut tree.children[0],
314 event,
315 layout.children().next().unwrap(),
316 cursor,
317 renderer,
318 shell,
319 viewport,
320 );
321
322 if shell.is_event_captured() {
323 return;
324 }
325
326 match event {
327 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
328 | Event::Touch(touch::Event::FingerPressed { .. }) => {
329 let state = tree.state.downcast_mut::<State>();
330
331 if self.on_press.is_some() && cursor.is_over(layout.bounds()) {
332 state.is_pressed = true;
333 state.is_focused = true;
334 state.focus_visible = false;
335
336 shell.capture_event();
337 } else {
338 state.is_focused = false;
339 state.focus_visible = false;
340 }
341 }
342 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
343 | Event::Touch(touch::Event::FingerLifted { .. }) => {
344 if let Some(on_press) = &self.on_press {
345 let state = tree.state.downcast_mut::<State>();
346
347 if state.is_pressed {
348 state.is_pressed = false;
349
350 let bounds = layout.bounds();
351
352 if cursor.is_over(bounds) {
353 shell.publish(on_press.get());
354 }
355
356 shell.capture_event();
357 }
358 }
359 }
360 Event::Touch(touch::Event::FingerLost { .. }) => {
361 let state = tree.state.downcast_mut::<State>();
362
363 state.is_pressed = false;
364 }
365 Event::Keyboard(keyboard::Event::KeyPressed {
366 key: keyboard::Key::Named(key::Named::Space | key::Named::Enter),
367 ..
368 }) => {
369 if let Some(on_press) = &self.on_press {
370 let state = tree.state.downcast_mut::<State>();
371
372 if state.is_focused {
373 state.is_pressed = true;
374 shell.publish(on_press.get());
375 shell.capture_event();
376 }
377 }
378 }
379 Event::Keyboard(keyboard::Event::KeyReleased {
380 key: keyboard::Key::Named(key::Named::Space | key::Named::Enter),
381 ..
382 }) => {
383 let state = tree.state.downcast_mut::<State>();
384
385 if state.is_pressed && state.is_focused {
386 state.is_pressed = false;
387 shell.capture_event();
388 }
389 }
390 Event::Keyboard(keyboard::Event::KeyPressed {
391 key: keyboard::Key::Named(key::Named::Escape),
392 ..
393 }) => {
394 let state = tree.state.downcast_mut::<State>();
395 if state.is_focused {
396 state.is_focused = false;
397 state.focus_visible = false;
398 shell.capture_event();
399 }
400 }
401 _ => {}
402 }
403
404 let current_status = if self.on_press.is_none() {
405 Status::Disabled
406 } else {
407 let state = tree.state.downcast_ref::<State>();
408
409 if state.is_pressed {
410 Status::Pressed
411 } else if state.focus_visible {
412 Status::Focused
413 } else if cursor.is_over(layout.bounds()) {
414 Status::Hovered
415 } else {
416 Status::Active
417 }
418 };
419
420 if let Event::Window(window::Event::RedrawRequested(_now)) = event {
421 self.status = Some(current_status);
422 } else if self.status.is_some_and(|status| status != current_status) {
423 shell.request_redraw();
424 }
425 }
426
427 fn draw(
428 &self,
429 tree: &Tree,
430 renderer: &mut Renderer,
431 theme: &Theme,
432 _style: &renderer::Style,
433 layout: Layout<'_>,
434 cursor: mouse::Cursor,
435 viewport: &Rectangle,
436 ) {
437 let bounds = layout.bounds();
438 let content_layout = layout.children().next().unwrap();
439 let style = theme.style(&self.class, self.status.unwrap_or(Status::Disabled));
440
441 if style.background.is_some() || style.border.width > 0.0 || style.shadow.color.a > 0.0 {
442 renderer.fill_quad(
443 renderer::Quad {
444 bounds,
445 border: style.border,
446 shadow: style.shadow,
447 snap: style.snap,
448 },
449 style
450 .background
451 .unwrap_or(Background::Color(Color::TRANSPARENT)),
452 );
453 }
454
455 let viewport = if self.clip {
456 bounds.intersection(viewport).unwrap_or(*viewport)
457 } else {
458 *viewport
459 };
460
461 self.content.as_widget().draw(
462 &tree.children[0],
463 renderer,
464 theme,
465 &renderer::Style {
466 text_color: style.text_color,
467 },
468 content_layout,
469 cursor,
470 &viewport,
471 );
472 }
473
474 fn mouse_interaction(
475 &self,
476 _tree: &Tree,
477 layout: Layout<'_>,
478 cursor: mouse::Cursor,
479 _viewport: &Rectangle,
480 _renderer: &Renderer,
481 ) -> mouse::Interaction {
482 let is_mouse_over = cursor.is_over(layout.bounds());
483
484 if is_mouse_over && self.on_press.is_some() {
485 mouse::Interaction::Pointer
486 } else {
487 mouse::Interaction::default()
488 }
489 }
490
491 fn overlay<'b>(
492 &'b mut self,
493 tree: &'b mut Tree,
494 layout: Layout<'b>,
495 renderer: &Renderer,
496 viewport: &Rectangle,
497 translation: Vector,
498 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
499 self.content.as_widget_mut().overlay(
500 &mut tree.children[0],
501 layout.children().next().unwrap(),
502 renderer,
503 viewport,
504 translation,
505 )
506 }
507}
508
509impl<'a, Message, Theme, Renderer> From<Button<'a, Message, Theme, Renderer>>
510 for Element<'a, Message, Theme, Renderer>
511where
512 Message: Clone + 'a,
513 Theme: Catalog + 'a,
514 Renderer: crate::core::Renderer + 'a,
515{
516 fn from(button: Button<'a, Message, Theme, Renderer>) -> Self {
517 Self::new(button)
518 }
519}
520
521pub const DEFAULT_PADDING: Padding = Padding {
523 top: 5.0,
524 bottom: 5.0,
525 right: 10.0,
526 left: 10.0,
527};
528
529#[derive(Debug, Clone, Copy, PartialEq, Eq)]
531pub enum Status {
532 Active,
534 Hovered,
536 Pressed,
538 Focused,
540 Disabled,
542}
543
544#[derive(Debug, Clone, Copy, PartialEq)]
549pub struct Style {
550 pub background: Option<Background>,
552 pub text_color: Color,
554 pub border: Border,
556 pub shadow: Shadow,
558 pub snap: bool,
560}
561
562impl Style {
563 pub fn with_background(self, background: impl Into<Background>) -> Self {
565 Self {
566 background: Some(background.into()),
567 ..self
568 }
569 }
570}
571
572impl Default for Style {
573 fn default() -> Self {
574 Self {
575 background: None,
576 text_color: Color::BLACK,
577 border: Border::default(),
578 shadow: Shadow::default(),
579 snap: renderer::CRISP,
580 }
581 }
582}
583
584pub trait Catalog {
634 type Class<'a>;
636
637 fn default<'a>() -> Self::Class<'a>;
639
640 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
642}
643
644pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
646
647impl Catalog for Theme {
648 type Class<'a> = StyleFn<'a, Self>;
649
650 fn default<'a>() -> Self::Class<'a> {
651 Box::new(primary)
652 }
653
654 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
655 class(self, status)
656 }
657}
658
659pub fn primary(theme: &Theme, status: Status) -> Style {
661 let palette = theme.palette();
662 let base = styled(palette.primary.base);
663
664 match status {
665 Status::Active | Status::Pressed => base,
666 Status::Hovered => Style {
667 background: Some(Background::Color(palette.primary.strong.color)),
668 ..base
669 },
670 Status::Focused => focused(base, palette),
671 Status::Disabled => disabled(base),
672 }
673}
674
675pub fn secondary(theme: &Theme, status: Status) -> Style {
677 let palette = theme.palette();
678 let base = styled(palette.secondary.base);
679
680 match status {
681 Status::Active | Status::Pressed => base,
682 Status::Hovered => Style {
683 background: Some(Background::Color(palette.secondary.strong.color)),
684 ..base
685 },
686 Status::Focused => focused(base, palette),
687 Status::Disabled => disabled(base),
688 }
689}
690
691pub fn success(theme: &Theme, status: Status) -> Style {
693 let palette = theme.palette();
694 let base = styled(palette.success.base);
695
696 match status {
697 Status::Active | Status::Pressed => base,
698 Status::Hovered => Style {
699 background: Some(Background::Color(palette.success.strong.color)),
700 ..base
701 },
702 Status::Focused => focused(base, palette),
703 Status::Disabled => disabled(base),
704 }
705}
706
707pub fn warning(theme: &Theme, status: Status) -> Style {
709 let palette = theme.palette();
710 let base = styled(palette.warning.base);
711
712 match status {
713 Status::Active | Status::Pressed => base,
714 Status::Hovered => Style {
715 background: Some(Background::Color(palette.warning.strong.color)),
716 ..base
717 },
718 Status::Focused => focused(base, palette),
719 Status::Disabled => disabled(base),
720 }
721}
722
723pub fn danger(theme: &Theme, status: Status) -> Style {
725 let palette = theme.palette();
726 let base = styled(palette.danger.base);
727
728 match status {
729 Status::Active | Status::Pressed => base,
730 Status::Hovered => Style {
731 background: Some(Background::Color(palette.danger.strong.color)),
732 ..base
733 },
734 Status::Focused => focused(base, palette),
735 Status::Disabled => disabled(base),
736 }
737}
738
739pub fn text(theme: &Theme, status: Status) -> Style {
741 let palette = theme.palette();
742
743 let base = Style {
744 text_color: palette.background.base.text,
745 ..Style::default()
746 };
747
748 match status {
749 Status::Active | Status::Pressed => base,
750 Status::Hovered => Style {
751 text_color: palette.background.base.text.scale_alpha(0.8),
752 ..base
753 },
754 Status::Focused => focused(base, palette),
755 Status::Disabled => disabled(base),
756 }
757}
758
759pub fn background(theme: &Theme, status: Status) -> Style {
761 let palette = theme.palette();
762 let base = styled(palette.background.base);
763
764 match status {
765 Status::Active => base,
766 Status::Pressed => Style {
767 background: Some(Background::Color(palette.background.strong.color)),
768 ..base
769 },
770 Status::Hovered => Style {
771 background: Some(Background::Color(palette.background.weak.color)),
772 ..base
773 },
774 Status::Focused => focused(base, palette),
775 Status::Disabled => disabled(base),
776 }
777}
778
779pub fn subtle(theme: &Theme, status: Status) -> Style {
781 let palette = theme.palette();
782 let base = styled(palette.background.weakest);
783
784 match status {
785 Status::Active => base,
786 Status::Pressed => Style {
787 background: Some(Background::Color(palette.background.strong.color)),
788 ..base
789 },
790 Status::Hovered => Style {
791 background: Some(Background::Color(palette.background.weaker.color)),
792 ..base
793 },
794 Status::Focused => focused(base, palette),
795 Status::Disabled => disabled(base),
796 }
797}
798
799fn styled(pair: palette::Pair) -> Style {
800 Style {
801 background: Some(Background::Color(pair.color)),
802 text_color: pair.text,
803 border: border::rounded(2),
804 ..Style::default()
805 }
806}
807
808fn focused(base: Style, palette: &palette::Palette) -> Style {
809 let accent = palette.primary.strong.color;
810 let page_bg = palette.background.base.color;
811 let widget_bg = base.background.map_or(Color::TRANSPARENT, |bg| match bg {
812 Background::Color(c) => c,
813 Background::Gradient(_) => Color::TRANSPARENT,
814 });
815 let border_color = palette::focus_border_color(widget_bg, accent, page_bg);
816
817 Style {
818 border: Border {
819 color: border_color,
820 width: 2.0,
821 ..base.border
822 },
823 shadow: palette::focus_shadow_subtle(accent, page_bg),
824 ..base
825 }
826}
827
828fn disabled(style: Style) -> Style {
829 Style {
830 background: style
831 .background
832 .map(|background| background.scale_alpha(0.5)),
833 text_color: style.text_color.scale_alpha(0.5),
834 ..style
835 }
836}
837
838#[cfg(test)]
839mod tests {
840 use super::*;
841 use crate::core::widget::operation::focusable::Focusable;
842
843 #[test]
844 fn focusable_trait() {
845 let mut state = State::default();
846 assert!(!state.is_focused());
847 assert!(!state.focus_visible);
848 state.focus();
849 assert!(state.is_focused());
850 assert!(state.focus_visible);
851 state.unfocus();
852 assert!(!state.is_focused());
853 assert!(!state.focus_visible);
854 }
855
856 #[test]
857 fn focused_unfocuses_on_disable() {
858 let mut state = State::default();
859 state.focus();
860 assert!(state.is_focused());
861 assert!(state.focus_visible);
862 state.unfocus();
863 assert!(!state.is_focused());
864 assert!(!state.focus_visible);
865 }
866}