Skip to main content

fission_core/ui/widgets/
switch.rs

1use crate::lowering::{wrap_zstack_child, LoweringContext, NodeBuilder};
2use crate::ui::traits::Lower;
3use crate::ActionEnvelope;
4use fission_ir::{
5    op::{Color, LayoutOp, Op, PaintOp},
6    NodeId,
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<NodeId>,
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    pub fn into_node(self) -> crate::ui::Node {
37        crate::ui::Node::Switch(self)
38    }
39}
40
41impl Lower for Switch {
42    fn lower(&self, cx: &mut LoweringContext) -> NodeId {
43        let id = self.id.unwrap_or_else(|| cx.next_node_id());
44        cx.push_scope(id);
45
46        let tokens = &cx.env.theme.tokens;
47        let width = 36.0;
48        let height = 20.0;
49        let thumb_size = 16.0;
50        let padding = 2.0;
51
52        let track_color = if self.checked {
53            tokens.colors.primary
54        } else {
55            tokens.colors.border
56        };
57        let thumb_color = tokens.colors.on_primary;
58
59        // Track
60        let track_paint = Op::Paint(PaintOp::DrawRect {
61            fill: Some(fission_ir::op::Fill::Solid(track_color)),
62            stroke: None,
63            corner_radius: height / 2.0,
64            shadow: None,
65        });
66        let track_node = NodeBuilder::new(cx.next_node_id(), track_paint).build(cx);
67
68        // Thumb
69        let thumb_paint = Op::Paint(PaintOp::DrawRect {
70            fill: Some(fission_ir::op::Fill::Solid(thumb_color)),
71            stroke: None,
72            corner_radius: thumb_size / 2.0,
73            shadow: Some(fission_ir::op::BoxShadow {
74                color: Color {
75                    r: 0,
76                    g: 0,
77                    b: 0,
78                    a: 50,
79                },
80                blur_radius: 2.0,
81                offset: (0.0, 1.0),
82            }),
83        });
84        let thumb_paint_node = NodeBuilder::new(cx.next_node_id(), thumb_paint).build(cx);
85
86        let left_padding = if self.checked {
87            width - thumb_size - padding
88        } else {
89            padding
90        };
91
92        let mut thumb_wrapper = NodeBuilder::new(
93            cx.next_node_id(),
94            Op::Layout(LayoutOp::Box {
95                width: Some(thumb_size),
96                height: Some(thumb_size),
97                min_width: None,
98                max_width: None,
99                min_height: None,
100                max_height: None,
101                padding: [0.0; 4],
102                flex_grow: 0.0,
103                flex_shrink: 0.0,
104                aspect_ratio: None,
105            }),
106        );
107        thumb_wrapper.add_child(thumb_paint_node);
108        let thumb_id = thumb_wrapper.build(cx);
109
110        // ZStack for Track + Content
111        let layout_id = cx.next_node_id();
112        let bg_id = {
113            let mut bg_fill =
114                NodeBuilder::new(cx.next_node_id(), Op::Layout(LayoutOp::AbsoluteFill));
115            bg_fill.add_child(track_node);
116            bg_fill.build(cx)
117        };
118
119        let content_id = {
120            let mut thumb_track = NodeBuilder::new(
121                cx.next_node_id(),
122                Op::Layout(LayoutOp::Box {
123                    width: Some(width),
124                    height: Some(height),
125                    min_width: None,
126                    max_width: None,
127                    min_height: None,
128                    max_height: None,
129                    padding: [left_padding, 0.0, padding, 0.0],
130                    flex_grow: 0.0,
131                    flex_shrink: 0.0,
132                    aspect_ratio: None,
133                }),
134            );
135            thumb_track.add_child(thumb_id);
136            thumb_track.build(cx)
137        };
138
139        cx.push_scope(layout_id);
140        let bg_wrapped = wrap_zstack_child(cx, bg_id);
141        let content_wrapped = wrap_zstack_child(cx, content_id);
142        cx.pop_scope();
143
144        let mut root = NodeBuilder::new(layout_id, Op::Layout(LayoutOp::ZStack));
145        root.add_child(bg_wrapped);
146        root.add_child(content_wrapped);
147        root.build(cx);
148
149        cx.pop_scope();
150
151        let mut semantics = fission_ir::Semantics {
152            role: fission_ir::Role::Switch,
153            label: None,
154            identifier: None,
155            value: Some(if self.checked {
156                "true".into()
157            } else {
158                "false".into()
159            }),
160            actions: Default::default(),
161            action_scope_id: None,
162            focusable: true,
163            multiline: false,
164            masked: false,
165            input_mask: None,
166            ime_preedit_range: None,
167            checked: Some(self.checked),
168            disabled: false,
169            read_only: false,
170            autofocus: false,
171            draggable: false,
172            scrollable_x: false,
173            scrollable_y: false,
174            min_value: None,
175            max_value: None,
176            current_value: None,
177            is_focus_scope: false,
178            is_focus_barrier: false,
179            drag_payload: None,
180            hero_tag: None,
181            focus_index: None,
182            text_input_type: fission_ir::semantics::TextInputType::Text,
183            text_input_action: fission_ir::semantics::TextInputAction::Done,
184            text_capitalization: fission_ir::semantics::TextCapitalization::None,
185            max_length: None,
186            max_length_enforcement: fission_ir::semantics::MaxLengthEnforcement::Enforced,
187            input_formatters: Vec::new(),
188            autocorrect: true,
189            enable_suggestions: true,
190            spell_check: true,
191            smart_dashes: true,
192            smart_quotes: true,
193            autofill_hints: Vec::new(),
194            scroll_padding: None,
195            capture_tab: false,
196            auto_indent: false,
197        };
198        if let Some(action) = &self.on_toggle {
199            semantics.actions.entries.push(fission_ir::ActionEntry {
200                trigger: fission_ir::semantics::ActionTrigger::Default,
201                action_id: action.id.as_u128(),
202                payload_data: Some(action.payload.clone()),
203            });
204        }
205
206        let mut sem_node = NodeBuilder::new(id, Op::Semantics(semantics));
207        sem_node.add_child(layout_id);
208        sem_node.build(cx)
209    }
210}