use super::Widget;
use alloc::{format, string::String};
use core::marker::PhantomData;
use embedded_graphics::{
mono_font::MonoFont, pixelcolor::PixelColor, prelude::*, primitives::Rectangle, text::Alignment,
};
use zest_core::{Constraints, Length, RenderError, Renderer, TouchPhase, arc_sin_cos};
use zest_theme::Theme;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum ScaleMode {
Linear,
Circular,
}
pub struct Scale<'a, C: PixelColor, M: Clone> {
rect: Rectangle,
mode: ScaleMode,
min: f32,
max: f32,
major_ticks: u32,
minor_per_major: u32,
labels: bool,
start_deg: i32,
sweep_deg: i32,
marker_value: Option<f32>,
color: Option<C>,
label_color: Option<C>,
marker_color: Option<C>,
font: Option<&'a MonoFont<'a>>,
w: Length,
h: Length,
_phantom: PhantomData<M>,
}
impl<'a, C: PixelColor, M: Clone> Scale<'a, C, M> {
pub fn new(min: f32, max: f32) -> Self {
let (min, max) = if min <= max { (min, max) } else { (max, min) };
Self {
rect: Rectangle::zero(),
mode: ScaleMode::Linear,
min,
max,
major_ticks: 5,
minor_per_major: 4,
labels: true,
start_deg: 225,
sweep_deg: -270,
marker_value: None,
color: None,
label_color: None,
marker_color: None,
font: None,
w: Length::Fill,
h: Length::Fill,
_phantom: PhantomData,
}
}
#[must_use]
pub fn mode(mut self, mode: ScaleMode) -> Self {
self.mode = mode;
self
}
#[must_use]
pub fn major_ticks(mut self, n: u32) -> Self {
self.major_ticks = n.max(1);
self
}
#[must_use]
pub fn minor_per_major(mut self, n: u32) -> Self {
self.minor_per_major = n;
self
}
#[must_use]
pub fn labels(mut self, on: bool) -> Self {
self.labels = on;
self
}
#[must_use]
pub fn start_deg(mut self, start_deg: i32) -> Self {
self.start_deg = start_deg;
self
}
#[must_use]
pub fn sweep_deg(mut self, sweep_deg: i32) -> Self {
self.sweep_deg = sweep_deg;
self
}
#[must_use]
pub fn value_marker(mut self, value: f32) -> Self {
self.marker_value = Some(value);
self
}
#[must_use]
pub fn color(mut self, color: C) -> Self {
self.color = Some(color);
self
}
#[must_use]
pub fn label_color(mut self, color: C) -> Self {
self.label_color = Some(color);
self
}
#[must_use]
pub fn marker_color(mut self, color: C) -> Self {
self.marker_color = Some(color);
self
}
#[must_use]
pub fn font(mut self, font: &'a MonoFont<'a>) -> Self {
self.font = Some(font);
self
}
#[must_use]
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.w = width.into();
self
}
#[must_use]
pub fn height(mut self, height: impl Into<Length>) -> Self {
self.h = height.into();
self
}
fn value_at(&self, i: u32) -> f32 {
let t = i as f32 / self.major_ticks as f32;
self.min + (self.max - self.min) * t
}
fn label_for(v: f32) -> String {
if (v - libm_round(v)).abs() < 0.05 {
format!("{}", libm_round(v) as i64)
} else {
format!("{v:.1}")
}
}
fn total_ticks(&self) -> u32 {
self.major_ticks * (self.minor_per_major + 1)
}
fn marker_fraction(&self) -> Option<f32> {
let value = self.marker_value?;
let span = self.max - self.min;
if span <= 0.0 {
Some(0.0)
} else {
Some(((value - self.min) / span).clamp(0.0, 1.0))
}
}
}
fn libm_round(v: f32) -> f32 {
if v >= 0.0 {
(v + 0.5) as i64 as f32
} else {
(v - 0.5) as i64 as f32
}
}
impl<'a, C: PixelColor, M: Clone> Widget<C, M> for Scale<'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 tick = self.color.unwrap_or(theme.background.on_base);
let label_color = self.label_color.unwrap_or(theme.palette.neutral_2);
let marker = self.marker_color.unwrap_or(theme.accent.base);
let font = self.font.unwrap_or(theme.typography.caption);
match self.mode {
ScaleMode::Linear => self.draw_linear(renderer, tick, label_color, marker, font),
ScaleMode::Circular => self.draw_circular(renderer, tick, label_color, marker, font),
}
}
}
impl<'a, C: PixelColor, M: Clone> Scale<'a, C, M> {
fn draw_linear(
&self,
renderer: &mut dyn Renderer<C>,
tick: C,
label_color: C,
marker: C,
font: &MonoFont<'_>,
) -> Result<(), RenderError> {
let r = self.rect;
if r.size.width == 0 || r.size.height == 0 {
return Ok(());
}
let major_len = (r.size.height / 3).max(4) as i32;
let minor_len = (major_len / 2).max(2);
let baseline_y = r.top_left.y + 1;
let left = r.top_left.x;
let width = r.size.width.saturating_sub(1) as i32;
renderer.stroke_line(
Point::new(left, baseline_y),
Point::new(left + width, baseline_y),
tick,
1,
)?;
let total = self.total_ticks();
for i in 0..=total {
let is_major = i % (self.minor_per_major + 1) == 0;
let x = left + (width * i as i32) / total as i32;
let len = if is_major { major_len } else { minor_len };
renderer.stroke_line(
Point::new(x, baseline_y),
Point::new(x, baseline_y + len),
tick,
1,
)?;
if is_major && self.labels {
let major_index = i / (self.minor_per_major + 1);
let text = Self::label_for(self.value_at(major_index));
let label_y = baseline_y + major_len + 2 + font.character_size.height as i32;
renderer.draw_text(
&text,
Point::new(x, label_y),
font,
label_color,
Alignment::Center,
)?;
}
}
if let Some(frac) = self.marker_fraction() {
let x = left + (width as f32 * frac) as i32;
renderer.stroke_line(
Point::new(x, baseline_y.saturating_sub(3)),
Point::new(x, baseline_y + major_len + 4),
marker,
2,
)?;
}
Ok(())
}
fn draw_circular(
&self,
renderer: &mut dyn Renderer<C>,
tick: C,
label_color: C,
marker: C,
font: &MonoFont<'_>,
) -> Result<(), RenderError> {
let r = self.rect;
let center = Point::new(
r.top_left.x + r.size.width as i32 / 2,
r.top_left.y + r.size.height as i32 / 2,
);
let smaller = r.size.width.min(r.size.height);
let outer = (smaller / 2).saturating_sub(2);
if outer == 0 {
return Ok(());
}
let major_len = (outer / 6).max(4);
let minor_len = (major_len / 2).max(2);
renderer.stroke_arc(center, outer, self.start_deg, self.sweep_deg, 2, tick)?;
let total = self.total_ticks();
let outer_r = outer as f32;
let label_r = (outer as f32) - major_len as f32 - font.character_size.height as f32;
for i in 0..=total {
let is_major = i % (self.minor_per_major + 1) == 0;
let frac = i as f32 / total as f32;
let deg = self.start_deg + (self.sweep_deg as f32 * frac) as i32;
let (s, c) = arc_sin_cos(deg);
let inner_r = outer_r - if is_major { major_len } else { minor_len } as f32;
let p_outer = Point::new(
center.x + (c * outer_r) as i32,
center.y - (s * outer_r) as i32,
);
let p_inner = Point::new(
center.x + (c * inner_r) as i32,
center.y - (s * inner_r) as i32,
);
renderer.stroke_line(p_outer, p_inner, tick, 1)?;
if is_major && self.labels && label_r > 0.0 {
let major_index = i / (self.minor_per_major + 1);
let text = Self::label_for(self.value_at(major_index));
let lp = Point::new(
center.x + (c * label_r) as i32,
center.y - (s * label_r) as i32 + font.character_size.height as i32 / 3,
);
renderer.draw_text(&text, lp, font, label_color, Alignment::Center)?;
}
}
if let Some(frac) = self.marker_fraction() {
let deg = self.start_deg + (self.sweep_deg as f32 * frac) as i32;
let (s, c) = arc_sin_cos(deg);
let marker_outer = outer_r;
let marker_inner = (outer_r - major_len as f32 - 6.0).max(0.0);
let p_outer = Point::new(
center.x + (c * marker_outer) as i32,
center.y - (s * marker_outer) as i32,
);
let p_inner = Point::new(
center.x + (c * marker_inner) as i32,
center.y - (s * marker_inner) as i32,
);
renderer.stroke_line(p_outer, p_inner, marker, 2)?;
}
Ok(())
}
}