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) {
77 let slices = collect_slices_from_modifier(modifier);
78 let handlers = slices.pointer_inputs();
79 assert_eq!(handlers.len(), 1, "toggleable takes pointer input once");
80 let at = Point { x: 4.0, y: 4.0 };
81 for kind in [PointerEventKind::Down, PointerEventKind::Up] {
82 let mut event = PointerEvent::new(kind, at, at);
83 event.buttons = PointerButtons::new().with(PointerButton::Primary);
84 handlers[0](event);
85 }
86 }
87
88 #[test]
89 fn a_click_reports_the_new_value_not_the_old_one() {
90 let _app_context = crate::render_state::app_context_test_scope();
91 for start in [false, true] {
92 let seen: Rc<Cell<Option<bool>>> = Rc::new(Cell::new(None));
93 let sink = seen.clone();
94 let modifier =
95 Modifier::empty().toggleable(start, None, None, move |next| sink.set(Some(next)));
96 tap(&modifier);
97 assert_eq!(
98 seen.get(),
99 Some(!start),
100 "a toggle hands over the value it is moving to"
101 );
102 }
103 }
104
105 #[test]
106 fn a_toggleable_row_reads_as_clickable_and_carries_its_description() {
107 let modifier =
108 Modifier::empty().toggleable(true, Some("Haptics, on".to_string()), None, |_| {});
109 let semantics = collect_semantics_from_modifier(&modifier)
110 .expect("a toggleable row publishes semantics");
111 assert!(semantics.is_clickable);
112 assert_eq!(
113 semantics.content_description.as_deref(),
114 Some("Haptics, on")
115 );
116 assert_eq!(semantics.toggled, Some(true));
119 assert_eq!(semantics.role, None);
122 }
123
124 #[test]
125 fn a_role_reaches_the_semantics_so_a_reader_can_say_what_the_control_is() {
126 let modifier = Modifier::empty().toggleable(
127 false,
128 Some("Haptics, off".to_string()),
129 Some(SemanticsWidgetRole::Switch),
130 |_| {},
131 );
132 let semantics = collect_semantics_from_modifier(&modifier).expect("semantics");
133 assert_eq!(semantics.role, Some(SemanticsWidgetRole::Switch));
134 assert!(semantics.is_clickable);
136 assert_eq!(semantics.toggled, Some(false));
137 assert_eq!(
138 semantics.content_description.as_deref(),
139 Some("Haptics, off")
140 );
141 }
142}