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