Skip to main content

fission_core/ui/widgets/
checkbox.rs

1use crate::internal::InternalLower;
2use crate::lowering::{InternalIrBuilder, InternalLoweringCx};
3use crate::ActionEnvelope;
4use fission_ir::{
5    op::{LayoutOp, Op, PaintOp},
6    WidgetId,
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, reduce_with!(handle_toggle));
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<WidgetId>,
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
41impl InternalLower for Checkbox {
42    fn lower(&self, cx: &mut InternalLoweringCx) -> WidgetId {
43        let id = self.id.map(Into::into).unwrap_or_else(|| cx.next_node_id());
44        cx.push_scope(id);
45
46        let tokens = &cx.env.theme.tokens;
47        let size = 18.0;
48        let radius = tokens.radii.small;
49        let border_color = tokens.colors.text_secondary;
50        let active_color = tokens.colors.primary;
51        let text_color = tokens.colors.text_primary;
52
53        // Square indicator
54        let square_id = cx.next_node_id();
55
56        let bg_paint = if self.checked {
57            Op::Paint(PaintOp::DrawRect {
58                fill: Some(fission_ir::op::Fill::Solid(active_color)),
59                stroke: None,
60                corner_radius: radius,
61                shadow: None,
62            })
63        } else {
64            Op::Paint(PaintOp::DrawRect {
65                fill: None,
66                stroke: Some(fission_ir::op::Stroke {
67                    fill: fission_ir::op::Fill::Solid(border_color),
68                    width: 1.5,
69                    dash_array: None,
70                    line_cap: fission_ir::op::LineCap::Butt,
71                    line_join: fission_ir::op::LineJoin::Miter,
72                }),
73                corner_radius: radius,
74                shadow: None,
75            })
76        };
77        let bg_node = InternalIrBuilder::new(cx.next_node_id(), bg_paint).build(cx);
78
79        // Checkmark
80        let check_node = if self.checked {
81            let check = InternalIrBuilder::new(
82                cx.next_node_id(),
83                Op::Paint(PaintOp::DrawRect {
84                    fill: Some(fission_ir::op::Fill::Solid(tokens.colors.on_primary)),
85                    stroke: None,
86                    corner_radius: 1.0,
87                    shadow: None,
88                }),
89            )
90            .build(cx);
91            let mut check_box = InternalIrBuilder::new(
92                cx.next_node_id(),
93                Op::Layout(LayoutOp::Box {
94                    width: Some(10.0),
95                    height: Some(10.0),
96                    min_width: None,
97                    max_width: None,
98                    min_height: None,
99                    max_height: None,
100                    padding: [0.0; 4],
101                    flex_grow: 0.0,
102                    flex_shrink: 0.0,
103                    aspect_ratio: None,
104                }),
105            );
106            check_box.add_child(check);
107            let check_box_id = check_box.build(cx);
108            let mut align = InternalIrBuilder::new(cx.next_node_id(), Op::Layout(LayoutOp::Align));
109            align.add_child(check_box_id);
110            Some(align.build(cx))
111        } else {
112            None
113        };
114
115        let mut square_box = InternalIrBuilder::new(
116            square_id,
117            Op::Layout(LayoutOp::Box {
118                width: Some(size),
119                height: Some(size),
120                min_width: None,
121                max_width: None,
122                min_height: None,
123                max_height: None,
124                padding: [0.0; 4],
125                flex_grow: 0.0,
126                flex_shrink: 0.0,
127                aspect_ratio: None,
128            }),
129        );
130        square_box.add_child(bg_node);
131        if let Some(c) = check_node {
132            square_box.add_child(c);
133        }
134        let square_final = square_box.build(cx);
135
136        // Label
137        let label_id = if let Some(text) = &self.label {
138            let text_id = InternalIrBuilder::new(
139                cx.next_node_id(),
140                Op::Paint(PaintOp::DrawText {
141                    text: text.clone(),
142                    size: tokens.typography.body_medium_size,
143                    color: text_color,
144                    underline: false,
145                    wrap: false,
146                    caret_index: None,
147                    caret_color: None,
148                    caret_width: None,
149                    caret_height: None,
150                    caret_radius: None,
151                    paragraph_style: None,
152                }),
153            )
154            .build(cx);
155            let mut layout = InternalIrBuilder::new(
156                cx.next_node_id(),
157                Op::Layout(LayoutOp::Box {
158                    width: None,
159                    height: None,
160                    min_width: None,
161                    max_width: None,
162                    min_height: None,
163                    max_height: None,
164                    padding: [tokens.spacing.s, 0.0, 0.0, 0.0],
165                    flex_grow: 0.0,
166                    flex_shrink: 0.0,
167                    aspect_ratio: None,
168                }),
169            );
170            layout.add_child(text_id);
171            Some(layout.build(cx))
172        } else {
173            None
174        };
175
176        let layout_id = cx.next_node_id();
177        let mut row = InternalIrBuilder::new(
178            layout_id,
179            Op::Layout(LayoutOp::Flex {
180                direction: fission_ir::FlexDirection::Row,
181                wrap: fission_ir::op::FlexWrap::NoWrap,
182                flex_grow: 0.0,
183                flex_shrink: 1.0,
184                padding: [0.0; 4],
185                gap: Some(8.0),
186                align_items: fission_ir::op::AlignItems::Center,
187                justify_content: fission_ir::op::JustifyContent::Start,
188            }),
189        );
190        row.add_child(square_final);
191        if let Some(l) = label_id {
192            row.add_child(l);
193        }
194        row.build(cx);
195
196        cx.pop_scope();
197
198        let mut semantics = fission_ir::Semantics {
199            role: fission_ir::Role::Checkbox,
200            label: self.label.clone(),
201            identifier: None,
202            value: Some(if self.checked {
203                "true".into()
204            } else {
205                "false".into()
206            }),
207            actions: Default::default(),
208            action_scope_id: None,
209            focusable: true,
210            multiline: false,
211            masked: false,
212            input_mask: None,
213            ime_preedit_range: None,
214            checked: Some(self.checked),
215            disabled: false,
216            read_only: false,
217            autofocus: false,
218            draggable: false,
219            scrollable_x: false,
220            scrollable_y: false,
221            min_value: None,
222            max_value: None,
223            current_value: None,
224            is_focus_scope: false,
225            is_focus_barrier: false,
226            drag_payload: None,
227            hero_tag: None,
228            focus_index: None,
229            text_input_type: fission_ir::semantics::TextInputType::Text,
230            text_input_action: fission_ir::semantics::TextInputAction::Done,
231            text_capitalization: fission_ir::semantics::TextCapitalization::None,
232            max_length: None,
233            max_length_enforcement: fission_ir::semantics::MaxLengthEnforcement::Enforced,
234            input_formatters: Vec::new(),
235            autocorrect: true,
236            enable_suggestions: true,
237            spell_check: true,
238            smart_dashes: true,
239            smart_quotes: true,
240            autofill_hints: Vec::new(),
241            scroll_padding: None,
242            capture_tab: false,
243            auto_indent: false,
244        };
245        if let Some(action) = &self.on_toggle {
246            semantics.actions.entries.push(fission_ir::ActionEntry {
247                trigger: fission_ir::semantics::ActionTrigger::Default,
248                action_id: action.id.as_u128(),
249                payload_data: Some(action.payload.clone()),
250            });
251        }
252
253        let mut sem_node = InternalIrBuilder::new(id, Op::Semantics(semantics));
254        sem_node.add_child(layout_id);
255        sem_node.build(cx)
256    }
257}