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, FlowRow, 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            focused: self.focused,
85            pressed: self.pressed,
86            dragged: self.dragged,
87            disabled: self.disabled,
88        }
89    }
90}
91
92impl Default for ChipElevation {
93    fn default() -> Self {
94        Self {
95            default: ChipDefaults::elevation_default(),
96            hovered: ChipDefaults::elevation_hovered(),
97            focused: ChipDefaults::elevation_focused(),
98            pressed: ChipDefaults::elevation_pressed(),
99            dragged: ChipDefaults::elevation_dragged(),
100            disabled: ChipDefaults::elevation_disabled(),
101        }
102    }
103}
104
105/// Configuration for chips.
106#[derive(Clone, Debug)]
107pub struct ChipConfig {
108    pub modifier: Modifier,
109    pub enabled: bool,
110    pub colors: ChipColors,
111    pub elevation: ChipElevation,
112    pub border_width: f32,
113    pub border_color: Color,
114    pub selected_border_color: Color,
115    pub disabled_border_color: Color,
116    pub disabled_selected_border_color: Color,
117    pub shape_radius: f32,
118    pub horizontal_padding: f32,
119    pub interaction_source: Option<MutableInteractionSource>,
120}
121
122impl Default for ChipConfig {
123    fn default() -> Self {
124        Self {
125            modifier: Modifier::new(),
126            enabled: true,
127            colors: ChipColors {
128                container_color: ChipDefaults::container_color(),
129                label_color: ChipDefaults::label_color(),
130                leading_icon_color: ChipDefaults::leading_icon_color(),
131                trailing_icon_color: ChipDefaults::trailing_icon_color(),
132                disabled_container_color: ChipDefaults::disabled_container_color(),
133                disabled_label_color: ChipDefaults::disabled_label_color(),
134                disabled_leading_icon_color: ChipDefaults::disabled_leading_icon_color(),
135                disabled_trailing_icon_color: ChipDefaults::disabled_trailing_icon_color(),
136                selected_container_color: ChipDefaults::selected_container_color(),
137                selected_label_color: ChipDefaults::selected_label_color(),
138                selected_leading_icon_color: ChipDefaults::selected_leading_icon_color(),
139                selected_trailing_icon_color: ChipDefaults::selected_trailing_icon_color(),
140                disabled_selected_container_color: ChipDefaults::disabled_selected_container_color(
141                ),
142            },
143            elevation: ChipElevation::default(),
144            border_width: ChipDefaults::BORDER_WIDTH,
145            border_color: ChipDefaults::border_color(),
146            selected_border_color: ChipDefaults::selected_border_color(),
147            disabled_border_color: ChipDefaults::disabled_border_color(),
148            disabled_selected_border_color: ChipDefaults::disabled_selected_border_color(),
149            shape_radius: ChipDefaults::SHAPE_RADIUS,
150            horizontal_padding: ChipDefaults::HORIZONTAL_PADDING,
151            interaction_source: None,
152        }
153    }
154}
155
156/// M3 Assist Chip - a chip for triggering actions.
157pub fn AssistChip(
158    on_click: impl Fn() + 'static,
159    label: View,
160    leading_icon: Option<View>,
161    trailing_icon: Option<View>,
162    config: ChipConfig,
163) -> View {
164    let th = theme();
165    let is_enabled = config.enabled;
166    let colors = &config.colors;
167    let bg = colors.container(is_enabled, false);
168    let label_color = colors.label(is_enabled, false);
169    let leading_color = colors.leading_icon(is_enabled, false);
170    let trailing_color = colors.trailing_icon(is_enabled, false);
171    let border = if is_enabled {
172        config.border_color
173    } else {
174        config.disabled_border_color
175    };
176    let shape = config.shape_radius;
177    let ch_source: Rc<MutableInteractionSource> = config
178        .interaction_source
179        .clone()
180        .map(Rc::new)
181        .unwrap_or_else(|| remember(MutableInteractionSource::new));
182
183    let mut m = Modifier::new()
184        .flex_shrink(0.0)
185        .state_colors(StateColors {
186            default: Color::TRANSPARENT,
187            hovered: Color::TRANSPARENT,
188            focused: Color::TRANSPARENT,
189            pressed: Color::TRANSPARENT,
190            dragged: th.on_surface.with_alpha_f32(0.12),
191            disabled: Color::TRANSPARENT,
192        })
193        .padding_values(PaddingValues {
194            left: config.horizontal_padding,
195            right: config.horizontal_padding,
196            top: 8.0,
197            bottom: 8.0,
198        })
199        .background(bg)
200        .clip_rounded(shape)
201        .interaction_source(&ch_source)
202        .indication(crate::ripple::ripple(crate::ripple::RippleConfig {
203            color: Some(label_color),
204            bounded: true,
205            ..Default::default()
206        }))
207        .then(config.modifier);
208
209    if config.border_width > 0.0 && border != Color::TRANSPARENT {
210        m = m.border(config.border_width, border, shape);
211    }
212
213    if is_enabled {
214        m = m.clickable().on_click(on_click);
215    }
216
217    Box(m).child(
218        Row(Modifier::new().align_items(AlignItems::CENTER)).child((
219            leading_icon
220                .map(|v| {
221                    Box(Modifier::new().padding_values(PaddingValues {
222                        left: 0.0,
223                        right: 8.0,
224                        top: 0.0,
225                        bottom: 0.0,
226                    }))
227                    .child(with_content_color(leading_color, move || v))
228                })
229                .unwrap_or(Box(Modifier::new())),
230            with_content_color(label_color, move || label),
231            trailing_icon
232                .map(|v| {
233                    Box(Modifier::new().padding_values(PaddingValues {
234                        left: 8.0,
235                        right: 0.0,
236                        top: 0.0,
237                        bottom: 0.0,
238                    }))
239                    .child(with_content_color(trailing_color, move || v))
240                })
241                .unwrap_or(Box(Modifier::new())),
242        )),
243    )
244}
245
246/// M3 Elevated Assist Chip - like [`AssistChip`] but with elevated container.
247pub fn ElevatedAssistChip(
248    on_click: impl Fn() + 'static,
249    label: View,
250    leading_icon: Option<View>,
251    trailing_icon: Option<View>,
252    config: ChipConfig,
253) -> View {
254    let th = theme();
255    let is_enabled = config.enabled;
256    let colors = &config.colors;
257    let bg = colors.container(is_enabled, false);
258    let label_color = colors.label(is_enabled, false);
259    let leading_color = colors.leading_icon(is_enabled, false);
260    let trailing_color = colors.trailing_icon(is_enabled, false);
261    let shape = config.shape_radius;
262    let ch_source: Rc<MutableInteractionSource> = config
263        .interaction_source
264        .clone()
265        .map(Rc::new)
266        .unwrap_or_else(|| remember(MutableInteractionSource::new));
267
268    let mut m = Modifier::new()
269        .flex_shrink(0.0)
270        .state_colors(StateColors {
271            default: Color::TRANSPARENT,
272            hovered: Color::TRANSPARENT,
273            focused: Color::TRANSPARENT,
274            pressed: Color::TRANSPARENT,
275            dragged: th.on_surface.with_alpha_f32(0.12),
276            disabled: Color::TRANSPARENT,
277        })
278        .state_elevation(config.elevation.to_state_elevation())
279        .padding_values(PaddingValues {
280            left: config.horizontal_padding,
281            right: config.horizontal_padding,
282            top: 8.0,
283            bottom: 8.0,
284        })
285        .background(bg)
286        .clip_rounded(shape)
287        .interaction_source(&ch_source)
288        .indication(crate::ripple::ripple(crate::ripple::RippleConfig {
289            color: Some(label_color),
290            bounded: true,
291            ..Default::default()
292        }))
293        .then(config.modifier);
294
295    if is_enabled {
296        m = m.clickable().on_click(on_click);
297    }
298
299    Box(m).child(
300        Row(Modifier::new().align_items(AlignItems::CENTER)).child((
301            leading_icon
302                .map(|v| {
303                    Box(Modifier::new().padding_values(PaddingValues {
304                        left: 0.0,
305                        right: 8.0,
306                        top: 0.0,
307                        bottom: 0.0,
308                    }))
309                    .child(with_content_color(leading_color, move || v))
310                })
311                .unwrap_or(Box(Modifier::new())),
312            with_content_color(label_color, move || label),
313            trailing_icon
314                .map(|v| {
315                    Box(Modifier::new().padding_values(PaddingValues {
316                        left: 8.0,
317                        right: 0.0,
318                        top: 0.0,
319                        bottom: 0.0,
320                    }))
321                    .child(with_content_color(trailing_color, move || v))
322                })
323                .unwrap_or(Box(Modifier::new())),
324        )),
325    )
326}
327
328pub fn FilterChip(
329    selected: bool,
330    on_click: impl Fn() + 'static,
331    label: View,
332    leading_icon: Option<View>,
333    trailing_icon: Option<View>,
334    config: ChipConfig,
335) -> View {
336    let th = theme();
337    let id = remember(|| FILTERCHIP_COUNTER.fetch_add(1, Ordering::Relaxed));
338    let spec = th.motion.color;
339    let is_enabled = config.enabled;
340    let colors = &config.colors;
341
342    let bg = animate_color(
343        format!("fc_bg_{}", id),
344        colors.container(is_enabled, selected),
345        spec,
346    );
347    let label_color = animate_color(
348        format!("fc_lc_{}", id),
349        colors.label(is_enabled, selected),
350        spec,
351    );
352    let leading_color = animate_color(
353        format!("fc_lic_{}", id),
354        colors.leading_icon(is_enabled, selected),
355        spec,
356    );
357    let trailing_color = animate_color(
358        format!("fc_tic_{}", id),
359        colors.trailing_icon(is_enabled, selected),
360        spec,
361    );
362    let border = if !is_enabled {
363        if selected {
364            config.disabled_selected_border_color
365        } else {
366            config.disabled_border_color
367        }
368    } else {
369        if selected {
370            config.selected_border_color
371        } else {
372            config.border_color
373        }
374    };
375    let shape = config.shape_radius;
376
377    let ch_source: Rc<MutableInteractionSource> = config
378        .interaction_source
379        .clone()
380        .map(Rc::new)
381        .unwrap_or_else(|| remember(MutableInteractionSource::new));
382
383    let mut m = Modifier::new()
384        .flex_shrink(0.0)
385        .state_colors(StateColors {
386            default: Color::TRANSPARENT,
387            hovered: Color::TRANSPARENT,
388            focused: Color::TRANSPARENT,
389            pressed: Color::TRANSPARENT,
390            dragged: th.on_surface.with_alpha_f32(0.12),
391            disabled: Color::TRANSPARENT,
392        })
393        .padding_values(PaddingValues {
394            left: config.horizontal_padding,
395            right: config.horizontal_padding,
396            top: 8.0,
397            bottom: 8.0,
398        })
399        .background(bg)
400        .clip_rounded(shape)
401        .interaction_source(&ch_source)
402        .indication(crate::ripple::ripple(crate::ripple::RippleConfig {
403            color: Some(label_color),
404            bounded: true,
405            ..Default::default()
406        }))
407        .then(config.modifier);
408
409    if config.border_width > 0.0 && border != Color::TRANSPARENT {
410        m = m.border(config.border_width, border, shape);
411    }
412    if is_enabled {
413        m = m.clickable().on_click(on_click);
414    }
415
416    Box(m).child(
417        Row(Modifier::new().align_items(AlignItems::CENTER)).child((
418            leading_icon
419                .map(|v| {
420                    Box(Modifier::new().padding_values(PaddingValues {
421                        left: 0.0,
422                        right: 8.0,
423                        top: 0.0,
424                        bottom: 0.0,
425                    }))
426                    .child(with_content_color(leading_color, move || v))
427                })
428                .unwrap_or(Box(Modifier::new())),
429            with_content_color(label_color, move || label),
430            trailing_icon
431                .map(|v| {
432                    Box(Modifier::new().padding_values(PaddingValues {
433                        left: 8.0,
434                        right: 0.0,
435                        top: 0.0,
436                        bottom: 0.0,
437                    }))
438                    .child(with_content_color(trailing_color, move || v))
439                })
440                .unwrap_or(Box(Modifier::new())),
441        )),
442    )
443}
444
445/// M3 Elevated Filter Chip - like [`FilterChip`] but with elevation and filled container.
446pub fn ElevatedFilterChip(
447    selected: bool,
448    on_click: impl Fn() + 'static,
449    label: View,
450    leading_icon: Option<View>,
451    trailing_icon: Option<View>,
452    config: ChipConfig,
453) -> View {
454    let th = theme();
455    let id = remember(|| FILTERCHIP_COUNTER.fetch_add(1, Ordering::Relaxed));
456    let spec = th.motion.color;
457    let is_enabled = config.enabled;
458    let colors = &config.colors;
459
460    let bg = animate_color(
461        format!("efc_bg_{}", id),
462        colors.container(is_enabled, selected),
463        spec,
464    );
465    let label_color = animate_color(
466        format!("efc_lc_{}", id),
467        colors.label(is_enabled, selected),
468        spec,
469    );
470    let leading_color = animate_color(
471        format!("efc_lic_{}", id),
472        colors.leading_icon(is_enabled, selected),
473        spec,
474    );
475    let trailing_color = animate_color(
476        format!("efc_tic_{}", id),
477        colors.trailing_icon(is_enabled, selected),
478        spec,
479    );
480    let shape = config.shape_radius;
481
482    let ch_source: Rc<MutableInteractionSource> = config
483        .interaction_source
484        .clone()
485        .map(Rc::new)
486        .unwrap_or_else(|| remember(MutableInteractionSource::new));
487
488    let mut m = Modifier::new()
489        .flex_shrink(0.0)
490        .state_colors(StateColors {
491            default: Color::TRANSPARENT,
492            hovered: Color::TRANSPARENT,
493            focused: Color::TRANSPARENT,
494            pressed: Color::TRANSPARENT,
495            dragged: th.on_surface.with_alpha_f32(0.12),
496            disabled: Color::TRANSPARENT,
497        })
498        .state_elevation(config.elevation.to_state_elevation())
499        .padding_values(PaddingValues {
500            left: config.horizontal_padding,
501            right: config.horizontal_padding,
502            top: 8.0,
503            bottom: 8.0,
504        })
505        .background(bg)
506        .clip_rounded(shape)
507        .interaction_source(&ch_source)
508        .indication(crate::ripple::ripple(crate::ripple::RippleConfig {
509            color: Some(label_color),
510            bounded: true,
511            ..Default::default()
512        }))
513        .then(config.modifier);
514
515    if is_enabled {
516        m = m.clickable().on_click(on_click);
517    }
518
519    Box(m).child(
520        Row(Modifier::new().align_items(AlignItems::CENTER)).child((
521            leading_icon
522                .map(|v| {
523                    Box(Modifier::new().padding_values(PaddingValues {
524                        left: 0.0,
525                        right: 8.0,
526                        top: 0.0,
527                        bottom: 0.0,
528                    }))
529                    .child(with_content_color(leading_color, move || v))
530                })
531                .unwrap_or(Box(Modifier::new())),
532            with_content_color(label_color, move || label),
533            trailing_icon
534                .map(|v| {
535                    Box(Modifier::new().padding_values(PaddingValues {
536                        left: 8.0,
537                        right: 0.0,
538                        top: 0.0,
539                        bottom: 0.0,
540                    }))
541                    .child(with_content_color(trailing_color, move || v))
542                })
543                .unwrap_or(Box(Modifier::new())),
544        )),
545    )
546}
547
548pub fn SuggestionChip(
549    on_click: impl Fn() + 'static,
550    label: View,
551    icon: Option<View>,
552    config: ChipConfig,
553) -> View {
554    let th = theme();
555    let is_enabled = config.enabled;
556    let colors = &config.colors;
557    let bg = colors.container(is_enabled, false);
558    let label_color = colors.label(is_enabled, false);
559    let leading_color = colors.leading_icon(is_enabled, false);
560    let border = if is_enabled {
561        config.border_color
562    } else {
563        config.disabled_border_color
564    };
565    let shape = config.shape_radius;
566
567    let ch_source: Rc<MutableInteractionSource> = config
568        .interaction_source
569        .clone()
570        .map(Rc::new)
571        .unwrap_or_else(|| remember(MutableInteractionSource::new));
572
573    let mut m = Modifier::new()
574        .flex_shrink(0.0)
575        .state_colors(StateColors {
576            default: Color::TRANSPARENT,
577            hovered: Color::TRANSPARENT,
578            focused: Color::TRANSPARENT,
579            pressed: Color::TRANSPARENT,
580            dragged: th.on_surface.with_alpha_f32(0.12),
581            disabled: Color::TRANSPARENT,
582        })
583        .padding_values(PaddingValues {
584            left: config.horizontal_padding,
585            right: config.horizontal_padding,
586            top: 8.0,
587            bottom: 8.0,
588        })
589        .background(bg)
590        .clip_rounded(shape)
591        .interaction_source(&ch_source)
592        .indication(crate::ripple::ripple(crate::ripple::RippleConfig {
593            color: Some(label_color),
594            bounded: true,
595            ..Default::default()
596        }))
597        .then(config.modifier);
598
599    if config.border_width > 0.0 && border != Color::TRANSPARENT {
600        m = m.border(config.border_width, border, shape);
601    }
602    if is_enabled {
603        m = m.clickable().on_click(on_click);
604    }
605
606    Box(m).child(
607        Row(Modifier::new().align_items(AlignItems::CENTER)).child((
608            icon.map(|v| {
609                Box(Modifier::new().padding_values(PaddingValues {
610                    left: 0.0,
611                    right: 8.0,
612                    top: 0.0,
613                    bottom: 0.0,
614                }))
615                .child(with_content_color(leading_color, move || v))
616            })
617            .unwrap_or(Box(Modifier::new())),
618            with_content_color(label_color, move || label),
619        )),
620    )
621}
622
623/// M3 Elevated Suggestion Chip - like [`SuggestionChip`] but with elevation and filled bg.
624pub fn ElevatedSuggestionChip(
625    on_click: impl Fn() + 'static,
626    label: View,
627    icon: Option<View>,
628    config: ChipConfig,
629) -> View {
630    let th = theme();
631    let is_enabled = config.enabled;
632    let colors = &config.colors;
633    let bg = colors.container(is_enabled, false);
634    let label_color = colors.label(is_enabled, false);
635    let leading_color = colors.leading_icon(is_enabled, false);
636    let shape = config.shape_radius;
637
638    let ch_source: Rc<MutableInteractionSource> = config
639        .interaction_source
640        .clone()
641        .map(Rc::new)
642        .unwrap_or_else(|| remember(MutableInteractionSource::new));
643
644    let mut m = Modifier::new()
645        .flex_shrink(0.0)
646        .state_colors(StateColors {
647            default: Color::TRANSPARENT,
648            hovered: Color::TRANSPARENT,
649            focused: Color::TRANSPARENT,
650            pressed: Color::TRANSPARENT,
651            dragged: th.on_surface.with_alpha_f32(0.12),
652            disabled: Color::TRANSPARENT,
653        })
654        .state_elevation(config.elevation.to_state_elevation())
655        .padding_values(PaddingValues {
656            left: config.horizontal_padding,
657            right: config.horizontal_padding,
658            top: 8.0,
659            bottom: 8.0,
660        })
661        .background(bg)
662        .clip_rounded(shape)
663        .interaction_source(&ch_source)
664        .indication(crate::ripple::ripple(crate::ripple::RippleConfig {
665            color: Some(label_color),
666            bounded: true,
667            ..Default::default()
668        }))
669        .then(config.modifier);
670
671    if is_enabled {
672        m = m.clickable().on_click(on_click);
673    }
674
675    Box(m).child(
676        Row(Modifier::new().align_items(AlignItems::CENTER)).child((
677            icon.map(|v| {
678                Box(Modifier::new().padding_values(PaddingValues {
679                    left: 0.0,
680                    right: 8.0,
681                    top: 0.0,
682                    bottom: 0.0,
683                }))
684                .child(with_content_color(leading_color, move || v))
685            })
686            .unwrap_or(Box(Modifier::new())),
687            with_content_color(label_color, move || label),
688        )),
689    )
690}
691
692pub fn InputChip(
693    selected: bool,
694    on_click: impl Fn() + 'static,
695    label: View,
696    leading_icon: Option<View>,
697    avatar: Option<View>,
698    trailing_icon: Option<View>,
699    config: ChipConfig,
700) -> View {
701    let th = theme();
702    let id = remember(|| FILTERCHIP_COUNTER.fetch_add(1, Ordering::Relaxed));
703    let spec = th.motion.color;
704    let is_enabled = config.enabled;
705    let colors = &config.colors;
706
707    let bg = animate_color(
708        format!("ic_bg_{}", id),
709        colors.container(is_enabled, selected),
710        spec,
711    );
712    let label_color = animate_color(
713        format!("ic_lc_{}", id),
714        colors.label(is_enabled, selected),
715        spec,
716    );
717    let leading_color = animate_color(
718        format!("ic_lic_{}", id),
719        colors.leading_icon(is_enabled, selected),
720        spec,
721    );
722    let trailing_color = animate_color(
723        format!("ic_tic_{}", id),
724        colors.trailing_icon(is_enabled, selected),
725        spec,
726    );
727    let border = if !is_enabled {
728        if selected {
729            config.disabled_selected_border_color
730        } else {
731            config.disabled_border_color
732        }
733    } else {
734        if selected {
735            config.selected_border_color
736        } else {
737            config.border_color
738        }
739    };
740    let shape = config.shape_radius;
741
742    let ch_source: Rc<MutableInteractionSource> = config
743        .interaction_source
744        .clone()
745        .map(Rc::new)
746        .unwrap_or_else(|| remember(MutableInteractionSource::new));
747
748    let mut m = Modifier::new()
749        .flex_shrink(0.0)
750        .state_colors(StateColors {
751            default: Color::TRANSPARENT,
752            hovered: Color::TRANSPARENT,
753            focused: Color::TRANSPARENT,
754            pressed: Color::TRANSPARENT,
755            dragged: th.on_surface.with_alpha_f32(0.12),
756            disabled: Color::TRANSPARENT,
757        })
758        .padding_values(PaddingValues {
759            left: config.horizontal_padding,
760            right: config.horizontal_padding,
761            top: 8.0,
762            bottom: 8.0,
763        })
764        .background(bg)
765        .clip_rounded(shape)
766        .interaction_source(&ch_source)
767        .indication(crate::ripple::ripple(crate::ripple::RippleConfig {
768            color: Some(label_color),
769            bounded: true,
770            ..Default::default()
771        }))
772        .then(config.modifier);
773
774    if config.border_width > 0.0 && border != Color::TRANSPARENT {
775        m = m.border(config.border_width, border, shape);
776    }
777    if is_enabled {
778        m = m.clickable().on_click(on_click);
779    }
780
781    Box(m).child(
782        Row(Modifier::new().align_items(AlignItems::CENTER)).child((
783            avatar
784                .or(leading_icon)
785                .map(|v| {
786                    Box(Modifier::new().padding_values(PaddingValues {
787                        left: 0.0,
788                        right: 8.0,
789                        top: 0.0,
790                        bottom: 0.0,
791                    }))
792                    .child(with_content_color(leading_color, move || v))
793                })
794                .unwrap_or(Box(Modifier::new())),
795            with_content_color(label_color, move || label),
796            trailing_icon
797                .map(|v| {
798                    Box(Modifier::new().padding_values(PaddingValues {
799                        left: 8.0,
800                        right: 0.0,
801                        top: 0.0,
802                        bottom: 0.0,
803                    }))
804                    .child(with_content_color(trailing_color, move || v))
805                })
806                .unwrap_or(Box(Modifier::new())),
807        )),
808    )
809}
810
811/// Shared layout for the M3 chip group composables: a full-width wrapping
812/// `FlowRow` whose chips keep their intrinsic width (via `flex_shrink(0)`)
813/// instead of shrinking under constrained/centered parents.
814pub fn chip_group_flow(modifier: Modifier, children: impl repose_ui::IntoChildren) -> View {
815    FlowRow(
816        Modifier::new()
817            .fill_max_width()
818            .gap(8.0)
819            .align_items(AlignItems::CENTER)
820            .then(modifier),
821    )
822    .child(children)
823}
824
825/// M3 Filter Chip Group.
826pub fn FilterChipGroup(modifier: Modifier, children: impl repose_ui::IntoChildren) -> View {
827    chip_group_flow(modifier, children)
828}
829
830/// M3 Elevated Filter Chip Group.
831pub fn ElevatedFilterChipGroup(modifier: Modifier, children: impl repose_ui::IntoChildren) -> View {
832    chip_group_flow(modifier, children)
833}
834
835/// M3 Assist Chip Group.
836pub fn AssistChipGroup(modifier: Modifier, children: impl repose_ui::IntoChildren) -> View {
837    chip_group_flow(modifier, children)
838}
839
840/// M3 Elevated Assist Chip Group.
841/// [`ElevatedAssistChip`]s.
842pub fn ElevatedAssistChipGroup(modifier: Modifier, children: impl repose_ui::IntoChildren) -> View {
843    chip_group_flow(modifier, children)
844}
845
846/// M3 Suggestion Chip Group.
847pub fn SuggestionChipGroup(modifier: Modifier, children: impl repose_ui::IntoChildren) -> View {
848    chip_group_flow(modifier, children)
849}
850
851/// M3 Elevated Suggestion Chip Group.
852pub fn ElevatedSuggestionChipGroup(
853    modifier: Modifier,
854    children: impl repose_ui::IntoChildren,
855) -> View {
856    chip_group_flow(modifier, children)
857}
858
859/// M3 Input Chip Group.
860pub fn InputChipGroup(modifier: Modifier, children: impl repose_ui::IntoChildren) -> View {
861    chip_group_flow(modifier, children)
862}