nigig-org/crates/apps/spreadsheet/spreadsheet-ui/src/slider.rs
andodeki 4d681dfdbe
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
nigig-build (CAD) / cad-widget-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
spreadsheet / engine-coverage (push) Has been cancelled
spreadsheet / ui-controller-coverage (push) Has been cancelled
feat(spreadsheet): slider cells and inline-Markdown cells (#9 remainder)
Complete the widget-in-cell gap the same value/style-convention way as the
checkbox: two more cell kinds, both plain data with a render flag.

Engine:
- CellStyle gains `markdown` and `slider` bool flags, serialized as two
  trailing columns on the CELL line (older files default them off), plus
  WorkbookCommand::{SetMarkdown, SetSlider} routed through apply and
  apply_command via mutate_cell — undoable like every style mutation.

UI:
- markdown.rs (measured): a flat inline parser splitting a cell value into
  Regular/Bold/Italic/Code runs (`**bold**`, `*italic*`, `` `code` ``);
  unclosed markers and empty spans stay literal/dropped. The grid draws
  each run with the bold or regular resource (code tinted like formulas).
- slider.rs (measured): 0-100 fraction/value mapping (rounded to whole
  steps) and track/fill/handle geometry. A slider cell draws the control
  instead of text, and a press/drag sets the value through set_cell — one
  undo step per drag (reverse-order ChangeSet application restores the
  pre-drag value).
- Toolbar "Md" and "Slider" buttons toggle the flags on the selection;
  the render cache carries the two flags so cached cells stay styled.

Dropdown and button cells are intentionally not included: a dropdown
needs a per-cell choices list and a button needs an action semantic that
a spreadsheet does not have — both would require a CellKind in the data
model rather than a render flag.

Engine: 481 lib tests (+2) + integration. UI controllers: 133 tests (+13,
markdown 10 + slider 3). Coverage: engine 96.62%, ui-controllers 99.42%
(floors 96); markdown.rs and slider.rs at 100%.
2026-08-20 21:36:21 +00:00

122 lines
4.5 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Slider geometry and value mapping, extracted from the grid for
//! testability.
//!
//! A numeric value cell whose `style.slider` flag is set renders as a 0100
//! 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);
}
}
}