🔒 Repository is read-only – file editing is disabled.

luna/crates/libluna-widgets/src/slider.rs main

91 lines Raw ← Back
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
    }
}