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;