🔒 Repository is read-only – file editing is disabled.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
//! `Slider` — suwak (zakres 0..1). Przeciąganie obsługuje aplikacja, wołając
//! [`Slider::value_at`] na zdarzeniach ruchu; widget odpowiada za wygląd i geometrię.
use libluna_core::{Color, Insets, Point, Rect, Theme};
use crate::ctx::PaintCtx;
/// Suwak; `rect` oznacza tor (cienki pasek), a uchwyt rysowany jest ponad niego.
#[derive(Debug, Clone, Copy, Default)]
pub struct Slider {
value: f64,
}
impl Slider {
pub fn new(value: f64) -> Self {
Self {
value: value.clamp(0.0, 1.0),
}
}
pub fn value(&self) -> f64 {
self.value
}
pub fn set_value(&mut self, value: f64) {
self.value = value.clamp(0.0, 1.0);
}
/// Wartość dla pozycji wskaźnika (0..1) względem toru.
pub fn value_at(&self, track: Rect, p: Point) -> f64 {
if track.width() <= 0.0 {
return 0.0;
}
((p.x - track.x()) / track.width()).clamp(0.0, 1.0)
}
/// Czy punkt trafia w obszar suwaka (z tolerancją z metryk motywu).
pub fn hit(&self, track: Rect, p: Point, theme: &Theme) -> bool {
let m = theme.metrics.slider;
let area = track.inflate(Insets::new(m.hit_pad, m.knob_r, m.hit_pad, m.knob_r));
area.contains(p)
}
pub fn paint(&self, ctx: &mut PaintCtx, track: Rect) {
let theme = *ctx.theme;
let colors = theme.colors;
let knob_r = theme.metrics.slider.knob_r;
ctx.scene
.fill_rounded_rect(track, track.height() / 2.0, colors.shade);
let filled_w = track.width() * self.value;
ctx.scene.fill_rounded_rect(
Rect::from_xywh(track.x(), track.y(), filled_w, track.height()),
track.height() / 2.0,
colors.accent,
);
let cy = track.center().y;
let knob_cx = track.x() + filled_w;
let knob = Rect::from_xywh(knob_cx - knob_r, cy - knob_r, knob_r * 2.0, knob_r * 2.0);
ctx.scene.fill_rounded_rect(knob, knob_r, Color::WHITE);
ctx.scene
.stroke_rounded_rect(knob, knob_r, 1.0, colors.border);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn track() -> Rect {
Rect::from_xywh(100.0, 50.0, 200.0, 6.0)
}
#[test]
fn value_at_maps_and_clamps() {
let s = Slider::new(0.0);
assert!((s.value_at(track(), Point::new(100.0, 53.0)) - 0.0).abs() < 1e-9);
assert!((s.value_at(track(), Point::new(200.0, 53.0)) - 0.5).abs() < 1e-9);
assert!((s.value_at(track(), Point::new(500.0, 53.0)) - 1.0).abs() < 1e-9);
}
#[test]
fn hit_has_vertical_tolerance() {
let theme = Theme::light();
let s = Slider::new(0.5);
assert!(s.hit(track(), Point::new(150.0, 53.0), &theme));
assert!(s.hit(track(), Point::new(150.0, 40.0), &theme)); // nad torem, w tolerancji
assert!(!s.hit(track(), Point::new(50.0, 53.0), &theme)); // poza torem w poziomie
}
}