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