use super::Widget;
use alloc::boxed::Box;
use core::marker::PhantomData;
use embedded_graphics::{pixelcolor::PixelColor, prelude::*, primitives::Rectangle};
use zest_core::{Constraints, Length, RenderError, Renderer, TouchPhase, UiAction, WidgetId};
use zest_theme::Theme;
const TRACK_THICKNESS: u32 = 6;
const KNOB_RADIUS: u32 = 9;
const INTRINSIC_W: u32 = 160;
pub struct Slider<'a, C: PixelColor, M: Clone> {
rect: Rectangle,
id: Option<WidgetId>,
value: f32,
min: f32,
max: f32,
step: Option<f32>,
on_change: Option<Box<dyn Fn(f32) -> M + 'a>>,
focused: bool,
pressed: bool,
width: Length,
height: Length,
_color: PhantomData<C>,
}
impl<'a, C: PixelColor, M: Clone> Slider<'a, C, M> {
pub fn new(value: f32) -> Self {
Self {
rect: Rectangle::zero(),
id: None,
value,
min: 0.0,
max: 1.0,
step: None,
on_change: None,
focused: false,
pressed: false,
width: Length::Fill,
height: Length::Fixed(2 * KNOB_RADIUS),
_color: PhantomData,
}
}
#[must_use]
pub fn range(mut self, min: f32, max: f32) -> Self {
self.min = min;
self.max = max;
self
}
#[must_use]
pub fn id(mut self, id: WidgetId) -> Self {
self.id = Some(id);
self
}
#[must_use]
pub fn step(mut self, step: f32) -> Self {
self.step = Some(step.abs());
self
}
#[must_use]
pub fn on_change<F: Fn(f32) -> M + 'a>(mut self, f: F) -> Self {
self.on_change = Some(Box::new(f));
self
}
#[must_use]
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
#[must_use]
pub fn height(mut self, height: impl Into<Length>) -> Self {
self.height = height.into();
self
}
pub fn is_enabled(&self) -> bool {
self.on_change.is_some()
}
fn fraction(&self) -> f32 {
if self.max <= self.min {
0.0
} else {
((self.value - self.min) / (self.max - self.min)).clamp(0.0, 1.0)
}
}
fn span(&self) -> (i32, i32) {
let left = self.rect.top_left.x + KNOB_RADIUS as i32;
let right = self.rect.top_left.x + self.rect.size.width as i32 - KNOB_RADIUS as i32;
(left, right.max(left))
}
fn hit_test(&self, point: Point) -> bool {
let tl = self.rect.top_left;
let br = tl + Point::new(self.rect.size.width as i32, self.rect.size.height as i32);
point.x >= tl.x && point.x < br.x && point.y >= tl.y && point.y < br.y
}
fn value_at(&self, x: i32) -> f32 {
let (left, right) = self.span();
let frac = if right <= left {
0.0
} else {
((x - left) as f32 / (right - left) as f32).clamp(0.0, 1.0)
};
let v = self.min + frac * (self.max - self.min);
v.clamp(self.min.min(self.max), self.min.max(self.max))
}
fn action_step(&self) -> f32 {
if let Some(step) = self.step
&& step > 0.0
{
return step;
}
let range = (self.max - self.min).abs();
if range <= f32::EPSILON {
0.0
} else {
(range / 20.0).max(f32::EPSILON)
}
}
fn adjusted_value(&self, delta: f32) -> f32 {
(self.value + delta).clamp(self.min.min(self.max), self.min.max(self.max))
}
}
impl<'a, C: PixelColor, M: Clone> Widget<C, M> for Slider<'a, C, M> {
fn measure(&mut self, constraints: Constraints) -> Size {
let w = self.width.resolve(INTRINSIC_W, constraints.max.width);
let h = self.height.resolve(2 * KNOB_RADIUS, constraints.max.height);
constraints.clamp(Size::new(w, h))
}
fn preferred_size(&self) -> (Length, Length) {
(self.width, self.height)
}
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> {
let Some(cb) = self.on_change.as_ref() else {
return None;
};
match phase {
TouchPhase::Down => {
if self.hit_test(point) {
self.pressed = true;
Some(cb(self.value_at(point.x)))
} else {
self.pressed = false;
None
}
}
TouchPhase::Moved => {
if self.hit_test(point) {
self.pressed = true;
Some(cb(self.value_at(point.x)))
} else {
self.pressed = false;
None
}
}
TouchPhase::Up => {
self.pressed = false;
None
}
}
}
fn mark_pressed(&mut self, point: Point) {
if self.is_enabled() && self.hit_test(point) {
self.pressed = true;
}
}
fn widget_id(&self) -> Option<WidgetId> {
self.id
}
fn is_focusable(&self) -> bool {
self.id.is_some() && self.is_enabled()
}
fn handle_action(&mut self, action: UiAction) -> Option<M> {
if !self.is_enabled() {
return None;
}
let step = self.action_step();
match action {
UiAction::Increment | UiAction::NavigateRight | UiAction::NavigateUp if step > 0.0 => {
self.on_change
.as_ref()
.map(|cb| cb(self.adjusted_value(step)))
}
UiAction::Decrement | UiAction::NavigateLeft | UiAction::NavigateDown if step > 0.0 => {
self.on_change
.as_ref()
.map(|cb| cb(self.adjusted_value(-step)))
}
_ => None,
}
}
fn sync_focus(&mut self, focused: Option<WidgetId>) {
self.focused = self.id.is_some() && self.id == focused;
}
fn focus_at(&self, point: Point) -> Option<WidgetId> {
if self.is_focusable() && self.hit_test(point) {
self.id
} else {
None
}
}
fn draw<'t>(
&self,
renderer: &mut dyn Renderer<C>,
theme: &Theme<'t, C>,
) -> Result<(), RenderError> {
let accent = &theme.accent;
let (left, right) = self.span();
let cy = self.rect.top_left.y + self.rect.size.height as i32 / 2;
let track_top = cy - TRACK_THICKNESS as i32 / 2;
let track_color = theme.background.divider;
let fill_color = if !self.is_enabled() {
theme.background.divider
} else if self.pressed {
accent.pressed
} else {
accent.base
};
let track = Rectangle::new(
Point::new(left, track_top),
Size::new((right - left).max(0) as u32, TRACK_THICKNESS),
);
renderer.fill_rect(track, track_color)?;
let knob_x = left + ((right - left) as f32 * self.fraction()) as i32;
let filled = Rectangle::new(
Point::new(left, track_top),
Size::new((knob_x - left).max(0) as u32, TRACK_THICKNESS),
);
renderer.fill_rect(filled, fill_color)?;
let knob_color = if self.is_enabled() {
accent.on_base
} else {
theme.background.base
};
let center = Point::new(knob_x, cy);
let knob_border = if self.focused {
accent.base
} else {
accent.border
};
renderer.fill_circle(center, KNOB_RADIUS, knob_border)?;
renderer.fill_circle(center, KNOB_RADIUS.saturating_sub(2), knob_color)?;
Ok(())
}
}