Skip to main content

fission_core/ui/widgets/
switch.rs

1use crate::lowering::{LoweringContext, NodeBuilder, wrap_zstack_child};
2use crate::ui::traits::Lower;
3use crate::ActionEnvelope;
4use fission_ir::{
5    op::{Color, Fill, LayoutOp, Op, PaintOp, Stroke},
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, handler as fn(&mut S, ToggleDarkMode))),
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 { tokens.colors.primary } else { tokens.colors.border };
53        let thumb_color = tokens.colors.on_primary;
54
55        // Track
56        let track_paint = Op::Paint(PaintOp::DrawRect {
57            fill: Some(Fill { color: track_color }),
58            stroke: None,
59            corner_radius: height / 2.0,
60            shadow: None,
61        });
62        let track_node = NodeBuilder::new(cx.next_node_id(), track_paint).build(cx);
63
64        // Thumb
65        let thumb_paint = Op::Paint(PaintOp::DrawRect {
66            fill: Some(Fill { color: thumb_color }),
67            stroke: None,
68            corner_radius: thumb_size / 2.0,
69            shadow: Some(fission_ir::op::BoxShadow { 
70                color: Color { r:0, g:0, b:0, a:50 }, 
71                blur_radius: 2.0, 
72                offset: (0.0, 1.0) 
73            }),
74        });
75        let thumb_paint_node = NodeBuilder::new(cx.next_node_id(), thumb_paint).build(cx);
76        
77        let left_padding = if self.checked { width - thumb_size - padding } else { padding };
78
79        let mut thumb_wrapper = NodeBuilder::new(
80            cx.next_node_id(),
81            Op::Layout(LayoutOp::Box {
82                width: Some(thumb_size), height: Some(thumb_size),
83                min_width: None, max_width: None, min_height: None, max_height: None,
84                padding: [0.0; 4],
85                flex_grow: 0.0,
86                flex_shrink: 0.0,
87                aspect_ratio: None,
88            })
89        );
90        thumb_wrapper.add_child(thumb_paint_node);
91        let thumb_id = thumb_wrapper.build(cx);
92
93        // ZStack for Track + Content
94        let layout_id = cx.next_node_id();
95        let bg_id = {
96            let mut bg_fill = NodeBuilder::new(cx.next_node_id(), Op::Layout(LayoutOp::AbsoluteFill));
97            bg_fill.add_child(track_node);
98            bg_fill.build(cx)
99        };
100        
101        let content_id = {
102            let mut thumb_track = NodeBuilder::new(
103                cx.next_node_id(),
104                Op::Layout(LayoutOp::Box {
105                    width: Some(width), height: Some(height),
106                    min_width: None, max_width: None, min_height: None, max_height: None,
107                    padding: [left_padding, 0.0, padding, 0.0],
108                    flex_grow: 0.0,
109                    flex_shrink: 0.0,
110                    aspect_ratio: None,
111                })
112            );
113            thumb_track.add_child(thumb_id);
114            thumb_track.build(cx)
115        };
116        
117        cx.push_scope(layout_id);
118        let bg_wrapped = wrap_zstack_child(cx, bg_id);
119        let content_wrapped = wrap_zstack_child(cx, content_id);
120        cx.pop_scope();
121
122        let mut root = NodeBuilder::new(layout_id, Op::Layout(LayoutOp::ZStack));
123        root.add_child(bg_wrapped);
124        root.add_child(content_wrapped);
125        root.build(cx);
126
127        cx.pop_scope();
128
129        let mut semantics = fission_ir::Semantics {
130            role: fission_ir::Role::Switch,
131            label: None,
132            value: Some(if self.checked { "true".into() } else { "false".into() }),
133            actions: Default::default(),
134            focusable: true,
135            multiline: false,
136            masked: false,
137            input_mask: None,
138            ime_preedit_range: None,
139            checked: Some(self.checked),
140            disabled: false,
141            draggable: false,
142            scrollable_x: false,
143            scrollable_y: false,
144            min_value: None,
145            max_value: None,
146            current_value: None,
147            is_focus_scope: false,
148            is_focus_barrier: false,
149            drag_payload: None,
150            hero_tag: None,
151            focus_index: None, capture_tab: false, auto_indent: false,
152        };
153        if let Some(action) = &self.on_toggle {
154             semantics.actions.entries.push(fission_ir::ActionEntry { 
155                 trigger: fission_ir::semantics::ActionTrigger::Default,
156                 action_id: action.id.as_u128(), 
157                 payload_data: Some(action.payload.clone()) 
158             });
159        }
160        
161        let mut sem_node = NodeBuilder::new(id, Op::Semantics(semantics));
162        sem_node.add_child(layout_id);
163        sem_node.build(cx)
164    }
165}
166