1use frus_core::{BorderRadius, Color, Point, Rect, Scene, ShapeBorder, TextStyle};
20use frus_layout::{Dimension, Style};
21
22use crate::disabled::{disabled_container, disabled_content};
23use crate::interaction::Status;
24use crate::theme::Theme;
25use crate::widget::Widget;
26
27pub const BUTTON_HEIGHT: f32 = 40.0;
29pub const BUTTON_MIN_WIDTH: f32 = 64.0;
31pub const BUTTON_PADDING: f32 = 24.0;
33pub const BUTTON_TEXT_PADDING: f32 = 12.0;
35pub const BUTTON_ELEVATION: f32 = 1.0;
38const ELEVATED_HOVER_ELEVATION: f32 = 3.0;
41const FILLED_HOVER_ELEVATION: f32 = 1.0;
44pub const BUTTON_BORDER_WIDTH: f32 = 1.0;
46
47#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
49pub enum Variant {
50 #[default]
52 Filled,
53 Tonal,
55 Elevated,
57 Outlined,
59 Text,
61 Danger,
68}
69
70impl Variant {
71 const fn heights(self) -> Heights {
80 match self {
81 Variant::Elevated => Heights {
82 rest: BUTTON_ELEVATION,
83 hovered: ELEVATED_HOVER_ELEVATION,
84 focused: BUTTON_ELEVATION,
85 pressed: BUTTON_ELEVATION,
86 },
87 Variant::Filled | Variant::Tonal | Variant::Danger => Heights {
88 rest: 0.0,
89 hovered: FILLED_HOVER_ELEVATION,
90 focused: 0.0,
91 pressed: 0.0,
92 },
93 Variant::Outlined | Variant::Text => Heights::all(0.0),
94 }
95 }
96
97 const fn padding(self) -> f32 {
99 match self {
100 Variant::Text => BUTTON_TEXT_PADDING,
101 _ => BUTTON_PADDING,
102 }
103 }
104}
105
106#[derive(Clone, Copy, Debug, PartialEq)]
109struct Heights {
110 rest: f32,
111 hovered: f32,
112 focused: f32,
113 pressed: f32,
114}
115
116impl Heights {
117 const fn all(height: f32) -> Self {
118 Self {
119 rest: height,
120 hovered: height,
121 focused: height,
122 pressed: height,
123 }
124 }
125
126 fn at(self, status: &Status) -> f32 {
134 let toward =
135 |from: f32, to: f32, progress: f32| from + (to - from) * progress.clamp(0.0, 1.0);
136 let height = toward(self.rest, self.focused, status.focus_progress);
137 let height = toward(height, self.hovered, status.hover_progress);
138 toward(height, self.pressed, status.press_progress)
139 }
140}
141
142pub struct Button<Msg = crate::callback::Callback> {
144 label: String,
145 variant: Variant,
146 on_press: Option<Msg>,
147 enabled: bool,
149 label_style: Option<TextStyle>,
150 color: Option<Color>,
151 label_color: Option<Color>,
152 border_color: Option<Color>,
153 border_width: Option<f32>,
154 shape: Option<ShapeBorder>,
155 padding: Option<f32>,
156 height: Option<f32>,
157 min_width: Option<f32>,
158 elevation: Option<f32>,
159}
160
161impl<Msg> Button<Msg> {
162 pub fn new(label: impl Into<String>) -> Self {
164 Self {
165 label: label.into(),
166 variant: Variant::default(),
167 on_press: None,
168 enabled: true,
169 label_style: None,
170 color: None,
171 label_color: None,
172 border_color: None,
173 border_width: None,
174 shape: None,
175 padding: None,
176 height: None,
177 min_width: None,
178 elevation: None,
179 }
180 }
181
182 pub fn enabled(mut self, enabled: bool) -> Self {
186 self.enabled = enabled;
187 self
188 }
189
190 pub fn radius(mut self, radius: impl Into<BorderRadius>) -> Self {
194 self.shape = Some(ShapeBorder::rounded(radius));
195 self
196 }
197
198 #[must_use]
202 pub fn shape(mut self, shape: ShapeBorder) -> Self {
203 self.shape = Some(shape);
204 self
205 }
206
207 pub fn variant(mut self, variant: Variant) -> Self {
209 self.variant = variant;
210 self
211 }
212
213 pub fn label_style(mut self, style: TextStyle) -> Self {
215 self.label_style = Some(style);
216 self
217 }
218
219 pub fn size(mut self, size: f32) -> Self {
221 let mut style = self.label_style.unwrap_or(TextStyle::new(size));
222 style.size = Some(size);
223 self.label_style = Some(style);
224 self
225 }
226
227 pub fn color(mut self, color: Color) -> Self {
229 self.color = Some(color);
230 self
231 }
232
233 pub fn label_color(mut self, color: Color) -> Self {
235 self.label_color = Some(color);
236 self
237 }
238
239 pub fn border_color(mut self, color: Color) -> Self {
241 self.border_color = Some(color);
242 self
243 }
244
245 pub fn border_width(mut self, width: f32) -> Self {
247 self.border_width = Some(width);
248 self
249 }
250
251 pub fn padding(mut self, padding: f32) -> Self {
253 self.padding = Some(padding);
254 self
255 }
256
257 pub fn height(mut self, height: f32) -> Self {
259 self.height = Some(height);
260 self
261 }
262
263 pub fn min_width(mut self, width: f32) -> Self {
265 self.min_width = Some(width);
266 self
267 }
268
269 pub fn elevation(mut self, elevation: f32) -> Self {
277 self.elevation = Some(elevation);
278 self
279 }
280
281 pub fn on_press(mut self, message: impl Into<Msg>) -> Self {
283 self.on_press = Some(message.into());
284 self
285 }
286
287 fn label_style_of(&self, theme: &Theme) -> TextStyle {
288 self.label_style
289 .or(theme.widgets.button.label_style)
290 .unwrap_or(theme.text.label_large)
291 }
292
293 fn padding_of(&self, theme: &Theme) -> f32 {
294 self.padding
295 .or(theme.widgets.button.padding)
296 .unwrap_or(self.variant.padding())
297 }
298
299 fn height_of(&self, theme: &Theme) -> f32 {
300 self.height
301 .or(theme.widgets.button.height)
302 .unwrap_or(BUTTON_HEIGHT)
303 }
304
305 fn min_width_of(&self, theme: &Theme) -> f32 {
306 self.min_width
307 .or(theme.widgets.button.min_width)
308 .unwrap_or(BUTTON_MIN_WIDTH)
309 }
310
311 fn elevation_of(&self, theme: &Theme, status: &Status) -> f32 {
317 if !self.enabled {
318 return 0.0;
319 }
320 match self.elevation.or(theme.widgets.button.elevation) {
321 Some(height) => height,
322 None => self.variant.heights().at(status),
323 }
324 }
325
326 fn shape_of(&self, theme: &Theme) -> ShapeBorder {
335 crate::resolve_shape(
336 self.shape,
337 theme.widgets.button.shape,
338 theme.widgets.button.radius,
339 ShapeBorder::stadium(),
340 )
341 }
342
343 fn palette(&self, theme: &Theme) -> (Color, Color, Option<Color>) {
356 if !self.enabled {
357 let label = disabled_content(theme);
358 return match self.variant {
359 Variant::Text => (Color::TRANSPARENT, label, None),
361 Variant::Outlined => (Color::TRANSPARENT, label, Some(disabled_container(theme))),
363 _ => (disabled_container(theme), label, None),
365 };
366 }
367 let (background, label, outline) = match self.variant {
368 Variant::Filled => (theme.scheme.primary, theme.scheme.on_primary, None),
369 Variant::Tonal => (
370 theme.scheme.secondary_container,
371 theme.scheme.on_secondary_container,
372 None,
373 ),
374 Variant::Elevated => (
376 theme.scheme.surface_container_low,
377 theme.scheme.primary,
378 None,
379 ),
380 Variant::Outlined => (
381 Color::TRANSPARENT,
382 theme.scheme.primary,
383 Some(theme.scheme.outline),
384 ),
385 Variant::Text => (Color::TRANSPARENT, theme.scheme.primary, None),
386 Variant::Danger => (theme.scheme.error, theme.scheme.on_error, None),
387 };
388 (
389 self.color
390 .or(theme.widgets.button.color)
391 .unwrap_or(background),
392 self.label_color
393 .or(theme.widgets.button.label_color)
394 .unwrap_or(label),
395 self.border_color
396 .or(theme.widgets.button.border_color)
397 .or(outline),
398 )
399 }
400}
401
402impl<Msg: Clone> Widget<Msg> for Button<Msg> {
403 fn style(&self) -> Style {
404 Widget::<Msg>::style_themed(self, &Theme::default())
405 }
406
407 fn style_themed(&self, theme: &Theme) -> Style {
408 let style = self.label_style_of(theme);
409 let measured = frus_text::measure_style(&self.label, style);
410 let width = (measured.width + self.padding_of(theme) * 2.0).max(self.min_width_of(theme));
411 Style {
412 width: Dimension::Length(width.ceil()),
413 height: Dimension::Length(self.height_of(theme).max(measured.height).ceil()),
414 ..Default::default()
415 }
416 }
417
418 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
419 &[]
420 }
421
422 fn paint(&self, bounds: Rect, status: Status, theme: &Theme, scene: &mut Scene) {
423 let o = status.opacity;
424 let (base, on_color, border) = self.palette(theme);
425 let shape = self.shape_of(theme);
426 let radius = shape
427 .as_rounded(bounds)
428 .map(|(_, radius)| radius)
429 .unwrap_or(BorderRadius::ZERO);
430 let elevation = self.elevation_of(theme, &status);
431
432 if elevation > 0.0 {
436 let blur = elevation * 4.0 + 8.0;
437 scene.shadow(
438 Rect::new(
439 bounds.x - blur,
440 bounds.y + elevation * 2.0 - blur,
441 bounds.width + 2.0 * blur,
442 bounds.height + 2.0 * blur,
443 ),
444 theme.scheme.shadow.with_alpha(0.35).fade(o),
445 radius.inflate(blur),
446 blur,
447 );
448 }
449
450 let color = if self.enabled {
454 theme.state_layer(base, on_color, &status)
455 } else {
456 base
457 };
458 let (border_width, border_color) = match (
459 self.border_width.or(theme.widgets.button.border_width),
460 border,
461 ) {
462 (Some(width), Some(color)) => (width, color),
463 (Some(width), None) => (width, theme.scheme.outline),
464 (None, Some(color)) => (BUTTON_BORDER_WIDTH, color),
465 (None, None) => (0.0, Color::TRANSPARENT),
466 };
467 scene.draw_shape(
468 bounds,
469 shape.with_side(frus_core::BorderSide::new(
470 border_color.fade(o),
471 border_width,
472 )),
473 color.fade(o),
474 );
475
476 let style = self.label_style_of(theme);
479 let resolved = style.resolved();
480 let room = bounds.width - self.padding_of(theme) * 2.0;
486 let mut label = self.label.clone();
487 let mut measured = frus_text::measure_style(&label, style);
488 if room > 0.0 && measured.width > room + 0.5 {
489 label = crate::text::ellipsise(&label, &resolved, room);
490 measured = frus_text::measure_resolved(&label, &resolved);
491 }
492 scene.text(
493 Point::new(
494 bounds.x + (bounds.width - measured.width) / 2.0,
495 bounds.y + (bounds.height - measured.height) / 2.0,
496 ),
497 label,
498 &resolved,
499 on_color.fade(o),
500 );
501 }
502
503 fn on_click(&self) -> Option<Msg> {
504 if self.enabled {
505 self.on_press.clone()
506 } else {
507 None
508 }
509 }
510
511 fn ink(&self, theme: &Theme) -> Option<crate::InkStyle> {
512 if !self.enabled {
514 return None;
515 }
516 let (_, on_color, _) = self.palette(theme);
520 let splash = theme
523 .widgets
524 .ink
525 .color
526 .unwrap_or_else(|| on_color.fade(0.16));
527 Some(
528 crate::InkStyle::of(theme)
529 .color(splash)
530 .radius(
535 self.shape_of(theme)
536 .as_rounded(Rect::new(
537 0.0,
538 0.0,
539 self.height_of(theme),
540 self.height_of(theme),
541 ))
542 .map(|(_, radius)| radius)
543 .unwrap_or(BorderRadius::ZERO),
544 ),
545 )
546 }
547
548 fn focusable(&self) -> bool {
549 self.enabled
550 }
551
552 fn semantics(&self) -> Option<frus_core::SemanticsProperties> {
553 let semantics =
554 frus_core::SemanticsProperties::new(frus_core::Role::Button).label(self.label.clone());
555 Some(if self.enabled {
557 semantics.clickable()
558 } else {
559 semantics.disabled(true)
560 })
561 }
562}
563
564#[cfg(test)]
565mod tests {
566 use super::*;
567 use frus_core::Primitive;
568
569 #[derive(Clone, Debug, PartialEq)]
570 enum Msg {
571 Pressed,
572 }
573
574 fn painted(button: &Button<Msg>) -> Vec<Primitive> {
577 let mut scene = Scene::new();
578 Widget::<Msg>::paint(
579 button,
580 Rect::new(0.0, 0.0, 120.0, BUTTON_HEIGHT),
581 Status::default(),
582 &Theme::default(),
583 &mut scene,
584 );
585 scene.primitives().to_vec()
586 }
587
588 #[test]
593 fn a_squeezed_label_is_cut_rather_than_painted_out_of_the_pill() {
594 let button = crate::dsl::button("Delete permanently", Msg::Pressed);
595 let drawn = |width: f32| {
596 let mut scene = Scene::new();
597 Widget::<Msg>::paint(
598 &button,
599 Rect::new(0.0, 0.0, width, BUTTON_HEIGHT),
600 Status::default(),
601 &Theme::default(),
602 &mut scene,
603 );
604 scene
605 .primitives()
606 .iter()
607 .find_map(|p| match p {
608 Primitive::Text { text, position, .. } => Some((text.clone(), *position)),
609 _ => None,
610 })
611 .expect("a label")
612 };
613 let (whole, _) = drawn(400.0);
614 assert_eq!(whole, "Delete permanently", "room enough: nothing is cut");
615 let (cut, at) = drawn(120.0);
616 assert!(cut.ends_with('…'), "cut short: {cut:?}");
617 assert!(at.x >= 0.0, "and it starts inside the pill: {at:?}");
618 }
619
620 #[test]
627 fn a_button_is_a_pill_at_any_size() {
628 let corners = |w: f32, h: f32, button: &Button<Msg>| {
629 let mut scene = Scene::new();
630 Widget::<Msg>::paint(
631 button,
632 Rect::new(0.0, 0.0, w, h),
633 Status::default(),
634 &Theme::default(),
635 &mut scene,
636 );
637 scene.primitives().iter().find_map(|p| match p {
641 Primitive::Rect {
642 rect, radius, blur, ..
643 } if *blur == 0.0 => Some((*rect, *radius)),
644 _ => None,
645 })
646 };
647 let button = Button::new("OK").on_press(Msg::Pressed);
648
649 let (_, wide) = corners(120.0, 40.0, &button).expect("a box");
650 assert_eq!(
651 wide,
652 BorderRadius::uniform(20.0),
653 "half the height, being shorter"
654 );
655
656 let (_, narrow) = corners(24.0, 80.0, &button).expect("a box");
657 assert_eq!(
658 narrow,
659 BorderRadius::uniform(12.0),
660 "half the *width* here — the old `height / 2` said 40 and turned the box \
661 inside out"
662 );
663
664 let boxed = Button::new("OK")
666 .on_press(Msg::Pressed)
667 .shape(frus_core::ShapeBorder::rounded(4.0));
668 assert_eq!(
669 corners(120.0, 40.0, &boxed).unwrap().1,
670 BorderRadius::uniform(4.0)
671 );
672
673 let round = Button::new("OK")
675 .on_press(Msg::Pressed)
676 .shape(frus_core::ShapeBorder::circle());
677 let (box_, _) = corners(120.0, 40.0, &round).expect("a box");
678 assert_eq!(box_, Rect::new(40.0, 0.0, 40.0, 40.0));
679 }
680
681 fn surface(button: &Button<Msg>) -> (Color, BorderRadius, f32) {
682 painted(button)
683 .iter()
684 .find_map(|p| match p {
685 Primitive::Rect {
686 color,
687 radius,
688 border_width,
689 blur,
690 ..
691 } if *blur == 0.0 => Some((*color, *radius, *border_width)),
692 _ => None,
693 })
694 .expect("a button paints its box")
695 }
696
697 #[test]
698 fn on_click_returns_message() {
699 let button = Button::new("OK").on_press(Msg::Pressed);
700 assert_eq!(Widget::on_click(&button), Some(Msg::Pressed));
701 }
702
703 #[test]
704 fn disabled_button_is_inert_and_unfocusable() {
705 let button = Button::new("Next").on_press(Msg::Pressed).enabled(false);
706 assert_eq!(Widget::on_click(&button), None, "disabled: no message");
707 assert!(
708 !Widget::<Msg>::focusable(&button),
709 "disabled: out of the tab order"
710 );
711 let semantics = Widget::<Msg>::semantics(&button).expect("semantics present");
712 assert!(!semantics.clickable, "disabled: not announced as clickable");
713 assert!(semantics.disabled, "and announced as unavailable");
714 let enabled = Button::new("Next").on_press(Msg::Pressed).enabled(true);
715 assert_eq!(Widget::on_click(&enabled), Some(Msg::Pressed));
716 }
717
718 #[test]
719 fn only_the_elevated_variant_casts_a_shadow() {
720 let shadows = |variant: Variant| {
723 painted(&Button::<Msg>::new("Go").variant(variant))
724 .iter()
725 .filter(|p| matches!(p, Primitive::Rect { blur, .. } if *blur > 0.0))
726 .count()
727 };
728 assert_eq!(shadows(Variant::Elevated), 1);
729 for flat in [
730 Variant::Filled,
731 Variant::Tonal,
732 Variant::Outlined,
733 Variant::Text,
734 Variant::Danger,
735 ] {
736 assert_eq!(shadows(flat), 0, "{flat:?} is flat");
737 }
738 assert_eq!(
739 shadows(Variant::Elevated).min(
740 painted(
741 &Button::<Msg>::new("Go")
742 .variant(Variant::Elevated)
743 .enabled(false)
744 )
745 .iter()
746 .filter(|p| matches!(p, Primitive::Rect { blur, .. } if *blur > 0.0))
747 .count()
748 ),
749 0,
750 "and a disabled one does not float"
751 );
752 }
753
754 #[test]
755 fn the_variants_are_told_apart_by_their_surface() {
756 let theme = Theme::default();
757 assert_eq!(surface(&Button::<Msg>::new("Go")).0, theme.scheme.primary);
758 assert_eq!(
759 surface(&Button::<Msg>::new("Go").variant(Variant::Tonal)).0,
760 theme.scheme.secondary_container
761 );
762 assert_eq!(
763 surface(&Button::<Msg>::new("Go").variant(Variant::Danger)).0,
764 theme.scheme.error
765 );
766 assert_eq!(
768 surface(&Button::<Msg>::new("Go").variant(Variant::Text)).0,
769 Color::TRANSPARENT
770 );
771 let outlined = surface(&Button::<Msg>::new("Go").variant(Variant::Outlined));
772 assert_eq!(outlined.0, Color::TRANSPARENT);
773 assert_eq!(
774 outlined.2, BUTTON_BORDER_WIDTH,
775 "an outline, and only there"
776 );
777 assert_eq!(
778 surface(&Button::<Msg>::new("Go").variant(Variant::Text)).2,
779 0.0
780 );
781 }
782
783 #[test]
784 fn a_button_is_a_stadium_whatever_its_height() {
785 assert_eq!(
788 surface(&Button::<Msg>::new("Go")).1,
789 BorderRadius::uniform(BUTTON_HEIGHT / 2.0)
790 );
791 assert_eq!(
793 surface(&Button::<Msg>::new("Go").radius(4.0)).1,
794 BorderRadius::uniform(4.0)
795 );
796 }
797
798 #[test]
799 fn a_button_is_the_references_size() {
800 let theme = Theme::default();
801 let size = |button: Button<Msg>| {
802 let style = Widget::<Msg>::style_themed(&button, &theme);
803 match (style.width, style.height) {
804 (Dimension::Length(w), Dimension::Length(h)) => (w, h),
805 other => panic!("{other:?}"),
806 }
807 };
808 let (width, height) = size(Button::new("OK"));
809 assert_eq!(height, BUTTON_HEIGHT);
810 assert!(width >= BUTTON_MIN_WIDTH);
813 assert_eq!(
814 size(Button::new("I")).0,
815 BUTTON_MIN_WIDTH,
816 "a one-letter label does not give a narrower button"
817 );
818 let (wide, _) = size(Button::new("A considerably longer label"));
820 assert!(wide > BUTTON_MIN_WIDTH);
821 let (narrow, _) = size(Button::new("A considerably longer label").variant(Variant::Text));
823 assert_eq!(
824 wide - narrow,
825 (BUTTON_PADDING - BUTTON_TEXT_PADDING) * 2.0,
826 "a text button's label sits closer to its edges"
827 );
828 }
829
830 #[test]
838 fn a_disabled_button_drops_its_accent_but_keeps_its_shape() {
839 let theme = Theme::default();
840 let grey = disabled_container(&theme);
841 for variant in [
843 Variant::Filled,
844 Variant::Tonal,
845 Variant::Elevated,
846 Variant::Danger,
847 ] {
848 let (fill, _, _) = surface(&Button::<Msg>::new("Go").variant(variant).enabled(false));
849 assert_eq!(fill, grey, "{variant:?} should flatten to a container");
850 }
851 let (fill, _, width) = surface(
853 &Button::<Msg>::new("Go")
854 .variant(Variant::Text)
855 .enabled(false),
856 );
857 assert_eq!(fill.a, 0.0, "a disabled text button gains no fill");
858 assert_eq!(width, 0.0, "and no outline");
859 let (fill, _, width) = surface(
861 &Button::<Msg>::new("Go")
862 .variant(Variant::Outlined)
863 .enabled(false),
864 );
865 assert_eq!(fill.a, 0.0, "a disabled outlined button gains no fill");
866 assert!(width > 0.0, "and keeps its outline");
867 }
868
869 #[test]
872 fn a_disabled_label_is_the_same_in_every_variant() {
873 let theme = Theme::default();
874 for variant in [
875 Variant::Filled,
876 Variant::Tonal,
877 Variant::Elevated,
878 Variant::Outlined,
879 Variant::Text,
880 Variant::Danger,
881 ] {
882 let label = painted(&Button::<Msg>::new("Go").variant(variant).enabled(false))
883 .iter()
884 .find_map(|p| match p {
885 Primitive::Text { color, .. } => Some(*color),
886 _ => None,
887 })
888 .expect("a button paints its label");
889 assert_eq!(label, disabled_content(&theme), "{variant:?}");
890 assert_ne!(label, theme.scheme.primary, "never the accent: {variant:?}");
891 }
892 }
893
894 #[test]
895 fn every_measurement_is_the_callers_and_then_the_themes() {
896 let mut theme = Theme::default();
897 theme.widgets.button.height = Some(56.0);
898 let height =
899 |button: Button<Msg>, theme: &Theme| match Widget::<Msg>::style_themed(&button, theme)
900 .height
901 {
902 Dimension::Length(h) => h,
903 other => panic!("{other:?}"),
904 };
905 assert_eq!(
906 height(Button::new("Go"), &Theme::default()),
907 BUTTON_HEIGHT,
908 "the framework's"
909 );
910 assert_eq!(height(Button::new("Go"), &theme), 56.0, "the theme's");
911 assert_eq!(
912 height(Button::new("Go").height(30.0), &theme),
913 30.0,
914 "the caller's, over the theme's"
915 );
916 }
917
918 #[test]
924 fn the_height_follows_the_state() {
925 let height = |button: &Button<Msg>, status: Status, theme: &Theme| {
927 let mut scene = Scene::new();
928 Widget::<Msg>::paint(
929 button,
930 Rect::new(0.0, 0.0, 120.0, BUTTON_HEIGHT),
931 status,
932 theme,
933 &mut scene,
934 );
935 scene
936 .primitives()
937 .iter()
938 .find_map(|p| match p {
939 Primitive::Rect { blur, .. } if *blur > 0.0 => Some((blur - 8.0) / 4.0),
940 _ => None,
941 })
942 .unwrap_or(0.0)
943 };
944 let theme = Theme::default();
945 let rest = Status::default();
946 let hovered = Status {
947 hover_progress: 1.0,
948 ..Default::default()
949 };
950 let held = Status {
951 hover_progress: 1.0,
952 press_progress: 1.0,
953 ..Default::default()
954 };
955 let focused = Status {
956 focus_progress: 1.0,
957 ..Default::default()
958 };
959 let half = Status {
960 hover_progress: 0.5,
961 ..Default::default()
962 };
963
964 let elevated = Button::new("Go")
965 .variant(Variant::Elevated)
966 .on_press(Msg::Pressed);
967 assert_eq!(height(&elevated, rest, &theme), 1.0);
968 assert_eq!(height(&elevated, hovered, &theme), 3.0);
969 assert_eq!(
970 height(&elevated, held, &theme),
971 1.0,
972 "a press outranks the hover"
973 );
974 assert_eq!(height(&elevated, focused, &theme), 1.0);
975 assert_eq!(
976 height(&elevated, half, &theme),
977 2.0,
978 "it moves, it does not jump"
979 );
980
981 for variant in [Variant::Filled, Variant::Tonal, Variant::Danger] {
982 let button = Button::new("Go").variant(variant).on_press(Msg::Pressed);
983 assert_eq!(height(&button, rest, &theme), 0.0, "{variant:?} rests flat");
984 assert_eq!(height(&button, hovered, &theme), 1.0, "{variant:?} rises");
985 assert_eq!(height(&button, held, &theme), 0.0, "{variant:?} pressed");
986 assert_eq!(height(&button, focused, &theme), 0.0, "{variant:?} focused");
987 }
988 for variant in [Variant::Outlined, Variant::Text] {
989 let button = Button::new("Go").variant(variant).on_press(Msg::Pressed);
990 assert_eq!(
991 height(&button, hovered, &theme),
992 0.0,
993 "{variant:?} stays flat"
994 );
995 }
996 let disabled = Button::new("Go")
997 .variant(Variant::Elevated)
998 .on_press(Msg::Pressed)
999 .enabled(false);
1000 assert_eq!(
1001 height(&disabled, hovered, &theme),
1002 0.0,
1003 "a disabled one does not rise"
1004 );
1005
1006 let told = Button::new("Go").elevation(2.0).on_press(Msg::Pressed);
1008 assert_eq!(height(&told, rest, &theme), 2.0);
1009 assert_eq!(height(&told, held, &theme), 2.0);
1010 let mut flat = Theme::default();
1011 flat.widgets.button.elevation = Some(0.0);
1012 assert_eq!(height(&elevated, hovered, &flat), 0.0, "the theme's word");
1013 assert_eq!(
1014 height(&told, hovered, &flat),
1015 2.0,
1016 "and the caller's over it"
1017 );
1018 }
1019}