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