1use tessera_ui::{
7 Callback, Color, Dp, LayoutResult, MeasurementError, Modifier, Px, PxPosition,
8 accesskit::{Role, Toggled},
9 current_frame_nanos,
10 layout::{MeasureScope, layout},
11 provide_context, receive_frame_nanos, remember, tessera, use_context,
12};
13
14use crate::{
15 alignment::{Alignment, CrossAxisAlignment},
16 animation::shape_motion::ShapeMotion,
17 boxed::boxed,
18 icon::icon,
19 image::image,
20 material_icons::filled,
21 modifier::{InteractionState, ModifierExt as _, Padding},
22 painter::Painter,
23 row::row,
24 shape_def::{RoundedCorner, Shape},
25 spacer::spacer,
26 surface::{SurfaceStyle, surface},
27 text::text,
28 theme::{MaterialAlpha, MaterialColorScheme, MaterialTheme, TextStyle},
29};
30
31use motion::{Elevation, Interaction};
32
33mod motion;
34
35#[derive(Clone, PartialEq, Copy, Debug, Default)]
37pub enum ChipVariant {
38 #[default]
40 Assist,
41 Suggestion,
43 Filter,
45 Input,
47}
48
49#[derive(Clone, PartialEq, Copy, Debug, Default)]
51pub enum ChipStyle {
52 #[default]
54 Flat,
55 Elevated,
57}
58
59#[derive(Clone, Copy, Debug, Default, PartialEq)]
61pub enum ChipShapeStyle {
62 #[default]
65 Expressive,
66 Standard,
68}
69
70#[derive(Clone, PartialEq, Copy, Debug)]
73pub struct ChipColors {
74 pub container_color: Color,
76 pub label_color: Color,
78 pub leading_icon_color: Color,
80 pub trailing_icon_color: Color,
82 pub disabled_container_color: Color,
84 pub disabled_label_color: Color,
86 pub disabled_leading_icon_color: Color,
88 pub disabled_trailing_icon_color: Color,
90 pub selected_container_color: Color,
92 pub selected_label_color: Color,
94 pub selected_leading_icon_color: Color,
96 pub selected_trailing_icon_color: Color,
98 pub disabled_selected_container_color: Color,
100 pub disabled_selected_label_color: Color,
102 pub disabled_selected_leading_icon_color: Color,
104 pub disabled_selected_trailing_icon_color: Color,
106}
107
108impl ChipColors {
109 fn container_color(self, enabled: bool, selected: bool) -> Color {
110 if enabled {
111 if selected {
112 self.selected_container_color
113 } else {
114 self.container_color
115 }
116 } else if selected {
117 self.disabled_selected_container_color
118 } else {
119 self.disabled_container_color
120 }
121 }
122
123 fn label_color(self, enabled: bool, selected: bool) -> Color {
124 if enabled {
125 if selected {
126 self.selected_label_color
127 } else {
128 self.label_color
129 }
130 } else if selected {
131 self.disabled_selected_label_color
132 } else {
133 self.disabled_label_color
134 }
135 }
136
137 fn leading_icon_color(self, enabled: bool, selected: bool) -> Color {
138 if enabled {
139 if selected {
140 self.selected_leading_icon_color
141 } else {
142 self.leading_icon_color
143 }
144 } else if selected {
145 self.disabled_selected_leading_icon_color
146 } else {
147 self.disabled_leading_icon_color
148 }
149 }
150
151 fn trailing_icon_color(self, enabled: bool, selected: bool) -> Color {
152 if enabled {
153 if selected {
154 self.selected_trailing_icon_color
155 } else {
156 self.trailing_icon_color
157 }
158 } else if selected {
159 self.disabled_selected_trailing_icon_color
160 } else {
161 self.disabled_trailing_icon_color
162 }
163 }
164}
165
166#[derive(Clone, PartialEq, Copy, Debug)]
168pub struct ChipBorder {
169 pub width: Dp,
171 pub color: Color,
173 pub selected_width: Dp,
175 pub selected_color: Color,
177 pub disabled_color: Color,
179 pub disabled_selected_color: Color,
181}
182
183impl ChipBorder {
184 fn resolve(self, enabled: bool, selected: bool) -> Option<(Dp, Color)> {
185 let width = if selected {
186 self.selected_width
187 } else {
188 self.width
189 };
190
191 let color = if enabled {
192 if selected {
193 self.selected_color
194 } else {
195 self.color
196 }
197 } else if selected {
198 self.disabled_selected_color
199 } else {
200 self.disabled_color
201 };
202
203 if width.0 <= 0.0 || color.a <= 0.0 {
204 None
205 } else {
206 Some((width, color))
207 }
208 }
209}
210
211pub struct ChipDefaults;
213
214impl ChipDefaults {
215 pub const HEIGHT: Dp = Dp(32.0);
217 pub const ICON_SIZE: Dp = Dp(18.0);
219 pub const AVATAR_SIZE: Dp = Dp(24.0);
221 pub const ELEMENT_SPACING: Dp = Dp(8.0);
223 pub const HORIZONTAL_PADDING: Dp = Dp(8.0);
225 pub const INPUT_EDGE_PADDING: Dp = Dp(4.0);
227 pub const INPUT_ICON_PADDING: Dp = Dp(8.0);
229
230 pub fn shape() -> Shape {
232 Shape::RoundedRectangle {
233 top_left: RoundedCorner::manual(Dp(8.0), 2.0),
234 top_right: RoundedCorner::manual(Dp(8.0), 2.0),
235 bottom_right: RoundedCorner::manual(Dp(8.0), 2.0),
236 bottom_left: RoundedCorner::manual(Dp(8.0), 2.0),
237 }
238 }
239
240 pub fn colors(
242 scheme: &MaterialColorScheme,
243 variant: ChipVariant,
244 style: ChipStyle,
245 ) -> ChipColors {
246 let scheme = scheme.clone();
247 let disabled_content = scheme
248 .on_surface
249 .with_alpha(MaterialAlpha::DISABLED_CONTENT);
250 let disabled_container = match style {
251 ChipStyle::Elevated => scheme
252 .on_surface
253 .with_alpha(MaterialAlpha::DISABLED_CONTAINER),
254 ChipStyle::Flat => Color::TRANSPARENT,
255 };
256
257 let container = match style {
258 ChipStyle::Elevated => scheme.surface_container_low,
259 ChipStyle::Flat => Color::TRANSPARENT,
260 };
261 match variant {
262 ChipVariant::Assist | ChipVariant::Suggestion => {
263 let label = if variant == ChipVariant::Assist {
264 scheme.on_surface
265 } else {
266 scheme.on_surface_variant
267 };
268 let icon = scheme.primary;
269 ChipColors {
270 container_color: container,
271 label_color: label,
272 leading_icon_color: icon,
273 trailing_icon_color: icon,
274 disabled_container_color: disabled_container,
275 disabled_label_color: disabled_content,
276 disabled_leading_icon_color: disabled_content,
277 disabled_trailing_icon_color: disabled_content,
278 selected_container_color: container,
279 selected_label_color: label,
280 selected_leading_icon_color: icon,
281 selected_trailing_icon_color: icon,
282 disabled_selected_container_color: disabled_container,
283 disabled_selected_label_color: disabled_content,
284 disabled_selected_leading_icon_color: disabled_content,
285 disabled_selected_trailing_icon_color: disabled_content,
286 }
287 }
288 ChipVariant::Filter | ChipVariant::Input => {
289 let disabled_selected_container = scheme
290 .on_surface
291 .with_alpha(MaterialAlpha::DISABLED_CONTAINER);
292 ChipColors {
293 container_color: container,
294 label_color: scheme.on_surface_variant,
295 leading_icon_color: if variant == ChipVariant::Filter {
296 scheme.primary
297 } else {
298 scheme.on_surface_variant
299 },
300 trailing_icon_color: scheme.on_surface_variant,
301 disabled_container_color: disabled_container,
302 disabled_label_color: disabled_content,
303 disabled_leading_icon_color: disabled_content,
304 disabled_trailing_icon_color: disabled_content,
305 selected_container_color: scheme.secondary_container,
306 selected_label_color: scheme.on_secondary_container,
307 selected_leading_icon_color: if variant == ChipVariant::Filter {
308 scheme.on_secondary_container
309 } else {
310 scheme.primary
311 },
312 selected_trailing_icon_color: scheme.on_secondary_container,
313 disabled_selected_container_color: disabled_selected_container,
314 disabled_selected_label_color: disabled_content,
315 disabled_selected_leading_icon_color: disabled_content,
316 disabled_selected_trailing_icon_color: disabled_content,
317 }
318 }
319 }
320 }
321
322 pub fn border(
324 scheme: &MaterialColorScheme,
325 variant: ChipVariant,
326 style: ChipStyle,
327 ) -> Option<ChipBorder> {
328 if matches!(style, ChipStyle::Elevated) {
329 return None;
330 }
331
332 let scheme = scheme.clone();
333 let outline = scheme.outline_variant;
334 let disabled_outline = scheme
335 .on_surface
336 .with_alpha(MaterialAlpha::DISABLED_CONTAINER);
337
338 let (selected_width, selected_color, disabled_selected_color) = match variant {
339 ChipVariant::Assist | ChipVariant::Suggestion => (Dp(1.0), outline, disabled_outline),
340 ChipVariant::Filter | ChipVariant::Input => {
341 (Dp(0.0), Color::TRANSPARENT, Color::TRANSPARENT)
342 }
343 };
344
345 Some(ChipBorder {
346 width: Dp(1.0),
347 color: outline,
348 selected_width,
349 selected_color,
350 disabled_color: disabled_outline,
351 disabled_selected_color,
352 })
353 }
354
355 pub fn elevation(style: ChipStyle) -> Option<Dp> {
357 match style {
358 ChipStyle::Flat => None,
359 ChipStyle::Elevated => Some(Dp(1.0)),
360 }
361 }
362}
363
364#[derive(Clone)]
365struct ChipResolvedArgs {
366 variant: ChipVariant,
367 style: ChipStyle,
368 label: String,
369 leading_icon: Option<Painter>,
370 avatar: Option<Painter>,
371 trailing_icon: Option<Painter>,
372 selected: bool,
373 enabled: bool,
374 modifier: Modifier,
375 colors: Option<ChipColors>,
376 border: Option<ChipBorder>,
377 shape: Option<Shape>,
378 shape_style: ChipShapeStyle,
379 elevation: Option<Dp>,
380 on_click: Option<Callback>,
381 on_remove: Option<Callback>,
382 remove_accessibility_label: Option<String>,
383 accessibility_label: Option<String>,
384 accessibility_description: Option<String>,
385}
386
387impl ChipBuilder {
388 pub fn assist(&self, label: impl Into<String>) -> Self {
390 self.variant(ChipVariant::Assist).label(label.into())
391 }
392
393 pub fn suggestion(&self, label: impl Into<String>) -> Self {
395 self.variant(ChipVariant::Suggestion).label(label.into())
396 }
397
398 pub fn filter(&self, label: impl Into<String>) -> Self {
400 self.variant(ChipVariant::Filter).label(label.into())
401 }
402
403 pub fn input(&self, label: impl Into<String>) -> Self {
405 self.variant(ChipVariant::Input).label(label.into())
406 }
407
408 pub fn leading_icon(&self, icon: impl Into<Painter>) -> Self {
410 let value = Some(icon.into());
411 self.props
412 .configure("leading_icon", move |props| props.leading_icon = value);
413 Self { props: self.props }
414 }
415
416 pub fn clear_leading_icon(&self) -> Self {
418 let value = None;
419 self.props
420 .configure("leading_icon", move |props| props.leading_icon = value);
421 Self { props: self.props }
422 }
423
424 pub fn trailing_icon(&self, icon: impl Into<Painter>) -> Self {
426 let value = Some(icon.into());
427 self.props
428 .configure("trailing_icon", move |props| props.trailing_icon = value);
429 Self { props: self.props }
430 }
431
432 pub fn clear_trailing_icon(&self) -> Self {
434 let value = None;
435 self.props
436 .configure("trailing_icon", move |props| props.trailing_icon = value);
437 Self { props: self.props }
438 }
439}
440
441#[tessera]
504pub fn chip(
505 variant: Option<ChipVariant>,
506 style: Option<ChipStyle>,
507 #[prop(into)] label: Option<String>,
508 #[prop(skip_setter)] leading_icon: Option<Painter>,
509 #[prop(skip_setter)] trailing_icon: Option<Painter>,
510 #[prop(into)] avatar: Option<Painter>,
511 selected: Option<bool>,
512 enabled: Option<bool>,
513 modifier: Option<Modifier>,
514 colors: Option<ChipColors>,
515 border: Option<ChipBorder>,
516 shape: Option<Shape>,
517 shape_style: Option<ChipShapeStyle>,
518 elevation: Option<Dp>,
519 on_click: Option<Callback>,
520 on_remove: Option<Callback>,
521 #[prop(into)] remove_accessibility_label: Option<String>,
522 #[prop(into)] accessibility_label: Option<String>,
523 #[prop(into)] accessibility_description: Option<String>,
524) {
525 let label = label.unwrap_or_default();
526 let modifier = modifier.unwrap_or_default();
527 let mut args = ChipResolvedArgs {
528 variant: variant.unwrap_or_default(),
529 style: style.unwrap_or_default(),
530 label,
531 leading_icon,
532 avatar,
533 trailing_icon,
534 selected: selected.unwrap_or(false),
535 enabled: enabled.unwrap_or(true),
536 modifier,
537 colors,
538 border,
539 shape,
540 shape_style: shape_style.unwrap_or_default(),
541 elevation,
542 on_click,
543 on_remove,
544 remove_accessibility_label,
545 accessibility_label,
546 accessibility_description,
547 };
548 let on_remove = if args.variant == ChipVariant::Input {
549 args.on_remove
550 } else {
551 None
552 };
553 let separate_remove = on_remove.is_some() && args.on_click.is_some();
554 let remove_only = on_remove.is_some() && args.on_click.is_none();
555 if on_remove.is_some() && args.trailing_icon.is_none() {
556 args.trailing_icon = Some(filled::CLOSE_SVG.into());
557 }
558 if remove_only {
559 args.on_click = on_remove;
560 }
561 let theme = use_context::<MaterialTheme>()
562 .expect("MaterialTheme must be provided")
563 .get();
564 let typography = theme.typography;
565 let variant = args.variant;
566 let style = args.style;
567 let selectable = matches!(variant, ChipVariant::Filter | ChipVariant::Input) && !remove_only;
568 let selected = args.selected && selectable;
569 let custom_colors = args.colors.is_some();
570 let colors = args
571 .colors
572 .unwrap_or_else(|| ChipDefaults::colors(&theme.color_scheme, variant, style));
573 let custom_border = args.border.is_some();
574 let mut border = args
575 .border
576 .or_else(|| ChipDefaults::border(&theme.color_scheme, variant, style));
577 let interaction_state = remember(InteractionState::new);
578 if (!args.enabled || args.on_click.is_none())
579 && interaction_state.get() != InteractionState::default()
580 {
581 interaction_state.set(InteractionState::default());
582 }
583 let interaction = Interaction::from_state(interaction_state.get());
584 if !custom_border && args.enabled && interaction == Interaction::Focus {
585 if let Some(border) = &mut border {
586 border.color = if variant == ChipVariant::Assist {
587 theme.color_scheme.on_surface
588 } else {
589 theme.color_scheme.on_surface_variant
590 };
591 }
592 }
593 let target_elevation = if !args.enabled {
594 Dp::ZERO
595 } else if let Some(elevation) = args.elevation {
596 elevation
597 } else if interaction == Interaction::Drag {
598 Dp(8.0)
599 } else if style == ChipStyle::Elevated {
600 if interaction == Interaction::Hover {
601 Dp(3.0)
602 } else {
603 Dp(1.0)
604 }
605 } else if variant == ChipVariant::Filter && selected && interaction == Interaction::Hover {
606 Dp(1.0)
607 } else {
608 Dp::ZERO
609 };
610 let elevation = remember(|| Elevation::new(target_elevation));
611 let mut next = elevation.get();
612 next.update(
613 target_elevation,
614 interaction,
615 args.enabled,
616 current_frame_nanos(),
617 );
618 if next != elevation.get() {
619 elevation.set(next);
620 }
621 if next.is_animating() {
622 receive_frame_nanos(move |now| {
623 let mut next = elevation.get();
624 next.advance(now);
625 if next != elevation.get() {
626 elevation.set(next);
627 }
628 if next.is_animating() {
629 tessera_ui::FrameNanosControl::Continue
630 } else {
631 tessera_ui::FrameNanosControl::Stop
632 }
633 });
634 }
635 let shadow_elevation = elevation.get().value();
636 let target_shape = args.shape.unwrap_or_else(|| {
637 if !selectable || args.shape_style == ChipShapeStyle::Standard {
638 theme.shapes.small
639 } else if interaction == Interaction::Press {
640 theme.shapes.small
641 } else if selected {
642 Shape::CAPSULE
643 } else {
644 theme.shapes.medium
645 }
646 });
647 let shape_motion = remember(|| ShapeMotion::new(target_shape));
648 let mut next = shape_motion.get();
649 next.retarget(
650 target_shape,
651 args.enabled
652 && args.shape.is_none()
653 && selectable
654 && args.shape_style == ChipShapeStyle::Expressive,
655 current_frame_nanos(),
656 );
657 if next != shape_motion.get() {
658 shape_motion.set(next);
659 }
660 if next.is_animating() {
661 receive_frame_nanos(move |now| {
662 let mut next = shape_motion.get();
663 next.advance(now);
664 if next != shape_motion.get() {
665 shape_motion.set(next);
666 }
667 if next.is_animating() {
668 tessera_ui::FrameNanosControl::Continue
669 } else {
670 tessera_ui::FrameNanosControl::Stop
671 }
672 });
673 }
674 let shape = shape_motion.get().shape();
675 let avatar = if variant == ChipVariant::Input {
676 args.avatar
677 } else {
678 None
679 };
680 let padding = chip_padding(
681 variant,
682 avatar.is_some(),
683 args.leading_icon.is_some(),
684 args.trailing_icon.is_some(),
685 );
686
687 let container_color = colors.container_color(args.enabled, selected);
688 let label_color = colors.label_color(args.enabled, selected);
689 let leading_icon_color = if !custom_colors
690 && args.enabled
691 && variant == ChipVariant::Input
692 && matches!(
693 interaction,
694 Interaction::Hover | Interaction::Focus | Interaction::Press
695 ) {
696 theme.color_scheme.primary
697 } else {
698 colors.leading_icon_color(args.enabled, selected)
699 };
700 let trailing_icon_color = colors.trailing_icon_color(args.enabled, selected);
701
702 let border_style = border.and_then(|border| border.resolve(args.enabled, selected));
703 let surface_style = match border_style {
704 Some((border_width, border_color)) => SurfaceStyle::FilledOutlined {
705 fill_color: container_color,
706 border_color,
707 border_width,
708 },
709 None => SurfaceStyle::Filled {
710 color: container_color,
711 },
712 };
713
714 let label = args.label;
715 let remove_label = args
716 .remove_accessibility_label
717 .unwrap_or_else(|| format!("Remove {label}"));
718 let removal_icon = args.trailing_icon.clone();
719 let accessibility_label = args.accessibility_label.or_else(|| {
720 if remove_only {
721 Some(remove_label.clone())
722 } else {
723 (!label.is_empty()).then(|| label.clone())
724 }
725 });
726
727 let modifier = args
728 .modifier
729 .default_min_size(
730 if separate_remove { Dp(88.0) } else { Dp::ZERO },
731 ChipDefaults::HEIGHT,
732 )
733 .minimum_interactive_component_size();
734 let has_on_click = args.on_click.is_some();
735 let leading_icon = args.leading_icon;
736 let trailing_icon = args.trailing_icon;
737 let has_label = !label.is_empty();
738 let avatar_alpha = if args.enabled {
739 1.0
740 } else {
741 MaterialAlpha::DISABLED_CONTENT
742 };
743
744 layout()
745 .policy(ChipRemovalLayout {
746 separate_remove,
747 trailing_padding: padding.right,
748 })
749 .child(move || {
750 let avatar = avatar.clone();
751 let leading_icon = leading_icon.clone();
752 let trailing_icon = trailing_icon.clone();
753 let label = label.clone();
754 surface()
755 .modifier(modifier.clone())
756 .style(surface_style.clone())
757 .shape(shape)
758 .content_alignment(Alignment::Center)
759 .content_color(label_color)
760 .enabled(args.enabled)
761 .ripple_color(label_color)
762 .elevation(shadow_elevation)
763 .interaction_state(interaction_state)
764 .on_click_optional(args.on_click)
765 .accessibility_role_optional((has_on_click || selectable).then_some(
766 if selectable {
767 Role::CheckBox
768 } else {
769 Role::Button
770 },
771 ))
772 .accessibility_toggled_optional(selectable.then_some(if selected {
773 Toggled::True
774 } else {
775 Toggled::False
776 }))
777 .accessibility_focusable_optional(has_on_click.then_some(true))
778 .accessibility_label_optional(accessibility_label.clone())
779 .accessibility_description_optional(args.accessibility_description.clone())
780 .child(move || {
781 let avatar = avatar.clone();
782 let leading_icon = leading_icon.clone();
783 let trailing_icon = trailing_icon.clone();
784 let label = label.clone();
785 provide_context(
786 || typography.label_large,
787 move || {
788 let inherited = use_context::<TextStyle>().expect("chip text style");
789 if inherited.get() != typography.label_large {
790 inherited.set(typography.label_large);
791 }
792 layout()
793 .modifier(Modifier::new().padding(padding))
794 .child(move || {
795 let avatar = avatar.clone();
796 let leading_icon = leading_icon.clone();
797 let trailing_icon = trailing_icon.clone();
798 let label = label.clone();
799 row()
800 .cross_axis_alignment(CrossAxisAlignment::Center)
801 .children(move || {
802 let spacing = ChipDefaults::ELEMENT_SPACING;
803 let mut item_count = 0;
804
805 if let Some(avatar) = avatar.clone() {
806 item_count += 1;
807 chip_avatar().painter(avatar).alpha(avatar_alpha);
808 } else if let Some(icon_content) = leading_icon.clone()
809 {
810 item_count += 1;
811 icon()
812 .painter(icon_content.clone())
813 .size(ChipDefaults::ICON_SIZE)
814 .tint(leading_icon_color);
815 }
816
817 if has_label {
818 if item_count > 0 {
819 spacer()
820 .modifier(Modifier::new().width(spacing));
821 }
822 item_count += 1;
823 text(label.clone());
824 }
825
826 if let Some(icon_content) = trailing_icon.clone() {
827 if item_count > 0 {
828 spacer()
829 .modifier(Modifier::new().width(spacing));
830 }
831 if separate_remove {
832 spacer().modifier(Modifier::new().size(
833 ChipDefaults::ICON_SIZE,
834 ChipDefaults::ICON_SIZE,
835 ));
836 } else {
837 icon()
838 .painter(icon_content.clone())
839 .size(ChipDefaults::ICON_SIZE)
840 .tint(trailing_icon_color);
841 }
842 }
843 });
844 });
845 },
846 );
847 });
848 if separate_remove {
849 chip_remove_action(remove_label.clone(), on_remove.expect("input chip removal"))
850 .icon_optional(removal_icon.clone())
851 .enabled(args.enabled)
852 .color(trailing_icon_color);
853 }
854 });
855}
856
857#[derive(Clone, Copy, PartialEq)]
858struct ChipRemovalLayout {
859 separate_remove: bool,
860 trailing_padding: Dp,
861}
862
863impl tessera_ui::LayoutModifierNode for ChipRemovalLayout {
864 fn measure(&self, scope: &mut MeasureScope<'_>) -> Result<LayoutResult, MeasurementError> {
865 let children = scope.children();
866 let constraints = scope.parent_constraint().without_min();
867 let body = children[0];
868 let size = body.measure(scope, &constraints)?.size();
869 let mut result = LayoutResult::new(size);
870 result.place_child(body, PxPosition::ZERO);
871 if self.separate_remove {
872 let remove = children[1];
873 let remove_size = remove.measure(scope, &constraints)?.size();
874 let center = size.width
875 - Px::from(self.trailing_padding)
876 - Px::from(ChipDefaults::ICON_SIZE) / 2;
877 result.place_child(
878 remove,
879 PxPosition::new(
880 center - remove_size.width / 2,
881 (size.height - remove_size.height) / 2,
882 ),
883 );
884 }
885 Ok(result)
886 }
887}
888
889#[tessera]
890fn chip_remove_action(
891 icon: Option<Painter>,
892 enabled: Option<bool>,
893 color: Option<Color>,
894 label: String,
895 on_remove: Callback,
896) {
897 let enabled = enabled.unwrap_or(true);
898 surface()
899 .modifier(Modifier::new().size(Dp(48.0), Dp(48.0)))
900 .shape(Shape::Ellipse)
901 .style(Color::TRANSPARENT.into())
902 .content_alignment(Alignment::Center)
903 .enabled(enabled)
904 .accessibility_role(Role::Button)
905 .accessibility_label(label)
906 .accessibility_focusable(enabled)
907 .ripple_color(color.unwrap_or(Color::BLACK))
908 .on_click_shared(on_remove)
909 .child(move || {
910 if let Some(icon) = icon.clone() {
911 crate::icon::icon()
912 .painter(icon)
913 .size(ChipDefaults::ICON_SIZE)
914 .tint_optional(color);
915 }
916 });
917}
918
919#[tessera]
920fn chip_avatar(painter: Option<Painter>, alpha: Option<f32>) {
921 let Some(painter) = painter.and_then(Painter::resolve) else {
922 crate::spacer::spacer()
923 .modifier(Modifier::new().size(ChipDefaults::AVATAR_SIZE, ChipDefaults::AVATAR_SIZE));
924 return;
925 };
926 let (width, height) = match &painter {
927 Painter::Source(_) => unreachable!("painter is resolved"),
928 Painter::Raster(data) => (f64::from(data.width), f64::from(data.height)),
929 Painter::Vector(data) => (
930 f64::from(data.viewport_width),
931 f64::from(data.viewport_height),
932 ),
933 };
934 let shortest = width.min(height);
935 let scale = if shortest.is_finite() && shortest > 0.0 {
936 ChipDefaults::AVATAR_SIZE.0 / shortest
937 } else {
938 1.0
939 };
940 let width = Dp(width * scale);
941 let height = Dp(height * scale);
942 boxed()
943 .modifier(
944 Modifier::new()
945 .size(ChipDefaults::AVATAR_SIZE, ChipDefaults::AVATAR_SIZE)
946 .clip_shape(Shape::Ellipse)
947 .alpha(alpha.unwrap_or(1.0)),
948 )
949 .alignment(Alignment::Center)
950 .children(move || {
951 image()
952 .painter(painter.clone())
953 .modifier(Modifier::new().required_size(width, height));
954 });
955}
956
957fn chip_padding(
958 variant: ChipVariant,
959 has_avatar: bool,
960 has_leading_icon: bool,
961 has_trailing_icon: bool,
962) -> Padding {
963 let (start, end) = match variant {
964 ChipVariant::Input => {
965 let start = if has_leading_icon && !has_avatar {
966 ChipDefaults::INPUT_ICON_PADDING
967 } else {
968 ChipDefaults::INPUT_EDGE_PADDING
969 };
970 let end = if has_trailing_icon {
971 ChipDefaults::INPUT_ICON_PADDING
972 } else {
973 ChipDefaults::INPUT_EDGE_PADDING
974 };
975 (start, end)
976 }
977 _ => (
978 ChipDefaults::HORIZONTAL_PADDING,
979 ChipDefaults::HORIZONTAL_PADDING,
980 ),
981 };
982 let start = Dp(start.0
984 + if has_leading_icon || has_avatar {
985 0.0
986 } else {
987 ChipDefaults::ELEMENT_SPACING.0
988 });
989 let end = Dp(end.0
990 + if has_trailing_icon {
991 0.0
992 } else {
993 ChipDefaults::ELEMENT_SPACING.0
994 });
995 Padding::new(start, Dp::ZERO, end, Dp::ZERO)
996}
997
998#[cfg(test)]
999mod tests;
1000
1001#[cfg(test)]
1002mod appearance_tests;
1003
1004#[cfg(test)]
1005mod avatar_tests;
1006
1007#[cfg(test)]
1008mod shape_tests;
1009
1010#[cfg(all(test, not(target_family = "wasm")))]
1011mod remove_tests;