Skip to main content

embedded_gui/widgets/
scale.rs

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/// Orientation and mode of the graduated scale.
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17pub enum ScaleMode {
18    LinearHorizontal,
19    LinearVertical,
20    Radial,
21}
22
23/// Graduated scale widget with customizable major/minor ticks, labels, and needle indicator.
24#[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        // Draw baseline
132        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        // Draw needle pointer
170        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        // Draw center pivot and needle pointer
311        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}