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 color: Color {
80 r: 0,
81 g: 0,
82 b: 0,
83 a: 50,
84 },
85 blur_radius: 2.0,
86 offset: (0.0, 1.0),
87 }),
88 });
89 let thumb_paint_node = InternalIrBuilder::new(cx.next_node_id(), thumb_paint).build(cx);
90
91 let left_padding = if self.checked {
92 width - thumb_size - padding
93 } else {
94 padding
95 };
96
97 let mut thumb_wrapper = InternalIrBuilder::new(
98 cx.next_node_id(),
99 Op::Layout(LayoutOp::Box {
100 width: Some(thumb_size),
101 height: Some(thumb_size),
102 min_width: None,
103 max_width: None,
104 min_height: None,
105 max_height: None,
106 padding: [0.0; 4],
107 flex_grow: 0.0,
108 flex_shrink: 0.0,
109 aspect_ratio: None,
110 }),
111 );
112 thumb_wrapper.add_child(thumb_paint_node);
113 let thumb_id = thumb_wrapper.build(cx);
114
115 let layout_id = cx.next_node_id();
117 let bg_id = {
118 let mut bg_fill =
119 InternalIrBuilder::new(cx.next_node_id(), Op::Layout(LayoutOp::AbsoluteFill));
120 bg_fill.add_child(track_node);
121 bg_fill.build(cx)
122 };
123
124 let content_id = {
125 let mut thumb_track = InternalIrBuilder::new(
126 cx.next_node_id(),
127 Op::Layout(LayoutOp::Box {
128 width: Some(width),
129 height: Some(height),
130 min_width: None,
131 max_width: None,
132 min_height: None,
133 max_height: None,
134 padding: [left_padding, 0.0, padding, 0.0],
135 flex_grow: 0.0,
136 flex_shrink: 0.0,
137 aspect_ratio: None,
138 }),
139 );
140 thumb_track.add_child(thumb_id);
141 thumb_track.build(cx)
142 };
143
144 cx.push_scope(layout_id);
145 let bg_wrapped = wrap_zstack_child(cx, bg_id);
146 let content_wrapped = wrap_zstack_child(cx, content_id);
147 cx.pop_scope();
148
149 let mut root = InternalIrBuilder::new(layout_id, Op::Layout(LayoutOp::ZStack));
150 root.add_child(bg_wrapped);
151 root.add_child(content_wrapped);
152 root.build(cx);
153
154 cx.pop_scope();
155
156 let mut semantics = fission_ir::Semantics {
157 role: fission_ir::Role::Switch,
158 label: None,
159 identifier: self.semantics_identifier.clone(),
160 value: Some(if self.checked {
161 "true".into()
162 } else {
163 "false".into()
164 }),
165 actions: Default::default(),
166 action_scope_id: None,
167 focusable: true,
168 focus_policy: fission_ir::FocusPolicy::FocusOnPointer,
169 multiline: false,
170 masked: false,
171 input_mask: None,
172 ime_preedit_range: None,
173 ime_preedit_cursor_range: None,
174 text_selection: None,
175 checked: Some(self.checked),
176 disabled: false,
177 read_only: false,
178 autofocus: false,
179 draggable: false,
180 scrollable_x: false,
181 scrollable_y: false,
182 min_value: None,
183 max_value: None,
184 current_value: None,
185 is_focus_scope: false,
186 is_focus_barrier: false,
187 drag_payload: None,
188 hero_tag: None,
189 focus_index: None,
190 text_input_type: fission_ir::semantics::TextInputType::Text,
191 text_input_action: fission_ir::semantics::TextInputAction::Done,
192 text_capitalization: fission_ir::semantics::TextCapitalization::None,
193 max_length: None,
194 max_length_enforcement: fission_ir::semantics::MaxLengthEnforcement::Enforced,
195 input_formatters: Vec::new(),
196 autocorrect: true,
197 enable_suggestions: true,
198 spell_check: true,
199 smart_dashes: true,
200 smart_quotes: true,
201 autofill_hints: Vec::new(),
202 scroll_padding: None,
203 capture_tab: false,
204 auto_indent: false,
205 };
206 if let Some(action) = &self.on_toggle {
207 semantics.actions.entries.push(fission_ir::ActionEntry {
208 trigger: fission_ir::semantics::ActionTrigger::Default,
209 action_id: action.id.as_u128(),
210 payload_data: Some(action.payload.clone()),
211 });
212 }
213
214 let mut sem_node = InternalIrBuilder::new(id, Op::Semantics(semantics));
215 sem_node.add_child(layout_id);
216 sem_node.build(cx)
217 }
218}