1use super::Widget;
16use alloc::boxed::Box;
17use core::marker::PhantomData;
18use embedded_graphics::{pixelcolor::PixelColor, prelude::*, primitives::Rectangle};
19use zest_core::{Constraints, Length, RenderError, Renderer, TouchPhase, UiAction, WidgetId};
20use zest_theme::Theme;
21
22const TRACK_THICKNESS: u32 = 6;
24const KNOB_RADIUS: u32 = 9;
26const INTRINSIC_W: u32 = 160;
28
29pub struct Slider<'a, C: PixelColor, M: Clone> {
32 rect: Rectangle,
33 id: Option<WidgetId>,
34 value: f32,
35 min: f32,
36 max: f32,
37 step: Option<f32>,
38 on_change: Option<Box<dyn Fn(f32) -> M + 'a>>,
39 focused: bool,
40 pressed: bool,
41 width: Length,
42 height: Length,
43 _color: PhantomData<C>,
44}
45
46impl<'a, C: PixelColor, M: Clone> Slider<'a, C, M> {
47 pub fn new(value: f32) -> Self {
50 Self {
51 rect: Rectangle::zero(),
52 id: None,
53 value,
54 min: 0.0,
55 max: 1.0,
56 step: None,
57 on_change: None,
58 focused: false,
59 pressed: false,
60 width: Length::Fill,
61 height: Length::Fixed(2 * KNOB_RADIUS),
62 _color: PhantomData,
63 }
64 }
65
66 #[must_use]
69 pub fn range(mut self, min: f32, max: f32) -> Self {
70 self.min = min;
71 self.max = max;
72 self
73 }
74
75 #[must_use]
77 pub fn id(mut self, id: WidgetId) -> Self {
78 self.id = Some(id);
79 self
80 }
81
82 #[must_use]
84 pub fn step(mut self, step: f32) -> Self {
85 self.step = Some(step.abs());
86 self
87 }
88
89 #[must_use]
93 pub fn on_change<F: Fn(f32) -> M + 'a>(mut self, f: F) -> Self {
94 self.on_change = Some(Box::new(f));
95 self
96 }
97
98 #[must_use]
100 pub fn width(mut self, width: impl Into<Length>) -> Self {
101 self.width = width.into();
102 self
103 }
104
105 #[must_use]
107 pub fn height(mut self, height: impl Into<Length>) -> Self {
108 self.height = height.into();
109 self
110 }
111
112 pub fn is_enabled(&self) -> bool {
115 self.on_change.is_some()
116 }
117
118 fn fraction(&self) -> f32 {
120 if self.max <= self.min {
121 0.0
122 } else {
123 ((self.value - self.min) / (self.max - self.min)).clamp(0.0, 1.0)
124 }
125 }
126
127 fn span(&self) -> (i32, i32) {
130 let left = self.rect.top_left.x + KNOB_RADIUS as i32;
131 let right = self.rect.top_left.x + self.rect.size.width as i32 - KNOB_RADIUS as i32;
132 (left, right.max(left))
133 }
134
135 fn hit_test(&self, point: Point) -> bool {
136 let tl = self.rect.top_left;
137 let br = tl + Point::new(self.rect.size.width as i32, self.rect.size.height as i32);
138 point.x >= tl.x && point.x < br.x && point.y >= tl.y && point.y < br.y
139 }
140
141 fn value_at(&self, x: i32) -> f32 {
143 let (left, right) = self.span();
144 let frac = if right <= left {
145 0.0
146 } else {
147 ((x - left) as f32 / (right - left) as f32).clamp(0.0, 1.0)
148 };
149 let v = self.min + frac * (self.max - self.min);
150 v.clamp(self.min.min(self.max), self.min.max(self.max))
151 }
152
153 fn action_step(&self) -> f32 {
154 if let Some(step) = self.step
155 && step > 0.0
156 {
157 return step;
158 }
159
160 let range = (self.max - self.min).abs();
161 if range <= f32::EPSILON {
162 0.0
163 } else {
164 (range / 20.0).max(f32::EPSILON)
165 }
166 }
167
168 fn adjusted_value(&self, delta: f32) -> f32 {
169 (self.value + delta).clamp(self.min.min(self.max), self.min.max(self.max))
170 }
171}
172
173impl<'a, C: PixelColor, M: Clone> Widget<C, M> for Slider<'a, C, M> {
174 fn measure(&mut self, constraints: Constraints) -> Size {
175 let w = self.width.resolve(INTRINSIC_W, constraints.max.width);
176 let h = self.height.resolve(2 * KNOB_RADIUS, constraints.max.height);
177 constraints.clamp(Size::new(w, h))
178 }
179
180 fn preferred_size(&self) -> (Length, Length) {
181 (self.width, self.height)
182 }
183
184 fn arrange(&mut self, rect: Rectangle) {
185 self.rect = rect;
186 }
187
188 fn rect(&self) -> Rectangle {
189 self.rect
190 }
191
192 fn handle_touch(&mut self, point: Point, phase: TouchPhase) -> Option<M> {
193 let Some(cb) = self.on_change.as_ref() else {
194 return None;
195 };
196 match phase {
197 TouchPhase::Down => {
199 if self.hit_test(point) {
200 self.pressed = true;
201 Some(cb(self.value_at(point.x)))
202 } else {
203 self.pressed = false;
204 None
205 }
206 }
207 TouchPhase::Moved => {
208 if self.hit_test(point) {
211 self.pressed = true;
212 Some(cb(self.value_at(point.x)))
213 } else {
214 self.pressed = false;
215 None
216 }
217 }
218 TouchPhase::Up => {
219 self.pressed = false;
220 None
221 }
222 }
223 }
224
225 fn mark_pressed(&mut self, point: Point) {
226 if self.is_enabled() && self.hit_test(point) {
227 self.pressed = true;
228 }
229 }
230
231 fn widget_id(&self) -> Option<WidgetId> {
232 self.id
233 }
234
235 fn is_focusable(&self) -> bool {
236 self.id.is_some() && self.is_enabled()
237 }
238
239 fn handle_action(&mut self, action: UiAction) -> Option<M> {
240 if !self.is_enabled() {
241 return None;
242 }
243
244 let step = self.action_step();
245 match action {
246 UiAction::Increment | UiAction::NavigateRight | UiAction::NavigateUp if step > 0.0 => {
247 self.on_change
248 .as_ref()
249 .map(|cb| cb(self.adjusted_value(step)))
250 }
251 UiAction::Decrement | UiAction::NavigateLeft | UiAction::NavigateDown if step > 0.0 => {
252 self.on_change
253 .as_ref()
254 .map(|cb| cb(self.adjusted_value(-step)))
255 }
256 _ => None,
257 }
258 }
259
260 fn sync_focus(&mut self, focused: Option<WidgetId>) {
261 self.focused = self.id.is_some() && self.id == focused;
262 }
263
264 fn focus_at(&self, point: Point) -> Option<WidgetId> {
265 if self.is_focusable() && self.hit_test(point) {
266 self.id
267 } else {
268 None
269 }
270 }
271
272 fn draw<'t>(
273 &self,
274 renderer: &mut dyn Renderer<C>,
275 theme: &Theme<'t, C>,
276 ) -> Result<(), RenderError> {
277 let accent = &theme.accent;
278 let (left, right) = self.span();
279 let cy = self.rect.top_left.y + self.rect.size.height as i32 / 2;
280 let track_top = cy - TRACK_THICKNESS as i32 / 2;
281
282 let track_color = theme.background.divider;
283 let fill_color = if !self.is_enabled() {
284 theme.background.divider
285 } else if self.pressed {
286 accent.pressed
287 } else {
288 accent.base
289 };
290
291 let track = Rectangle::new(
293 Point::new(left, track_top),
294 Size::new((right - left).max(0) as u32, TRACK_THICKNESS),
295 );
296 renderer.fill_rect(track, track_color)?;
297
298 let knob_x = left + ((right - left) as f32 * self.fraction()) as i32;
300
301 let filled = Rectangle::new(
303 Point::new(left, track_top),
304 Size::new((knob_x - left).max(0) as u32, TRACK_THICKNESS),
305 );
306 renderer.fill_rect(filled, fill_color)?;
307
308 let knob_color = if self.is_enabled() {
312 accent.on_base
313 } else {
314 theme.background.base
315 };
316 let center = Point::new(knob_x, cy);
317 let knob_border = if self.focused {
318 accent.base
319 } else {
320 accent.border
321 };
322 renderer.fill_circle(center, KNOB_RADIUS, knob_border)?;
323 renderer.fill_circle(center, KNOB_RADIUS.saturating_sub(2), knob_color)?;
324
325 Ok(())
326 }
327}