Skip to main content

zest_widget/widget/
arc.rs

1//! Passive arc gauge: a background track arc with a value arc drawn on
2//! top, sweeping from a start angle toward an end angle in proportion to
3//! a value within `min..=max`.
4//!
5//! The center and radius are derived from the arranged rectangle: the arc
6//! is centered in the rect and the radius is half the smaller dimension
7//! (inset by the stroke width so the stroke stays inside the bounds).
8//! Angles follow the [`Renderer::stroke_arc`] convention — 0° points
9//! right and a positive sweep travels counter-clockwise (toward the
10//! top). The widget is non-interactive.
11
12use super::Widget;
13use core::marker::PhantomData;
14use embedded_graphics::{pixelcolor::PixelColor, prelude::*, primitives::Rectangle};
15use zest_core::{Constraints, Length, RenderError, Renderer, TouchPhase};
16use zest_theme::Theme;
17
18/// Passive value arc with a background track.
19pub struct Arc<'a, C: PixelColor, M: Clone> {
20    rect: Rectangle,
21    /// Current value, clamped to `min..=max` at draw time.
22    value: f32,
23    min: f32,
24    max: f32,
25    /// Angle (degrees) at which the track/value arc begins.
26    start_deg: i32,
27    /// Total sweep (degrees) the full range maps to. Positive sweeps
28    /// counter-clockwise; negative sweeps clockwise.
29    sweep_deg: i32,
30    /// Stroke thickness of both arcs, in pixels.
31    width: u32,
32    track_color: Option<C>,
33    value_color: Option<C>,
34    w: Length,
35    h: Length,
36    _phantom: PhantomData<&'a M>,
37}
38
39impl<'a, C: PixelColor, M: Clone> Arc<'a, C, M> {
40    /// New arc displaying `value` within `min..=max`. Defaults: a 270°
41    /// sweep starting at 225° (a bottom-gap gauge), 6px stroke, fills
42    /// its slot. Theme colors are used unless overridden.
43    pub fn new(value: f32, min: f32, max: f32) -> Self {
44        Self {
45            rect: Rectangle::zero(),
46            value,
47            min,
48            max,
49            start_deg: 225,
50            sweep_deg: -270,
51            width: 6,
52            track_color: None,
53            value_color: None,
54            w: Length::Fill,
55            h: Length::Fill,
56            _phantom: PhantomData,
57        }
58    }
59
60    /// Set the value (clamped to `min..=max` at draw time).
61    #[must_use]
62    pub fn value(mut self, value: f32) -> Self {
63        self.value = value;
64        self
65    }
66
67    /// The value range. Reordered so `min <= max`.
68    #[must_use]
69    pub fn range(mut self, min: f32, max: f32) -> Self {
70        if min <= max {
71            self.min = min;
72            self.max = max;
73        } else {
74            self.min = max;
75            self.max = min;
76        }
77        self
78    }
79
80    /// Starting angle in degrees (0° points right).
81    #[must_use]
82    pub fn start_deg(mut self, start_deg: i32) -> Self {
83        self.start_deg = start_deg;
84        self
85    }
86
87    /// Total sweep in degrees that the full range spans.
88    /// Positive sweeps counter-clockwise; negative clockwise.
89    #[must_use]
90    pub fn sweep_deg(mut self, sweep_deg: i32) -> Self {
91        self.sweep_deg = sweep_deg;
92        self
93    }
94
95    /// Stroke thickness of both arcs in pixels.
96    #[must_use]
97    pub fn width_px(mut self, width: u32) -> Self {
98        self.width = width;
99        self
100    }
101
102    /// Override the background-track color (default:
103    /// `theme.background.divider`).
104    #[must_use]
105    pub fn track_color(mut self, color: C) -> Self {
106        self.track_color = Some(color);
107        self
108    }
109
110    /// Override the value-arc color (default: `theme.accent.base`).
111    #[must_use]
112    pub fn value_color(mut self, color: C) -> Self {
113        self.value_color = Some(color);
114        self
115    }
116
117    /// Width sizing intent.
118    #[must_use]
119    pub fn width(mut self, width: impl Into<Length>) -> Self {
120        self.w = width.into();
121        self
122    }
123
124    /// Height sizing intent.
125    #[must_use]
126    pub fn height(mut self, height: impl Into<Length>) -> Self {
127        self.h = height.into();
128        self
129    }
130
131    /// Fraction of the range the current value represents, in `0.0..=1.0`.
132    fn fraction(&self) -> f32 {
133        let span = self.max - self.min;
134        if span <= 0.0 {
135            return 0.0;
136        }
137        ((self.value - self.min) / span).clamp(0.0, 1.0)
138    }
139
140    /// Center point of the arc within the arranged rect.
141    fn center(&self) -> Point {
142        Point::new(
143            self.rect.top_left.x + self.rect.size.width as i32 / 2,
144            self.rect.top_left.y + self.rect.size.height as i32 / 2,
145        )
146    }
147
148    /// Radius derived from the rect, inset so the stroke stays inside.
149    fn radius(&self) -> u32 {
150        let smaller = self.rect.size.width.min(self.rect.size.height);
151        (smaller / 2).saturating_sub(self.width.div_ceil(2))
152    }
153}
154
155impl<'a, C: PixelColor, M: Clone> Widget<C, M> for Arc<'a, C, M> {
156    fn measure(&mut self, constraints: Constraints) -> Size {
157        let w = self.w.resolve(constraints.max.width, constraints.max.width);
158        let h = self
159            .h
160            .resolve(constraints.max.height, constraints.max.height);
161        constraints.clamp(Size::new(w, h))
162    }
163
164    fn preferred_size(&self) -> (Length, Length) {
165        (self.w, self.h)
166    }
167
168    fn arrange(&mut self, rect: Rectangle) {
169        self.rect = rect;
170    }
171
172    fn rect(&self) -> Rectangle {
173        self.rect
174    }
175
176    fn handle_touch(&mut self, _point: Point, _phase: TouchPhase) -> Option<M> {
177        None
178    }
179
180    fn draw<'t>(
181        &self,
182        renderer: &mut dyn Renderer<C>,
183        theme: &Theme<'t, C>,
184    ) -> Result<(), RenderError> {
185        let center = self.center();
186        let radius = self.radius();
187        if radius == 0 {
188            return Ok(());
189        }
190
191        let track = self.track_color.unwrap_or(theme.background.divider);
192        let value = self.value_color.unwrap_or(theme.accent.base);
193
194        // Background track spans the full range.
195        renderer.stroke_arc(
196            center,
197            radius,
198            self.start_deg,
199            self.sweep_deg,
200            self.width,
201            track,
202        )?;
203
204        // Value arc spans a fraction of the sweep.
205        let value_sweep = (self.sweep_deg as f32 * self.fraction()) as i32;
206        if value_sweep != 0 {
207            renderer.stroke_arc(
208                center,
209                radius,
210                self.start_deg,
211                value_sweep,
212                self.width,
213                value,
214            )?;
215        }
216
217        Ok(())
218    }
219}