Skip to main content

fission_core/ui/widgets/
checkbox.rs

1use crate::lowering::{LoweringContext, NodeBuilder};
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 with a square check indicator and optional label.
11///
12/// When pressed, the `on_toggle` action is dispatched. The application is
13/// responsible for toggling `checked` in the corresponding reducer.
14///
15/// # Example
16///
17/// ```rust,ignore
18/// let on_toggle = ctx.bind(ToggleAgree, handle_toggle as fn(&mut S, ToggleAgree));
19///
20/// Checkbox {
21///     checked: view.state.agreed,
22///     on_toggle: Some(on_toggle),
23///     label: Some("I agree to the terms".into()),
24///     ..Default::default()
25/// }
26/// ```
27#[derive(Debug, Default, Clone, Serialize, Deserialize)]
28pub struct Checkbox {
29    /// Explicit node identity.
30    pub id: Option<NodeId>,
31    /// Current checked state.
32    pub checked: bool,
33    /// Action dispatched when the checkbox is tapped.
34    pub on_toggle: Option<ActionEnvelope>,
35    /// Optional text label rendered next to the indicator.
36    pub label: Option<String>,
37}
38
39impl Checkbox {
40    pub fn into_node(self) -> crate::ui::Node {
41        crate::ui::Node::Checkbox(self)
42    }
43}
44
45impl Lower for Checkbox {
46    fn lower(&self, cx: &mut LoweringContext) -> NodeId {
47        let id = self.id.unwrap_or_else(|| cx.next_node_id());
48        cx.push_scope(id);
49
50        let tokens = &cx.env.theme.tokens;
51        let size = 18.0;
52        let radius = tokens.radii.small;
53        let border_color = tokens.colors.text_secondary;
54        let active_color = tokens.colors.primary;
55        let text_color = tokens.colors.text_primary;
56
57        // Square indicator
58        let square_id = cx.next_node_id();
59        
60        let bg_paint = if self.checked {
61            Op::Paint(PaintOp::DrawRect {
62                fill: Some(Fill { color: active_color }),
63                stroke: None,
64                corner_radius: radius,
65                shadow: None,
66            })
67        } else {
68            Op::Paint(PaintOp::DrawRect {
69                fill: None,
70                stroke: Some(Stroke { color: border_color, width: 1.5 }),
71                corner_radius: radius,
72                shadow: None,
73            })
74        };
75        let bg_node = NodeBuilder::new(cx.next_node_id(), bg_paint).build(cx);
76        
77        // Checkmark
78        let check_node = if self.checked {
79            let check = NodeBuilder::new(cx.next_node_id(), Op::Paint(PaintOp::DrawRect {
80                fill: Some(Fill { color: tokens.colors.on_primary }),
81                stroke: None,
82                corner_radius: 1.0,
83                shadow: None,
84            })).build(cx);
85            let mut check_box = NodeBuilder::new(cx.next_node_id(), Op::Layout(LayoutOp::Box {
86                width: Some(10.0), height: Some(10.0), 
87                min_width: None, max_width: None, min_height: None, max_height: None, padding: [0.0;4],
88                flex_grow: 0.0, flex_shrink: 0.0,
89                aspect_ratio: None,
90            }));
91            check_box.add_child(check);
92            let check_box_id = check_box.build(cx);
93            let mut align = NodeBuilder::new(cx.next_node_id(), Op::Layout(LayoutOp::Align));
94            align.add_child(check_box_id);
95            Some(align.build(cx))
96        } else { None };
97
98        let mut square_box = NodeBuilder::new(
99            square_id,
100            Op::Layout(LayoutOp::Box { width: Some(size), height: Some(size), min_width: None, max_width: None, min_height: None, max_height: None, padding: [0.0; 4], flex_grow: 0.0, flex_shrink: 0.0, aspect_ratio: None }),
101        );
102        square_box.add_child(bg_node);
103        if let Some(c) = check_node { square_box.add_child(c); }
104        let square_final = square_box.build(cx);
105
106        // Label
107        let label_id = if let Some(text) = &self.label {
108            let text_id = NodeBuilder::new(
109                cx.next_node_id(),
110                Op::Paint(PaintOp::DrawText { 
111                    text: text.clone(), 
112                    size: tokens.typography.body_medium_size, 
113                    color: text_color, 
114                    underline: false, 
115                    caret_index: None 
116                }),
117            ).build(cx);
118            let mut layout = NodeBuilder::new(
119                cx.next_node_id(),
120                Op::Layout(LayoutOp::Box { width: None, height: None, min_width: None, max_width: None, min_height: None, max_height: None, padding: [tokens.spacing.s, 0.0, 0.0, 0.0], flex_grow: 0.0, flex_shrink: 0.0, aspect_ratio: None }), 
121            );
122            layout.add_child(text_id);
123            Some(layout.build(cx))
124        } else { None };
125
126        let layout_id = cx.next_node_id();
127        let mut row = NodeBuilder::new(
128            layout_id,
129            Op::Layout(LayoutOp::Flex { direction: fission_ir::FlexDirection::Row, wrap: fission_ir::op::FlexWrap::NoWrap, flex_grow: 0.0, flex_shrink: 1.0, padding: [0.0; 4], gap: Some(8.0), align_items: fission_ir::op::AlignItems::Center, justify_content: fission_ir::op::JustifyContent::Start }),
130        );
131        row.add_child(square_final);
132        if let Some(l) = label_id { row.add_child(l); }
133        row.build(cx);
134
135        cx.pop_scope();
136
137        let mut semantics = fission_ir::Semantics {
138            role: fission_ir::Role::Checkbox,
139            label: self.label.clone(),
140            value: Some(if self.checked { "true".into() } else { "false".into() }),
141            actions: Default::default(),
142            focusable: true,
143            multiline: false,
144            masked: false,
145            input_mask: None,
146            ime_preedit_range: None,
147            checked: Some(self.checked),
148            disabled: false,
149            draggable: false,
150            scrollable_x: false,
151            scrollable_y: false,
152            min_value: None,
153            max_value: None,
154            current_value: None,
155            is_focus_scope: false,
156            is_focus_barrier: false,
157            drag_payload: None,
158            hero_tag: None,
159            focus_index: None, capture_tab: false, auto_indent: false,
160        };
161        if let Some(action) = &self.on_toggle {
162             semantics.actions.entries.push(fission_ir::ActionEntry { 
163                 trigger: fission_ir::semantics::ActionTrigger::Default,
164                 action_id: action.id.as_u128(), 
165                 payload_data: Some(action.payload.clone()) 
166             });
167        }
168        
169        let mut sem_node = NodeBuilder::new(id, Op::Semantics(semantics));
170        sem_node.add_child(layout_id);
171        sem_node.build(cx)
172    }
173}