Skip to main content

fret_ui_kit/primitives/
switch.rs

1//! Switch primitives (Radix-aligned outcomes).
2//!
3//! Upstream reference:
4//! - `repo-ref/primitives/packages/react/switch/src/switch.tsx`
5//!
6//! In Radix, `Switch` is a button-like control with `role="switch"` and a boolean checked state.
7//! In Fret, this maps onto [`fret_core::SemanticsRole::Switch`] and `checked: Some(bool)`.
8
9use std::sync::Arc;
10
11use fret_runtime::Model;
12use fret_ui::element::PressableA11y;
13use fret_ui::{ElementContext, UiHost};
14
15/// A11y metadata for a Radix-style switch pressable.
16pub fn switch_a11y(label: Option<Arc<str>>, checked: bool) -> PressableA11y {
17    PressableA11y {
18        role: Some(fret_core::SemanticsRole::Switch),
19        label,
20        checked: Some(checked),
21        ..Default::default()
22    }
23}
24
25/// Returns a checked-state model that behaves like Radix `useControllableState` (`checked` /
26/// `defaultChecked`).
27pub fn switch_use_checked_model<H: UiHost>(
28    cx: &mut ElementContext<'_, H>,
29    controlled: Option<Model<bool>>,
30    default_checked: impl FnOnce() -> bool,
31) -> crate::primitives::controllable_state::ControllableModel<bool> {
32    crate::primitives::controllable_state::use_controllable_model(cx, controlled, default_checked)
33}
34
35/// shadcn-friendly helper for mapping optional boolean values onto a switch checked state.
36///
37/// Radix `Switch` is a boolean control. Some shadcn authoring patterns treat missing values as
38/// "off" (`value || false`), so this helper preserves that ergonomic while keeping the core
39/// primitives surface discoverable.
40pub fn switch_checked_from_optional_bool(value: Option<bool>) -> bool {
41    value.unwrap_or(false)
42}
43
44/// shadcn-friendly toggle policy for `Option<bool>` switch bindings.
45pub fn toggle_optional_bool(value: Option<bool>) -> Option<bool> {
46    Some(!switch_checked_from_optional_bool(value))
47}
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52
53    use std::cell::Cell;
54
55    use fret_app::App;
56    use fret_core::{AppWindowId, Point, Px, Rect, Size};
57
58    fn bounds() -> Rect {
59        Rect::new(
60            Point::new(Px(0.0), Px(0.0)),
61            Size::new(Px(200.0), Px(120.0)),
62        )
63    }
64
65    #[test]
66    fn switch_use_checked_model_prefers_controlled_and_does_not_call_default() {
67        let window = AppWindowId::default();
68        let mut app = App::new();
69        let b = bounds();
70
71        let controlled = app.models_mut().insert(true);
72        let called = Cell::new(0);
73
74        fret_ui::elements::with_element_cx(&mut app, window, b, "test", |cx| {
75            let out = switch_use_checked_model(cx, Some(controlled.clone()), || {
76                called.set(called.get() + 1);
77                false
78            });
79            assert!(out.is_controlled());
80            assert_eq!(out.model(), controlled);
81        });
82
83        assert_eq!(called.get(), 0);
84    }
85
86    #[test]
87    fn switch_a11y_sets_role_and_checked() {
88        let a11y = switch_a11y(Some(Arc::from("Airplane mode")), true);
89        assert_eq!(a11y.role, Some(fret_core::SemanticsRole::Switch));
90        assert_eq!(a11y.checked, Some(true));
91        assert_eq!(a11y.label.as_deref(), Some("Airplane mode"));
92    }
93
94    #[test]
95    fn optional_bool_maps_to_checked_state() {
96        assert_eq!(switch_checked_from_optional_bool(None), false);
97        assert_eq!(switch_checked_from_optional_bool(Some(false)), false);
98        assert_eq!(switch_checked_from_optional_bool(Some(true)), true);
99    }
100
101    #[test]
102    fn toggle_optional_bool_inverts_and_sets_some() {
103        assert_eq!(toggle_optional_bool(None), Some(true));
104        assert_eq!(toggle_optional_bool(Some(false)), Some(true));
105        assert_eq!(toggle_optional_bool(Some(true)), Some(false));
106    }
107}