Skip to main content

fission_core/ui/widgets/
switch.rs

1use crate::internal::InternalLower;
2use crate::lowering::{wrap_zstack_child, InternalIrBuilder, InternalLoweringCx};
3use crate::ActionEnvelope;
4use fission_ir::{
5    op::{Color, LayoutOp, Op, PaintOp},
6    WidgetId,
7};
8use serde::{Deserialize, Serialize};
9
10/// A boolean toggle rendered as a sliding thumb on a track.
11///
12/// Visually similar to iOS/Material "switch" controls. The `on_toggle` action
13/// is dispatched when the user taps the switch; the application toggles
14/// `checked` in the reducer.
15///
16/// # Example
17///
18/// ```rust,ignore
19/// Switch {
20///     checked: view.state().dark_mode,
21///     on_toggle: Some(ctx.bind(ToggleDarkMode, reduce_with!(handler))),
22///     ..Default::default()
23/// }
24/// ```
25#[derive(Debug, Default, Clone, Serialize, Deserialize)]
26pub struct Switch {
27    /// Explicit node identity.
28    pub id: Option<WidgetId>,
29    /// Current on/off state.
30    pub checked: bool,
31    /// Action dispatched when the switch is tapped.
32    pub on_toggle: Option<ActionEnvelope>,
33}
34
35impl Switch {}
36
37impl InternalLower for Switch {
38    fn lower(&self, cx: &mut InternalLoweringCx) -> WidgetId {
39        let id = self.id.map(Into::into).unwrap_or_else(|| cx.next_node_id());
40        cx.push_scope(id);
41
42        let tokens = &cx.env.theme.tokens;
43        let width = 36.0;
44        let height = 20.0;
45        let thumb_size = 16.0;
46        let padding = 2.0;
47
48        let track_color = if self.checked {
49            tokens.colors.primary
50        } else {
51            tokens.colors.border
52        };
53        let thumb_color = tokens.colors.on_primary;
54
55        // Track
56        let track_paint = Op::Paint(PaintOp::DrawRect {
57            fill: Some(fission_ir::op::Fill::Solid(track_color)),
58            stroke: None,
59            corner_radius: height / 2.0,
60            shadow: None,
61        });
62        let track_node = InternalIrBuilder::new(cx.next_node_id(), track_paint).build(cx);
63
64        // Thumb
65        let thumb_paint = Op::Paint(PaintOp::DrawRect {
66            fill: Some(fission_ir::op::Fill::Solid(thumb_color)),
67            stroke: None,
68            corner_radius: thumb_size / 2.0,
69            shadow: Some(fission_ir::op::BoxShadow {
70                color: Color {
71                    r: 0,
72                    g: 0,
73                    b: 0,
74                    a: 50,
75                },
76                blur_radius: 2.0,
77                offset: (0.0, 1.0),
78            }),
79        });
80        let thumb_paint_node = InternalIrBuilder::new(cx.next_node_id(), thumb_paint).build(cx);
81
82        let left_padding = if self.checked {
83            width - thumb_size - padding
84        } else {
85            padding
86        };
87
88        let mut thumb_wrapper = InternalIrBuilder::new(
89            cx.next_node_id(),
90            Op::Layout(LayoutOp::Box {
91                width: Some(thumb_size),
92                height: Some(thumb_size),
93                min_width: None,
94                max_width: None,
95                min_height: None,
96                max_height: None,
97                padding: [0.0; 4],
98                flex_grow: 0.0,
99                flex_shrink: 0.0,
100                aspect_ratio: None,
101            }),
102        );
103        thumb_wrapper.add_child(thumb_paint_node);
104        let thumb_id = thumb_wrapper.build(cx);
105
106        // ZStack for Track + Content
107        let layout_id = cx.next_node_id();
108        let bg_id = {
109            let mut bg_fill =
110                InternalIrBuilder::new(cx.next_node_id(), Op::Layout(LayoutOp::AbsoluteFill));
111            bg_fill.add_child(track_node);
112            bg_fill.build(cx)
113        };
114
115        let content_id = {
116            let mut thumb_track = InternalIrBuilder::new(
117                cx.next_node_id(),
118                Op::Layout(LayoutOp::Box {
119                    width: Some(width),
120                    height: Some(height),
121                    min_width: None,
122                    max_width: None,
123                    min_height: None,
124                    max_height: None,
125                    padding: [left_padding, 0.0, padding, 0.0],
126                    flex_grow: 0.0,
127                    flex_shrink: 0.0,
128                    aspect_ratio: None,
129                }),
130            );
131            thumb_track.add_child(thumb_id);
132            thumb_track.build(cx)
133        };
134
135        cx.push_scope(layout_id);
136        let bg_wrapped = wrap_zstack_child(cx, bg_id);
137        let content_wrapped = wrap_zstack_child(cx, content_id);
138        cx.pop_scope();
139
140        let mut root = InternalIrBuilder::new(layout_id, Op::Layout(LayoutOp::ZStack));
141        root.add_child(bg_wrapped);
142        root.add_child(content_wrapped);
143        root.build(cx);
144
145        cx.pop_scope();
146
147        let mut semantics = fission_ir::Semantics {
148            role: fission_ir::Role::Switch,
149            label: None,
150            identifier: None,
151            value: Some(if self.checked {
152                "true".into()
153            } else {
154                "false".into()
155            }),
156            actions: Default::default(),
157            action_scope_id: None,
158            focusable: true,
159            multiline: false,
160            masked: false,
161            input_mask: None,
162            ime_preedit_range: None,
163            checked: Some(self.checked),
164            disabled: false,
165            read_only: false,
166            autofocus: false,
167            draggable: false,
168            scrollable_x: false,
169            scrollable_y: false,
170            min_value: None,
171            max_value: None,
172            current_value: None,
173            is_focus_scope: false,
174            is_focus_barrier: false,
175            drag_payload: None,
176            hero_tag: None,
177            focus_index: None,
178            text_input_type: fission_ir::semantics::TextInputType::Text,
179            text_input_action: fission_ir::semantics::TextInputAction::Done,
180            text_capitalization: fission_ir::semantics::TextCapitalization::None,
181            max_length: None,
182            max_length_enforcement: fission_ir::semantics::MaxLengthEnforcement::Enforced,
183            input_formatters: Vec::new(),
184            autocorrect: true,
185            enable_suggestions: true,
186            spell_check: true,
187            smart_dashes: true,
188            smart_quotes: true,
189            autofill_hints: Vec::new(),
190            scroll_padding: None,
191            capture_tab: false,
192            auto_indent: false,
193        };
194        if let Some(action) = &self.on_toggle {
195            semantics.actions.entries.push(fission_ir::ActionEntry {
196                trigger: fission_ir::semantics::ActionTrigger::Default,
197                action_id: action.id.as_u128(),
198                payload_data: Some(action.payload.clone()),
199            });
200        }
201
202        let mut sem_node = InternalIrBuilder::new(id, Op::Semantics(semantics));
203        sem_node.add_child(layout_id);
204        sem_node.build(cx)
205    }
206}