1use crate::lowering::{wrap_zstack_child, LoweringContext, NodeBuilder};
2use crate::ui::traits::Lower;
3use crate::ActionEnvelope;
4use fission_ir::{
5 op::{Color, LayoutOp, Op, PaintOp},
6 NodeId,
7};
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Default, Clone, Serialize, Deserialize)]
26pub struct Switch {
27 pub id: Option<NodeId>,
29 pub checked: bool,
31 pub on_toggle: Option<ActionEnvelope>,
33}
34
35impl Switch {
36 pub fn into_node(self) -> crate::ui::Node {
37 crate::ui::Node::Switch(self)
38 }
39}
40
41impl Lower for Switch {
42 fn lower(&self, cx: &mut LoweringContext) -> NodeId {
43 let id = self.id.unwrap_or_else(|| cx.next_node_id());
44 cx.push_scope(id);
45
46 let tokens = &cx.env.theme.tokens;
47 let width = 36.0;
48 let height = 20.0;
49 let thumb_size = 16.0;
50 let padding = 2.0;
51
52 let track_color = if self.checked {
53 tokens.colors.primary
54 } else {
55 tokens.colors.border
56 };
57 let thumb_color = tokens.colors.on_primary;
58
59 let track_paint = Op::Paint(PaintOp::DrawRect {
61 fill: Some(fission_ir::op::Fill::Solid(track_color)),
62 stroke: None,
63 corner_radius: height / 2.0,
64 shadow: None,
65 });
66 let track_node = NodeBuilder::new(cx.next_node_id(), track_paint).build(cx);
67
68 let thumb_paint = Op::Paint(PaintOp::DrawRect {
70 fill: Some(fission_ir::op::Fill::Solid(thumb_color)),
71 stroke: None,
72 corner_radius: thumb_size / 2.0,
73 shadow: Some(fission_ir::op::BoxShadow {
74 color: Color {
75 r: 0,
76 g: 0,
77 b: 0,
78 a: 50,
79 },
80 blur_radius: 2.0,
81 offset: (0.0, 1.0),
82 }),
83 });
84 let thumb_paint_node = NodeBuilder::new(cx.next_node_id(), thumb_paint).build(cx);
85
86 let left_padding = if self.checked {
87 width - thumb_size - padding
88 } else {
89 padding
90 };
91
92 let mut thumb_wrapper = NodeBuilder::new(
93 cx.next_node_id(),
94 Op::Layout(LayoutOp::Box {
95 width: Some(thumb_size),
96 height: Some(thumb_size),
97 min_width: None,
98 max_width: None,
99 min_height: None,
100 max_height: None,
101 padding: [0.0; 4],
102 flex_grow: 0.0,
103 flex_shrink: 0.0,
104 aspect_ratio: None,
105 }),
106 );
107 thumb_wrapper.add_child(thumb_paint_node);
108 let thumb_id = thumb_wrapper.build(cx);
109
110 let layout_id = cx.next_node_id();
112 let bg_id = {
113 let mut bg_fill =
114 NodeBuilder::new(cx.next_node_id(), Op::Layout(LayoutOp::AbsoluteFill));
115 bg_fill.add_child(track_node);
116 bg_fill.build(cx)
117 };
118
119 let content_id = {
120 let mut thumb_track = NodeBuilder::new(
121 cx.next_node_id(),
122 Op::Layout(LayoutOp::Box {
123 width: Some(width),
124 height: Some(height),
125 min_width: None,
126 max_width: None,
127 min_height: None,
128 max_height: None,
129 padding: [left_padding, 0.0, padding, 0.0],
130 flex_grow: 0.0,
131 flex_shrink: 0.0,
132 aspect_ratio: None,
133 }),
134 );
135 thumb_track.add_child(thumb_id);
136 thumb_track.build(cx)
137 };
138
139 cx.push_scope(layout_id);
140 let bg_wrapped = wrap_zstack_child(cx, bg_id);
141 let content_wrapped = wrap_zstack_child(cx, content_id);
142 cx.pop_scope();
143
144 let mut root = NodeBuilder::new(layout_id, Op::Layout(LayoutOp::ZStack));
145 root.add_child(bg_wrapped);
146 root.add_child(content_wrapped);
147 root.build(cx);
148
149 cx.pop_scope();
150
151 let mut semantics = fission_ir::Semantics {
152 role: fission_ir::Role::Switch,
153 label: None,
154 identifier: None,
155 value: Some(if self.checked {
156 "true".into()
157 } else {
158 "false".into()
159 }),
160 actions: Default::default(),
161 action_scope_id: None,
162 focusable: true,
163 multiline: false,
164 masked: false,
165 input_mask: None,
166 ime_preedit_range: None,
167 checked: Some(self.checked),
168 disabled: false,
169 read_only: false,
170 autofocus: false,
171 draggable: false,
172 scrollable_x: false,
173 scrollable_y: false,
174 min_value: None,
175 max_value: None,
176 current_value: None,
177 is_focus_scope: false,
178 is_focus_barrier: false,
179 drag_payload: None,
180 hero_tag: None,
181 focus_index: None,
182 text_input_type: fission_ir::semantics::TextInputType::Text,
183 text_input_action: fission_ir::semantics::TextInputAction::Done,
184 text_capitalization: fission_ir::semantics::TextCapitalization::None,
185 max_length: None,
186 max_length_enforcement: fission_ir::semantics::MaxLengthEnforcement::Enforced,
187 input_formatters: Vec::new(),
188 autocorrect: true,
189 enable_suggestions: true,
190 spell_check: true,
191 smart_dashes: true,
192 smart_quotes: true,
193 autofill_hints: Vec::new(),
194 scroll_padding: None,
195 capture_tab: false,
196 auto_indent: false,
197 };
198 if let Some(action) = &self.on_toggle {
199 semantics.actions.entries.push(fission_ir::ActionEntry {
200 trigger: fission_ir::semantics::ActionTrigger::Default,
201 action_id: action.id.as_u128(),
202 payload_data: Some(action.payload.clone()),
203 });
204 }
205
206 let mut sem_node = NodeBuilder::new(id, Op::Semantics(semantics));
207 sem_node.add_child(layout_id);
208 sem_node.build(cx)
209 }
210}