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
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%.
122 lines
4.5 KiB
Rust
122 lines
4.5 KiB
Rust
//! 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);
|
||
}
|
||
}
|
||
}
|