1use super::Widget;
18use alloc::{format, string::String};
19use core::marker::PhantomData;
20use embedded_graphics::{
21 mono_font::MonoFont, pixelcolor::PixelColor, prelude::*, primitives::Rectangle, text::Alignment,
22};
23use zest_core::{Constraints, Length, RenderError, Renderer, TouchPhase, arc_sin_cos};
24use zest_theme::Theme;
25
26#[derive(Copy, Clone, Debug, PartialEq, Eq)]
28pub enum ScaleMode {
29 Linear,
31 Circular,
33}
34
35pub struct Scale<'a, C: PixelColor, M: Clone> {
37 rect: Rectangle,
38 mode: ScaleMode,
39 min: f32,
40 max: f32,
41 major_ticks: u32,
43 minor_per_major: u32,
45 labels: bool,
47 start_deg: i32,
49 sweep_deg: i32,
50 marker_value: Option<f32>,
51 color: Option<C>,
52 label_color: Option<C>,
53 marker_color: Option<C>,
54 font: Option<&'a MonoFont<'a>>,
55 w: Length,
56 h: Length,
57 _phantom: PhantomData<M>,
58}
59
60impl<'a, C: PixelColor, M: Clone> Scale<'a, C, M> {
61 pub fn new(min: f32, max: f32) -> Self {
64 let (min, max) = if min <= max { (min, max) } else { (max, min) };
65 Self {
66 rect: Rectangle::zero(),
67 mode: ScaleMode::Linear,
68 min,
69 max,
70 major_ticks: 5,
71 minor_per_major: 4,
72 labels: true,
73 start_deg: 225,
74 sweep_deg: -270,
75 marker_value: None,
76 color: None,
77 label_color: None,
78 marker_color: None,
79 font: None,
80 w: Length::Fill,
81 h: Length::Fill,
82 _phantom: PhantomData,
83 }
84 }
85
86 #[must_use]
88 pub fn mode(mut self, mode: ScaleMode) -> Self {
89 self.mode = mode;
90 self
91 }
92
93 #[must_use]
96 pub fn major_ticks(mut self, n: u32) -> Self {
97 self.major_ticks = n.max(1);
98 self
99 }
100
101 #[must_use]
103 pub fn minor_per_major(mut self, n: u32) -> Self {
104 self.minor_per_major = n;
105 self
106 }
107
108 #[must_use]
110 pub fn labels(mut self, on: bool) -> Self {
111 self.labels = on;
112 self
113 }
114
115 #[must_use]
117 pub fn start_deg(mut self, start_deg: i32) -> Self {
118 self.start_deg = start_deg;
119 self
120 }
121
122 #[must_use]
124 pub fn sweep_deg(mut self, sweep_deg: i32) -> Self {
125 self.sweep_deg = sweep_deg;
126 self
127 }
128
129 #[must_use]
131 pub fn value_marker(mut self, value: f32) -> Self {
132 self.marker_value = Some(value);
133 self
134 }
135
136 #[must_use]
139 pub fn color(mut self, color: C) -> Self {
140 self.color = Some(color);
141 self
142 }
143
144 #[must_use]
146 pub fn label_color(mut self, color: C) -> Self {
147 self.label_color = Some(color);
148 self
149 }
150
151 #[must_use]
153 pub fn marker_color(mut self, color: C) -> Self {
154 self.marker_color = Some(color);
155 self
156 }
157
158 #[must_use]
160 pub fn font(mut self, font: &'a MonoFont<'a>) -> Self {
161 self.font = Some(font);
162 self
163 }
164
165 #[must_use]
167 pub fn width(mut self, width: impl Into<Length>) -> Self {
168 self.w = width.into();
169 self
170 }
171
172 #[must_use]
174 pub fn height(mut self, height: impl Into<Length>) -> Self {
175 self.h = height.into();
176 self
177 }
178
179 fn value_at(&self, i: u32) -> f32 {
181 let t = i as f32 / self.major_ticks as f32;
182 self.min + (self.max - self.min) * t
183 }
184
185 fn label_for(v: f32) -> String {
187 if (v - libm_round(v)).abs() < 0.05 {
188 format!("{}", libm_round(v) as i64)
189 } else {
190 format!("{v:.1}")
191 }
192 }
193
194 fn total_ticks(&self) -> u32 {
196 self.major_ticks * (self.minor_per_major + 1)
197 }
198
199 fn marker_fraction(&self) -> Option<f32> {
200 let value = self.marker_value?;
201 let span = self.max - self.min;
202 if span <= 0.0 {
203 Some(0.0)
204 } else {
205 Some(((value - self.min) / span).clamp(0.0, 1.0))
206 }
207 }
208}
209
210fn libm_round(v: f32) -> f32 {
213 if v >= 0.0 {
214 (v + 0.5) as i64 as f32
215 } else {
216 (v - 0.5) as i64 as f32
217 }
218}
219
220impl<'a, C: PixelColor, M: Clone> Widget<C, M> for Scale<'a, C, M> {
221 fn measure(&mut self, constraints: Constraints) -> Size {
222 let w = self.w.resolve(constraints.max.width, constraints.max.width);
223 let h = self
224 .h
225 .resolve(constraints.max.height, constraints.max.height);
226 constraints.clamp(Size::new(w, h))
227 }
228
229 fn preferred_size(&self) -> (Length, Length) {
230 (self.w, self.h)
231 }
232
233 fn arrange(&mut self, rect: Rectangle) {
234 self.rect = rect;
235 }
236
237 fn rect(&self) -> Rectangle {
238 self.rect
239 }
240
241 fn handle_touch(&mut self, _point: Point, _phase: TouchPhase) -> Option<M> {
242 None
243 }
244
245 fn draw<'t>(
246 &self,
247 renderer: &mut dyn Renderer<C>,
248 theme: &Theme<'t, C>,
249 ) -> Result<(), RenderError> {
250 let tick = self.color.unwrap_or(theme.background.on_base);
251 let label_color = self.label_color.unwrap_or(theme.palette.neutral_2);
252 let marker = self.marker_color.unwrap_or(theme.accent.base);
253 let font = self.font.unwrap_or(theme.typography.caption);
254
255 match self.mode {
256 ScaleMode::Linear => self.draw_linear(renderer, tick, label_color, marker, font),
257 ScaleMode::Circular => self.draw_circular(renderer, tick, label_color, marker, font),
258 }
259 }
260}
261
262impl<'a, C: PixelColor, M: Clone> Scale<'a, C, M> {
263 fn draw_linear(
264 &self,
265 renderer: &mut dyn Renderer<C>,
266 tick: C,
267 label_color: C,
268 marker: C,
269 font: &MonoFont<'_>,
270 ) -> Result<(), RenderError> {
271 let r = self.rect;
272 if r.size.width == 0 || r.size.height == 0 {
273 return Ok(());
274 }
275 let major_len = (r.size.height / 3).max(4) as i32;
276 let minor_len = (major_len / 2).max(2);
277 let baseline_y = r.top_left.y + 1;
279 let left = r.top_left.x;
280 let width = r.size.width.saturating_sub(1) as i32;
281
282 renderer.stroke_line(
284 Point::new(left, baseline_y),
285 Point::new(left + width, baseline_y),
286 tick,
287 1,
288 )?;
289
290 let total = self.total_ticks();
291 for i in 0..=total {
292 let is_major = i % (self.minor_per_major + 1) == 0;
293 let x = left + (width * i as i32) / total as i32;
294 let len = if is_major { major_len } else { minor_len };
295 renderer.stroke_line(
296 Point::new(x, baseline_y),
297 Point::new(x, baseline_y + len),
298 tick,
299 1,
300 )?;
301 if is_major && self.labels {
302 let major_index = i / (self.minor_per_major + 1);
303 let text = Self::label_for(self.value_at(major_index));
304 let label_y = baseline_y + major_len + 2 + font.character_size.height as i32;
305 renderer.draw_text(
306 &text,
307 Point::new(x, label_y),
308 font,
309 label_color,
310 Alignment::Center,
311 )?;
312 }
313 }
314
315 if let Some(frac) = self.marker_fraction() {
316 let x = left + (width as f32 * frac) as i32;
317 renderer.stroke_line(
318 Point::new(x, baseline_y.saturating_sub(3)),
319 Point::new(x, baseline_y + major_len + 4),
320 marker,
321 2,
322 )?;
323 }
324 Ok(())
325 }
326
327 fn draw_circular(
328 &self,
329 renderer: &mut dyn Renderer<C>,
330 tick: C,
331 label_color: C,
332 marker: C,
333 font: &MonoFont<'_>,
334 ) -> Result<(), RenderError> {
335 let r = self.rect;
336 let center = Point::new(
337 r.top_left.x + r.size.width as i32 / 2,
338 r.top_left.y + r.size.height as i32 / 2,
339 );
340 let smaller = r.size.width.min(r.size.height);
341 let outer = (smaller / 2).saturating_sub(2);
342 if outer == 0 {
343 return Ok(());
344 }
345 let major_len = (outer / 6).max(4);
346 let minor_len = (major_len / 2).max(2);
347
348 renderer.stroke_arc(center, outer, self.start_deg, self.sweep_deg, 2, tick)?;
350
351 let total = self.total_ticks();
352 let outer_r = outer as f32;
353 let label_r = (outer as f32) - major_len as f32 - font.character_size.height as f32;
355
356 for i in 0..=total {
357 let is_major = i % (self.minor_per_major + 1) == 0;
358 let frac = i as f32 / total as f32;
359 let deg = self.start_deg + (self.sweep_deg as f32 * frac) as i32;
360 let (s, c) = arc_sin_cos(deg);
361 let inner_r = outer_r - if is_major { major_len } else { minor_len } as f32;
362
363 let p_outer = Point::new(
364 center.x + (c * outer_r) as i32,
365 center.y - (s * outer_r) as i32,
366 );
367 let p_inner = Point::new(
368 center.x + (c * inner_r) as i32,
369 center.y - (s * inner_r) as i32,
370 );
371 renderer.stroke_line(p_outer, p_inner, tick, 1)?;
372
373 if is_major && self.labels && label_r > 0.0 {
374 let major_index = i / (self.minor_per_major + 1);
375 let text = Self::label_for(self.value_at(major_index));
376 let lp = Point::new(
377 center.x + (c * label_r) as i32,
378 center.y - (s * label_r) as i32 + font.character_size.height as i32 / 3,
380 );
381 renderer.draw_text(&text, lp, font, label_color, Alignment::Center)?;
382 }
383 }
384
385 if let Some(frac) = self.marker_fraction() {
386 let deg = self.start_deg + (self.sweep_deg as f32 * frac) as i32;
387 let (s, c) = arc_sin_cos(deg);
388 let marker_outer = outer_r;
389 let marker_inner = (outer_r - major_len as f32 - 6.0).max(0.0);
390 let p_outer = Point::new(
391 center.x + (c * marker_outer) as i32,
392 center.y - (s * marker_outer) as i32,
393 );
394 let p_inner = Point::new(
395 center.x + (c * marker_inner) as i32,
396 center.y - (s * marker_inner) as i32,
397 );
398 renderer.stroke_line(p_outer, p_inner, marker, 2)?;
399 }
400 Ok(())
401 }
402}