Skip to main content

repose_material/material3/
chips.rs

1#![allow(non_snake_case)]
2
3use std::rc::Rc;
4use std::sync::atomic::Ordering;
5
6use repose_core::*;
7use repose_ui::{
8    Box, Row,
9    ViewExt,
10    anim::animate_color,
11};
12
13use super::*;
14use super::util::FILTERCHIP_COUNTER;
15
16/// Color slots for chips (both non-selectable and selectable).
17#[derive(Clone, Copy, Debug)]
18pub struct ChipColors {
19    pub container_color: Color,
20    pub label_color: Color,
21    pub leading_icon_color: Color,
22    pub trailing_icon_color: Color,
23    pub disabled_container_color: Color,
24    pub disabled_label_color: Color,
25    pub disabled_leading_icon_color: Color,
26    pub disabled_trailing_icon_color: Color,
27    pub selected_container_color: Color,
28    pub selected_label_color: Color,
29    pub selected_leading_icon_color: Color,
30    pub selected_trailing_icon_color: Color,
31    pub disabled_selected_container_color: Color,
32}
33
34impl ChipColors {
35    pub fn container(&self, enabled: bool, selected: bool) -> Color {
36        match (enabled, selected) {
37            (true, true) => self.selected_container_color,
38            (true, false) => self.container_color,
39            (false, true) => self.disabled_selected_container_color,
40            (false, false) => self.disabled_container_color,
41        }
42    }
43    pub fn label(&self, enabled: bool, selected: bool) -> Color {
44        if !enabled {
45            self.disabled_label_color
46        } else if selected {
47            self.selected_label_color
48        } else {
49            self.label_color
50        }
51    }
52    pub fn leading_icon(&self, enabled: bool, selected: bool) -> Color {
53        if !enabled {
54            self.disabled_leading_icon_color
55        } else if selected {
56            self.selected_leading_icon_color
57        } else {
58            self.leading_icon_color
59        }
60    }
61    pub fn trailing_icon(&self, enabled: bool, selected: bool) -> Color {
62        if !enabled {
63            self.disabled_trailing_icon_color
64        } else if selected {
65            self.selected_trailing_icon_color
66        } else {
67            self.trailing_icon_color
68        }
69    }
70}
71
72/// Elevation levels for chips.
73#[derive(Clone, Copy, Debug)]
74pub struct ChipElevation {
75    pub default: f32,
76    pub hovered: f32,
77    pub focused: f32,
78    pub pressed: f32,
79    pub dragged: f32,
80    pub disabled: f32,
81}
82
83impl ChipElevation {
84    pub fn to_state_elevation(&self) -> StateElevation {
85        StateElevation {
86            default: self.default,
87            hovered: self.hovered,
88            pressed: self.pressed,
89            dragged: self.dragged,
90            disabled: self.disabled,
91        }
92    }
93}
94
95impl Default for ChipElevation {
96    fn default() -> Self {
97        Self {
98            default: ChipDefaults::elevation_default(),
99            hovered: ChipDefaults::elevation_hovered(),
100            focused: ChipDefaults::elevation_focused(),
101            pressed: ChipDefaults::elevation_pressed(),
102            dragged: ChipDefaults::elevation_dragged(),
103            disabled: ChipDefaults::elevation_disabled(),
104        }
105    }
106}
107
108/// Configuration for chips.
109#[derive(Clone, Debug)]
110pub struct ChipConfig {
111    pub modifier: Modifier,
112    pub enabled: bool,
113    pub colors: ChipColors,
114    pub elevation: ChipElevation,
115    pub border_width: f32,
116    pub border_color: Color,
117    pub selected_border_color: Color,
118    pub disabled_border_color: Color,
119    pub disabled_selected_border_color: Color,
120    pub shape_radius: f32,
121    pub horizontal_padding: f32,
122    pub interaction_source: Option<MutableInteractionSource>,
123}
124
125impl Default for ChipConfig {
126    fn default() -> Self {
127        Self {
128            modifier: Modifier::new(),
129            enabled: true,
130            colors: ChipColors {
131                container_color: ChipDefaults::container_color(),
132                label_color: ChipDefaults::label_color(),
133                leading_icon_color: ChipDefaults::leading_icon_color(),
134                trailing_icon_color: ChipDefaults::trailing_icon_color(),
135                disabled_container_color: ChipDefaults::disabled_container_color(),
136                disabled_label_color: ChipDefaults::disabled_label_color(),
137                disabled_leading_icon_color: ChipDefaults::disabled_leading_icon_color(),
138                disabled_trailing_icon_color: ChipDefaults::disabled_trailing_icon_color(),
139                selected_container_color: ChipDefaults::selected_container_color(),
140                selected_label_color: ChipDefaults::selected_label_color(),
141                selected_leading_icon_color: ChipDefaults::selected_leading_icon_color(),
142                selected_trailing_icon_color: ChipDefaults::selected_trailing_icon_color(),
143                disabled_selected_container_color: ChipDefaults::disabled_selected_container_color(
144                ),
145            },
146            elevation: ChipElevation::default(),
147            border_width: ChipDefaults::BORDER_WIDTH,
148            border_color: ChipDefaults::border_color(),
149            selected_border_color: ChipDefaults::selected_border_color(),
150            disabled_border_color: ChipDefaults::disabled_border_color(),
151            disabled_selected_border_color: ChipDefaults::disabled_selected_border_color(),
152            shape_radius: ChipDefaults::SHAPE_RADIUS,
153            horizontal_padding: ChipDefaults::HORIZONTAL_PADDING,
154            interaction_source: None,
155        }
156    }
157}
158
159/// M3 Assist Chip - a chip for triggering actions.
160pub fn AssistChip(
161    on_click: impl Fn() + 'static,
162    label: View,
163    leading_icon: Option<View>,
164    trailing_icon: Option<View>,
165    config: ChipConfig,
166) -> View {
167    let th = theme();
168    let is_enabled = config.enabled;
169    let colors = &config.colors;
170    let bg = colors.container(is_enabled, false);
171    let label_color = colors.label(is_enabled, false);
172    let leading_color = colors.leading_icon(is_enabled, false);
173    let trailing_color = colors.trailing_icon(is_enabled, false);
174    let border = if is_enabled {
175        config.border_color
176    } else {
177        config.disabled_border_color
178    };
179    let shape = config.shape_radius;
180    let ch_source: Rc<MutableInteractionSource> = config
181        .interaction_source
182        .clone()
183        .map(Rc::new)
184        .unwrap_or_else(|| remember(MutableInteractionSource::new));
185
186    let mut m = Modifier::new()
187        .state_colors(StateColors {
188            default: Color::TRANSPARENT,
189            hovered: th.on_surface.with_alpha_f32(0.08),
190            pressed: th.on_surface.with_alpha_f32(0.12),
191            dragged: th.on_surface.with_alpha_f32(0.12),
192            disabled: Color::TRANSPARENT,
193        })
194        .padding_values(PaddingValues {
195            left: config.horizontal_padding,
196            right: config.horizontal_padding,
197            top: 8.0,
198            bottom: 8.0,
199        })
200        .background(bg)
201        .clip_rounded(shape)
202        .interaction_source(&*ch_source)
203        .then(config.modifier);
204
205    if config.border_width > 0.0 && border != Color::TRANSPARENT {
206        m = m.border(config.border_width, border, shape);
207    }
208
209    if is_enabled {
210        m = m.clickable().on_click(move || on_click());
211    }
212
213    Box(m).child(
214        Row(Modifier::new().align_items(AlignItems::CENTER)).child((
215            leading_icon
216                .map(|v| {
217                    Box(Modifier::new().padding_values(PaddingValues {
218                        left: 0.0,
219                        right: 8.0,
220                        top: 0.0,
221                        bottom: 0.0,
222                    }))
223                    .child(with_content_color(leading_color, move || v))
224                })
225                .unwrap_or(Box(Modifier::new())),
226            with_content_color(label_color, move || label),
227            trailing_icon
228                .map(|v| {
229                    Box(Modifier::new().padding_values(PaddingValues {
230                        left: 8.0,
231                        right: 0.0,
232                        top: 0.0,
233                        bottom: 0.0,
234                    }))
235                    .child(with_content_color(trailing_color, move || v))
236                })
237                .unwrap_or(Box(Modifier::new())),
238        )),
239    )
240}
241
242/// M3 Elevated Assist Chip - like [`AssistChip`] but with elevated container.
243pub fn ElevatedAssistChip(
244    on_click: impl Fn() + 'static,
245    label: View,
246    leading_icon: Option<View>,
247    trailing_icon: Option<View>,
248    config: ChipConfig,
249) -> View {
250    let th = theme();
251    let is_enabled = config.enabled;
252    let colors = &config.colors;
253    let bg = colors.container(is_enabled, false);
254    let label_color = colors.label(is_enabled, false);
255    let leading_color = colors.leading_icon(is_enabled, false);
256    let trailing_color = colors.trailing_icon(is_enabled, false);
257    let shape = config.shape_radius;
258    let ch_source: Rc<MutableInteractionSource> = config
259        .interaction_source
260        .clone()
261        .map(Rc::new)
262        .unwrap_or_else(|| remember(MutableInteractionSource::new));
263
264    let mut m = Modifier::new()
265        .state_colors(StateColors {
266            default: Color::TRANSPARENT,
267            hovered: th.on_surface.with_alpha_f32(0.08),
268            pressed: th.on_surface.with_alpha_f32(0.12),
269            dragged: th.on_surface.with_alpha_f32(0.12),
270            disabled: Color::TRANSPARENT,
271        })
272        .state_elevation(config.elevation.to_state_elevation())
273        .padding_values(PaddingValues {
274            left: config.horizontal_padding,
275            right: config.horizontal_padding,
276            top: 8.0,
277            bottom: 8.0,
278        })
279        .background(bg)
280        .clip_rounded(shape)
281        .interaction_source(&*ch_source)
282        .then(config.modifier);
283
284    if is_enabled {
285        m = m.clickable().on_click(move || on_click());
286    }
287
288    Box(m).child(
289        Row(Modifier::new().align_items(AlignItems::CENTER)).child((
290            leading_icon
291                .map(|v| {
292                    Box(Modifier::new().padding_values(PaddingValues {
293                        left: 0.0,
294                        right: 8.0,
295                        top: 0.0,
296                        bottom: 0.0,
297                    }))
298                    .child(with_content_color(leading_color, move || v))
299                })
300                .unwrap_or(Box(Modifier::new())),
301            with_content_color(label_color, move || label),
302            trailing_icon
303                .map(|v| {
304                    Box(Modifier::new().padding_values(PaddingValues {
305                        left: 8.0,
306                        right: 0.0,
307                        top: 0.0,
308                        bottom: 0.0,
309                    }))
310                    .child(with_content_color(trailing_color, move || v))
311                })
312                .unwrap_or(Box(Modifier::new())),
313        )),
314    )
315}
316
317pub fn FilterChip(
318    selected: bool,
319    on_click: impl Fn() + 'static,
320    label: View,
321    leading_icon: Option<View>,
322    trailing_icon: Option<View>,
323    config: ChipConfig,
324) -> View {
325    let th = theme();
326    let id = remember(|| FILTERCHIP_COUNTER.fetch_add(1, Ordering::Relaxed));
327    let spec = th.motion.color;
328    let is_enabled = config.enabled;
329    let colors = &config.colors;
330
331    let bg = animate_color(
332        format!("fc_bg_{}", id),
333        colors.container(is_enabled, selected),
334        spec,
335    );
336    let label_color = animate_color(
337        format!("fc_lc_{}", id),
338        colors.label(is_enabled, selected),
339        spec,
340    );
341    let leading_color = animate_color(
342        format!("fc_lic_{}", id),
343        colors.leading_icon(is_enabled, selected),
344        spec,
345    );
346    let trailing_color = animate_color(
347        format!("fc_tic_{}", id),
348        colors.trailing_icon(is_enabled, selected),
349        spec,
350    );
351    let border = if !is_enabled {
352        if selected {
353            config.disabled_selected_border_color
354        } else {
355            config.disabled_border_color
356        }
357    } else {
358        if selected {
359            config.selected_border_color
360        } else {
361            config.border_color
362        }
363    };
364    let shape = config.shape_radius;
365
366    let ch_source: Rc<MutableInteractionSource> = config
367        .interaction_source
368        .clone()
369        .map(Rc::new)
370        .unwrap_or_else(|| remember(MutableInteractionSource::new));
371
372    let mut m = Modifier::new()
373        .state_colors(StateColors {
374            default: Color::TRANSPARENT,
375            hovered: th.on_surface.with_alpha_f32(0.08),
376            pressed: th.on_surface.with_alpha_f32(0.12),
377            dragged: th.on_surface.with_alpha_f32(0.12),
378            disabled: Color::TRANSPARENT,
379        })
380        .padding_values(PaddingValues {
381            left: config.horizontal_padding,
382            right: config.horizontal_padding,
383            top: 8.0,
384            bottom: 8.0,
385        })
386        .background(bg)
387        .clip_rounded(shape)
388        .interaction_source(&*ch_source)
389        .then(config.modifier);
390
391    if config.border_width > 0.0 && border != Color::TRANSPARENT {
392        m = m.border(config.border_width, border, shape);
393    }
394    if is_enabled {
395        m = m.clickable().on_click(move || on_click());
396    }
397
398    Box(m).child(
399        Row(Modifier::new().align_items(AlignItems::CENTER)).child((
400            leading_icon
401                .map(|v| {
402                    Box(Modifier::new().padding_values(PaddingValues {
403                        left: 0.0,
404                        right: 8.0,
405                        top: 0.0,
406                        bottom: 0.0,
407                    }))
408                    .child(with_content_color(leading_color, move || v))
409                })
410                .unwrap_or(Box(Modifier::new())),
411            with_content_color(label_color, move || label),
412            trailing_icon
413                .map(|v| {
414                    Box(Modifier::new().padding_values(PaddingValues {
415                        left: 8.0,
416                        right: 0.0,
417                        top: 0.0,
418                        bottom: 0.0,
419                    }))
420                    .child(with_content_color(trailing_color, move || v))
421                })
422                .unwrap_or(Box(Modifier::new())),
423        )),
424    )
425}
426
427/// M3 Elevated Filter Chip - like [`FilterChip`] but with elevation and filled container.
428pub fn ElevatedFilterChip(
429    selected: bool,
430    on_click: impl Fn() + 'static,
431    label: View,
432    leading_icon: Option<View>,
433    trailing_icon: Option<View>,
434    config: ChipConfig,
435) -> View {
436    let th = theme();
437    let id = remember(|| FILTERCHIP_COUNTER.fetch_add(1, Ordering::Relaxed));
438    let spec = th.motion.color;
439    let is_enabled = config.enabled;
440    let colors = &config.colors;
441
442    let bg = animate_color(
443        format!("efc_bg_{}", id),
444        colors.container(is_enabled, selected),
445        spec,
446    );
447    let label_color = animate_color(
448        format!("efc_lc_{}", id),
449        colors.label(is_enabled, selected),
450        spec,
451    );
452    let leading_color = animate_color(
453        format!("efc_lic_{}", id),
454        colors.leading_icon(is_enabled, selected),
455        spec,
456    );
457    let trailing_color = animate_color(
458        format!("efc_tic_{}", id),
459        colors.trailing_icon(is_enabled, selected),
460        spec,
461    );
462    let shape = config.shape_radius;
463
464    let ch_source: Rc<MutableInteractionSource> = config
465        .interaction_source
466        .clone()
467        .map(Rc::new)
468        .unwrap_or_else(|| remember(MutableInteractionSource::new));
469
470    let mut m = Modifier::new()
471        .state_colors(StateColors {
472            default: Color::TRANSPARENT,
473            hovered: th.on_surface.with_alpha_f32(0.08),
474            pressed: th.on_surface.with_alpha_f32(0.12),
475            dragged: th.on_surface.with_alpha_f32(0.12),
476            disabled: Color::TRANSPARENT,
477        })
478        .state_elevation(config.elevation.to_state_elevation())
479        .padding_values(PaddingValues {
480            left: config.horizontal_padding,
481            right: config.horizontal_padding,
482            top: 8.0,
483            bottom: 8.0,
484        })
485        .background(bg)
486        .clip_rounded(shape)
487        .interaction_source(&*ch_source)
488        .then(config.modifier);
489
490    if is_enabled {
491        m = m.clickable().on_click(move || on_click());
492    }
493
494    Box(m).child(
495        Row(Modifier::new().align_items(AlignItems::CENTER)).child((
496            leading_icon
497                .map(|v| {
498                    Box(Modifier::new().padding_values(PaddingValues {
499                        left: 0.0,
500                        right: 8.0,
501                        top: 0.0,
502                        bottom: 0.0,
503                    }))
504                    .child(with_content_color(leading_color, move || v))
505                })
506                .unwrap_or(Box(Modifier::new())),
507            with_content_color(label_color, move || label),
508            trailing_icon
509                .map(|v| {
510                    Box(Modifier::new().padding_values(PaddingValues {
511                        left: 8.0,
512                        right: 0.0,
513                        top: 0.0,
514                        bottom: 0.0,
515                    }))
516                    .child(with_content_color(trailing_color, move || v))
517                })
518                .unwrap_or(Box(Modifier::new())),
519        )),
520    )
521}
522
523pub fn SuggestionChip(
524    on_click: impl Fn() + 'static,
525    label: View,
526    icon: Option<View>,
527    config: ChipConfig,
528) -> View {
529    let th = theme();
530    let is_enabled = config.enabled;
531    let colors = &config.colors;
532    let bg = colors.container(is_enabled, false);
533    let label_color = colors.label(is_enabled, false);
534    let leading_color = colors.leading_icon(is_enabled, false);
535    let border = if is_enabled {
536        config.border_color
537    } else {
538        config.disabled_border_color
539    };
540    let shape = config.shape_radius;
541
542    let ch_source: Rc<MutableInteractionSource> = config
543        .interaction_source
544        .clone()
545        .map(Rc::new)
546        .unwrap_or_else(|| remember(MutableInteractionSource::new));
547
548    let mut m = Modifier::new()
549        .state_colors(StateColors {
550            default: Color::TRANSPARENT,
551            hovered: th.on_surface.with_alpha_f32(0.08),
552            pressed: th.on_surface.with_alpha_f32(0.12),
553            dragged: th.on_surface.with_alpha_f32(0.12),
554            disabled: Color::TRANSPARENT,
555        })
556        .padding_values(PaddingValues {
557            left: config.horizontal_padding,
558            right: config.horizontal_padding,
559            top: 8.0,
560            bottom: 8.0,
561        })
562        .background(bg)
563        .clip_rounded(shape)
564        .interaction_source(&*ch_source)
565        .then(config.modifier);
566
567    if config.border_width > 0.0 && border != Color::TRANSPARENT {
568        m = m.border(config.border_width, border, shape);
569    }
570    if is_enabled {
571        m = m.clickable().on_click(move || on_click());
572    }
573
574    Box(m).child(
575        Row(Modifier::new().align_items(AlignItems::CENTER)).child((
576            icon.map(|v| {
577                Box(Modifier::new().padding_values(PaddingValues {
578                    left: 0.0,
579                    right: 8.0,
580                    top: 0.0,
581                    bottom: 0.0,
582                }))
583                .child(with_content_color(leading_color, move || v))
584            })
585            .unwrap_or(Box(Modifier::new())),
586            with_content_color(label_color, move || label),
587        )),
588    )
589}
590
591/// M3 Elevated Suggestion Chip - like [`SuggestionChip`] but with elevation and filled bg.
592pub fn ElevatedSuggestionChip(
593    on_click: impl Fn() + 'static,
594    label: View,
595    icon: Option<View>,
596    config: ChipConfig,
597) -> View {
598    let th = theme();
599    let is_enabled = config.enabled;
600    let colors = &config.colors;
601    let bg = colors.container(is_enabled, false);
602    let label_color = colors.label(is_enabled, false);
603    let leading_color = colors.leading_icon(is_enabled, false);
604    let shape = config.shape_radius;
605
606    let ch_source: Rc<MutableInteractionSource> = config
607        .interaction_source
608        .clone()
609        .map(Rc::new)
610        .unwrap_or_else(|| remember(MutableInteractionSource::new));
611
612    let mut m = Modifier::new()
613        .state_colors(StateColors {
614            default: Color::TRANSPARENT,
615            hovered: th.on_surface.with_alpha_f32(0.08),
616            pressed: th.on_surface.with_alpha_f32(0.12),
617            dragged: th.on_surface.with_alpha_f32(0.12),
618            disabled: Color::TRANSPARENT,
619        })
620        .state_elevation(config.elevation.to_state_elevation())
621        .padding_values(PaddingValues {
622            left: config.horizontal_padding,
623            right: config.horizontal_padding,
624            top: 8.0,
625            bottom: 8.0,
626        })
627        .background(bg)
628        .clip_rounded(shape)
629        .interaction_source(&*ch_source)
630        .then(config.modifier);
631
632    if is_enabled {
633        m = m.clickable().on_click(move || on_click());
634    }
635
636    Box(m).child(
637        Row(Modifier::new().align_items(AlignItems::CENTER)).child((
638            icon.map(|v| {
639                Box(Modifier::new().padding_values(PaddingValues {
640                    left: 0.0,
641                    right: 8.0,
642                    top: 0.0,
643                    bottom: 0.0,
644                }))
645                .child(with_content_color(leading_color, move || v))
646            })
647            .unwrap_or(Box(Modifier::new())),
648            with_content_color(label_color, move || label),
649        )),
650    )
651}
652
653pub fn InputChip(
654    selected: bool,
655    on_click: impl Fn() + 'static,
656    label: View,
657    leading_icon: Option<View>,
658    avatar: Option<View>,
659    trailing_icon: Option<View>,
660    config: ChipConfig,
661) -> View {
662    let th = theme();
663    let id = remember(|| FILTERCHIP_COUNTER.fetch_add(1, Ordering::Relaxed));
664    let spec = th.motion.color;
665    let is_enabled = config.enabled;
666    let colors = &config.colors;
667
668    let bg = animate_color(
669        format!("ic_bg_{}", id),
670        colors.container(is_enabled, selected),
671        spec,
672    );
673    let label_color = animate_color(
674        format!("ic_lc_{}", id),
675        colors.label(is_enabled, selected),
676        spec,
677    );
678    let leading_color = animate_color(
679        format!("ic_lic_{}", id),
680        colors.leading_icon(is_enabled, selected),
681        spec,
682    );
683    let trailing_color = animate_color(
684        format!("ic_tic_{}", id),
685        colors.trailing_icon(is_enabled, selected),
686        spec,
687    );
688    let border = if !is_enabled {
689        if selected {
690            config.disabled_selected_border_color
691        } else {
692            config.disabled_border_color
693        }
694    } else {
695        if selected {
696            config.selected_border_color
697        } else {
698            config.border_color
699        }
700    };
701    let shape = config.shape_radius;
702
703    let ch_source: Rc<MutableInteractionSource> = config
704        .interaction_source
705        .clone()
706        .map(Rc::new)
707        .unwrap_or_else(|| remember(MutableInteractionSource::new));
708
709    let mut m = Modifier::new()
710        .state_colors(StateColors {
711            default: Color::TRANSPARENT,
712            hovered: th.on_surface.with_alpha_f32(0.08),
713            pressed: th.on_surface.with_alpha_f32(0.12),
714            dragged: th.on_surface.with_alpha_f32(0.12),
715            disabled: Color::TRANSPARENT,
716        })
717        .padding_values(PaddingValues {
718            left: config.horizontal_padding,
719            right: config.horizontal_padding,
720            top: 8.0,
721            bottom: 8.0,
722        })
723        .background(bg)
724        .clip_rounded(shape)
725        .interaction_source(&*ch_source)
726        .then(config.modifier);
727
728    if config.border_width > 0.0 && border != Color::TRANSPARENT {
729        m = m.border(config.border_width, border, shape);
730    }
731    if is_enabled {
732        m = m.clickable().on_click(move || on_click());
733    }
734
735    Box(m).child(
736        Row(Modifier::new().align_items(AlignItems::CENTER)).child((
737            avatar
738                .or(leading_icon)
739                .map(|v| {
740                    Box(Modifier::new().padding_values(PaddingValues {
741                        left: 0.0,
742                        right: 8.0,
743                        top: 0.0,
744                        bottom: 0.0,
745                    }))
746                    .child(with_content_color(leading_color, move || v))
747                })
748                .unwrap_or(Box(Modifier::new())),
749            with_content_color(label_color, move || label),
750            trailing_icon
751                .map(|v| {
752                    Box(Modifier::new().padding_values(PaddingValues {
753                        left: 8.0,
754                        right: 0.0,
755                        top: 0.0,
756                        bottom: 0.0,
757                    }))
758                    .child(with_content_color(trailing_color, move || v))
759                })
760                .unwrap_or(Box(Modifier::new())),
761        )),
762    )
763}