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
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.
158 lines
5.9 KiB
Rust
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"
|
|
);
|
|
}
|
|
}
|