//! 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" ); } }