1use core::fmt::Write as _;
2use embedded_graphics_core::pixelcolor::{Rgb565, RgbColor, WebColors};
3use heapless::String;
4
5#[cfg(not(feature = "std"))]
6use crate::math::F32Ext as _;
7use crate::{
8 block::Block,
9 geometry::Rect,
10 render::{Compositor, RenderCtx, StrokeCap, StrokeStyle, TextAlign, TextStyle},
11 style::{Style, VisualState, WidgetStyle},
12 widget::{PropertyError, PropertyKey, PropertyValue, Widget},
13};
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17pub enum ScaleMode {
18 LinearHorizontal,
19 LinearVertical,
20 Radial,
21}
22
23#[derive(Clone, Copy, Debug, PartialEq)]
25pub struct ScaleWidget {
26 pub mode: ScaleMode,
27 pub value: f32,
28 pub min: f32,
29 pub max: f32,
30 pub major_ticks: u8,
31 pub minor_ticks: u8,
32 pub start_angle: i16,
33 pub end_angle: i16,
34 pub show_labels: bool,
35 pub show_needle: bool,
36 pub tick_color: Rgb565,
37 pub needle_color: Rgb565,
38}
39
40impl ScaleWidget {
41 pub fn new(min: f32, max: f32, value: f32) -> Self {
42 Self {
43 mode: ScaleMode::Radial,
44 value: value.clamp(min, max),
45 min,
46 max,
47 major_ticks: 5,
48 minor_ticks: 3,
49 start_angle: 135,
50 end_angle: 45,
51 show_labels: true,
52 show_needle: true,
53 tick_color: Rgb565::CSS_GRAY,
54 needle_color: Rgb565::CSS_RED,
55 }
56 }
57
58 pub fn linear_horizontal(min: f32, max: f32, value: f32) -> Self {
59 Self {
60 mode: ScaleMode::LinearHorizontal,
61 ..Self::new(min, max, value)
62 }
63 }
64
65 pub fn linear_vertical(min: f32, max: f32, value: f32) -> Self {
66 Self {
67 mode: ScaleMode::LinearVertical,
68 ..Self::new(min, max, value)
69 }
70 }
71
72 pub fn with_ticks(mut self, major: u8, minor: u8) -> Self {
73 self.major_ticks = major.max(1);
74 self.minor_ticks = minor.max(1);
75 self
76 }
77
78 pub fn with_angles(mut self, start_deg: i16, end_deg: i16) -> Self {
79 self.start_angle = start_deg;
80 self.end_angle = end_deg;
81 self
82 }
83
84 pub fn with_labels(mut self, show: bool) -> Self {
85 self.show_labels = show;
86 self
87 }
88
89 pub fn with_needle(mut self, show: bool, color: Rgb565) -> Self {
90 self.show_needle = show;
91 self.needle_color = color;
92 self
93 }
94
95 pub fn render<D, C>(
96 &self,
97 ctx: &mut RenderCtx<'_, D, C>,
98 rect: Rect,
99 style: WidgetStyle,
100 state: VisualState,
101 ) -> Result<(), D::Error>
102 where
103 D: embedded_graphics_core::draw_target::DrawTarget<Color = Rgb565>,
104 C: Compositor<D>,
105 {
106 let resolved = style.resolve(state);
107 let block = Block::styled(resolved);
108 block.render(rect, ctx)?;
109 let inner = block.inner(rect);
110
111 match self.mode {
112 ScaleMode::LinearHorizontal => self.render_linear_horizontal(ctx, inner, resolved),
113 ScaleMode::LinearVertical => self.render_linear_vertical(ctx, inner, resolved),
114 ScaleMode::Radial => self.render_radial(ctx, inner, resolved),
115 }
116 }
117
118 fn render_linear_horizontal<D, C>(
119 &self,
120 ctx: &mut RenderCtx<'_, D, C>,
121 inner: Rect,
122 style: Style,
123 ) -> Result<(), D::Error>
124 where
125 D: embedded_graphics_core::draw_target::DrawTarget<Color = Rgb565>,
126 C: Compositor<D>,
127 {
128 let total_steps = (self.major_ticks as u32).saturating_mul(self.minor_ticks as u32);
129 let baseline_y = inner.y + (inner.h as i32 * 2 / 3);
130
131 ctx.draw_line_styled(
133 inner.x,
134 baseline_y,
135 inner.right(),
136 baseline_y,
137 StrokeStyle::new(self.tick_color).with_width(1),
138 )?;
139
140 let range = (self.max - self.min).max(f32::EPSILON);
141 for step in 0..=total_steps {
142 let t = step as f32 / total_steps as f32;
143 let x = inner.x + (t * (inner.w as f32)) as i32;
144 let is_major = step % (self.minor_ticks as u32) == 0;
145 let tick_len = if is_major { 8 } else { 4 };
146
147 ctx.draw_line_styled(
148 x,
149 baseline_y,
150 x,
151 baseline_y - tick_len,
152 StrokeStyle::new(self.tick_color).with_width(if is_major { 2 } else { 1 }),
153 )?;
154
155 if is_major && self.show_labels {
156 let val = (self.min + t * range).round() as i32;
157 let mut label: String<8> = String::new();
158 let _ = write!(&mut label, "{}", val);
159 ctx.draw_text_in(
160 Rect::new(x - 15, baseline_y - 20, 30, style.font.line_height()),
161 label.as_str(),
162 TextStyle::new(style.text)
163 .with_font(style.font)
164 .with_align(TextAlign::Center),
165 )?;
166 }
167 }
168
169 if self.show_needle {
171 let t = ((self.value - self.min) / range).clamp(0.0, 1.0);
172 let nx = inner.x + (t * (inner.w as f32)) as i32;
173 ctx.draw_line_styled(
174 nx,
175 baseline_y - 12,
176 nx,
177 baseline_y + 6,
178 StrokeStyle::new(self.needle_color)
179 .with_width(2)
180 .with_cap(StrokeCap::Round),
181 )?;
182 ctx.fill_circle(nx, baseline_y, 3, self.needle_color)?;
183 }
184
185 Ok(())
186 }
187
188 fn render_linear_vertical<D, C>(
189 &self,
190 ctx: &mut RenderCtx<'_, D, C>,
191 inner: Rect,
192 style: Style,
193 ) -> Result<(), D::Error>
194 where
195 D: embedded_graphics_core::draw_target::DrawTarget<Color = Rgb565>,
196 C: Compositor<D>,
197 {
198 let total_steps = (self.major_ticks as u32).saturating_mul(self.minor_ticks as u32);
199 let baseline_x = inner.x + (inner.w as i32 / 3);
200
201 ctx.draw_line_styled(
202 baseline_x,
203 inner.y,
204 baseline_x,
205 inner.bottom(),
206 StrokeStyle::new(self.tick_color).with_width(1),
207 )?;
208
209 let range = (self.max - self.min).max(f32::EPSILON);
210 for step in 0..=total_steps {
211 let t = step as f32 / total_steps as f32;
212 let y = inner.bottom() - (t * (inner.h as f32)) as i32;
213 let is_major = step % (self.minor_ticks as u32) == 0;
214 let tick_len = if is_major { 8 } else { 4 };
215
216 ctx.draw_line_styled(
217 baseline_x,
218 y,
219 baseline_x + tick_len,
220 y,
221 StrokeStyle::new(self.tick_color).with_width(if is_major { 2 } else { 1 }),
222 )?;
223
224 if is_major && self.show_labels {
225 let val = (self.min + t * range).round() as i32;
226 let mut label: String<8> = String::new();
227 let _ = write!(&mut label, "{}", val);
228 ctx.draw_text_in(
229 Rect::new(baseline_x + 12, y - 4, 30, style.font.line_height()),
230 label.as_str(),
231 TextStyle::new(style.text).with_font(style.font),
232 )?;
233 }
234 }
235
236 if self.show_needle {
237 let t = ((self.value - self.min) / range).clamp(0.0, 1.0);
238 let ny = inner.bottom() - (t * (inner.h as f32)) as i32;
239 ctx.draw_line_styled(
240 baseline_x - 4,
241 ny,
242 baseline_x + 12,
243 ny,
244 StrokeStyle::new(self.needle_color)
245 .with_width(2)
246 .with_cap(StrokeCap::Round),
247 )?;
248 ctx.fill_circle(baseline_x, ny, 3, self.needle_color)?;
249 }
250
251 Ok(())
252 }
253
254 fn render_radial<D, C>(
255 &self,
256 ctx: &mut RenderCtx<'_, D, C>,
257 inner: Rect,
258 style: Style,
259 ) -> Result<(), D::Error>
260 where
261 D: embedded_graphics_core::draw_target::DrawTarget<Color = Rgb565>,
262 C: Compositor<D>,
263 {
264 let cx = inner.x + (inner.w as i32 / 2);
265 let cy = inner.y + (inner.h as i32 / 2);
266 let radius = (inner.w.min(inner.h) / 2).saturating_sub(4);
267 if radius < 8 {
268 return Ok(());
269 }
270
271 let total_steps = (self.major_ticks as u32).saturating_mul(self.minor_ticks as u32);
272 let sweep = (self.end_angle - self.start_angle) as f32;
273 let range = (self.max - self.min).max(f32::EPSILON);
274
275 for step in 0..=total_steps {
276 let t = step as f32 / total_steps as f32;
277 let angle = (self.start_angle as f32 + sweep * t).to_radians();
278 let is_major = step % (self.minor_ticks as u32) == 0;
279 let tick_len = if is_major { 8 } else { 4 };
280
281 let ox = cx + (radius as f32 * angle.cos()) as i32;
282 let oy = cy + (radius as f32 * angle.sin()) as i32;
283 let ix = cx + ((radius.saturating_sub(tick_len)) as f32 * angle.cos()) as i32;
284 let iy = cy + ((radius.saturating_sub(tick_len)) as f32 * angle.sin()) as i32;
285
286 ctx.draw_line_styled(
287 ix,
288 iy,
289 ox,
290 oy,
291 StrokeStyle::new(self.tick_color).with_width(if is_major { 2 } else { 1 }),
292 )?;
293
294 if is_major && self.show_labels && radius > 20 {
295 let val = (self.min + t * range).round() as i32;
296 let mut label: String<8> = String::new();
297 let _ = write!(&mut label, "{}", val);
298 let lx = cx + ((radius.saturating_sub(18)) as f32 * angle.cos()) as i32;
299 let ly = cy + ((radius.saturating_sub(18)) as f32 * angle.sin()) as i32;
300 ctx.draw_text_in(
301 Rect::new(lx - 12, ly - 6, 24, 12),
302 label.as_str(),
303 TextStyle::new(style.text)
304 .with_font(style.font)
305 .with_align(TextAlign::Center),
306 )?;
307 }
308 }
309
310 if self.show_needle {
312 let t = ((self.value - self.min) / range).clamp(0.0, 1.0);
313 let needle_angle = (self.start_angle as f32 + sweep * t).to_radians();
314 let needle_len = radius.saturating_sub(6) as f32;
315 let nx = cx + (needle_len * needle_angle.cos()) as i32;
316 let ny = cy + (needle_len * needle_angle.sin()) as i32;
317
318 ctx.draw_line_styled(
319 cx,
320 cy,
321 nx,
322 ny,
323 StrokeStyle::new(self.needle_color)
324 .with_width(2)
325 .with_cap(StrokeCap::Round),
326 )?;
327 ctx.fill_circle(cx, cy, 4, self.needle_color)?;
328 ctx.stroke_circle(cx, cy, 4, Rgb565::WHITE)?;
329 }
330
331 Ok(())
332 }
333}
334
335impl Widget for ScaleWidget {
336 fn render_widget_bounds(&self, _bounds: Rect, _style: &Style) {}
337
338 fn get_property(&self, key: PropertyKey) -> Option<PropertyValue<'_>> {
339 match key {
340 PropertyKey::Value => Some(PropertyValue::Float(self.value)),
341 PropertyKey::Min => Some(PropertyValue::Float(self.min)),
342 PropertyKey::Max => Some(PropertyValue::Float(self.max)),
343 _ => None,
344 }
345 }
346
347 fn set_property<'a>(
348 &mut self,
349 key: PropertyKey,
350 val: PropertyValue<'a>,
351 ) -> Result<(), PropertyError> {
352 match (key, val) {
353 (PropertyKey::Value, PropertyValue::Float(v)) => {
354 self.value = v.clamp(self.min, self.max);
355 Ok(())
356 }
357 (PropertyKey::Min, PropertyValue::Float(m)) => {
358 self.min = m;
359 Ok(())
360 }
361 (PropertyKey::Max, PropertyValue::Float(m)) => {
362 self.max = m;
363 Ok(())
364 }
365 _ => Err(PropertyError::NotFound),
366 }
367 }
368}