1use frus_core::{
5 BorderSide, Color, Path, Point, Rect, ResolvedTextStyle, Scene, Stroke, TextStyle,
6};
7use frus_layout::{Dimension, Style};
8
9use crate::disabled::{disabled_content, disabled_mark};
10use crate::interaction::Status;
11use crate::theme::{TapTarget, Theme};
12use crate::widget::Widget;
13use crate::widgetstate::{WidgetState, WidgetStateProperty, WidgetStates};
14
15const EDGE: f32 = 18.0;
17const STROKE: f32 = 2.0;
19const RADIUS: f32 = 2.0;
21const TOGGLE_SECONDS: f32 = 0.2;
23
24pub struct Checkbox<Msg = crate::callback::Callback> {
36 value: Option<bool>,
38 tristate: bool,
40 label: Option<String>,
41 size: f32,
42 enabled: bool,
43 error: bool,
44 semantic_label: Option<String>,
45 fill_color: Option<Color>,
46 check_color: Option<Color>,
47 border_color: Option<Color>,
48 active_border_color: Option<Color>,
49 overlay_color: Option<WidgetStateProperty<Color>>,
50 splash_radius: Option<f32>,
51 tap_target: Option<TapTarget>,
52 visual_density: Option<crate::VisualDensity>,
53 radius: Option<f32>,
54 label_color: Option<Color>,
55 on_toggle: Option<Box<dyn Fn(bool) -> Msg>>,
56 on_change: Option<Box<dyn Fn(Option<bool>) -> Msg>>,
57}
58
59impl<Msg> Checkbox<Msg> {
60 pub fn new(checked: bool) -> Self {
62 Self {
63 value: Some(checked),
64 tristate: false,
65 label: None,
66 size: 16.0,
67 enabled: true,
68 error: false,
69 semantic_label: None,
70 fill_color: None,
71 check_color: None,
72 border_color: None,
73 active_border_color: None,
74 overlay_color: None,
75 splash_radius: None,
76 tap_target: None,
77 visual_density: None,
78 radius: None,
79 label_color: None,
80 on_toggle: None,
81 on_change: None,
82 }
83 }
84
85 pub fn maybe(value: Option<bool>) -> Self {
108 let mut checkbox = Self::new(false);
109 checkbox.value = value;
110 checkbox.tristate = true;
111 checkbox
112 }
113
114 pub fn fill_color(mut self, color: Color) -> Self {
116 self.fill_color = Some(color);
117 self
118 }
119
120 pub fn check_color(mut self, color: Color) -> Self {
122 self.check_color = Some(color);
123 self
124 }
125
126 pub fn border_color(mut self, color: Color) -> Self {
132 self.border_color = Some(color);
133 self
134 }
135
136 pub fn active_border_color(mut self, color: Color) -> Self {
138 self.active_border_color = Some(color);
139 self
140 }
141
142 #[must_use]
146 pub fn overlay_color(mut self, overlay: WidgetStateProperty<Color>) -> Self {
147 self.overlay_color = Some(overlay);
148 self
149 }
150
151 #[must_use]
153 pub fn splash_radius(mut self, radius: f32) -> Self {
154 self.splash_radius = Some(radius);
155 self
156 }
157
158 #[must_use]
162 pub fn visual_density(mut self, density: crate::VisualDensity) -> Self {
163 self.visual_density = Some(density);
164 self
165 }
166
167 pub fn radius(mut self, radius: f32) -> Self {
169 self.radius = Some(radius);
170 self
171 }
172
173 pub fn label_color(mut self, color: Color) -> Self {
175 self.label_color = Some(color);
176 self
177 }
178
179 #[must_use]
182 pub fn error(mut self, error: bool) -> Self {
183 self.error = error;
184 self
185 }
186
187 #[must_use]
189 pub fn semantic_label(mut self, label: impl Into<String>) -> Self {
190 self.semantic_label = Some(label.into());
191 self
192 }
193
194 fn corner(&self, theme: &Theme) -> f32 {
196 self.radius
197 .or(theme.widgets.checkbox.radius)
198 .unwrap_or(RADIUS)
199 }
200
201 pub fn label(mut self, label: impl Into<String>) -> Self {
203 self.label = Some(label.into());
204 self
205 }
206
207 pub fn enabled(mut self, enabled: bool) -> Self {
213 self.enabled = enabled;
214 self
215 }
216
217 pub fn on_toggle<R: crate::callback::IntoMsg<Msg>>(
219 mut self,
220 on_toggle: impl Fn(bool) -> R + 'static,
221 ) -> Self {
222 self.on_toggle = Some(Box::new(crate::callback::handler1(on_toggle)));
223 self
224 }
225
226 pub fn on_change<R: crate::callback::IntoMsg<Msg>>(
230 mut self,
231 on_change: impl Fn(Option<bool>) -> R + 'static,
232 ) -> Self {
233 self.on_change = Some(Box::new(crate::callback::handler1(on_change)));
234 self
235 }
236
237 fn next(&self) -> Option<bool> {
239 if self.tristate {
240 match self.value {
241 Some(false) => Some(true),
242 Some(true) => None,
243 None => Some(false),
244 }
245 } else {
246 Some(self.value != Some(true))
247 }
248 }
249
250 pub fn tap_target(mut self, target: TapTarget) -> Self {
254 self.tap_target = Some(target);
255 self
256 }
257
258 fn label_style(&self) -> ResolvedTextStyle {
261 TextStyle::new(self.size).resolved()
262 }
263
264 fn square(&self, theme: &Theme) -> f32 {
267 let target = self
268 .tap_target
269 .or(theme.widgets.checkbox.tap_target)
270 .unwrap_or(theme.tap_target);
271 let side = match target {
272 TapTarget::Padded => crate::MIN_TAP_TARGET,
273 TapTarget::ShrinkWrap => crate::MIN_TAP_TARGET - 8.0,
274 };
275 let density = self
276 .visual_density
277 .or(theme.widgets.checkbox.visual_density)
278 .unwrap_or(crate::VisualDensity::STANDARD);
279 (side + density.base_size_adjustment().1).max(EDGE)
280 }
281
282 fn label_width(&self) -> f32 {
283 match &self.label {
284 Some(text) => frus_text::measure(text, self.size).width,
285 None => 0.0,
286 }
287 }
288
289 fn states(&self, status: &Status) -> WidgetStates {
291 let states = if self.enabled {
292 status.states()
293 } else {
294 WidgetStates::of(WidgetState::Disabled)
295 };
296 states
297 .set(WidgetState::Selected, self.value != Some(false))
298 .set(WidgetState::Error, self.error)
299 }
300
301 fn fill(&self, theme: &Theme, states: WidgetStates) -> Color {
303 let t = &theme.widgets.checkbox;
304 if states.contains(WidgetState::Disabled) {
305 return if states.contains(WidgetState::Selected) {
306 disabled_content(theme)
307 } else {
308 Color::TRANSPARENT
309 };
310 }
311 if !states.contains(WidgetState::Selected) {
312 return Color::TRANSPARENT;
313 }
314 if states.contains(WidgetState::Error) {
315 return theme.scheme.error;
316 }
317 self.fill_color
318 .or(t.fill_color)
319 .unwrap_or(theme.scheme.primary)
320 }
321
322 fn check(&self, theme: &Theme, states: WidgetStates) -> Color {
324 if states.contains(WidgetState::Disabled) {
325 return disabled_mark(theme);
326 }
327 if states.contains(WidgetState::Error) {
328 return theme.scheme.on_error;
329 }
330 self.check_color
331 .or(theme.widgets.checkbox.check_color)
332 .unwrap_or(theme.scheme.on_primary)
333 }
334
335 fn side(&self, theme: &Theme, states: WidgetStates) -> BorderSide {
339 let t = &theme.widgets.checkbox;
340 if states.contains(WidgetState::Selected) {
341 return BorderSide::new(Color::TRANSPARENT, 0.0);
342 }
343 let color = if states.contains(WidgetState::Disabled) {
344 disabled_content(theme)
345 } else if states.contains(WidgetState::Error) {
346 theme.scheme.error
347 } else {
348 let resting = self.border_color.or(t.border_color);
349 let active = states.contains(WidgetState::Pressed)
350 || states.contains(WidgetState::Hovered)
351 || states.contains(WidgetState::Focused);
352 if active {
353 self.active_border_color
354 .or(t.active_border_color)
355 .or(resting)
356 .unwrap_or(theme.scheme.on_surface)
357 } else {
358 resting.unwrap_or(theme.scheme.on_surface_variant)
359 }
360 };
361 BorderSide::new(color, STROKE)
362 }
363
364 fn overlay(&self, theme: &Theme, states: WidgetStates) -> Color {
368 if let Some(c) = self
369 .overlay_color
370 .as_ref()
371 .and_then(|p| p.resolve(states))
372 .or_else(|| {
373 theme
374 .widgets
375 .checkbox
376 .overlay_color
377 .as_ref()
378 .and_then(|p| p.resolve(states))
379 })
380 {
381 return *c;
382 }
383 if states.contains(WidgetState::Error) {
384 let c = theme.scheme.error;
385 if states.contains(WidgetState::Pressed) {
387 return c.with_alpha(0.1);
388 }
389 if states.contains(WidgetState::Hovered) {
390 return c.with_alpha(0.08);
391 }
392 if states.contains(WidgetState::Focused) {
393 return c.with_alpha(0.1);
394 }
395 }
396 crate::toggleable::overlay(theme, states)
397 }
398}
399
400fn outer_at(origin: Point, t: f32) -> Rect {
403 let inset = 1.0 - (t - 0.5).abs() * 2.0;
404 let size = EDGE - inset * STROKE;
405 Rect::new(origin.x + inset, origin.y + inset, size, size)
406}
407
408fn check_path(origin: Point, t: f32) -> Path {
411 let at = |x: f32, y: f32| Point::new(origin.x + EDGE * x, origin.y + EDGE * y);
412 let (start, mid, end) = (at(0.15, 0.45), at(0.4, 0.7), at(0.85, 0.25));
413 let lerp =
414 |a: Point, b: Point, k: f32| Point::new(a.x + (b.x - a.x) * k, a.y + (b.y - a.y) * k);
415 if t < 0.5 {
416 Path::new()
417 .move_to(start)
418 .line_to(lerp(start, mid, t * 2.0))
419 } else {
420 Path::new()
421 .move_to(start)
422 .line_to(mid)
423 .line_to(lerp(mid, end, (t - 0.5) * 2.0))
424 }
425}
426
427fn dash_path(origin: Point, t: f32) -> Path {
429 let y = origin.y + EDGE * 0.5;
430 let half = EDGE * 0.3 * t;
431 let mid = origin.x + EDGE * 0.5;
432 Path::new()
433 .move_to(Point::new(mid - half, y))
434 .line_to(Point::new(mid + half, y))
435}
436
437impl<Msg> Widget<Msg> for Checkbox<Msg> {
438 fn style(&self) -> Style {
439 Widget::<Msg>::style_themed(self, &Theme::default())
440 }
441
442 fn style_themed(&self, theme: &Theme) -> Style {
445 let square = self.square(theme);
446 let line = self.label_style().line_height();
447 Style {
448 width: Dimension::Length((square + self.label_width()).ceil()),
449 height: Dimension::Length(line.max(square).ceil()),
450 ..Default::default()
451 }
452 }
453
454 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
455 &[]
456 }
457
458 fn paint(&self, bounds: Rect, status: Status, theme: &Theme, scene: &mut Scene) {
459 let o = status.opacity;
460 let square = self.square(theme);
461 let centre = Point::new(bounds.x + square * 0.5, bounds.y + bounds.height * 0.5);
462 let states = self.states(&status);
463
464 if self.enabled {
467 let radius = self
468 .splash_radius
469 .or(theme.widgets.checkbox.splash_radius)
470 .unwrap_or(crate::toggleable::SPLASH);
471 crate::toggleable::paint_halos(scene, centre, radius, &status, |state| {
472 self.overlay(theme, states.set(state, true))
473 });
474 }
475
476 let t = status.value.clamp(0.0, 1.0);
478 let origin = Point::new(centre.x - EDGE * 0.5, centre.y - EDGE * 0.5);
479 let off = states.set(WidgetState::Selected, false);
480 let on = states.set(WidgetState::Selected, true);
481 let fill = if t >= 0.25 {
482 self.fill(theme, on)
483 } else {
484 self.fill(theme, off).lerp(self.fill(theme, on), t * 4.0)
485 };
486 let outer = outer_at(origin, t);
487 let corner = self.corner(theme);
488 let side = if t <= 0.5 {
489 let (a, b) = (self.side(theme, off), self.side(theme, on));
490 BorderSide::new(a.color.lerp(b.color, t), a.width + (b.width - a.width) * t)
491 } else {
492 self.side(theme, on)
493 };
494 scene.draw_rect(outer, fill.fade(o), corner, side.width, side.color.fade(o));
495 if t > 0.5 {
496 let k = (t - 0.5) * 2.0;
497 let path = match self.value {
498 None => dash_path(origin, k),
499 _ => check_path(origin, k),
500 };
501 scene.paint_path(
502 &path,
503 None,
504 Some(Stroke::new(self.check(theme, on).fade(o), STROKE)),
505 );
506 }
507
508 if let Some(label) = &self.label {
509 let color = if self.enabled {
510 self.label_color
511 .or(theme.widgets.checkbox.label_color)
512 .unwrap_or(theme.on_surface)
513 } else {
514 disabled_content(theme)
515 };
516 let style = self.label_style();
517 scene.text(
518 Point::new(
519 bounds.x + square,
520 bounds.y + (bounds.height - style.line_height()) * 0.5,
521 ),
522 label.clone(),
523 &style,
524 color.fade(o),
525 );
526 }
527 }
528
529 fn on_click(&self) -> Option<Msg> {
530 if !self.enabled {
531 return None;
532 }
533 let next = self.next();
534 if let Some(make) = self.on_change.as_ref() {
535 return Some(make(next));
536 }
537 self.on_toggle
541 .as_ref()
542 .map(|make| make(next.unwrap_or(true)))
543 }
544
545 fn focusable(&self) -> bool {
546 self.enabled
547 }
548
549 fn anim_target(&self) -> Option<f32> {
551 Some(if self.value == Some(false) { 0.0 } else { 1.0 })
552 }
553
554 fn anim_duration(&self) -> f32 {
555 TOGGLE_SECONDS
556 }
557
558 fn semantics(&self) -> Option<frus_core::SemanticsProperties> {
559 let mut s = frus_core::SemanticsProperties::new(frus_core::Role::CheckBox)
562 .maybe_toggled(self.value);
563 s = if self.enabled {
564 s.clickable()
565 } else {
566 s.disabled(true)
567 };
568 if let Some(label) = self.label.as_ref().or(self.semantic_label.as_ref()) {
569 s = s.label(label.clone());
570 }
571 Some(s)
572 }
573}
574
575#[cfg(test)]
576mod tests {
577 use super::*;
578 use frus_core::{PathVerb, Primitive};
579
580 #[derive(Clone, Debug, PartialEq)]
581 enum Msg {
582 Set(bool),
583 Maybe(Option<bool>),
584 }
585
586 fn settled(value: bool) -> Status {
588 Status {
589 opacity: 1.0,
590 value: if value { 1.0 } else { 0.0 },
591 ..Default::default()
592 }
593 }
594
595 fn painted(c: &Checkbox<Msg>, status: Status, theme: &Theme) -> Vec<Primitive> {
596 let side = c.square(theme);
597 let mut scene = Scene::new();
598 Widget::<Msg>::paint(
599 c,
600 Rect::new(0.0, 0.0, side, side),
601 status,
602 theme,
603 &mut scene,
604 );
605 scene.primitives().to_vec()
606 }
607
608 fn the_box(prims: &[Primitive]) -> (Rect, Color, f32, Color, f32) {
610 prims
611 .iter()
612 .rev()
613 .find_map(|p| match p {
614 Primitive::Rect {
615 rect,
616 color,
617 border_width,
618 border_color,
619 radius,
620 ..
621 } => Some((*rect, *color, *border_width, *border_color, radius.top_left)),
622 _ => None,
623 })
624 .expect("a box")
625 }
626
627 fn mark(prims: &[Primitive]) -> Option<(Vec<Point>, Color)> {
628 prims.iter().find_map(|p| match p {
629 Primitive::Path {
630 path,
631 stroke: Some(s),
632 ..
633 } => Some((
634 path.verbs()
635 .iter()
636 .filter_map(|v| match v {
637 PathVerb::MoveTo(p) | PathVerb::LineTo(p) => Some(*p),
638 _ => None,
639 })
640 .collect(),
641 s.color,
642 )),
643 _ => None,
644 })
645 }
646
647 #[test]
650 fn the_box_is_the_reference_s() {
651 let theme = Theme::dark().with_platform(frus_core::TargetPlatform::Windows);
652 let c = Checkbox::<Msg>::new(false);
653 let style = Widget::<Msg>::style_themed(&c, &theme);
654 assert_eq!(
655 (style.width, style.height),
656 (Dimension::Length(48.0), Dimension::Length(48.0)),
657 "standard density even on a desktop"
658 );
659 let (rect, fill, border, outline, corner) = the_box(&painted(&c, settled(false), &theme));
660 assert_eq!(rect, Rect::new(15.0, 15.0, 18.0, 18.0));
661 assert_eq!((fill, border, corner), (Color::TRANSPARENT, 2.0, 2.0));
662 assert_eq!(outline, theme.scheme.on_surface_variant);
663 let compact =
665 c_style(Checkbox::<Msg>::new(false).visual_density(crate::VisualDensity::COMPACT));
666 assert_eq!(compact, 40.0);
667 let shrunk = c_style(Checkbox::<Msg>::new(false).tap_target(TapTarget::ShrinkWrap));
668 assert_eq!(shrunk, 40.0);
669 }
670
671 fn c_style(c: Checkbox<Msg>) -> f32 {
672 match Widget::<Msg>::style_themed(&c, &Theme::dark()).height {
673 Dimension::Length(v) => v,
674 _ => unreachable!(),
675 }
676 }
677
678 #[test]
681 fn the_tick_is_drawn_through_the_reference_s_points() {
682 let theme = Theme::dark();
683 let prims = painted(&Checkbox::<Msg>::new(true), settled(true), &theme);
684 let (rect, fill, border, _, _) = the_box(&prims);
685 assert_eq!(
686 (rect, fill, border),
687 (Rect::new(15.0, 15.0, 18.0, 18.0), theme.scheme.primary, 0.0)
688 );
689 let (points, colour) = mark(&prims).expect("a tick");
690 assert_eq!(colour, theme.scheme.on_primary);
691 let at = |x: f32, y: f32| Point::new(15.0 + 18.0 * x, 15.0 + 18.0 * y);
692 assert_eq!(points, vec![at(0.15, 0.45), at(0.4, 0.7), at(0.85, 0.25)]);
693 let (dash, _) = mark(&painted(
694 &Checkbox::<Msg>::maybe(None),
695 settled(true),
696 &theme,
697 ))
698 .unwrap();
699 let near = |a: Point, b: Point| (a.x - b.x).abs() < 1e-3 && (a.y - b.y).abs() < 1e-3;
700 assert!(
701 near(dash[0], at(0.2, 0.5)) && near(dash[1], at(0.8, 0.5)),
702 "{dash:?}"
703 );
704 }
705
706 #[test]
709 fn ticking_animates_the_box_and_the_tick() {
710 let theme = Theme::dark();
711 let c = Checkbox::<Msg>::new(true);
712 assert_eq!(Widget::<Msg>::anim_target(&c), Some(1.0));
713 assert_eq!(
714 Widget::<Msg>::anim_target(&Checkbox::<Msg>::new(false)),
715 Some(0.0)
716 );
717 assert_eq!(
719 Widget::<Msg>::anim_target(&Checkbox::<Msg>::maybe(None)),
720 Some(1.0)
721 );
722 assert_eq!(Widget::<Msg>::anim_duration(&c), 0.2);
723 let half = painted(
724 &c,
725 Status {
726 value: 0.5,
727 ..settled(true)
728 },
729 &theme,
730 );
731 assert_eq!(the_box(&half).0, Rect::new(16.0, 16.0, 16.0, 16.0));
732 assert!(mark(&half).is_none(), "no tick yet");
733 let three = painted(
734 &c,
735 Status {
736 value: 0.75,
737 ..settled(true)
738 },
739 &theme,
740 );
741 let (points, _) = mark(&three).unwrap();
742 let at = |x: f32, y: f32| Point::new(15.0 + 18.0 * x, 15.0 + 18.0 * y);
743 assert_eq!(points, vec![at(0.15, 0.45), at(0.4, 0.7), at(0.4, 0.7)]);
746 }
747
748 #[test]
751 fn the_halo_is_the_reference_s() {
752 let theme = Theme::dark();
753 let hovered = |value: bool| Status {
754 hover_progress: 1.0,
755 interaction: crate::interaction::Interaction::Hovered,
756 ..settled(value)
757 };
758 let prims = painted(&Checkbox::<Msg>::new(false), hovered(false), &theme);
759 let halo = prims.iter().find_map(|p| match p {
760 Primitive::Rect { rect, color, .. } if rect.width == 40.0 => Some(*color),
761 _ => None,
762 });
763 assert_eq!(halo, Some(theme.scheme.on_surface.with_alpha(0.08)));
764 let prims = painted(&Checkbox::<Msg>::new(true), hovered(true), &theme);
765 let halo = prims.iter().find_map(|p| match p {
766 Primitive::Rect { rect, color, .. } if rect.width == 40.0 => Some(*color),
767 _ => None,
768 });
769 assert_eq!(halo, Some(theme.scheme.primary.with_alpha(0.08)));
770 let (_, _, _, outline, _) = the_box(&painted(
772 &Checkbox::<Msg>::new(false),
773 hovered(false),
774 &theme,
775 ));
776 assert_eq!(outline, theme.scheme.on_surface);
777 }
778
779 #[test]
781 fn an_error_takes_the_error_colours() {
782 let theme = Theme::dark();
783 let (_, _, _, outline, _) = the_box(&painted(
784 &Checkbox::<Msg>::new(false).error(true),
785 settled(false),
786 &theme,
787 ));
788 assert_eq!(outline, theme.scheme.error);
789 let prims = painted(
790 &Checkbox::<Msg>::new(true).error(true),
791 settled(true),
792 &theme,
793 );
794 assert_eq!(the_box(&prims).1, theme.scheme.error);
795 assert_eq!(mark(&prims).unwrap().1, theme.scheme.on_error);
796 }
797
798 #[test]
801 fn a_disabled_box_is_inert_but_still_says_whether_it_is_ticked() {
802 let theme = Theme::dark();
803 let c = Checkbox::<Msg>::new(true)
804 .enabled(false)
805 .on_toggle(Msg::Set);
806 assert_eq!(Widget::<Msg>::on_click(&c), None);
807 assert!(!Widget::<Msg>::focusable(&c));
808 let prims = painted(&c, settled(true), &theme);
809 assert_eq!(the_box(&prims).1, disabled_content(&theme));
810 assert_eq!(mark(&prims).unwrap().1, disabled_mark(&theme));
811 }
812
813 #[test]
814 fn click_toggles() {
815 let c = Checkbox::<Msg>::new(false).on_toggle(Msg::Set);
816 assert_eq!(Widget::<Msg>::on_click(&c), Some(Msg::Set(true)));
817 let c = Checkbox::<Msg>::new(true).on_toggle(Msg::Set);
818 assert_eq!(Widget::<Msg>::on_click(&c), Some(Msg::Set(false)));
819 }
820
821 #[test]
822 fn a_tristate_box_cycles_through_the_third_answer() {
823 let next = |v: Option<bool>| {
824 Widget::<Msg>::on_click(&Checkbox::<Msg>::maybe(v).on_change(Msg::Maybe))
825 };
826 assert_eq!(next(Some(false)), Some(Msg::Maybe(Some(true))));
827 assert_eq!(next(Some(true)), Some(Msg::Maybe(None)));
828 assert_eq!(next(None), Some(Msg::Maybe(Some(false))));
829 let old = Checkbox::<Msg>::maybe(Some(true)).on_toggle(Msg::Set);
831 assert_eq!(Widget::<Msg>::on_click(&old), Some(Msg::Set(true)));
832 }
833
834 #[test]
835 fn partly_ticked_is_announced_as_mixed() {
836 let s = Widget::<Msg>::semantics(&Checkbox::<Msg>::maybe(None)).unwrap();
837 assert_eq!(s.toggled, frus_core::Toggled::Mixed);
838 let named =
839 Widget::<Msg>::semantics(&Checkbox::<Msg>::new(true).semantic_label("Agree")).unwrap();
840 assert_eq!(named.label.as_deref(), Some("Agree"));
841 }
842
843 #[test]
845 fn the_theme_answers_and_the_instance_overrules_it() {
846 let mut theme = Theme::dark();
847 let green = Color::rgb8(0, 160, 80);
848 let blue = Color::rgb8(0, 0, 200);
849 theme.widgets.checkbox.fill_color = Some(green);
850 theme.widgets.checkbox.radius = Some(4.0);
851 let themed = the_box(&painted(&Checkbox::<Msg>::new(true), settled(true), &theme));
852 assert_eq!((themed.1, themed.4), (green, 4.0));
853 let mine = the_box(&painted(
854 &Checkbox::<Msg>::new(true).fill_color(blue).radius(1.0),
855 settled(true),
856 &theme,
857 ));
858 assert_eq!((mine.1, mine.4), (blue, 1.0));
859 let one = Checkbox::<Msg>::new(false).border_color(blue);
861 let hovered = Status {
862 interaction: crate::interaction::Interaction::Hovered,
863 ..settled(false)
864 };
865 assert_eq!(the_box(&painted(&one, hovered, &theme)).3, blue);
866 }
867
868 #[test]
870 fn the_label_follows_the_square() {
871 let theme = Theme::dark();
872 let c = Checkbox::<Msg>::new(false)
873 .label("Remember me")
874 .label_color(Color::rgb8(9, 9, 9));
875 let prims = painted(&c, settled(false), &theme);
876 let at = prims.iter().find_map(|p| match p {
877 Primitive::Text {
878 position, color, ..
879 } => Some((*position, *color)),
880 _ => None,
881 });
882 let (position, colour) = at.expect("a label");
883 assert_eq!(position.x, 48.0);
884 assert_eq!(colour, Color::rgb8(9, 9, 9));
885 }
886}