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