collective

Browse / github.com/storytold/gridcraft / patch

Fix row AutoFit wrapping hidden columns at zero width

Fix row AutoFit wrapping hidden columns at zero width Wrapped text in a hidden column was measured at its zero **display** width during row fitting. A short wrapped label could therefore inflate an automatic row to the maximum height on import or explicit AutoFit. This uses the column's stored width, falling back to the sheet default, only when measuring wrapped text. Hidden cells still contribute to row height. Display geometry, custom-height protection on import, merged-cell handling, and the existing font approximation stay unchanged. ### Evidence and coverage A synthetic workbook with `Wrapped text spans lines` in a width-8 column gives 57

Status
open
Score
0.00
Adopters
0 other operators
Author
tjheinzer (imported from pull request #2668, open)
Base
d8c5f5885fc5320f8f1023d5fdc9f484a185ba86
Size
+95 −2 in 3 files
Review
pending
Shared
2026-10-28
Id
56aed9e1953f343388ac09f6

How it went

No other operator has reported using it yet.

Diff

raw · crates/engine/src/cmd/mod.rs crates/engine/tests/it/row_autofit.rs docs/ui-parity.md

diff --git a/crates/engine/src/cmd/mod.rs b/crates/engine/src/cmd/mod.rs--- a/crates/engine/src/cmd/mod.rs+++ b/crates/engine/src/cmd/mod.rs@@ -574,7 +574,9 @@ pub fn auto_row_height(wb: &mut Workbook, si: usize, row: u32) {             // starts one, and each line folds at the column width.             if st.align.wrap {                 let text = crate::display::cell_text(wb, sh, CellRef::new(row, c));-                let w = sh.col_width(c).max(1.0) - 6.0;+                // Hidden columns have zero display width, but their cells still wrap at+                // the stored (or sheet-default) width when fitting a row.+                let w = sh.cols.get(&c).and_then(|info| info.size).unwrap_or(sh.default_col_width).max(1.0) - 6.0;                 lines = text.lines().map(|l| wrapped_line_count(l, w.max(1.0), size, st.font.bold)).sum::<f32>().max(1.0);             }         } else if st.font.size <= gridcraft_model::DEFAULT_FONT_SIZE {diff --git a/crates/engine/tests/it/row_autofit.rs b/crates/engine/tests/it/row_autofit.rs--- a/crates/engine/tests/it/row_autofit.rs+++ b/crates/engine/tests/it/row_autofit.rs@@ -1,5 +1,8 @@ //! Whole-row and whole-column formatting must re-fit row heights (issue #833). +use gridcraft_core::{CellRef, Value};+use gridcraft_engine::{DocState, Session, Trust};+use gridcraft_model::{StyleId, Workbook}; use serde_json::json;  fn sheet_mut(s: &mut gridcraft_engine::Session) -> &mut gridcraft_engine::model::Sheet {@@ -59,3 +62,84 @@ fn whole_row_format_keeps_hidden_rows_hidden() {     assert!(info.size.unwrap_or(before) > before + 10.0, "hidden row still grows to its font: before={before}, size={:?}", info.size);     assert!(info.hidden, "formatting a hidden row must not unhide it"); }++// Build the input without commands, which would already fit the row. The wrapped+// cell in B1 must contribute even when hidden, using its stored or default width.+fn wrapped_column_workbook(hidden: bool, width: Option<f32>, custom_height: bool) -> Workbook {+    let mut wb = Workbook::new();+    let wrapped = wb.styles.derive(StyleId::DEFAULT, |style| style.align.wrap = true);+    let sh = wb.sheet_mut(0).unwrap();+    sh.default_col_width = 80.0;+    sh.set_value(CellRef::new(0, 0), Value::text("Label"));+    sh.set_value(CellRef::new(0, 1), Value::text("Wrapped text spans lines"));+    sh.set_style(CellRef::new(0, 1), wrapped);+    let col = sh.cols.entry(1).or_default();+    col.hidden = hidden;+    col.size = width;+    let row = sh.rows.entry(0).or_default();+    row.size = Some(20.0);+    row.custom = custom_height;+    wb+}++fn open_wrapped_column(hidden: bool, width: Option<f32>, custom_height: bool) -> Session {+    let bytes = gridcraft_xlsx::write_xlsx(&wrapped_column_workbook(hidden, width, custom_height)).unwrap();+    let mut s = Session::new();+    s.execute("file.open", json!({"name": "wrapped-column.xlsx", "base64": gridcraft_engine::io::base64_encode(&bytes)})).unwrap();+    s+}++#[test]+fn imported_wrapped_rows_use_hidden_columns_stored_width() {+    let mut heights = Vec::new();+    for width in [None, Some(56.0), Some(120.0)] {+        let visible = open_wrapped_column(false, width, false);+        let hidden = open_wrapped_column(true, width, false);+        let expected = visible.doc().unwrap().wb.active().unwrap().row_height(0);+        let sh = hidden.doc().unwrap().wb.active().unwrap();+        assert_eq!(sh.row_height(0), expected, "hiding a column must not change the imported row's wrap width: {width:?}");+        assert!(expected > 20.0, "the wrapped hidden cell must still contribute to the row height: {width:?}");+        assert!(sh.cols.get(&1).unwrap().hidden, "fitting must not unhide the column");+        heights.push(expected);+    }+    assert!(heights[1] > heights[2], "a narrow stored width must wrap onto more lines than a wide one");+}++#[test]+fn explicit_row_autofit_uses_hidden_columns_stored_or_default_width() {+    let mut heights = Vec::new();+    for width in [None, Some(56.0), Some(120.0)] {+        let mut fitted = Vec::new();+        for hidden in [false, true] {+            let mut s = Session::new();+            s.add_document(DocState::new(wrapped_column_workbook(hidden, width, true), None, "Wrapped column".into(), Trust::Local));+            assert_eq!(s.doc().unwrap().wb.active().unwrap().row_height(0), 20.0);+            s.execute("home.autofitRowHeight", json!({"rows": "1:1"})).unwrap();+            let sh = s.doc().unwrap().wb.active().unwrap();+            fitted.push(sh.row_height(0));+            assert!(sh.row_height(0) > 20.0, "AutoFit must include the wrapped cell, including when hidden");+            assert!(!sh.rows.get(&0).unwrap().custom, "explicit AutoFit releases the custom height");+            assert_eq!(sh.cols.get(&1).unwrap().hidden, hidden);+            assert_eq!(sh.cols.get(&1).unwrap().size, width, "AutoFit must not change the stored column width");+            s.execute("edit.undo", json!({})).unwrap();+            let sh = s.doc().unwrap().wb.active().unwrap();+            assert_eq!(sh.row_height(0), 20.0, "undo restores the explicit row height");+            assert!(sh.rows.get(&0).unwrap().custom);+            assert_eq!(sh.cols.get(&1).unwrap().hidden, hidden);+        }+        assert_eq!(fitted[0], fitted[1], "hidden and visible columns must have the same AutoFit wrap width: {width:?}");+        heights.push(fitted[0]);+    }+    assert!(heights[1] > heights[2], "AutoFit must use the stored width when present");+}++#[test]+fn imported_wrapped_hidden_columns_keep_custom_row_heights() {+    for hidden in [false, true] {+        let s = open_wrapped_column(hidden, Some(56.0), true);+        let sh = s.doc().unwrap().wb.active().unwrap();+        assert_eq!(sh.row_height(0), 20.0, "import must preserve an explicit custom height");+        assert!(sh.rows.get(&0).unwrap().custom);+        assert_eq!(sh.cols.get(&1).unwrap().hidden, hidden);+    }+}diff --git a/docs/ui-parity.md b/docs/ui-parity.md--- 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-25 · **Change:** minor (AutoSum over a selected row puts its total to the right) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4+> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-26 · **Change:** minor (row AutoFit uses hidden columns' stored width) · **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@@ -47,6 +47,12 @@ uses that allowance in its grid units; its existing approximate font measurement Header text aligns and clips before the button. Command and header-double-click paths share this logic, with one undo step. +Row AutoFit, including fitting on open, measures wrapped text in hidden columns at their+stored width, or the sheet's default width when absent. Hidden and visible columns therefore+contribute equally to the row height, as observed with a synthetic workbook in Windows Excel+16.0 build 20527 on 2026-10-26; hiding a column no longer makes its wrapped text inflate the row. Custom row+heights stay intact on open until AutoFit is requested. Font measurement remains approximate.+ | Behaviour | Status | |---|---| | Click/drag/Shift/Ctrl selection, multi-area selection | rectangular drag works in one direction only in some cases (#28) |@@ -187,6 +193,7 @@ scrollbar bugs 2–4 · pixel/feel pass against observed Excel 7–12.  | Date | Change | Summary | |---|---|---|+| 2026-10-26 | minor | Row AutoFit and fitting on open use a hidden column's stored or default width for wrapped text, instead of its zero display width. Hidden and visible columns contribute equally; custom row heights remain intact on open and explicit AutoFit remains undoable | | 2026-10-25 | minor | AutoSum (Alt+=, Home › AutoSum) over a selected row puts its total to the right of the row, or in its last cell when that is empty, as in Excel; it wrote a one-cell total under each cell. A selection's empty last column takes each row's total and its empty last row each column's, with the block's total in the corner when both are empty | | 2026-10-25 | minor | Paste Name lays out only the names in view, so a workbook with thousands of names opens it at once; the arrow keys still scroll the chosen name into view. Go To matches a `Sheet2!Name` reference and hides a workbook name behind the active sheet's own name in any case and script (`Größe` = `GRÖSSE`), as formulas do. The two Page Layout view rows are one; the 0.7 in side margin used when Align with Page Margins is off is marked unverified | | 2026-10-24 | minor | Go To lists the named ranges on every sheet, as Excel's does ("The Go to popup window shows named ranges on every worksheet in your workbook"): a name scoped to another sheet appears as `Sheet2!Name`, and hidden names, constants and formulas are left out. It keeps the references entered (newest first, ten at most), and a double-click on an entry goes there. The Reference box and `edit.goTo` take several names or references separated by commas (`Price, Type`, `B14:C22,F19:G30,H21:H29`) and select them all; `edit.goTo {list: true}` returns the list |