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            disabled: self.disabled,
90        }
91    }
92}
93
94impl Default for ChipElevation {
95    fn default() -> Self {
96        Self {
97            default: ChipDefaults::elevation_default(),
98            hovered: ChipDefaults::elevation_hovered(),
99            focused: ChipDefaults::elevation_focused(),
100            pressed: ChipDefaults::elevation_pressed(),
101            dragged: ChipDefaults::elevation_dragged(),
102            disabled: ChipDefaults::elevation_disabled(),
103        }
104    }
105}
106
107/// Configuration for chips.
108#[derive(Clone, Debug)]
109pub struct ChipConfig {
110    pub modifier: Modifier,
111    pub enabled: bool,
112    pub colors: ChipColors,
113    pub elevation: ChipElevation,
114    pub border_width: f32,
115    pub border_color: Color,
116    pub selected_border_color: Color,
117    pub disabled_border_color: Color,
118    pub disabled_selected_border_color: Color,
119    pub shape_radius: f32,
120    pub horizontal_padding: f32,
121    pub interaction_source: Option<MutableInteractionSource>,
122}
123
124impl Default for ChipConfig {
125    fn default() -> Self {
126        Self {
127            modifier: Modifier::new(),
128            enabled: true,
129            colors: ChipColors {
130                container_color: ChipDefaults::container_color(),
131                label_color: ChipDefaults::label_color(),
132                leading_icon_color: ChipDefaults::leading_icon_color(),
133                trailing_icon_color: ChipDefaults::trailing_icon_color(),
134                disabled_container_color: ChipDefaults::disabled_container_color(),
135                disabled_label_color: ChipDefaults::disabled_label_color(),
136                disabled_leading_icon_color: ChipDefaults::disabled_leading_icon_color(),
137                disabled_trailing_icon_color: ChipDefaults::disabled_trailing_icon_color(),
138                selected_container_color: ChipDefaults::selected_container_color(),
139                selected_label_color: ChipDefaults::selected_label_color(),
140                selected_leading_icon_color: ChipDefaults::selected_leading_icon_color(),
141                selected_trailing_icon_color: ChipDefaults::selected_trailing_icon_color(),
142                disabled_selected_container_color: ChipDefaults::disabled_selected_container_color(
143                ),
144            },
145            elevation: ChipElevation::default(),
146            border_width: ChipDefaults::BORDER_WIDTH,
147            border_color: ChipDefaults::border_color(),
148            selected_border_color: ChipDefaults::selected_border_color(),
149            disabled_border_color: ChipDefaults::disabled_border_color(),
150            disabled_selected_border_color: ChipDefaults::disabled_selected_border_color(),
151            shape_radius: ChipDefaults::SHAPE_RADIUS,
152            horizontal_padding: ChipDefaults::HORIZONTAL_PADDING,
153            interaction_source: None,
154        }
155    }
156}
157
158/// M3 Assist Chip - a chip for triggering actions.
159pub fn AssistChip(
160    on_click: impl Fn() + 'static,
161    label: View,
162    leading_icon: Option<View>,
163    trailing_icon: Option<View>,
164    config: ChipConfig,
165) -> View {
166    let th = theme();
167    let is_enabled = config.enabled;
168    let colors = &config.colors;
169    let bg = colors.container(is_enabled, false);
170    let label_color = colors.label(is_enabled, false);
171    let leading_color = colors.leading_icon(is_enabled, false);
172    let trailing_color = colors.trailing_icon(is_enabled, false);
173    let border = if is_enabled {
174        config.border_color
175    } else {
176        config.disabled_border_color
177    };
178    let shape = config.shape_radius;
179    let ch_source: Rc<MutableInteractionSource> = config
180        .interaction_source
181        .clone()
182        .map(Rc::new)
183        .unwrap_or_else(|| remember(MutableInteractionSource::new));
184
185    let mut m = Modifier::new()
186        .state_colors(StateColors {
187            default: Color::TRANSPARENT,
188            hovered: th.on_surface.with_alpha_f32(0.08),
189            pressed: th.on_surface.with_alpha_f32(0.12),
190            disabled: Color::TRANSPARENT,
191        })
192        .padding_values(PaddingValues {
193            left: config.horizontal_padding,
194            right: config.horizontal_padding,
195            top: 8.0,
196            bottom: 8.0,
197        })
198        .background(bg)
199        .clip_rounded(shape)
200        .interaction_source(&*ch_source)
201        .then(config.modifier);
202
203    if config.border_width > 0.0 && border != Color::TRANSPARENT {
204        m = m.border(config.border_width, border, shape);
205    }
206
207    if is_enabled {
208        m = m.clickable().on_click(move || on_click());
209    }
210
211    Box(m).child(
212        Row(Modifier::new().align_items(AlignItems::CENTER)).child((
213            leading_icon
214                .map(|v| {
215                    Box(Modifier::new().padding_values(PaddingValues {
216                        left: 0.0,
217                        right: 8.0,
218                        top: 0.0,
219                        bottom: 0.0,
220                    }))
221                    .child(with_content_color(leading_color, move || v))
222                })
223                .unwrap_or(Box(Modifier::new())),
224            with_content_color(label_color, move || label),
225            trailing_icon
226                .map(|v| {
227                    Box(Modifier::new().padding_values(PaddingValues {
228                        left: 8.0,
229                        right: 0.0,
230                        top: 0.0,
231                        bottom: 0.0,
232                    }))
233                    .child(with_content_color(trailing_color, move || v))
234                })
235                .unwrap_or(Box(Modifier::new())),
236        )),
237    )
238}
239
240/// M3 Elevated Assist Chip - like [`AssistChip`] but with elevated container.
241pub fn ElevatedAssistChip(
242    on_click: impl Fn() + 'static,
243    label: View,
244    leading_icon: Option<View>,
245    trailing_icon: Option<View>,
246    config: ChipConfig,
247) -> View {
248    let th = theme();
249    let is_enabled = config.enabled;
250    let colors = &config.colors;
251    let bg = colors.container(is_enabled, false);
252    let label_color = colors.label(is_enabled, false);
253    let leading_color = colors.leading_icon(is_enabled, false);
254    let trailing_color = colors.trailing_icon(is_enabled, false);
255    let shape = config.shape_radius;
256    let ch_source: Rc<MutableInteractionSource> = config
257        .interaction_source
258        .clone()
259        .map(Rc::new)
260        .unwrap_or_else(|| remember(MutableInteractionSource::new));
261
262    let mut m = Modifier::new()
263        .state_colors(StateColors {
264            default: Color::TRANSPARENT,
265            hovered: th.on_surface.with_alpha_f32(0.08),
266            pressed: th.on_surface.with_alpha_f32(0.12),
267            disabled: Color::TRANSPARENT,
268        })
269        .state_elevation(config.elevation.to_state_elevation())
270        .padding_values(PaddingValues {
271            left: config.horizontal_padding,
272            right: config.horizontal_padding,
273            top: 8.0,
274            bottom: 8.0,
275        })
276        .background(bg)
277        .clip_rounded(shape)
278        .interaction_source(&*ch_source)
279        .then(config.modifier);
280
281    if is_enabled {
282        m = m.clickable().on_click(move || on_click());
283    }
284
285    Box(m).child(
286        Row(Modifier::new().align_items(AlignItems::CENTER)).child((
287            leading_icon
288                .map(|v| {
289                    Box(Modifier::new().padding_values(PaddingValues {
290                        left: 0.0,
291                        right: 8.0,
292                        top: 0.0,
293                        bottom: 0.0,
294                    }))
295                    .child(with_content_color(leading_color, move || v))
296                })
297                .unwrap_or(Box(Modifier::new())),
298            with_content_color(label_color, move || label),
299            trailing_icon
300                .map(|v| {
301                    Box(Modifier::new().padding_values(PaddingValues {
302                        left: 8.0,
303                        right: 0.0,
304                        top: 0.0,
305                        bottom: 0.0,
306                    }))
307                    .child(with_content_color(trailing_color, move || v))
308                })
309                .unwrap_or(Box(Modifier::new())),
310        )),
311    )
312}
313
314pub fn FilterChip(
315    selected: bool,
316    on_click: impl Fn() + 'static,
317    label: View,
318    leading_icon: Option<View>,
319    trailing_icon: Option<View>,
320    config: ChipConfig,
321) -> View {
322    let th = theme();
323    let id = remember(|| FILTERCHIP_COUNTER.fetch_add(1, Ordering::Relaxed));
324    let spec = th.motion.color;
325    let is_enabled = config.enabled;
326    let colors = &config.colors;
327
328    let bg = animate_color(
329        format!("fc_bg_{}", id),
330        colors.container(is_enabled, selected),
331        spec,
332    );
333    let label_color = animate_color(
334        format!("fc_lc_{}", id),
335        colors.label(is_enabled, selected),
336        spec,
337    );
338    let leading_color = animate_color(
339        format!("fc_lic_{}", id),
340        colors.leading_icon(is_enabled, selected),
341        spec,
342    );
343    let trailing_color = animate_color(
344        format!("fc_tic_{}", id),
345        colors.trailing_icon(is_enabled, selected),
346        spec,
347    );
348    let border = if !is_enabled {
349        if selected {
350            config.disabled_selected_border_color
351        } else {
352            config.disabled_border_color
353        }
354    } else {
355        if selected {
356            config.selected_border_color
357        } else {
358            config.border_color
359        }
360    };
361    let shape = config.shape_radius;
362
363    let ch_source: Rc<MutableInteractionSource> = config
364        .interaction_source
365        .clone()
366        .map(Rc::new)
367        .unwrap_or_else(|| remember(MutableInteractionSource::new));
368
369    let mut m = Modifier::new()
370        .state_colors(StateColors {
371            default: Color::TRANSPARENT,
372            hovered: th.on_surface.with_alpha_f32(0.08),
373            pressed: 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(move || 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            disabled: Color::TRANSPARENT,
472        })
473        .state_elevation(config.elevation.to_state_elevation())
474        .padding_values(PaddingValues {
475            left: config.horizontal_padding,
476            right: config.horizontal_padding,
477            top: 8.0,
478            bottom: 8.0,
479        })
480        .background(bg)
481        .clip_rounded(shape)
482        .interaction_source(&*ch_source)
483        .then(config.modifier);
484
485    if is_enabled {
486        m = m.clickable().on_click(move || on_click());
487    }
488
489    Box(m).child(
490        Row(Modifier::new().align_items(AlignItems::CENTER)).child((
491            leading_icon
492                .map(|v| {
493                    Box(Modifier::new().padding_values(PaddingValues {
494                        left: 0.0,
495                        right: 8.0,
496                        top: 0.0,
497                        bottom: 0.0,
498                    }))
499                    .child(with_content_color(leading_color, move || v))
500                })
501                .unwrap_or(Box(Modifier::new())),
502            with_content_color(label_color, move || label),
503            trailing_icon
504                .map(|v| {
505                    Box(Modifier::new().padding_values(PaddingValues {
506                        left: 8.0,
507                        right: 0.0,
508                        top: 0.0,
509                        bottom: 0.0,
510                    }))
511                    .child(with_content_color(trailing_color, move || v))
512                })
513                .unwrap_or(Box(Modifier::new())),
514        )),
515    )
516}
517
518pub fn SuggestionChip(
519    on_click: impl Fn() + 'static,
520    label: View,
521    icon: Option<View>,
522    config: ChipConfig,
523) -> View {
524    let th = theme();
525    let is_enabled = config.enabled;
526    let colors = &config.colors;
527    let bg = colors.container(is_enabled, false);
528    let label_color = colors.label(is_enabled, false);
529    let leading_color = colors.leading_icon(is_enabled, false);
530    let border = if is_enabled {
531        config.border_color
532    } else {
533        config.disabled_border_color
534    };
535    let shape = config.shape_radius;
536
537    let ch_source: Rc<MutableInteractionSource> = config
538        .interaction_source
539        .clone()
540        .map(Rc::new)
541        .unwrap_or_else(|| remember(MutableInteractionSource::new));
542
543    let mut m = Modifier::new()
544        .state_colors(StateColors {
545            default: Color::TRANSPARENT,
546            hovered: th.on_surface.with_alpha_f32(0.08),
547            pressed: th.on_surface.with_alpha_f32(0.12),
548            disabled: Color::TRANSPARENT,
549        })
550        .padding_values(PaddingValues {
551            left: config.horizontal_padding,
552            right: config.horizontal_padding,
553            top: 8.0,
554            bottom: 8.0,
555        })
556        .background(bg)
557        .clip_rounded(shape)
558        .interaction_source(&*ch_source)
559        .then(config.modifier);
560
561    if config.border_width > 0.0 && border != Color::TRANSPARENT {
562        m = m.border(config.border_width, border, shape);
563    }
564    if is_enabled {
565        m = m.clickable().on_click(move || on_click());
566    }
567
568    Box(m).child(
569        Row(Modifier::new().align_items(AlignItems::CENTER)).child((
570            icon.map(|v| {
571                Box(Modifier::new().padding_values(PaddingValues {
572                    left: 0.0,
573                    right: 8.0,
574                    top: 0.0,
575                    bottom: 0.0,
576                }))
577                .child(with_content_color(leading_color, move || v))
578            })
579            .unwrap_or(Box(Modifier::new())),
580            with_content_color(label_color, move || label),
581        )),
582    )
583}
584
585/// M3 Elevated Suggestion Chip - like [`SuggestionChip`] but with elevation and filled bg.
586pub fn ElevatedSuggestionChip(
587    on_click: impl Fn() + 'static,
588    label: View,
589    icon: Option<View>,
590    config: ChipConfig,
591) -> View {
592    let th = theme();
593    let is_enabled = config.enabled;
594    let colors = &config.colors;
595    let bg = colors.container(is_enabled, false);
596    let label_color = colors.label(is_enabled, false);
597    let leading_color = colors.leading_icon(is_enabled, false);
598    let shape = config.shape_radius;
599
600    let ch_source: Rc<MutableInteractionSource> = config
601        .interaction_source
602        .clone()
603        .map(Rc::new)
604        .unwrap_or_else(|| remember(MutableInteractionSource::new));
605
606    let mut m = Modifier::new()
607        .state_colors(StateColors {
608            default: Color::TRANSPARENT,
609            hovered: th.on_surface.with_alpha_f32(0.08),
610            pressed: th.on_surface.with_alpha_f32(0.12),
611            disabled: Color::TRANSPARENT,
612        })
613        .state_elevation(config.elevation.to_state_elevation())
614        .padding_values(PaddingValues {
615            left: config.horizontal_padding,
616            right: config.horizontal_padding,
617            top: 8.0,
618            bottom: 8.0,
619        })
620        .background(bg)
621        .clip_rounded(shape)
622        .interaction_source(&*ch_source)
623        .then(config.modifier);
624
625    if is_enabled {
626        m = m.clickable().on_click(move || on_click());
627    }
628
629    Box(m).child(
630        Row(Modifier::new().align_items(AlignItems::CENTER)).child((
631            icon.map(|v| {
632                Box(Modifier::new().padding_values(PaddingValues {
633                    left: 0.0,
634                    right: 8.0,
635                    top: 0.0,
636                    bottom: 0.0,
637                }))
638                .child(with_content_color(leading_color, move || v))
639            })
640            .unwrap_or(Box(Modifier::new())),
641            with_content_color(label_color, move || label),
642        )),
643    )
644}
645
646pub fn InputChip(
647    selected: bool,
648    on_click: impl Fn() + 'static,
649    label: View,
650    leading_icon: Option<View>,
651    avatar: Option<View>,
652    trailing_icon: Option<View>,
653    config: ChipConfig,
654) -> View {
655    let th = theme();
656    let id = remember(|| FILTERCHIP_COUNTER.fetch_add(1, Ordering::Relaxed));
657    let spec = th.motion.color;
658    let is_enabled = config.enabled;
659    let colors = &config.colors;
660
661    let bg = animate_color(
662        format!("ic_bg_{}", id),
663        colors.container(is_enabled, selected),
664        spec,
665    );
666    let label_color = animate_color(
667        format!("ic_lc_{}", id),
668        colors.label(is_enabled, selected),
669        spec,
670    );
671    let leading_color = animate_color(
672        format!("ic_lic_{}", id),
673        colors.leading_icon(is_enabled, selected),
674        spec,
675    );
676    let trailing_color = animate_color(
677        format!("ic_tic_{}", id),
678        colors.trailing_icon(is_enabled, selected),
679        spec,
680    );
681    let border = if !is_enabled {
682        if selected {
683            config.disabled_selected_border_color
684        } else {
685            config.disabled_border_color
686        }
687    } else {
688        if selected {
689            config.selected_border_color
690        } else {
691            config.border_color
692        }
693    };
694    let shape = config.shape_radius;
695
696    let ch_source: Rc<MutableInteractionSource> = config
697        .interaction_source
698        .clone()
699        .map(Rc::new)
700        .unwrap_or_else(|| remember(MutableInteractionSource::new));
701
702    let mut m = Modifier::new()
703        .state_colors(StateColors {
704            default: Color::TRANSPARENT,
705            hovered: th.on_surface.with_alpha_f32(0.08),
706            pressed: th.on_surface.with_alpha_f32(0.12),
707            disabled: Color::TRANSPARENT,
708        })
709        .padding_values(PaddingValues {
710            left: config.horizontal_padding,
711            right: config.horizontal_padding,
712            top: 8.0,
713            bottom: 8.0,
714        })
715        .background(bg)
716        .clip_rounded(shape)
717        .interaction_source(&*ch_source)
718        .then(config.modifier);
719
720    if config.border_width > 0.0 && border != Color::TRANSPARENT {
721        m = m.border(config.border_width, border, shape);
722    }
723    if is_enabled {
724        m = m.clickable().on_click(move || on_click());
725    }
726
727    Box(m).child(
728        Row(Modifier::new().align_items(AlignItems::CENTER)).child((
729            avatar
730                .or(leading_icon)
731                .map(|v| {
732                    Box(Modifier::new().padding_values(PaddingValues {
733                        left: 0.0,
734                        right: 8.0,
735                        top: 0.0,
736                        bottom: 0.0,
737                    }))
738                    .child(with_content_color(leading_color, move || v))
739                })
740                .unwrap_or(Box(Modifier::new())),
741            with_content_color(label_color, move || label),
742            trailing_icon
743                .map(|v| {
744                    Box(Modifier::new().padding_values(PaddingValues {
745                        left: 8.0,
746                        right: 0.0,
747                        top: 0.0,
748                        bottom: 0.0,
749                    }))
750                    .child(with_content_color(trailing_color, move || v))
751                })
752                .unwrap_or(Box(Modifier::new())),
753        )),
754    )
755}