cranpose_ui/modifier/
toggleable.rs1use std::rc::Rc;
2
3use cranpose_foundation::SemanticsWidgetRole;
4
5use super::{Modifier, SemanticsConfiguration, inspector_metadata};
6
7impl Modifier {
8 pub fn toggleable(
35 self,
36 value: bool,
37 description: Option<String>,
38 role: Option<SemanticsWidgetRole>,
39 on_value_change: impl Fn(bool) + 'static,
40 ) -> Self {
41 let on_value_change = Rc::new(on_value_change);
42 let toggled = value;
43 let modifier = Modifier::empty()
44 .clickable(move |_point| on_value_change(!toggled))
45 .with_inspector_metadata(inspector_metadata("toggleable", move |info| {
46 info.add_property("value", if toggled { "true" } else { "false" });
47 info.add_property("onValueChange", "provided");
48 }))
49 .then(
50 Modifier::empty().semantics(move |config: &mut SemanticsConfiguration| {
51 config.is_clickable = true;
52 config.toggled = Some(toggled);
53 if let Some(description) = &description {
54 config.content_description = Some(description.clone());
55 }
56 if let Some(role) = role {
57 config.role = Some(role);
58 }
59 }),
60 );
61 self.then(modifier)
62 }
63}
64
65#[cfg(test)]
66mod tests {
67 use std::cell::Cell;
68
69 use cranpose_foundation::{PointerButton, PointerButtons, PointerEvent, PointerEventKind};
70
71 use super::*;
72 use crate::modifier::{Point, collect_semantics_from_modifier, collect_slices_from_modifier};
73
74 fn tap(modifier: &Modifier) {
75 let slices = collect_slices_from_modifier(modifier);
76 let handlers = slices.pointer_inputs();
77 assert_eq!(handlers.len(), 1, "toggleable takes pointer input once");
78 let at = Point { x: 4.0, y: 4.0 };
79 for kind in [PointerEventKind::Down, PointerEventKind::Up] {
80 let mut event = PointerEvent::new(kind, at, at);
81 event.buttons = PointerButtons::new().with(PointerButton::Primary);
82 handlers[0](event);
83 }
84 }
85
86 #[test]
87 fn a_click_reports_the_new_value_not_the_old_one() {
88 let _app_context = crate::render_state::app_context_test_scope();
89 for start in [false, true] {
90 let seen: Rc<Cell<Option<bool>>> = Rc::new(Cell::new(None));
91 let sink = seen.clone();
92 let modifier =
93 Modifier::empty().toggleable(start, None, None, move |next| sink.set(Some(next)));
94 tap(&modifier);
95 assert_eq!(
96 seen.get(),
97 Some(!start),
98 "a toggle hands over the value it is moving to"
99 );
100 }
101 }
102
103 #[test]
104 fn a_toggleable_row_reads_as_clickable_and_carries_its_description() {
105 let modifier =
106 Modifier::empty().toggleable(true, Some("Haptics, on".to_string()), None, |_| {});
107 let semantics = collect_semantics_from_modifier(&modifier)
108 .expect("a toggleable row publishes semantics");
109 assert!(semantics.is_clickable);
110 assert_eq!(
111 semantics.content_description.as_deref(),
112 Some("Haptics, on")
113 );
114 assert_eq!(semantics.toggled, Some(true));
115 assert_eq!(semantics.role, None);
116 }
117
118 #[test]
119 fn a_role_reaches_the_semantics_so_a_reader_can_say_what_the_control_is() {
120 let modifier = Modifier::empty().toggleable(
121 false,
122 Some("Haptics, off".to_string()),
123 Some(SemanticsWidgetRole::Switch),
124 |_| {},
125 );
126 let semantics = collect_semantics_from_modifier(&modifier).expect("semantics");
127 assert_eq!(semantics.role, Some(SemanticsWidgetRole::Switch));
128 assert!(semantics.is_clickable);
129 assert_eq!(semantics.toggled, Some(false));
130 assert_eq!(
131 semantics.content_description.as_deref(),
132 Some("Haptics, off")
133 );
134 }
135}