nigig-org/crates/apps/spreadsheet/spreadsheet-ui/src/checkbox.rs
andodeki 1a36ca5538
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-ui): interactive checkbox cells (widget-in-cell)
Close gap #9 (cells hosting widgets) the spreadsheet-native way. The
datagrid reference hosts a live CheckBox widget per visible cell and
recycles instances from a per-template pool; our grid instead draws the
control from batched quads, so there is no per-cell widget object to
instantiate or recycle — the render cache and reused draw buffers already
fill the role the pool does.

- A plain value cell holding TRUE/FALSE now renders as a checkbox: a
  centred square box with a checkmark when TRUE, and its TRUE/FALSE label
  to the right. A single click toggles the value (through the normal
  set_cell path, so it is undoable and recalculates dependents), selects
  the cell, and does not open the editor — a checkbox is a button, not a
  text surface. Formula cells are never checkboxes, so a click can't
  clobber a formula.
- The predicate (is_checkbox), the value flip (toggled) and the box
  geometry (checkbox_layout) live in a new measured checkbox module with
  unit tests; grid.rs stays thin glue (draw the box/tick/label, reposition
  the label, and the click handler's toggle).

UI controllers: 96 tests (+6). Coverage: ui-controllers 99.20% (floor 96),
checkbox.rs at 100%. No engine changes.
2026-08-20 18:28:46 +00:00

158 lines
5.9 KiB
Rust

//! Interactive checkbox cells, extracted from the grid for testability.
//!
//! A plain value cell holding `TRUE` / `FALSE` renders as a checkbox and
//! toggles on a single click. The Makepad datagrid reference hosts a live
//! `CheckBox` widget per visible cell and recycles them from a per-template
//! pool; our grid instead draws the control from batched quads, so there is
//! no per-cell widget object to instantiate or recycle — the render cache
//! and the reused draw buffers already serve the role the pool does.
//!
//! The predicate (`is_checkbox`), the value flip (`toggled`) and the box
//! geometry (`checkbox_layout`) are all pure and live here, mirroring the
//! split used for `sparkline` and `selection`.
/// Whether `value` is a boolean literal (`TRUE` / `FALSE`), case-insensitive.
fn is_boolean(value: &str) -> bool {
value.eq_ignore_ascii_case("TRUE") || value.eq_ignore_ascii_case("FALSE")
}
/// Whether a cell renders as an interactive checkbox: a boolean literal in
/// a plain value cell. Formula cells are never checkboxes — a click must
/// not clobber a formula.
pub fn is_checkbox(display: &str, is_formula: bool) -> bool {
!is_formula && is_boolean(display)
}
/// The value a checkbox cell toggles to: `TRUE` ↔ `FALSE`. `None` when the
/// value is not a boolean literal.
pub fn toggled(value: &str) -> Option<&'static str> {
if value.eq_ignore_ascii_case("TRUE") {
Some("FALSE")
} else if value.eq_ignore_ascii_case("FALSE") {
Some("TRUE")
} else {
None
}
}
/// Geometry for a checkbox drawn inside a cell rect `(x, y, w, h)`.
#[derive(Clone, Debug, PartialEq)]
pub struct CheckboxLayout {
/// Top, bottom, left, right edges of the box, as `(x, y, width, height)`.
pub edges: [(f64, f64, f64, f64); 4],
/// The two checkmark strokes, as filled quads; empty when unchecked.
pub tick: Vec<(f64, f64, f64, f64)>,
/// Where the `TRUE` / `FALSE` label starts.
pub label_x: f64,
pub label_y: f64,
}
/// Compute a checkbox's geometry. The box is a square centred vertically at
/// the left inset; it shrinks to fit short rows rather than overflowing.
pub fn checkbox_layout(x: f64, y: f64, _w: f64, h: f64, checked: bool) -> CheckboxLayout {
let box_size = 13.0_f64.min((h - 4.0).max(2.0));
let bx = x + 6.0;
let by = y + (h - box_size) * 0.5;
let t = 1.5_f64; // edge thickness
let edges = [
(bx, by, box_size, t),
(bx, by + box_size - t, box_size, t),
(bx, by, t, box_size),
(bx + box_size - t, by, t, box_size),
];
let mut tick = Vec::new();
if checked {
// Two strokes approximating a checkmark: a short upper stroke and a
// longer lower stroke, both inside the box.
tick.push((bx + 2.0, by + box_size * 0.50, box_size * 0.32, t));
tick.push((
bx + box_size * 0.32,
by + box_size * 0.80,
box_size * 0.50,
t,
));
}
CheckboxLayout {
edges,
tick,
label_x: bx + box_size + 6.0,
label_y: y + (h - 14.0) * 0.5,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn boolean_literals_are_recognised_case_insensitively() {
for v in ["TRUE", "FALSE", "true", "false", "True"] {
assert!(is_boolean(v), "{v:?} is a boolean literal");
}
for v in ["", "0", "1", "YES", "NO", "=TRUE", "true "] {
assert!(!is_boolean(v), "{v:?} is not a boolean literal");
}
}
/// Only plain value cells are checkboxes; a formula cell's value must
/// never be overwritten by a click.
#[test]
fn formula_cells_are_not_checkboxes() {
assert!(is_checkbox("TRUE", false));
assert!(is_checkbox("false", false));
assert!(!is_checkbox("TRUE", true), "a formula must not toggle");
assert!(!is_checkbox("=A1", false));
assert!(!is_checkbox("hello", false));
}
#[test]
fn toggle_flips_booleans_and_rejects_the_rest() {
assert_eq!(toggled("TRUE"), Some("FALSE"));
assert_eq!(toggled("FALSE"), Some("TRUE"));
assert_eq!(toggled("true"), Some("FALSE"));
assert_eq!(toggled("false"), Some("TRUE"));
assert_eq!(toggled("hello"), None);
assert_eq!(toggled(""), None);
assert_eq!(toggled("=TRUE"), None);
}
/// The box is a vertically centred square at the left inset, with four
/// edges, and the label starts right of it.
#[test]
fn the_box_is_centred_and_the_label_follows_it() {
let layout = checkbox_layout(10.0, 4.0, 100.0, 26.0, false);
let (bx, by, bw, bh) = layout.edges[0]; // top edge = full box top
assert_eq!(bx, 16.0, "box sits at the 6px inset");
assert_eq!(bw, 13.0, "13px box for a 26px row");
assert_eq!(by, 4.0 + (26.0 - 13.0) * 0.5, "vertically centred");
assert_eq!(layout.edges.len(), 4);
assert!(layout.tick.is_empty(), "unchecked box has no tick");
assert_eq!(layout.label_x, bx + 13.0 + 6.0);
assert_eq!(layout.label_y, 4.0 + (26.0 - 14.0) * 0.5);
// The top edge height is the stroke thickness.
assert_eq!(bh, 1.5);
}
/// A checked box draws the checkmark; an unchecked one does not.
#[test]
fn a_checked_box_has_a_tick() {
let checked = checkbox_layout(0.0, 0.0, 100.0, 26.0, true);
assert_eq!(checked.tick.len(), 2);
let unchecked = checkbox_layout(0.0, 0.0, 100.0, 26.0, false);
assert!(unchecked.tick.is_empty());
}
/// A short row shrinks the box instead of overflowing the cell.
#[test]
fn the_box_shrinks_for_short_rows() {
let layout = checkbox_layout(0.0, 0.0, 100.0, 8.0, false);
let (_, by, box_w, _) = layout.edges[0];
assert_eq!(box_w, 4.0, "min(13, 8-4) = 4");
assert!(
by >= 0.0 && by + box_w <= 8.0 + 1e-9,
"box stays in the row"
);
}
}