1use crate::internal::InternalLower;
2use crate::lowering::wrap_zstack_child;
3use crate::lowering::{InternalIrBuilder, InternalLoweringCx};
4use crate::ActionEnvelope;
5use fission_ir::{
6 op::{Color, Fill, GridTrack, LayoutOp, Op, PaintOp},
7 WidgetId,
8};
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct Slider {
34 pub id: Option<WidgetId>,
36 pub value: f32,
38 pub min: f32,
40 pub max: f32,
42 pub on_change: Option<ActionEnvelope>,
44}
45
46impl Slider {}
47
48impl Default for Slider {
49 fn default() -> Self {
50 Self {
51 id: None,
52 value: 0.0,
53 min: 0.0,
54 max: 1.0,
55 on_change: None,
56 }
57 }
58}
59
60impl InternalLower for Slider {
61 fn lower(&self, cx: &mut InternalLoweringCx) -> WidgetId {
62 let id = self.id.map(Into::into).unwrap_or_else(|| cx.next_node_id());
63 cx.push_scope(id);
64
65 let tokens = &cx.env.theme.tokens;
66 let thumb_size = 16.0;
67 let track_height = 4.0;
68
69 let range = (self.max - self.min).max(0.0001);
70 let pct = ((self.value - self.min) / range).clamp(0.0, 1.0) * 100.0;
71
72 let layout_id = cx.next_node_id();
91
92 let track_layer = {
93 let p_y = (thumb_size - track_height) / 2.0;
96
97 let mut track_container = InternalIrBuilder::new(
98 cx.next_node_id(),
99 Op::Layout(LayoutOp::Box {
100 width: None,
101 height: None,
102 min_width: None,
103 max_width: None,
104 min_height: None,
105 max_height: None,
106 padding: [0.0, 0.0, p_y, p_y],
107 flex_grow: 0.0,
108 flex_shrink: 0.0,
109 aspect_ratio: None,
110 }),
111 );
112
113 let inner_paint = InternalIrBuilder::new(
114 cx.next_node_id(),
115 Op::Paint(PaintOp::DrawRect {
116 fill: Some(Fill::Solid(tokens.colors.border)),
117 stroke: None,
118 corner_radius: track_height / 2.0,
119 shadow: None,
120 }),
121 )
122 .build(cx);
123
124 let mut inner_box =
137 InternalIrBuilder::new(cx.next_node_id(), Op::Layout(LayoutOp::AbsoluteFill)); inner_box.add_child(inner_paint);
139 let inner_id = inner_box.build(cx);
140
141 track_container.add_child(inner_id);
142 track_container.build(cx)
143 };
144
145 let thumb_layer = {
147 let thumb_paint = InternalIrBuilder::new(
148 cx.next_node_id(),
149 Op::Paint(PaintOp::DrawRect {
150 fill: Some(Fill::Solid(tokens.colors.primary)),
151 stroke: None,
152 corner_radius: thumb_size / 2.0,
153 shadow: Some(fission_ir::op::BoxShadow {
154 color: Color {
155 r: 0,
156 g: 0,
157 b: 0,
158 a: 50,
159 },
160 blur_radius: 2.0,
161 offset: (0.0, 1.0),
162 }),
163 }),
164 )
165 .build(cx);
166
167 let mut thumb_box = InternalIrBuilder::new(
168 cx.next_node_id(),
169 Op::Layout(LayoutOp::Box {
170 width: Some(thumb_size),
171 height: Some(thumb_size),
172 min_width: None,
173 max_width: None,
174 min_height: None,
175 max_height: None,
176 padding: [0.0; 4],
177 flex_grow: 0.0,
178 flex_shrink: 0.0,
179 aspect_ratio: None,
180 }),
181 );
182 thumb_box.add_child(thumb_paint);
183 let thumb_id = thumb_box.build(cx);
184
185 let mut grid = InternalIrBuilder::new(
186 cx.next_node_id(),
187 Op::Layout(LayoutOp::Grid {
188 columns: vec![
189 GridTrack::Percent(pct),
190 GridTrack::Points(thumb_size),
191 GridTrack::Fr(1.0),
192 ],
193 rows: vec![GridTrack::Points(thumb_size)],
194 column_gap: None,
195 row_gap: None,
196 padding: [0.0; 4],
197 }),
198 );
199
200 let mut item = InternalIrBuilder::new(
202 cx.next_node_id(),
203 Op::Layout(LayoutOp::GridItem {
204 row_start: fission_ir::op::GridPlacement::Line(1),
205 row_end: fission_ir::op::GridPlacement::Auto,
206 col_start: fission_ir::op::GridPlacement::Line(2),
207 col_end: fission_ir::op::GridPlacement::Auto,
208 }),
209 );
210 item.add_child(thumb_id);
211 let item_id = item.build(cx);
212
213 grid.add_child(item_id);
214 grid.build(cx)
215 };
216
217 cx.push_scope(layout_id);
218 let track_wrapped = wrap_zstack_child(cx, track_layer);
219 let thumb_wrapped = wrap_zstack_child(cx, thumb_layer);
220 cx.pop_scope();
221
222 let mut zstack = InternalIrBuilder::new(layout_id, Op::Layout(LayoutOp::ZStack));
223 zstack.add_child(track_wrapped);
224 zstack.add_child(thumb_wrapped);
225 zstack.build(cx);
226
227 cx.pop_scope();
228
229 let mut semantics = fission_ir::Semantics {
230 role: fission_ir::Role::Slider,
231 label: None,
232 identifier: None,
233 value: Some(format!("{:.2}", self.value)),
234 actions: Default::default(),
235 action_scope_id: None,
236 focusable: true,
237 multiline: false,
238 masked: false,
239 input_mask: None,
240 ime_preedit_range: None,
241 checked: None,
242 disabled: false,
243 read_only: false,
244 autofocus: false,
245 draggable: true,
246 scrollable_x: false,
247 scrollable_y: false,
248 min_value: Some(self.min),
249 max_value: Some(self.max),
250 current_value: Some(self.value),
251 is_focus_scope: false,
252 is_focus_barrier: false,
253 drag_payload: None,
254 hero_tag: None,
255 focus_index: None,
256 text_input_type: fission_ir::semantics::TextInputType::Text,
257 text_input_action: fission_ir::semantics::TextInputAction::Done,
258 text_capitalization: fission_ir::semantics::TextCapitalization::None,
259 max_length: None,
260 max_length_enforcement: fission_ir::semantics::MaxLengthEnforcement::Enforced,
261 input_formatters: Vec::new(),
262 autocorrect: true,
263 enable_suggestions: true,
264 spell_check: true,
265 smart_dashes: true,
266 smart_quotes: true,
267 autofill_hints: Vec::new(),
268 scroll_padding: None,
269 capture_tab: false,
270 auto_indent: false,
271 };
272
273 if let Some(action) = &self.on_change {
274 semantics.actions.entries.push(fission_ir::ActionEntry {
275 trigger: fission_ir::semantics::ActionTrigger::Change,
276 action_id: action.id.as_u128(),
277 payload_data: Some(action.payload.clone()),
278 });
279 }
280
281 let mut sem_node = InternalIrBuilder::new(id, Op::Semantics(semantics));
282 sem_node.add_child(layout_id);
283 sem_node.build(cx)
284 }
285}