use std::cell::Cell;
use std::rc::Rc;
use std::sync::Arc;
use crate::event::{Event, EventResult, Key, MouseButton};
use crate::geometry::{Rect, Size};
use crate::draw_ctx::DrawCtx;
use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
use crate::text::Font;
use crate::widget::{Widget, paint_subtree};
use crate::widgets::label::{Label, LabelAlign};
const TRACK_H: f64 = 4.0;
const THUMB_R: f64 = 7.0;
const WIDGET_H: f64 = 22.0;
const VALUE_W: f64 = 44.0;
const VALUE_GAP: f64 = 6.0;
pub struct Slider {
bounds: Rect,
children: Vec<Box<dyn Widget>>, base: WidgetBase,
value: f64,
min: f64,
max: f64,
step: f64,
show_value: bool,
decimals: Option<usize>,
font: Arc<Font>,
font_size: f64,
dragging: bool,
focused: bool,
hovered: bool,
on_change: Option<Box<dyn FnMut(f64)>>,
value_cell: Option<Rc<Cell<f64>>>,
value_label: Label,
last_value_text: String,
}
impl Slider {
pub fn new(value: f64, min: f64, max: f64, font: Arc<Font>) -> Self {
let v = value.clamp(min, max);
let font_size = 12.0;
let value_label = Label::new("", Arc::clone(&font))
.with_font_size(font_size)
.with_align(LabelAlign::Right);
Self {
bounds: Rect::default(),
children: Vec::new(),
base: WidgetBase::new(),
value: v,
min,
max,
step: (max - min) / 100.0,
show_value: true,
decimals: None,
font,
font_size,
dragging: false,
focused: false,
hovered: false,
on_change: None,
value_cell: None,
value_label,
last_value_text: String::new(),
}
}
pub fn with_step(mut self, step: f64) -> Self { self.step = step; self }
pub fn with_value_cell(mut self, cell: Rc<Cell<f64>>) -> Self {
self.value = cell.get().clamp(self.min, self.max);
self.value_cell = Some(cell);
self
}
pub fn with_show_value(mut self, show: bool) -> Self { self.show_value = show; self }
pub fn with_decimals(mut self, decimals: usize) -> Self {
self.decimals = Some(decimals); self
}
pub fn with_margin(mut self, m: Insets) -> Self { self.base.margin = m; self }
pub fn with_h_anchor(mut self, h: HAnchor) -> Self { self.base.h_anchor = h; self }
pub fn with_v_anchor(mut self, v: VAnchor) -> Self { self.base.v_anchor = v; self }
pub fn with_min_size(mut self, s: Size) -> Self { self.base.min_size = s; self }
pub fn with_max_size(mut self, s: Size) -> Self { self.base.max_size = s; self }
pub fn on_change(mut self, cb: impl FnMut(f64) + 'static) -> Self {
self.on_change = Some(Box::new(cb));
self
}
pub fn value(&self) -> f64 { self.value }
pub fn set_value(&mut self, v: f64) {
self.value = v.clamp(self.min, self.max);
if let Some(cell) = &self.value_cell { cell.set(self.value); }
}
fn fire(&mut self) {
let v = self.value;
if let Some(cell) = &self.value_cell { cell.set(v); }
if let Some(cb) = self.on_change.as_mut() { cb(v); }
}
fn track_right(&self) -> f64 {
let reserved = if self.show_value { VALUE_W + VALUE_GAP } else { 0.0 };
(self.bounds.width - reserved - THUMB_R).max(THUMB_R + 1.0)
}
fn thumb_x(&self) -> f64 {
let track_left = THUMB_R;
let track_right = self.track_right();
let t = if self.max > self.min {
(self.value - self.min) / (self.max - self.min)
} else {
0.0
};
track_left + t * (track_right - track_left)
}
fn value_from_x(&self, x: f64) -> f64 {
let track_left = THUMB_R;
let track_right = self.track_right();
let t = ((x - track_left) / (track_right - track_left)).clamp(0.0, 1.0);
let raw = self.min + t * (self.max - self.min);
let snapped = (raw / self.step).round() * self.step;
snapped.clamp(self.min, self.max)
}
fn format_value(&self) -> String {
if let Some(d) = self.decimals {
return format!("{:.*}", d, self.value);
}
if self.step >= 1.0 {
format!("{:.0}", self.value)
} else if self.step >= 0.1 {
format!("{:.1}", self.value)
} else if self.step >= 0.01 {
format!("{:.2}", self.value)
} else {
format!("{:.3}", self.value)
}
}
}
impl Widget for Slider {
fn type_name(&self) -> &'static str { "Slider" }
fn bounds(&self) -> Rect { self.bounds }
fn set_bounds(&mut self, b: Rect) { self.bounds = b; }
fn children(&self) -> &[Box<dyn Widget>] { &self.children }
fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> { &mut self.children }
fn is_focusable(&self) -> bool { true }
fn margin(&self) -> Insets { self.base.margin }
fn h_anchor(&self) -> HAnchor { self.base.h_anchor }
fn v_anchor(&self) -> VAnchor { self.base.v_anchor }
fn min_size(&self) -> Size { self.base.min_size }
fn max_size(&self) -> Size { self.base.max_size }
fn layout(&mut self, available: Size) -> Size {
if !self.dragging {
if let Some(cell) = &self.value_cell {
self.value = cell.get().clamp(self.min, self.max);
}
}
if self.show_value {
let new_text = self.format_value();
if new_text != self.last_value_text {
self.value_label.set_text(new_text.clone());
self.last_value_text = new_text;
}
let lh = self.font_size * 1.5;
let _ = self.value_label.layout(Size::new(VALUE_W, lh));
self.value_label.set_bounds(Rect::new(0.0, 0.0, VALUE_W, lh));
}
Size::new(available.width, WIDGET_H)
}
fn paint(&mut self, ctx: &mut dyn DrawCtx) {
let v = ctx.visuals();
let w = self.bounds.width;
let h = self.bounds.height;
let cy = h * 0.5;
let track_right = self.track_right();
let track_w = (track_right - THUMB_R).max(0.0);
ctx.set_fill_color(v.track_bg);
ctx.begin_path();
ctx.rounded_rect(THUMB_R, cy - TRACK_H * 0.5, track_w, TRACK_H, TRACK_H * 0.5);
ctx.fill();
let tx = self.thumb_x();
if tx > THUMB_R {
ctx.set_fill_color(v.accent);
ctx.begin_path();
ctx.rounded_rect(THUMB_R, cy - TRACK_H * 0.5, tx - THUMB_R, TRACK_H, TRACK_H * 0.5);
ctx.fill();
}
if self.focused {
ctx.set_stroke_color(v.accent_focus);
ctx.set_line_width(2.0);
ctx.begin_path();
ctx.circle(tx, cy, THUMB_R + 3.0);
ctx.stroke();
}
let thumb_color = if self.dragging || self.focused {
v.accent_pressed
} else if self.hovered {
v.accent_hovered
} else {
v.accent
};
ctx.set_fill_color(thumb_color);
ctx.begin_path();
ctx.circle(tx, cy, THUMB_R);
ctx.fill();
ctx.set_fill_color(v.widget_bg);
ctx.begin_path();
ctx.circle(tx, cy, THUMB_R - 2.5);
ctx.fill();
if self.show_value {
self.value_label.set_color(v.text_color);
let lb = self.value_label.bounds();
let strip_left = track_right + VALUE_GAP;
let ly = cy - lb.height * 0.5;
self.value_label.set_bounds(Rect::new(strip_left, ly, lb.width, lb.height));
ctx.save();
ctx.translate(strip_left, ly);
paint_subtree(&mut self.value_label, ctx);
ctx.restore();
}
}
fn on_event(&mut self, event: &Event) -> EventResult {
match event {
Event::MouseMove { pos } => {
let was = self.hovered;
self.hovered = self.hit_test(*pos);
if self.dragging {
self.value = self.value_from_x(pos.x);
self.fire();
crate::animation::request_tick();
return EventResult::Consumed;
}
if was != self.hovered { crate::animation::request_tick(); }
EventResult::Ignored
}
Event::MouseDown { button: MouseButton::Left, pos, .. } => {
self.dragging = true;
self.value = self.value_from_x(pos.x);
self.fire();
crate::animation::request_tick();
EventResult::Consumed
}
Event::MouseUp { button: MouseButton::Left, .. } => {
let was = self.dragging;
self.dragging = false;
if was { crate::animation::request_tick(); }
EventResult::Consumed
}
Event::KeyDown { key, .. } => {
let changed = match key {
Key::ArrowLeft => { self.value = (self.value - self.step).clamp(self.min, self.max); true }
Key::ArrowRight => { self.value = (self.value + self.step).clamp(self.min, self.max); true }
Key::ArrowDown => { self.value = (self.value - self.step * 10.0).clamp(self.min, self.max); true }
Key::ArrowUp => { self.value = (self.value + self.step * 10.0).clamp(self.min, self.max); true }
_ => false,
};
if changed {
self.fire();
crate::animation::request_tick();
EventResult::Consumed
} else {
EventResult::Ignored
}
}
Event::FocusGained => {
self.focused = true;
crate::animation::request_tick();
EventResult::Ignored
}
Event::FocusLost => {
self.focused = false;
self.dragging = false;
crate::animation::request_tick();
EventResult::Ignored
}
_ => EventResult::Ignored,
}
}
}