diff --git a/crates/engine/src/cmd/print/draw.rs b/crates/engine/src/cmd/print/draw.rs index d90335390..7a49b8e85 100644 --- a/crates/engine/src/cmd/print/draw.rs +++ b/crates/engine/src/cmd/print/draw.rs @@ -2,8 +2,8 @@ use super::layout::{DENSE_BLOCK, SheetLayout}; use super::objects::draw_cell_picture; use super::text::{TextPlacement, draw_text}; use super::{ - BorderLine, BorderStyle, Borders, CellRef, Color, Font, Page, PatternType, Point, RangeRef, Rgb, Sheet, Style, StyleId, Underline, Workbook, - split_sheet, + BorderLine, BorderStyle, Borders, CellRef, Color, Font, Page, PatternType, Point, RangeRef, Rgb, Sheet, Style, StyleId, Underline, Value, + Workbook, split_sheet, }; // ---------------------------------------------------------------- drawing @@ -280,6 +280,7 @@ pub(super) fn draw_block(pg: &mut Page, b: &Block, gridlines: bool, cf: &mut Opt || merges.iter().any(|m| m.start == *c) || !sh.value(*c).is_empty() || l.style.borders != Borders::default() + || l.style.num_fmt.as_str() == "checkbox" }) .collect(); let rect_of = |c: CellRef| -> (f32, f32, f32, f32, bool) { @@ -343,6 +344,8 @@ pub(super) fn draw_block(pg: &mut Page, b: &Block, gridlines: bool, cf: &mut Opt } if let Some(picture) = sh.cell_pictures.get(c) { draw_cell_picture(pg, &picture.data, (x, y, w, h), b.k); + } else if let Some(on) = checkbox_state(l, &sh.value(*c)) { + draw_checkbox(pg, (x, y, w, h), b.k, on); } else { draw_text(pg, b, TextPlacement { cell: *c, look: l, rect: (x, y, w, h), merged, merges: &merges, limits: (bx, bx + bw) }); } @@ -350,6 +353,37 @@ pub(super) fn draw_block(pg: &mut Page, b: &Block, gridlines: bool, cf: &mut Opt pg.restore(); } +/// A checkbox cell prints TRUE/FALSE (and empty) as a box, as on screen (#2972); other values +/// print as text. +fn checkbox_state(l: &Look, v: &Value) -> Option { + if l.style.num_fmt.as_str() != "checkbox" || l.cf.as_ref().is_some_and(|cf| cf.hide_value) { + return None; + } + match v { + Value::Bool(on) => Some(*on), + Value::Empty => Some(false), + _ => None, + } +} + +/// A checkbox centred in the cell: white with a grey outline, or green with a white tick. +fn draw_checkbox(pg: &mut Page, (x, y, w, h): (f32, f32, f32, f32), k: f32, on: bool) { + let s = (13.2 * k).min(w - 2.0 * k).min(h - 2.0 * k); + if s <= 0.0 { + return; + } + let (bx, by) = (x + (w - s) / 2.0, y + (h - s) / 2.0); + let corners = [[bx, by], [bx + s, by], [bx + s, by + s], [bx, by + s]]; + if on { + let green = [0x10, 0x7C, 0x41]; + pg.polygon(&corners, Some(green), Some((green, 0.75 * k))); + let tick = [[bx + 0.22 * s, by + 0.52 * s], [bx + 0.42 * s, by + 0.72 * s], [bx + 0.78 * s, by + 0.30 * s]]; + pg.polyline(&tick, 0.14 * s, [0xFF, 0xFF, 0xFF], &[]); + } else { + pg.polygon(&corners, Some([0xFF, 0xFF, 0xFF]), Some(([110, 110, 110], 0.75 * k))); + } +} + pub(super) struct PendingPdfLink { pub(super) page: usize, pub(super) sheet: usize, diff --git a/crates/engine/src/cmd/print/tests.rs b/crates/engine/src/cmd/print/tests.rs index 21b13338f..d678d441a 100644 --- a/crates/engine/src/cmd/print/tests.rs +++ b/crates/engine/src/cmd/print/tests.rs @@ -565,3 +565,18 @@ fn rotated_wrapped_text_prints_rotated() { assert_eq!(up + down, blocks.len(), "a header line printed flat: {blocks:?}"); assert!((1..=3).contains(&up) && (1..=3).contains(&down), "up {up}, down {down}: {blocks:?}"); } + +#[test] +fn checkbox_cells_print_as_boxes_not_true_false() { + // #2972: a checkbox cell printed its value as TRUE or FALSE. + let mut s = s(); + s.execute("insert.checkbox", json!({"range": "A1:A2"})).unwrap(); + s.execute("cell.toggleCheckbox", json!({"cell": "A2"})).unwrap(); + s.execute("cell.set", json!({"cell": "C1", "input": "TRUE"})).unwrap(); + let (pdf, _) = pdf_of(&mut s, json!({})); + assert!(!contains(&pdf, "(FALSE) Tj"), "the unchecked box printed as FALSE"); + // Only the plain TRUE in C1 prints as text; the checked box is a filled box. + let text = String::from_utf8_lossy(&pdf); + assert_eq!(text.matches("(TRUE) Tj").count(), 1, "the checked box printed as TRUE"); + assert!(contains(&pdf, "0.063 0.486 0.255 rg"), "the checked box is not filled"); +} diff --git a/docs/ui-parity.md b/docs/ui-parity.md index b5f6d591a..0e1a98f47 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,6 +1,6 @@ # UI parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-29 · **Change:** minor (Ctrl+arrow and Ctrl+Shift+arrow while pointing a formula reference; earlier: editing a hyperlink loads its existing fields; earlier: AutoSum over a selected row puts its total to the right; ribbon catalog count 276 / 292) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (checkbox cells print as boxes; earlier: Ctrl+arrow and Ctrl+Shift+arrow while pointing a formula reference; earlier: editing a hyperlink loads its existing fields; earlier: AutoSum over a selected row puts its total to the right; ribbon catalog count 276 / 292) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4 Interaction, controls and feel, against Excel. Part of [target-app-parity.md](target-app-parity.md). **UI/UX fidelity: ~55% ready, 50–80 h** (estimated). Ribbon command coverage is measured @@ -194,6 +194,7 @@ scrollbar bugs 2–4 · pixel/feel pass against observed Excel 7–12. | Date | Change | Summary | |---|---|---| +| 2026-10-30 | minor | Checkbox cells print and export to PDF as boxes, as on screen: unchecked white with a grey outline, checked green with a white tick (#2972). They printed their value as TRUE or FALSE; other values in a checkbox cell still print as text | | 2026-10-29 | minor | Formula pointing from the keyboard: Ctrl+arrow (⌘arrow) moves the pointed cell to the edge of its data and Ctrl+Shift+arrow (⌘⇧arrow) extends the pointed range there, as the same keys move and extend the selection, so typing `=COUNTIF(`, clicking the top of a column and pressing ⌘⇧↓ points at the whole column of data; the keys used to move or extend by one cell. A range reaching the sheet edge is written as whole columns or rows (`A:B`). The editor reads the modifiers of the arrow's own key event, so a keystroke pressed and released within one frame (a control client's `ui.key`, an automation tool) keeps its Shift and Ctrl | | 2026-10-29 | minor | Insert Hyperlink opens with an existing link's address, display text and ScreenTip (#2876). An address-only edit preserves the cell's content and formula; Cancel leaves the link unchanged, and Undo restores the edit | | 2026-10-27 | minor | Ctrl with the numpad's + inserts rows, columns or cells as Ctrl+Shift+= does, as in Excel; it did nothing because the numpad key arrives without Shift |