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#[derive(Debug, Default, Clone, Serialize, Deserialize)]
28pub struct Checkbox {
29 pub id: Option<NodeId>,
31 pub checked: bool,
33 pub on_toggle: Option<ActionEnvelope>,
35 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 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 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 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}