cranpose_ui/modifier/toggleable.rs
1use std::rc::Rc;
2
3use cranpose_foundation::{SemanticsWidgetRole, ToggleableState};
4
5use super::{Modifier, SemanticsConfiguration, inspector_metadata};
6
7impl Modifier {
8 /// Make the component a two-state control.
9 ///
10 /// This is Compose's `Modifier.toggleable(value, enabled, role,
11 /// onValueChange)`: a click hands the callback the **new** value, so a
12 /// caller writes `.toggleable(checked, None, None, move |next|
13 /// state.set(next))` and never has to read the old one back out of its own
14 /// state to invert it.
15 ///
16 /// `role` is what a screen reader announces the control **as** — "switch",
17 /// "checkbox" — before it is acted on, and it is genuinely optional: a
18 /// toggleable row could be either, so Compose's parameter defaults to
19 /// `null` and so does passing `None` here. Wear's own `SwitchButton` leaves
20 /// it unset on the row and puts `Role.Switch` on the `Switch` control
21 /// inside, which merges up into the same node; naming it on the row reaches
22 /// the same announcement without leaning on a merge rule.
23 ///
24 /// A toggleable control with no role is not silent — the description and
25 /// the state below still speak — but it is announced as an unnamed
26 /// something the reader cannot say is toggleable, which is the whole of the
27 /// difference.
28 ///
29 /// The **state** it publishes regardless: `toggled`, Compose's
30 /// `toggleableState`, so a reader landing on the row says whether it is on
31 /// without the caller spelling it into the description. `description` stays
32 /// because a row still needs a name, and a caller that wants the state
33 /// spoken a particular way ("Haptics, on") can still say it there.
34 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 self.tri_state_toggleable(value.into(), description, role, move || {
42 on_value_change(!value);
43 })
44 }
45
46 /// Makes the component a control that is on, off or partly on, such as
47 /// a check box over a list whose items are partly checked. A click calls
48 /// `on_click`, which decides the next state.
49 ///
50 /// This is Compose's `Modifier.triStateToggleable(state, enabled, role,
51 /// onClick)`. Readers hear the state: a partly checked box reads as
52 /// "mixed" or "partially checked".
53 pub fn tri_state_toggleable(
54 self,
55 state: ToggleableState,
56 description: Option<String>,
57 role: Option<SemanticsWidgetRole>,
58 on_click: impl Fn() + 'static,
59 ) -> Self {
60 let on_click = Rc::new(on_click);
61 let modifier = Modifier::empty()
62 .clickable(move |_point| on_click())
63 .with_inspector_metadata(inspector_metadata("toggleable", move |info| {
64 info.add_property("state", format!("{state:?}"));
65 info.add_property("onClick", "provided");
66 }))
67 .then(Modifier::empty().stable_semantics(
68 move |config: &mut SemanticsConfiguration| {
69 config.is_clickable = true;
70 config.toggled = Some(state);
71 if let Some(description) = &description {
72 config.content_description = Some(description.clone());
73 }
74 if let Some(role) = role {
75 config.role = Some(role);
76 }
77 },
78 ));
79 self.then(modifier)
80 }
81}
82
83#[cfg(test)]
84#[path = "tests/toggleable_tests.rs"]
85mod tests;