Skip to main content

material_ui_rs/design/style/
button.rs

1use iced_widget::button::{Catalog, Status, Style, StyleFn};
2use iced_widget::core::{Background, Border, Color, border};
3
4use crate::Theme;
5use crate::tokens;
6use crate::tokens::component::button::ElevationLevels;
7use crate::utils::{
8    HOVERED_LAYER_OPACITY, disabled_container, disabled_text, mix, shadow_from_level, state_layer,
9};
10
11impl Catalog for Theme {
12    type Class<'a> = StyleFn<'a, Self>;
13
14    fn default<'a>() -> Self::Class<'a> {
15        Box::new(filled)
16    }
17
18    fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
19        class(self, status)
20    }
21}
22
23pub fn styled(
24    background: Color,
25    foreground: Color,
26    disabled: Color,
27    shadow_color: Color,
28    elevation_level: u8,
29    status: Status,
30) -> Style {
31    styled_with_elevations(
32        background,
33        foreground,
34        disabled,
35        shadow_color,
36        ElevationLevels {
37            active: elevation_level,
38            hovered: elevation_level + 1,
39            pressed: elevation_level,
40            disabled: 0,
41        },
42        status,
43    )
44}
45
46fn styled_with_elevations(
47    background: Color,
48    foreground: Color,
49    disabled: Color,
50    shadow_color: Color,
51    elevations: ElevationLevels,
52    status: Status,
53) -> Style {
54    styled_with_elevations_and_shape(
55        background,
56        foreground,
57        disabled,
58        shadow_color,
59        elevations,
60        tokens::component::button::CONTAINER_SHAPE,
61        status,
62    )
63}
64
65fn styled_with_elevations_and_shape(
66    background: Color,
67    foreground: Color,
68    disabled: Color,
69    shadow_color: Color,
70    elevations: ElevationLevels,
71    shape: f32,
72    status: Status,
73) -> Style {
74    let active = Style {
75        background: Some(Background::Color(background)),
76        text_color: foreground,
77        border: border::rounded(shape),
78        shadow: shadow_from_level(elevations.active, shadow_color),
79        snap: cfg!(feature = "crisp"),
80    };
81
82    match status {
83        Status::Active => active,
84        Status::Pressed => active,
85        Status::Hovered => Style {
86            background: Some(Background::Color(mix(
87                background,
88                foreground,
89                HOVERED_LAYER_OPACITY,
90            ))),
91            shadow: shadow_from_level(elevations.hovered, shadow_color),
92            ..active
93        },
94        Status::Disabled => Style {
95            background: Some(Background::Color(disabled_container(disabled))),
96            text_color: disabled_text(disabled),
97            border: border::rounded(shape),
98            shadow: shadow_from_level(elevations.disabled, shadow_color),
99            ..Default::default()
100        },
101    }
102}
103
104pub fn elevated(theme: &Theme, status: Status) -> Style {
105    let surface = theme.colors().surface;
106
107    let foreground = theme.colors().primary.color;
108    let background = surface.container.low;
109    let disabled = surface.text;
110
111    let shadow_color = theme.colors().shadow;
112
113    styled_with_elevations(
114        background,
115        foreground,
116        disabled,
117        shadow_color,
118        tokens::component::button::ELEVATED_ELEVATION,
119        status,
120    )
121}
122
123pub fn filled(theme: &Theme, status: Status) -> Style {
124    let primary = theme.colors().primary;
125
126    let foreground = primary.text;
127    let background = primary.color;
128    let disabled = theme.colors().surface.text;
129
130    let shadow_color = theme.colors().shadow;
131
132    styled_with_elevations(
133        background,
134        foreground,
135        disabled,
136        shadow_color,
137        tokens::component::button::FILLED_ELEVATION,
138        status,
139    )
140}
141
142pub fn filled_tonal(theme: &Theme, status: Status) -> Style {
143    let secondary = theme.colors().secondary;
144
145    let foreground = secondary.container_text;
146    let background = secondary.container;
147    let disabled = theme.colors().surface.text;
148    let shadow_color = theme.colors().shadow;
149
150    styled_with_elevations(
151        background,
152        foreground,
153        disabled,
154        shadow_color,
155        tokens::component::button::FILLED_TONAL_ELEVATION,
156        status,
157    )
158}
159
160pub fn outlined(theme: &Theme, status: Status) -> Style {
161    let foreground = theme.colors().primary.color;
162    let background = Color::TRANSPARENT;
163    let disabled = theme.colors().surface.text;
164
165    let outline = theme.colors().outline.color;
166
167    let border = match status {
168        Status::Active | Status::Pressed | Status::Hovered => Border {
169            color: outline,
170            width: tokens::component::button::OUTLINED_OUTLINE_WIDTH,
171            radius: tokens::component::button::CONTAINER_SHAPE.into(),
172        },
173        Status::Disabled => Border {
174            color: disabled_container(disabled),
175            width: tokens::component::button::OUTLINED_OUTLINE_WIDTH,
176            radius: tokens::component::button::CONTAINER_SHAPE.into(),
177        },
178    };
179
180    let mut style = styled_with_elevations(
181        background,
182        foreground,
183        disabled,
184        Color::TRANSPARENT,
185        tokens::component::button::FLAT_ELEVATION,
186        status,
187    );
188
189    if matches!(status, Status::Disabled) {
190        style.background = None;
191    }
192
193    Style { border, ..style }
194}
195
196pub fn text(theme: &Theme, status: Status) -> Style {
197    let foreground = theme.colors().primary.color;
198    let background = Color::TRANSPARENT;
199    let disabled = theme.colors().surface.text;
200
201    let style = styled_with_elevations(
202        background,
203        foreground,
204        disabled,
205        Color::TRANSPARENT,
206        tokens::component::button::FLAT_ELEVATION,
207        status,
208    );
209
210    match status {
211        Status::Hovered => style,
212        Status::Active | Status::Pressed | Status::Disabled => Style {
213            background: None,
214            ..style
215        },
216    }
217}
218
219fn fab_style_with_tokens(
220    theme: &Theme,
221    background: Color,
222    foreground: Color,
223    elevations: ElevationLevels,
224    shape: f32,
225    status: Status,
226) -> Style {
227    styled_with_elevations_and_shape(
228        background,
229        foreground,
230        theme.colors().surface.text,
231        theme.colors().shadow,
232        elevations,
233        shape,
234        status,
235    )
236}
237
238fn fab_style(theme: &Theme, background: Color, foreground: Color, status: Status) -> Style {
239    fab_style_with_tokens(
240        theme,
241        background,
242        foreground,
243        tokens::component::fab::ELEVATION,
244        tokens::component::fab::CONTAINER_SHAPE,
245        status,
246    )
247}
248
249fn fab_small_style(theme: &Theme, background: Color, foreground: Color, status: Status) -> Style {
250    fab_style_with_tokens(
251        theme,
252        background,
253        foreground,
254        tokens::component::fab::ELEVATION,
255        tokens::component::fab::SMALL_CONTAINER_SHAPE,
256        status,
257    )
258}
259
260fn fab_large_style(theme: &Theme, background: Color, foreground: Color, status: Status) -> Style {
261    fab_style_with_tokens(
262        theme,
263        background,
264        foreground,
265        tokens::component::fab::ELEVATION,
266        tokens::component::fab::LARGE_CONTAINER_SHAPE,
267        status,
268    )
269}
270
271fn extended_fab_style(
272    theme: &Theme,
273    background: Color,
274    foreground: Color,
275    status: Status,
276) -> Style {
277    fab_style_with_tokens(
278        theme,
279        background,
280        foreground,
281        tokens::component::fab::EXTENDED_ELEVATION,
282        tokens::component::fab::EXTENDED_CONTAINER_SHAPE,
283        status,
284    )
285}
286
287pub fn fab_primary(theme: &Theme, status: Status) -> Style {
288    let primary = theme.colors().primary;
289
290    fab_style(theme, primary.color, primary.text, status)
291}
292
293pub fn fab_primary_small(theme: &Theme, status: Status) -> Style {
294    let primary = theme.colors().primary;
295
296    fab_small_style(theme, primary.color, primary.text, status)
297}
298
299pub fn fab_primary_large(theme: &Theme, status: Status) -> Style {
300    let primary = theme.colors().primary;
301
302    fab_large_style(theme, primary.color, primary.text, status)
303}
304
305pub fn fab_secondary(theme: &Theme, status: Status) -> Style {
306    let secondary = theme.colors().secondary;
307
308    fab_style(theme, secondary.color, secondary.text, status)
309}
310
311pub fn fab_secondary_small(theme: &Theme, status: Status) -> Style {
312    let secondary = theme.colors().secondary;
313
314    fab_small_style(theme, secondary.color, secondary.text, status)
315}
316
317pub fn fab_secondary_large(theme: &Theme, status: Status) -> Style {
318    let secondary = theme.colors().secondary;
319
320    fab_large_style(theme, secondary.color, secondary.text, status)
321}
322
323pub fn fab_tertiary(theme: &Theme, status: Status) -> Style {
324    let tertiary = theme.colors().tertiary;
325
326    fab_style(theme, tertiary.color, tertiary.text, status)
327}
328
329pub fn fab_tertiary_small(theme: &Theme, status: Status) -> Style {
330    let tertiary = theme.colors().tertiary;
331
332    fab_small_style(theme, tertiary.color, tertiary.text, status)
333}
334
335pub fn fab_tertiary_large(theme: &Theme, status: Status) -> Style {
336    let tertiary = theme.colors().tertiary;
337
338    fab_large_style(theme, tertiary.color, tertiary.text, status)
339}
340
341pub fn fab_surface(theme: &Theme, status: Status) -> Style {
342    let colors = theme.colors();
343
344    fab_style(
345        theme,
346        colors.surface.container.high,
347        colors.primary.color,
348        status,
349    )
350}
351
352pub fn fab_surface_small(theme: &Theme, status: Status) -> Style {
353    let colors = theme.colors();
354
355    fab_small_style(
356        theme,
357        colors.surface.container.high,
358        colors.primary.color,
359        status,
360    )
361}
362
363pub fn fab_surface_large(theme: &Theme, status: Status) -> Style {
364    let colors = theme.colors();
365
366    fab_large_style(
367        theme,
368        colors.surface.container.high,
369        colors.primary.color,
370        status,
371    )
372}
373
374pub fn extended_fab_primary(theme: &Theme, status: Status) -> Style {
375    let primary = theme.colors().primary;
376
377    extended_fab_style(theme, primary.color, primary.text, status)
378}
379
380pub fn extended_fab_secondary(theme: &Theme, status: Status) -> Style {
381    let secondary = theme.colors().secondary;
382
383    extended_fab_style(theme, secondary.color, secondary.text, status)
384}
385
386pub fn extended_fab_tertiary(theme: &Theme, status: Status) -> Style {
387    let tertiary = theme.colors().tertiary;
388
389    extended_fab_style(theme, tertiary.color, tertiary.text, status)
390}
391
392pub fn extended_fab_surface(theme: &Theme, status: Status) -> Style {
393    let colors = theme.colors();
394
395    extended_fab_style(
396        theme,
397        colors.surface.container.high,
398        colors.primary.color,
399        status,
400    )
401}
402
403pub fn icon(theme: &Theme, status: Status) -> Style {
404    let surface = theme.colors().surface;
405
406    let active = Style {
407        background: None,
408        text_color: surface.text_variant,
409        border: border::rounded(tokens::component::icon_button::CONTAINER_SHAPE),
410        shadow: shadow_from_level(0, Color::TRANSPARENT),
411        snap: cfg!(feature = "crisp"),
412    };
413
414    match status {
415        Status::Active => active,
416        Status::Hovered => Style {
417            background: Some(Background::Color(mix(
418                Color::TRANSPARENT,
419                surface.text_variant,
420                HOVERED_LAYER_OPACITY,
421            ))),
422            ..active
423        },
424        Status::Pressed => active,
425        Status::Disabled => Style {
426            text_color: Color {
427                a: tokens::component::icon_button::DISABLED_ICON_OPACITY,
428                ..surface.text
429            },
430            ..active
431        },
432    }
433}
434
435pub fn filled_icon(theme: &Theme, status: Status) -> Style {
436    let colors = theme.colors();
437
438    styled_with_elevations_and_shape(
439        colors.primary.color,
440        colors.primary.text,
441        colors.surface.text,
442        Color::TRANSPARENT,
443        tokens::component::button::FLAT_ELEVATION,
444        tokens::component::icon_button::CONTAINER_SHAPE,
445        status,
446    )
447}
448
449pub fn filled_tonal_icon(theme: &Theme, status: Status) -> Style {
450    let colors = theme.colors();
451
452    styled_with_elevations_and_shape(
453        colors.secondary.container,
454        colors.secondary.container_text,
455        colors.surface.text,
456        Color::TRANSPARENT,
457        tokens::component::button::FLAT_ELEVATION,
458        tokens::component::icon_button::CONTAINER_SHAPE,
459        status,
460    )
461}
462
463pub fn outlined_icon(theme: &Theme, status: Status) -> Style {
464    let colors = theme.colors();
465    let surface = colors.surface;
466
467    let active = Style {
468        background: None,
469        text_color: surface.text_variant,
470        border: Border {
471            color: colors.outline.color,
472            width: tokens::component::icon_button::OUTLINED_OUTLINE_WIDTH,
473            radius: tokens::component::icon_button::CONTAINER_SHAPE.into(),
474        },
475        shadow: shadow_from_level(0, Color::TRANSPARENT),
476        snap: cfg!(feature = "crisp"),
477    };
478
479    match status {
480        Status::Active => active,
481        Status::Hovered => Style {
482            background: Some(Background::Color(mix(
483                Color::TRANSPARENT,
484                surface.text_variant,
485                HOVERED_LAYER_OPACITY,
486            ))),
487            ..active
488        },
489        Status::Pressed => active,
490        Status::Disabled => Style {
491            text_color: Color {
492                a: tokens::component::icon_button::DISABLED_ICON_OPACITY,
493                ..surface.text
494            },
495            border: Border {
496                color: Color {
497                    a: tokens::component::icon_button::OUTLINED_DISABLED_OUTLINE_OPACITY,
498                    ..surface.text
499                },
500                ..active.border
501            },
502            ..active
503        },
504    }
505}
506
507#[derive(Debug, Clone, Copy)]
508struct ChipSpec {
509    background: Option<Color>,
510    foreground: Color,
511    outline: Option<Color>,
512    disabled_background: Option<Color>,
513    disabled_outline: Option<Color>,
514    hover_layer: Color,
515    elevations: ElevationLevels,
516    shadow_color: Color,
517}
518
519fn chip_style(spec: ChipSpec, status: Status) -> Style {
520    let border = Border {
521        color: spec.outline.unwrap_or(Color::TRANSPARENT),
522        width: spec
523            .outline
524            .map_or(tokens::component::chip::SELECTED_OUTLINE_WIDTH, |_| {
525                tokens::component::chip::OUTLINE_WIDTH
526            }),
527        radius: tokens::component::chip::CONTAINER_SHAPE.into(),
528    };
529
530    let active = Style {
531        background: spec.background.map(Background::Color),
532        text_color: spec.foreground,
533        border,
534        shadow: shadow_from_level(spec.elevations.active, spec.shadow_color),
535        snap: cfg!(feature = "crisp"),
536    };
537
538    match status {
539        Status::Active => active,
540        Status::Hovered => Style {
541            background: Some(Background::Color(chip_state_background(
542                spec.background,
543                spec.hover_layer,
544                HOVERED_LAYER_OPACITY,
545            ))),
546            shadow: shadow_from_level(spec.elevations.hovered, spec.shadow_color),
547            ..active
548        },
549        Status::Pressed => active,
550        Status::Disabled => Style {
551            background: spec.disabled_background.map(Background::Color),
552            text_color: Color {
553                a: tokens::component::chip::DISABLED_LABEL_TEXT_OPACITY,
554                ..spec.foreground
555            },
556            border: Border {
557                color: spec.disabled_outline.unwrap_or(Color::TRANSPARENT),
558                ..border
559            },
560            shadow: shadow_from_level(spec.elevations.disabled, spec.shadow_color),
561            snap: cfg!(feature = "crisp"),
562        },
563    }
564}
565
566fn chip_state_background(background: Option<Color>, layer: Color, opacity: f32) -> Color {
567    background.map_or_else(
568        || state_layer(layer, opacity),
569        |background| mix(background, layer, opacity),
570    )
571}
572
573fn outlined_chip_spec(foreground: Color, outline: Color, disabled_color: Color) -> ChipSpec {
574    ChipSpec {
575        background: None,
576        foreground,
577        outline: Some(outline),
578        disabled_background: None,
579        disabled_outline: Some(Color {
580            a: tokens::component::chip::DISABLED_OUTLINE_OPACITY,
581            ..disabled_color
582        }),
583        hover_layer: foreground,
584        elevations: tokens::component::chip::FLAT_ELEVATION,
585        shadow_color: Color::TRANSPARENT,
586    }
587}
588
589fn elevated_chip_spec(
590    background: Color,
591    foreground: Color,
592    disabled_color: Color,
593    shadow_color: Color,
594) -> ChipSpec {
595    ChipSpec {
596        background: Some(background),
597        foreground,
598        outline: None,
599        disabled_background: Some(Color {
600            a: tokens::component::chip::DISABLED_CONTAINER_OPACITY,
601            ..disabled_color
602        }),
603        disabled_outline: None,
604        hover_layer: foreground,
605        elevations: tokens::component::chip::ELEVATED_ELEVATION,
606        shadow_color,
607    }
608}
609
610pub fn assist_chip(theme: &Theme, status: Status) -> Style {
611    let colors = theme.colors();
612
613    chip_style(
614        outlined_chip_spec(
615            colors.surface.text,
616            colors.outline.color,
617            colors.surface.text,
618        ),
619        status,
620    )
621}
622
623pub fn elevated_assist_chip(theme: &Theme, status: Status) -> Style {
624    let colors = theme.colors();
625
626    chip_style(
627        elevated_chip_spec(
628            colors.surface.container.low,
629            colors.surface.text,
630            colors.surface.text,
631            colors.shadow,
632        ),
633        status,
634    )
635}
636
637pub fn suggestion_chip(theme: &Theme, status: Status) -> Style {
638    let colors = theme.colors();
639
640    chip_style(
641        outlined_chip_spec(
642            colors.surface.text_variant,
643            colors.outline.color,
644            colors.surface.text,
645        ),
646        status,
647    )
648}
649
650pub fn elevated_suggestion_chip(theme: &Theme, status: Status) -> Style {
651    let colors = theme.colors();
652
653    chip_style(
654        elevated_chip_spec(
655            colors.surface.container.low,
656            colors.surface.text_variant,
657            colors.surface.text,
658            colors.shadow,
659        ),
660        status,
661    )
662}
663
664pub fn filter_chip(theme: &Theme, status: Status) -> Style {
665    let colors = theme.colors();
666
667    chip_style(
668        outlined_chip_spec(
669            colors.surface.text_variant,
670            colors.outline.color,
671            colors.surface.text,
672        ),
673        status,
674    )
675}
676
677pub fn selected_filter_chip(theme: &Theme, status: Status) -> Style {
678    let colors = theme.colors();
679
680    chip_style(
681        ChipSpec {
682            background: Some(colors.secondary.container),
683            foreground: colors.secondary.container_text,
684            outline: None,
685            disabled_background: Some(Color {
686                a: tokens::component::chip::DISABLED_CONTAINER_OPACITY,
687                ..colors.surface.text
688            }),
689            disabled_outline: None,
690            hover_layer: colors.secondary.container_text,
691            elevations: tokens::component::chip::SELECTED_FLAT_ELEVATION,
692            shadow_color: colors.shadow,
693        },
694        status,
695    )
696}
697
698pub fn input_chip(theme: &Theme, status: Status) -> Style {
699    let colors = theme.colors();
700
701    chip_style(
702        outlined_chip_spec(
703            colors.surface.text_variant,
704            colors.outline.color,
705            colors.surface.text,
706        ),
707        status,
708    )
709}
710
711pub fn selected_input_chip(theme: &Theme, status: Status) -> Style {
712    let colors = theme.colors();
713
714    chip_style(
715        ChipSpec {
716            background: Some(colors.secondary.container),
717            foreground: colors.secondary.container_text,
718            outline: None,
719            disabled_background: Some(Color {
720                a: tokens::component::chip::DISABLED_CONTAINER_OPACITY,
721                ..colors.surface.text
722            }),
723            disabled_outline: None,
724            hover_layer: colors.secondary.container_text,
725            elevations: tokens::component::chip::FLAT_ELEVATION,
726            shadow_color: Color::TRANSPARENT,
727        },
728        status,
729    )
730}
731
732#[cfg(test)]
733#[path = "../../../tests/design/style/button.rs"]
734mod tests;