Row auto-fit: count line breaks only in cells that wrap

auto_row_height grew a row for every line break in a cell's text whether
or not the cell wrapped. Excel counts breaks only when Wrap Text is on:
with it off a multi-line value displays on one line and AutoFit gives a
one-line row. A file whose unwrapped cells hold pasted multi-line text
(the shape generating libraries write) opened with tall rows and nothing
visibly tall in them.

The fast path no longer treats a line break as a reason to measure, and
the measuring path lays text out over lines only for a wrapping cell.
Typing a break still turns wrap on (the Alt+Enter rule in cell entry) and
grows the row, as before.

Test: an unwrapped multi-line cell from a file keeps the default height
on open; wrap on grows the row; wrap off returns it; a typed break wraps
and grows. The open-time fit tests from #833 are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
RedBearAK
2026-10-19 22:45:22 -08:00
co-authored by Claude Opus 5.5
parent 23829d8c67
commit 4faa3ff098
4 changed files with 52 additions and 14 deletions
+2 -1
View File
@@ -2,7 +2,7 @@
**Stage: alpha** · next: beta, ~25 points of "ready for real work" and ~160–240 h away
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-12 · **Change:** minor (rows auto-fit oversized fonts on open and whole-row/column formatting) · **Target:** Microsoft Excel (Microsoft 365)
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-15 · **Change:** minor (row auto-fit counts line breaks only in wrapping cells) · **Target:** Microsoft Excel (Microsoft 365)
GridCraft aims at full Microsoft Excel parity, and at being better in places: faster, open (XLSX
is the native format), fully scriptable by agents (MCP, control channel, CLI), and on the web.
@@ -132,6 +132,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in
| Date | What landed |
|---|---|
| 2026-10-15 | Row auto-fit counts a cell's line breaks only when the cell wraps, as Excel does: a file whose unwrapped cells hold multi-line text no longer opens with tall rows (typing a break still turns wrap on and grows the row) |
| 2026-10-12 | Rows auto-fit oversized fonts: opening a file and whole-row/column formatting grow non-custom rows to their tallest font (#833) |
| 2026-10-12 | Number formats: parsing a format code is linear in its length (long runs of `[`, `,` or `/` no longer take quadratic time), and an unclosed `[` is a literal when splitting sections as well as when tokenizing (#810) |
| 2026-10-12 | Number formats: a fraction denominator with 65,536 zeros no longer crashes; format codes are capped at 255 characters like Excel (longer codes display as General, `TEXT` gives `#VALUE!`) (#807) |
+17 -12
View File
@@ -453,18 +453,23 @@ fn same_content(a: Option<&gridcraft_model::Cell>, b: Option<&gridcraft_model::C
}
}
/// Recomputes an automatic row height from its content: the largest font, and wrapped or
/// multi-line text. Rows with a user-set height keep it.
/// Recomputes an automatic row height from its content: the largest font, and wrapped text.
/// Rows with a user-set height keep it.
///
/// Line breaks in a cell's text count only when the cell wraps, as in Excel: with Wrap Text off
/// a multi-line value shows on one line and AutoFit gives a one-line row. (Until 2026-10-15 a
/// line break grew the row whether or not the cell wrapped, so a sheet whose notes column held
/// pasted multi-line text had tall rows with nothing visibly tall in them.)
pub fn auto_row_height(wb: &mut Workbook, si: usize, row: u32) {
let Some(sh) = wb.sheet(si) else { return };
if sh.rows.get(&row).is_some_and(|i| i.custom) {
return;
}
let default = sh.default_row_height;
// Fast path: rows of default-size, unwrapped, single-line content keep the default height.
// Fast path: rows of default-size, unwrapped content keep the default height.
let plain = sh.cells.row(row, 0, gridcraft_core::MAX_COLS - 1).all(|(_, cell)| {
let st = wb.styles.get(cell.style);
st.font.size <= gridcraft_model::DEFAULT_FONT_SIZE && !st.align.wrap && !cell.value.as_text().is_some_and(|t| t.contains('\n'))
st.font.size <= gridcraft_model::DEFAULT_FONT_SIZE && !st.align.wrap
});
if plain {
if sh.rows.get(&row).is_some_and(|i| i.size.is_some())
@@ -487,17 +492,17 @@ pub fn auto_row_height(wb: &mut Workbook, si: usize, row: u32) {
let line = (size * 1.8).round().max(default * size / gridcraft_model::DEFAULT_FONT_SIZE);
let mut lines = 1.0f32;
if !cell.value.is_empty() || cell.formula.is_some() {
let text = crate::display::cell_text(wb, sh, CellRef::new(row, c));
let explicit = text.lines().count().max(1) as f32;
lines = if st.align.wrap {
// Only a wrapping cell is laid out over several lines: each explicit line break
// 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;
text.lines()
lines = text
.lines()
.map(|l| (crate::cmd::format::approx_text_width(l, size, st.font.bold) / w.max(1.0)).ceil().max(1.0))
.sum::<f32>()
.max(1.0)
} else {
explicit
};
.max(1.0);
}
} else if st.font.size <= gridcraft_model::DEFAULT_FONT_SIZE {
continue;
}
+31
View File
@@ -1119,6 +1119,37 @@ fn open_grows_default_height_rows_to_their_font() {
assert_eq!(s.doc().unwrap().wb.active().unwrap().row_height(0), default_h, "11pt row stays default");
}
#[test]
fn line_breaks_grow_a_row_only_when_the_cell_wraps() {
// Excel: a cell holding "a\nb" with Wrap Text off displays on one line and AutoFit gives a
// one-line row; with Wrap Text on it gets one line per break. Until 2026-10-15 the break grew
// the row either way, so a file whose notes column held pasted multi-line text (wrap off, as
// generating libraries write it) opened with tall rows and nothing visibly tall in them.
// Typing a break (Alt+Enter) turns wrap on, as in Excel, so the unwrapped case comes from a
// file, built with the model API and exported without row heights.
let text = "first line\n\n\nfourth line";
let bytes = {
let mut wb = gridcraft_model::Workbook::new();
let sh = wb.sheet_mut(0).unwrap();
sh.set_value(CellRef::new(1, 1), Value::text(text));
gridcraft_xlsx::write_xlsx(&wb).unwrap()
};
let default_h = gridcraft_model::Workbook::new().active().unwrap().row_height(1);
let mut s = Session::new();
s.execute("file.open", json!({"name": "notes.xlsx", "base64": crate::io::base64_encode(&bytes)})).unwrap();
assert_eq!(s.doc().unwrap().wb.active().unwrap().row_height(1), default_h, "an unwrapped multi-line cell keeps the default row height on open");
// Turning wrap on lays the text out over its lines and the row grows to hold them.
s.execute("home.wrapText", json!({"range": "B2", "on": true})).unwrap();
let wrapped_h = s.doc().unwrap().wb.active().unwrap().row_height(1);
assert!(wrapped_h > default_h * 2.5, "wrapped four-line text grows the row, got {wrapped_h} vs default {default_h}");
// Turning it off again returns the row to one line.
s.execute("home.wrapText", json!({"range": "B2", "on": false})).unwrap();
assert_eq!(s.doc().unwrap().wb.active().unwrap().row_height(1), default_h, "wrap off: back to a one-line row");
// Entering a break by hand wraps the cell (Alt+Enter), so that row does grow.
s.execute("cell.set", json!({"cell": "D4", "input": text})).unwrap();
assert!(s.doc().unwrap().wb.active().unwrap().row_height(3) > default_h * 2.5, "typed line breaks turn wrap on and grow the row");
}
#[test] // B — Review Focus 1
fn open_keeps_custom_height_rows() {
let mut s = Session::new();
+2 -1
View File
@@ -1,6 +1,6 @@
# Target-app parity: GridCraft vs Microsoft Excel
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-12 · **Change:** minor (rows auto-fit oversized fonts on open and row formatting) · **Target:** Microsoft Excel (Microsoft 365)
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-15 · **Change:** minor (row auto-fit counts line breaks only in wrapping cells) · **Target:** Microsoft Excel (Microsoft 365)
This is the authoritative parity assessment. [`ROADMAP.md`](../ROADMAP.md) summarizes it,
[`gaps.md`](gaps.md) lists every shortfall, and the area checklists hold the detail:
@@ -332,6 +332,7 @@ The inventory carried over from the 2026-10-07 ROADMAP.md, updated for what land
| Date | Change | Summary |
|---|---|---|
| 2026-10-15 | minor | Row auto-fit: line breaks count only in wrapping cells (Excel: wrap off shows one line, AutoFit gives one line); unwrapped multi-line cells from files no longer grow rows on open (no score change) |
| 2026-10-12 | minor | Essentials AutoFit evidence: rows auto-fit oversized fonts on open and row formatting (#833) |
| 2026-10-12 | minor | Sheet tabs: reveal the selected tab on navigation and resize while preserving manual scrolling (#681); no score change |
| 2026-10-12 | minor | PivotTables: values on rows (Σ Values in Rows at any position, all three layouts; `pivot.moveField` and the field pane move it; `dataOnRows` round-trips) (no score change) |