//! Slider geometry and value mapping, extracted from the grid for //! testability. //! //! A numeric value cell whose `style.slider` flag is set renders as a 0–100 //! slider (track + fill + handle) instead of text, and a horizontal drag //! sets the value. This mirrors the reference datagrid's `CellSlider` //! widget but is drawn from quads, with the arithmetic here. /// The slider's fixed range, matching the reference widget. pub const SLIDER_MIN: f64 = 0.0; pub const SLIDER_MAX: f64 = 100.0; /// The value's position in `[0, 1]`, clamped. pub fn fraction(value: f64) -> f64 { ((value - SLIDER_MIN) / (SLIDER_MAX - SLIDER_MIN)).clamp(0.0, 1.0) } /// The value at an absolute x inside a cell, inverse of [`fraction`]: /// rounded to the nearest whole step so a drag produces clean cell values. pub fn value_at(x: f64, cell_x: f64, cell_width: f64) -> f64 { let inset = 6.0_f64; let usable = (cell_width - inset * 2.0).max(1.0); let fx = ((x - cell_x - inset) / usable).clamp(0.0, 1.0); (fx * (SLIDER_MAX - SLIDER_MIN) + SLIDER_MIN).round() } /// A slider's drawable parts inside a cell rect `(x, y, w, h)`. #[derive(Clone, Debug, PartialEq)] pub struct SliderLayout { /// The track `(x, y, width, height)`. pub track: (f64, f64, f64, f64), /// The filled portion left of the handle. pub fill: (f64, f64, f64, f64), /// The handle. pub handle: (f64, f64, f64, f64), } /// Lay out a slider for `value` inside the cell. pub fn slider_layout(x: f64, y: f64, w: f64, h: f64, value: f64) -> SliderLayout { let inset = 6.0_f64; let track_x = x + inset; let track_w = (w - inset * 2.0).max(1.0); let track_h = 4.0_f64; let track_y = y + (h - track_h) * 0.5; let fx = fraction(value); let fill_w = (track_w * fx).max(1.0); let handle_w = 6.0_f64; let handle_h = (h - 4.0).max(4.0); let handle_x = (track_x + track_w * fx - handle_w * 0.5).clamp(track_x, track_x + track_w - handle_w); let handle_y = y + (h - handle_h) * 0.5; SliderLayout { track: (track_x, track_y, track_w, track_h), fill: (track_x, track_y, fill_w, track_h), handle: (handle_x, handle_y, handle_w, handle_h), } } #[cfg(test)] mod tests { use super::*; /// The fraction maps the endpoints and the midpoint. #[test] fn fraction_maps_endpoints_and_midpoint() { assert_eq!(fraction(0.0), 0.0); assert_eq!(fraction(50.0), 0.5); assert_eq!(fraction(100.0), 1.0); // Out-of-range values clamp. assert_eq!(fraction(-10.0), 0.0); assert_eq!(fraction(150.0), 1.0); } /// `value_at` is the inverse of `fraction`, rounded to whole steps. #[test] fn value_at_inverts_fraction() { let cell_x = 100.0; let cell_w = 200.0; // Left edge → 0, right edge → 100, middle → 50. assert_eq!(value_at(cell_x, cell_x, cell_w), 0.0); assert_eq!(value_at(cell_x + cell_w, cell_x, cell_w), 100.0); assert_eq!(value_at(cell_x + cell_w * 0.5, cell_x, cell_w), 50.0); // Clamped outside the cell. assert_eq!(value_at(cell_x - 50.0, cell_x, cell_w), 0.0); assert_eq!(value_at(cell_x + cell_w + 50.0, cell_x, cell_w), 100.0); } /// `value_at` rounds to whole numbers (clean cell values). #[test] fn value_at_rounds_to_whole_steps() { let cell_x = 0.0; let cell_w = 100.0; let v = value_at(cell_x + 42.3, cell_x, cell_w); assert_eq!(v, v.round(), "slider values are integers"); assert!((0.0..=100.0).contains(&v)); } /// The track spans the inset width, the fill grows with the value, and /// the handle tracks the fill position. #[test] fn layout_tracks_the_value() { let low = slider_layout(10.0, 10.0, 100.0, 24.0, 0.0); let high = slider_layout(10.0, 10.0, 100.0, 24.0, 100.0); let mid = slider_layout(10.0, 10.0, 100.0, 24.0, 50.0); assert_eq!(low.track, (16.0, 20.0, 88.0, 4.0)); assert!(low.fill.2 < mid.fill.2 && mid.fill.2 < high.fill.2); assert!(low.handle.0 <= mid.handle.0 && mid.handle.0 <= high.handle.0); // Everything stays inside the cell. for layout in [&low, &mid, &high] { let (tx, ty, tw, th) = layout.track; assert!(tx >= 10.0 && tx + tw <= 110.0 + 1e-9); assert!(ty >= 10.0 && ty + th <= 34.0 + 1e-9); let (hx, hy, hw, hh) = layout.handle; assert!(hx >= 10.0 && hx + hw <= 110.0 + 1e-9); assert!(hy >= 10.0 && hy + hh <= 34.0 + 1e-9); } } }