1use frus_core::{BorderRadius, Color, Insets, Point, Rect, Scene, ShapeBorder, TextStyle};
20use frus_layout::{Align, Dimension, FlexDirection, Style};
21
22use crate::disabled::{disabled_container, disabled_content};
23use crate::icons::{IconData, Icons};
24use crate::interaction::Status;
25use crate::theme::Theme;
26use crate::widget::Widget;
27
28pub const CHIP_HEIGHT: f32 = 32.0;
30pub const CHIP_RADIUS: f32 = 8.0;
32pub const CHIP_PADDING: f32 = 8.0;
34pub const CHIP_LABEL_PADDING: f32 = 8.0;
36pub const CHIP_ICON_SIZE: f32 = 18.0;
38pub const CHIP_BORDER_WIDTH: f32 = 1.0;
40
41#[derive(Clone, Copy, Debug, Default, PartialEq)]
43pub(crate) struct ChipStyle {
44 pub color: Option<Color>,
45 pub selected_color: Option<Color>,
46 pub label_color: Option<Color>,
47 pub selected_label_color: Option<Color>,
48 pub label_style: Option<TextStyle>,
49 pub border_color: Option<Color>,
50 pub border_width: Option<f32>,
51 pub shape: Option<ShapeBorder>,
54 pub padding: Option<f32>,
55 pub label_padding: Option<f32>,
56 pub height: Option<f32>,
57 pub icon_size: Option<f32>,
58 pub show_checkmark: Option<bool>,
59}
60
61impl ChipStyle {
62 fn background(&self, theme: &Theme, selected: bool) -> Color {
65 if selected {
66 self.selected_color
67 .or(theme.widgets.chip.selected_color)
68 .unwrap_or(theme.scheme.secondary_container)
69 } else {
70 self.color
71 .or(theme.widgets.chip.color)
72 .unwrap_or(Color::TRANSPARENT)
73 }
74 }
75
76 fn label_color(&self, theme: &Theme, selected: bool) -> Color {
77 if selected {
78 self.selected_label_color
79 .or(theme.widgets.chip.selected_label_color)
80 .unwrap_or(theme.scheme.on_secondary_container)
81 } else {
82 self.label_color
83 .or(theme.widgets.chip.label_color)
84 .unwrap_or(theme.scheme.on_surface_variant)
85 }
86 }
87
88 fn label_style(&self, theme: &Theme) -> TextStyle {
89 self.label_style
90 .or(theme.widgets.chip.label_style)
91 .unwrap_or(theme.text.label_large)
92 }
93
94 fn border(&self, theme: &Theme, selected: bool) -> (f32, Color) {
97 let width = self
98 .border_width
99 .or(theme.widgets.chip.border_width)
100 .unwrap_or(if selected { 0.0 } else { CHIP_BORDER_WIDTH });
101 let color = self
102 .border_color
103 .or(theme.widgets.chip.border_color)
104 .unwrap_or(theme.scheme.outline_variant);
105 (width, color)
106 }
107
108 fn shape(&self, theme: &Theme) -> ShapeBorder {
111 crate::resolve_shape(
112 self.shape,
113 theme.widgets.chip.shape,
114 theme.widgets.chip.radius,
115 ShapeBorder::rounded(CHIP_RADIUS),
116 )
117 }
118
119 fn radius(&self, theme: &Theme) -> BorderRadius {
122 let side = self.height(theme);
123 self.shape(theme)
124 .as_rounded(Rect::new(0.0, 0.0, side, side))
125 .map(|(_, radius)| radius)
126 .unwrap_or(BorderRadius::ZERO)
127 }
128
129 fn padding(&self, theme: &Theme) -> f32 {
130 self.padding
131 .or(theme.widgets.chip.padding)
132 .unwrap_or(CHIP_PADDING)
133 }
134
135 fn label_padding(&self, theme: &Theme) -> f32 {
136 self.label_padding
137 .or(theme.widgets.chip.label_padding)
138 .unwrap_or(CHIP_LABEL_PADDING)
139 }
140
141 fn height(&self, theme: &Theme) -> f32 {
142 self.height
143 .or(theme.widgets.chip.height)
144 .unwrap_or(CHIP_HEIGHT)
145 }
146
147 fn icon_size(&self, theme: &Theme) -> f32 {
148 self.icon_size
149 .or(theme.widgets.chip.icon_size)
150 .unwrap_or(CHIP_ICON_SIZE)
151 }
152
153 fn show_checkmark(&self, theme: &Theme) -> bool {
154 self.show_checkmark
155 .or(theme.widgets.chip.show_checkmark)
156 .unwrap_or(true)
157 }
158}
159
160struct Remove<Msg> {
163 message: Msg,
164 style: ChipStyle,
167 selected: bool,
168 enabled: bool,
171}
172
173impl<Msg: Clone> Widget<Msg> for Remove<Msg> {
174 fn style(&self) -> Style {
175 Widget::<Msg>::style_themed(self, &Theme::default())
176 }
177
178 fn style_themed(&self, theme: &Theme) -> Style {
179 let size = self.style.icon_size(theme);
180 Style {
181 width: Dimension::Length(size),
182 height: Dimension::Length(size),
183 ..Default::default()
184 }
185 }
186
187 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
188 &[]
189 }
190
191 fn paint(&self, bounds: Rect, status: Status, theme: &Theme, scene: &mut Scene) {
192 let size = self.style.icon_size(theme);
195 let color = if self.enabled {
196 self.style
197 .label_color(theme, self.selected)
198 .lerp(theme.scheme.on_surface, status.hover_progress)
199 } else {
200 disabled_content(theme)
201 }
202 .fade(status.opacity);
203 let path = Icons::CLOSE.placed(
204 size,
205 bounds.x,
206 bounds.y + (bounds.height - size) * 0.5,
207 theme.direction,
208 );
209 scene.fill_path(&path, color);
210 }
211
212 fn on_click(&self) -> Option<Msg> {
213 if !self.enabled {
214 return None;
215 }
216 Some(self.message.clone())
217 }
218
219 fn focusable(&self) -> bool {
220 self.enabled
221 }
222
223 fn semantics(&self) -> Option<frus_core::SemanticsProperties> {
224 let semantics =
225 frus_core::SemanticsProperties::new(frus_core::Role::Button).label("Remove");
226 Some(if self.enabled {
231 semantics.clickable()
232 } else {
233 semantics.disabled(true)
234 })
235 }
236}
237
238pub struct Chip<Msg = crate::callback::Callback> {
240 label: String,
241 selected: bool,
242 leading: Option<IconData>,
243 on_press: Option<Msg>,
244 on_remove: Option<Msg>,
245 enabled: bool,
249 style: ChipStyle,
250 children: Vec<Box<dyn Widget<Msg>>>,
253}
254
255impl<Msg: Clone + 'static> Chip<Msg> {
256 pub fn new(label: impl Into<String>) -> Self {
258 Self {
259 label: label.into(),
260 selected: false,
261 leading: None,
262 on_press: None,
263 on_remove: None,
264 enabled: true,
265 style: ChipStyle::default(),
266 children: Vec::new(),
267 }
268 }
269
270 fn rebuild(mut self) -> Self {
276 self.children.clear();
277 if let Some(message) = self.on_remove.clone() {
278 self.children.push(Box::new(Remove {
279 message,
280 style: self.style,
281 selected: self.selected,
282 enabled: self.enabled,
283 }));
284 }
285 self
286 }
287
288 pub fn selected(mut self, selected: bool) -> Self {
292 self.selected = selected;
293 self.rebuild()
294 }
295
296 pub fn leading(mut self, icon: IconData) -> Self {
298 self.leading = Some(icon);
299 self.rebuild()
300 }
301
302 pub fn enabled(mut self, enabled: bool) -> Self {
306 self.enabled = enabled;
307 self.rebuild()
308 }
309
310 pub fn on_press(mut self, message: impl Into<Msg>) -> Self {
313 self.on_press = Some(message.into());
314 self
315 }
316
317 pub fn on_remove(mut self, message: impl Into<Msg>) -> Self {
319 self.on_remove = Some(message.into());
320 self.rebuild()
321 }
322
323 pub fn show_checkmark(mut self, show: bool) -> Self {
325 self.style.show_checkmark = Some(show);
326 self.rebuild()
327 }
328
329 pub fn color(mut self, color: Color) -> Self {
331 self.style.color = Some(color);
332 self.rebuild()
333 }
334
335 pub fn selected_color(mut self, color: Color) -> Self {
337 self.style.selected_color = Some(color);
338 self.rebuild()
339 }
340
341 pub fn label_color(mut self, color: Color) -> Self {
343 self.style.label_color = Some(color);
344 self.rebuild()
345 }
346
347 pub fn selected_label_color(mut self, color: Color) -> Self {
349 self.style.selected_label_color = Some(color);
350 self.rebuild()
351 }
352
353 pub fn label_style(mut self, style: TextStyle) -> Self {
355 self.style.label_style = Some(style);
356 self.rebuild()
357 }
358
359 pub fn border_color(mut self, color: Color) -> Self {
361 self.style.border_color = Some(color);
362 self.rebuild()
363 }
364
365 pub fn border_width(mut self, width: f32) -> Self {
367 self.style.border_width = Some(width);
368 self.rebuild()
369 }
370
371 pub fn radius(mut self, radius: impl Into<BorderRadius>) -> Self {
374 self.style.shape = Some(ShapeBorder::rounded(radius));
375 self.rebuild()
376 }
377
378 pub fn shape(mut self, shape: ShapeBorder) -> Self {
381 self.style.shape = Some(shape);
382 self.rebuild()
383 }
384
385 pub fn padding(mut self, padding: f32) -> Self {
387 self.style.padding = Some(padding);
388 self.rebuild()
389 }
390
391 pub fn label_padding(mut self, padding: f32) -> Self {
393 self.style.label_padding = Some(padding);
394 self.rebuild()
395 }
396
397 pub fn height(mut self, height: f32) -> Self {
399 self.style.height = Some(height);
400 self.rebuild()
401 }
402
403 pub fn icon_size(mut self, size: f32) -> Self {
405 self.style.icon_size = Some(size);
406 self.rebuild()
407 }
408
409 fn leading_glyph(&self, theme: &Theme) -> Option<IconData> {
412 self.leading
413 .or_else(|| (self.selected && self.style.show_checkmark(theme)).then_some(Icons::CHECK))
414 }
415
416 fn label_offset(&self, theme: &Theme) -> f32 {
418 let leading = self
419 .leading_glyph(theme)
420 .map(|_| self.style.icon_size(theme))
421 .unwrap_or(0.0);
422 self.style.padding(theme) + leading + self.style.label_padding(theme)
423 }
424
425 fn label_size(&self, theme: &Theme) -> frus_core::Size {
426 frus_text::measure_resolved(&self.label, &self.style.label_style(theme).resolved())
427 }
428
429 fn box_height(&self, theme: &Theme) -> f32 {
442 frus_text::line_box(
443 self.style.height(theme),
444 &self.style.label_style(theme).resolved(),
445 0.0,
446 )
447 }
448}
449
450impl<Msg: Clone + 'static> Widget<Msg> for Chip<Msg> {
451 fn style(&self) -> Style {
452 Widget::<Msg>::style_themed(self, &Theme::default())
453 }
454
455 fn style_themed(&self, theme: &Theme) -> Style {
456 let pad = self.style.padding(theme);
461 let label_pad = self.style.label_padding(theme);
462 let icon = self.style.icon_size(theme);
463 let after_label = self.label_offset(theme) + self.label_size(theme).width + label_pad;
464 let removable = !self.children.is_empty();
465 Style {
466 width: Dimension::Length(after_label + if removable { icon } else { 0.0 } + pad),
467 height: Dimension::Length(self.box_height(theme)),
468 flex_direction: FlexDirection::Row,
469 align: Align::Center,
470 padding: Insets::new(0.0, pad, 0.0, after_label),
471 ..Default::default()
472 }
473 }
474
475 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
476 &self.children
477 }
478
479 fn paint(&self, bounds: Rect, status: Status, theme: &Theme, scene: &mut Scene) {
480 let o = status.opacity;
481 let selected = self.selected;
482 let (border_width, border_color) = self.style.border(theme, selected);
483 let (background, border_color, label_color) = if self.enabled {
489 (
490 self.style.background(theme, selected),
491 border_color,
492 self.style.label_color(theme, selected),
493 )
494 } else {
495 (
496 if selected {
497 disabled_container(theme)
498 } else {
499 Color::TRANSPARENT
500 },
501 disabled_container(theme),
502 disabled_content(theme),
503 )
504 };
505
506 scene.draw_shape(
507 bounds,
508 self.style
509 .shape(theme)
510 .with_side(frus_core::BorderSide::new(
511 if border_width > 0.0 {
512 border_color.fade(o)
513 } else {
514 Color::TRANSPARENT
515 },
516 border_width,
517 )),
518 background.fade(o),
519 );
520
521 let icon_size = self.style.icon_size(theme);
526 if let Some(name) = self.leading_glyph(theme) {
527 let color = if selected {
528 label_color
529 } else {
530 theme.scheme.primary
531 };
532 let path = name.placed(
533 icon_size,
534 bounds.x + self.style.padding(theme),
535 bounds.y + (bounds.height - icon_size) * 0.5,
536 theme.direction,
537 );
538 scene.fill_path(&path, color.fade(o));
539 }
540
541 let label_style = self.style.label_style(theme);
542 let measured = self.label_size(theme);
543 scene.text(
544 Point::new(
545 bounds.x + self.label_offset(theme),
546 bounds.y + (bounds.height - measured.height) * 0.5,
547 ),
548 self.label.clone(),
549 &label_style.resolved(),
550 label_color.fade(o),
551 );
552 }
553
554 fn on_click(&self) -> Option<Msg> {
555 self.enabled.then(|| self.on_press.clone()).flatten()
556 }
557
558 fn ink(&self, theme: &Theme) -> Option<crate::InkStyle> {
559 if !self.enabled {
562 return None;
563 }
564 self.on_press.as_ref()?;
565 Some(crate::InkStyle::of(theme).radius(self.style.radius(theme)))
566 }
567
568 fn focusable(&self) -> bool {
569 self.enabled && self.on_press.is_some()
570 }
571
572 fn semantics(&self) -> Option<frus_core::SemanticsProperties> {
573 let semantics =
574 frus_core::SemanticsProperties::new(frus_core::Role::Button).label(self.label.clone());
575 let semantics = if self.on_press.is_some() && self.enabled {
578 semantics.toggled(self.selected).clickable()
579 } else {
580 semantics
581 };
582 let semantics = if self.enabled {
585 semantics
586 } else {
587 semantics.disabled(true)
588 };
589 Some(semantics)
590 }
591}
592
593#[cfg(test)]
594mod tests {
595 use super::*;
596 use crate::{build_ui, Point as P, Runtime, Size};
597 use frus_core::Primitive;
598
599 #[derive(Clone, Debug, PartialEq)]
600 enum Msg {
601 Press,
602 Remove,
603 }
604
605 fn frame(chip: Chip<Msg>) -> crate::Ui<Msg> {
606 let root = crate::flex::Flex::column()
607 .width(200.0)
608 .height(60.0)
609 .child(chip);
610 build_ui(
611 &root,
612 Size::new(200.0, 60.0),
613 &Runtime::default(),
614 &Theme::default(),
615 )
616 }
617
618 fn primitives(chip: Chip<Msg>) -> Vec<Primitive> {
619 frame(chip).scene().primitives().to_vec()
620 }
621
622 fn surface(chip: Chip<Msg>) -> (Rect, Color, BorderRadius, f32) {
624 primitives(chip)
625 .iter()
626 .find_map(|p| match p {
627 Primitive::Rect {
628 rect,
629 color,
630 radius,
631 border_width,
632 ..
633 } => Some((*rect, *color, *radius, *border_width)),
634 _ => None,
635 })
636 .expect("a chip paints its box")
637 }
638
639 #[test]
644 fn a_disabled_chip_is_inert_including_its_cross() {
645 let live = Chip::new("Filter")
646 .on_press(Msg::Press)
647 .on_remove(Msg::Remove);
648 let dead = Chip::new("Filter")
649 .on_press(Msg::Press)
650 .on_remove(Msg::Remove)
651 .enabled(false);
652 assert_eq!(Widget::on_click(&live), Some(Msg::Press));
653 assert_eq!(Widget::on_click(&dead), None, "the press goes nowhere");
654 assert!(Widget::<Msg>::focusable(&live));
655 assert!(!Widget::<Msg>::focusable(&dead), "out of the tab order");
656 assert!(Widget::<Msg>::ink(&live, &Theme::default()).is_some());
657 assert!(
658 Widget::<Msg>::ink(&dead, &Theme::default()).is_none(),
659 "nothing to splash for"
660 );
661 let cross = |c: &Chip<Msg>| Widget::<Msg>::children(c)[0].on_click();
663 assert_eq!(cross(&live), Some(Msg::Remove));
664 assert_eq!(cross(&dead), None, "the cross follows the chip");
665 let semantics = Widget::<Msg>::semantics(&dead).unwrap();
668 assert!(semantics.disabled);
669 assert_eq!(semantics.label.as_deref(), Some("Filter"));
670 }
671
672 #[test]
681 fn disabled_is_never_louder_than_live() {
682 for (name, theme) in [("dark", Theme::dark()), ("light", Theme::light())] {
683 let bg = theme.scheme.surface;
684 let lum = |r: f32, g: f32, b: f32| 0.2126 * r + 0.7152 * g + 0.0722 * b;
685 let ground = lum(bg.r, bg.g, bg.b);
686 let contrast = |c: frus_core::Color| {
688 let over = |f: f32, b: f32| f * c.a + b * (1.0 - c.a);
689 (lum(over(c.r, bg.r), over(c.g, bg.g), over(c.b, bg.b)) - ground).abs()
690 };
691 let live = ChipStyle::default().label_color(&theme, false);
692 let dead = theme.scheme.on_surface.fade(0.38);
693 assert!(
694 contrast(dead) < contrast(live),
695 "{name}: the disabled label must be quieter, got {} against {}",
696 contrast(dead),
697 contrast(live)
698 );
699 let live_edge = ChipStyle::default().border(&theme, false).1;
706 let dead_edge = theme.scheme.on_surface.fade(0.12);
707 assert!(
708 contrast(dead_edge) < contrast(live_edge),
709 "{name}: the disabled outline must be quieter, got {} against {}",
710 contrast(dead_edge),
711 contrast(live_edge)
712 );
713 }
714 }
715
716 #[test]
720 fn a_disabled_chip_flattens_rather_than_fades() {
721 let theme = Theme::default();
722 let accent = ChipStyle::default().background(&theme, true);
723 let fills = |chip: Chip<Msg>| -> Vec<frus_core::Color> {
724 frame(chip)
725 .scene()
726 .primitives()
727 .iter()
728 .filter_map(|p| match p {
729 Primitive::Rect { color, .. } if color.a > 0.0 => Some(*color),
730 _ => None,
731 })
732 .collect()
733 };
734 let selected = || Chip::new("On").on_press(Msg::Press).selected(true);
735 assert!(
736 fills(selected()).contains(&accent),
737 "a live selected chip fills with the accent"
738 );
739 assert!(
740 !fills(selected().enabled(false)).contains(&accent),
741 "a disabled one never does"
742 );
743 }
744
745 #[test]
746 fn a_chip_is_the_references_size_and_shape() {
747 let (rect, _, radius, _) = surface(Chip::new("Draft"));
750 assert_eq!(rect.height, CHIP_HEIGHT);
751 assert_eq!(radius, BorderRadius::uniform(CHIP_RADIUS));
752 assert!(
753 radius.top_left < rect.height / 2.0,
754 "a rounded rectangle, not a pill"
755 );
756 }
757
758 #[test]
759 fn at_rest_it_is_an_outline_and_nothing_else() {
760 let theme = Theme::default();
761 let (_, color, _, border) = surface(Chip::new("Draft"));
762 assert_eq!(
763 color,
764 Color::TRANSPARENT,
765 "no fill under an unselected chip"
766 );
767 assert_eq!(border, CHIP_BORDER_WIDTH);
768 let (_, color, _, border) = surface(Chip::new("Draft").selected(true));
770 assert_eq!(color, theme.scheme.secondary_container);
771 assert_eq!(border, 0.0, "an outline around a fill reads as two borders");
772 }
773
774 #[test]
775 fn a_selected_chip_shows_a_checkmark_and_the_label_makes_room_for_it() {
776 let count_paths = |chip: Chip<Msg>| {
777 primitives(chip)
778 .iter()
779 .filter(|p| matches!(p, Primitive::Path { .. }))
780 .count()
781 };
782 assert_eq!(count_paths(Chip::new("Unread")), 0);
783 assert_eq!(count_paths(Chip::new("Unread").selected(true)), 1);
784 assert_eq!(
785 count_paths(Chip::new("Unread").selected(true).show_checkmark(false)),
786 0,
787 "and it can be refused"
788 );
789 let text_x = |chip: Chip<Msg>| {
791 primitives(chip)
792 .iter()
793 .find_map(|p| match p {
794 Primitive::Text { position, .. } => Some(position.x),
795 _ => None,
796 })
797 .expect("a label")
798 };
799 assert_eq!(
800 text_x(Chip::new("Unread").selected(true)) - text_x(Chip::new("Unread")),
801 CHIP_ICON_SIZE
802 );
803 }
804
805 #[test]
806 fn a_leading_icon_takes_the_slot_the_checkmark_would_have() {
807 let paths = |chip: Chip<Msg>| {
810 primitives(chip)
811 .iter()
812 .filter(|p| matches!(p, Primitive::Path { .. }))
813 .count()
814 };
815 assert_eq!(paths(Chip::new("Ada").leading(Icons::STAR)), 1);
816 assert_eq!(
817 paths(Chip::new("Ada").leading(Icons::STAR).selected(true)),
818 1,
819 "the icon, not the icon and a checkmark"
820 );
821 }
822
823 #[test]
824 fn the_cross_is_a_target_of_its_own() {
825 let chip = || Chip::new("Ada").on_press(Msg::Press).on_remove(Msg::Remove);
828 let ui = frame(chip());
829 let at = |x: f32| ui.hit(P::new(x, 16.0)).and_then(|id| ui.msg_for(id));
830 let width = surface(chip()).0.width;
831 assert_eq!(at(20.0), Some(Msg::Press), "the chip presses");
832 assert_eq!(at(width - 14.0), Some(Msg::Remove), "the cross removes");
833 }
834
835 #[test]
836 fn the_cross_is_visible_before_anyone_hovers_it() {
837 let theme = Theme::default();
841 let cross = |chip: Chip<Msg>| {
842 primitives(chip)
843 .iter()
844 .rev()
845 .find_map(|p| match p {
846 Primitive::Path { fill, .. } => *fill,
847 _ => None,
848 })
849 .expect("the cross is painted")
850 };
851 assert_eq!(
852 cross(Chip::new("Ada").on_remove(Msg::Remove)),
853 theme.scheme.on_surface_variant
854 );
855 assert_eq!(
856 cross(Chip::new("Ada").on_remove(Msg::Remove).selected(true)),
857 theme.scheme.on_secondary_container,
858 "a builder after `on_remove` still reaches the cross"
859 );
860 }
861
862 #[test]
863 fn an_inert_chip_neither_splashes_nor_takes_focus() {
864 let plain = Chip::<Msg>::new("Draft");
866 assert!(Widget::<Msg>::ink(&plain, &Theme::default()).is_none());
867 assert!(!Widget::<Msg>::focusable(&plain));
868 let pressable = Chip::new("Draft").on_press(Msg::Press);
869 assert!(Widget::<Msg>::ink(&pressable, &Theme::default()).is_some());
870 assert!(Widget::<Msg>::focusable(&pressable));
871 }
872
873 #[test]
874 fn a_filter_chip_announces_whether_it_is_on() {
875 let on = Chip::new("Unread").selected(true).on_press(Msg::Press);
876 let semantics = Widget::<Msg>::semantics(&on).expect("semantics");
877 assert_eq!(semantics.label.as_deref(), Some("Unread"));
878 assert_eq!(semantics.toggled, frus_core::Toggled::True);
879 let off = Chip::new("Unread").on_press(Msg::Press);
880 assert_eq!(
881 Widget::<Msg>::semantics(&off).unwrap().toggled,
882 frus_core::Toggled::False
883 );
884 }
885
886 #[test]
887 fn every_measurement_is_the_callers_and_then_the_themes() {
888 let mut theme = Theme::default();
889 theme.widgets.chip.height = Some(40.0);
890 let height =
891 |chip: Chip<Msg>, theme: &Theme| match Widget::<Msg>::style_themed(&chip, theme).height
892 {
893 Dimension::Length(h) => h,
894 other => panic!("{other:?}"),
895 };
896 assert_eq!(
897 height(Chip::new("x"), &Theme::default()),
898 CHIP_HEIGHT,
899 "the framework's"
900 );
901 assert_eq!(height(Chip::new("x"), &theme), 40.0, "the theme's");
902 assert_eq!(
903 height(Chip::new("x").height(24.0), &theme),
904 24.0,
905 "the caller's, over the theme's"
906 );
907 }
908}