1use crate::lowering::{LoweringContext, NodeBuilder};
2use crate::ui::traits::Lower;
3use crate::ActionEnvelope;
4use fission_ir::{
5 op::{LayoutOp, Op, PaintOp},
6 NodeId,
7};
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Default, Clone, Serialize, Deserialize)]
33pub struct Radio {
34 pub id: Option<NodeId>,
36 pub checked: bool,
38 pub on_select: Option<ActionEnvelope>,
40 pub label: Option<String>,
42}
43
44impl Radio {
45 pub fn into_node(self) -> crate::ui::Node {
46 crate::ui::Node::Radio(self)
47 }
48}
49
50impl Lower for Radio {
51 fn lower(&self, cx: &mut LoweringContext) -> NodeId {
52 let id = self.id.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 dot_size = size * 0.5;
58 let _radius = size / 2.0;
59 let border_color = tokens.colors.text_secondary;
60 let active_color = tokens.colors.primary;
61 let text_color = tokens.colors.text_primary;
62
63 let bg_paint = if self.checked {
65 Op::Paint(PaintOp::DrawRect {
66 fill: None,
67 stroke: Some(fission_ir::op::Stroke {
68 fill: fission_ir::op::Fill::Solid(active_color),
69 width: 2.0,
70 dash_array: None,
71 line_cap: fission_ir::op::LineCap::Butt,
72 line_join: fission_ir::op::LineJoin::Miter,
73 }),
74 corner_radius: size / 2.0,
75 shadow: None,
76 })
77 } else {
78 Op::Paint(PaintOp::DrawRect {
79 fill: None,
80 stroke: Some(fission_ir::op::Stroke {
81 fill: fission_ir::op::Fill::Solid(border_color),
82 width: 1.5,
83 dash_array: None,
84 line_cap: fission_ir::op::LineCap::Butt,
85 line_join: fission_ir::op::LineJoin::Miter,
86 }),
87 corner_radius: size / 2.0,
88 shadow: None,
89 })
90 };
91 let outer_node = NodeBuilder::new(cx.next_node_id(), bg_paint).build(cx);
92
93 let dot_node = if self.checked {
95 let dot = NodeBuilder::new(
96 cx.next_node_id(),
97 Op::Paint(PaintOp::DrawRect {
98 fill: Some(fission_ir::op::Fill::Solid(active_color)),
99 stroke: None,
100 corner_radius: dot_size / 2.0,
101 shadow: None,
102 }),
103 )
104 .build(cx);
105 let mut dot_box = NodeBuilder::new(
106 cx.next_node_id(),
107 Op::Layout(LayoutOp::Box {
108 width: Some(dot_size),
109 height: Some(dot_size),
110 min_width: None,
111 max_width: None,
112 min_height: None,
113 max_height: None,
114 padding: [0.0; 4],
115 flex_grow: 0.0,
116 flex_shrink: 0.0,
117 aspect_ratio: None,
118 }),
119 );
120 dot_box.add_child(dot);
121 let dot_box_id = dot_box.build(cx);
122 let mut dot_align = NodeBuilder::new(cx.next_node_id(), Op::Layout(LayoutOp::Align));
123 dot_align.add_child(dot_box_id);
124 let dot_align_id = dot_align.build(cx);
125 let mut dot_container = NodeBuilder::new(
126 cx.next_node_id(),
127 Op::Layout(LayoutOp::Box {
128 width: Some(size),
129 height: Some(size),
130 min_width: None,
131 max_width: None,
132 min_height: None,
133 max_height: None,
134 padding: [0.0; 4],
135 flex_grow: 0.0,
136 flex_shrink: 0.0,
137 aspect_ratio: None,
138 }),
139 );
140 dot_container.add_child(dot_align_id);
141 Some(dot_container.build(cx))
142 } else {
143 None
144 };
145
146 let mut radio_box = NodeBuilder::new(
147 cx.next_node_id(),
148 Op::Layout(LayoutOp::Box {
149 width: Some(size),
150 height: Some(size),
151 min_width: None,
152 max_width: None,
153 min_height: None,
154 max_height: None,
155 padding: [0.0; 4],
156 flex_grow: 0.0,
157 flex_shrink: 0.0,
158 aspect_ratio: None,
159 }),
160 );
161 radio_box.add_child(outer_node);
162 if let Some(d) = dot_node {
163 radio_box.add_child(d);
164 }
165 let radio_final = radio_box.build(cx);
166
167 let label_id = if let Some(text) = &self.label {
169 let text_id = NodeBuilder::new(
170 cx.next_node_id(),
171 Op::Paint(PaintOp::DrawText {
172 text: text.clone(),
173 size: tokens.typography.body_medium_size,
174 color: text_color,
175 underline: false,
176 wrap: false,
177 caret_index: None,
178 caret_color: None,
179 caret_width: None,
180 caret_height: None,
181 caret_radius: None,
182 paragraph_style: None,
183 }),
184 )
185 .build(cx);
186 let mut layout = NodeBuilder::new(
187 cx.next_node_id(),
188 Op::Layout(LayoutOp::Box {
189 width: None,
190 height: None,
191 min_width: None,
192 max_width: None,
193 min_height: None,
194 max_height: None,
195 padding: [tokens.spacing.s, 0.0, 0.0, 0.0],
196 flex_grow: 0.0,
197 flex_shrink: 0.0,
198 aspect_ratio: None,
199 }),
200 );
201 layout.add_child(text_id);
202 Some(layout.build(cx))
203 } else {
204 None
205 };
206
207 let layout_id = cx.next_node_id();
208 let mut row = NodeBuilder::new(
209 layout_id,
210 Op::Layout(LayoutOp::Flex {
211 direction: fission_ir::FlexDirection::Row,
212 wrap: fission_ir::op::FlexWrap::NoWrap,
213 flex_grow: 0.0,
214 flex_shrink: 1.0,
215 padding: [0.0; 4],
216 gap: Some(8.0),
217 align_items: fission_ir::op::AlignItems::Center,
218 justify_content: fission_ir::op::JustifyContent::Start,
219 }),
220 );
221 row.add_child(radio_final);
222 if let Some(l) = label_id {
223 row.add_child(l);
224 }
225 row.build(cx);
226
227 cx.pop_scope();
228
229 let mut semantics = fission_ir::Semantics {
230 role: fission_ir::Role::Checkbox, label: self.label.clone(),
232 identifier: None,
233 value: Some(if self.checked {
234 "true".into()
235 } else {
236 "false".into()
237 }),
238 actions: Default::default(),
239 action_scope_id: None,
240 focusable: true,
241 multiline: false,
242 masked: false,
243 input_mask: None,
244 ime_preedit_range: None,
245 checked: Some(self.checked),
246 disabled: false,
247 read_only: false,
248 autofocus: false,
249 draggable: false,
250 scrollable_x: false,
251 scrollable_y: false,
252 min_value: None,
253 max_value: None,
254 current_value: None,
255 is_focus_scope: false,
256 is_focus_barrier: false,
257 drag_payload: None,
258 hero_tag: None,
259 focus_index: None,
260 text_input_type: fission_ir::semantics::TextInputType::Text,
261 text_input_action: fission_ir::semantics::TextInputAction::Done,
262 text_capitalization: fission_ir::semantics::TextCapitalization::None,
263 max_length: None,
264 max_length_enforcement: fission_ir::semantics::MaxLengthEnforcement::Enforced,
265 input_formatters: Vec::new(),
266 autocorrect: true,
267 enable_suggestions: true,
268 spell_check: true,
269 smart_dashes: true,
270 smart_quotes: true,
271 autofill_hints: Vec::new(),
272 scroll_padding: None,
273 capture_tab: false,
274 auto_indent: false,
275 };
276 if let Some(action) = &self.on_select {
277 semantics.actions.entries.push(fission_ir::ActionEntry {
278 trigger: fission_ir::semantics::ActionTrigger::Default,
279 action_id: action.id.as_u128(),
280 payload_data: Some(action.payload.clone()),
281 });
282 }
283
284 let mut sem_node = NodeBuilder::new(id, Op::Semantics(semantics));
285 sem_node.add_child(layout_id);
286 sem_node.build(cx)
287 }
288}