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