diff --git a/crates/ui-egui/src/grid.rs b/crates/ui-egui/src/grid.rs index a0efbfae7..87aaa3c73 100644 --- a/crates/ui-egui/src/grid.rs +++ b/crates/ui-egui/src/grid.rs @@ -1268,13 +1268,16 @@ pub(crate) fn paint_quadrant( let mut galley = p.layout_no_wrap(first_line.clone(), font.clone(), color); let mut w = galley.size().x; // Numbers and dates never overflow: they show #### when too narrow. - if !shrunk && (numeric || matches!(v, Value::Number(_))) && w > avail_w - 2.0 * pad && st.align.h != HAlign::Fill { + let hashed = !shrunk && (numeric || matches!(v, Value::Number(_))) && w > avail_w - 2.0 * pad && st.align.h != HAlign::Fill; + if hashed { let n = ((avail_w - 2.0 * pad) / char_w).floor().max(1.0) as usize; galley = p.layout_no_wrap("#".repeat(n), font.clone(), color); w = galley.size().x; } - // Repeat fill (`*` in number formats) / Fill alignment. + // Repeat fill (`*` in number formats) / Fill alignment. A number already shown as #### keeps + // it: rebuilding it from the text would overflow and clip its leading digits. if let Some((ch, pos)) = fill_char + && !hashed && pos <= first_line.len() { let base_w = w; diff --git a/crates/ui-egui/tests/accounting_fill.rs b/crates/ui-egui/tests/accounting_fill.rs new file mode 100644 index 000000000..492f581cd --- /dev/null +++ b/crates/ui-egui/tests/accounting_fill.rs @@ -0,0 +1,77 @@ +//! Repeat fill (`*` in a number format, as in the Accounting format): a number that fits is drawn +//! across its cell, and one too wide for its column shows #### instead of losing its leading digits. + +use egui::Shape; +use gridcraft_engine::Session; +use gridcraft_engine::core::CellRef; +use gridcraft_ui_egui::{SheetApp, grid::Geo}; +use serde_json::json; + +type Harness = egui_kittest::Harness<'static, SheetApp>; + +const ACCOUNTING: &str = r#"_-* #,##0.00\ "CHF"_-;\-* #,##0.00\ "CHF"_-;_-* "-"??\ "CHF"_-;_-@_-"#; + +fn harness() -> Harness { + let mut s = Session::new(); + s.new_workbook(); + s.execute("range.setValues", json!({"range": "A1:B2", "values": [[344.75, 344.75], [2509, 2509]]})).unwrap(); + s.execute("home.formatCells", json!({"range": "A1:B2", "style": {"num_fmt": ACCOUNTING}})).unwrap(); + // Column A is too narrow for 2,509.00 CHF; column B fits both. + s.execute("home.columnWidth", json!({"cols": "A:A", "chars": 7})).unwrap(); + s.execute("home.columnWidth", json!({"cols": "B:B", "chars": 20})).unwrap(); + s.execute("selection.set", json!({"range": "H20"})).unwrap(); + let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1200.0, 800.0)).build_ui_state( + |ui, app: &mut SheetApp| { + app.logic(ui.ctx()); + app.ui(ui); + }, + SheetApp::new(s, Default::default()), + ); + h.run_steps(4); + h +} + +/// Painted text shapes whose text satisfies `f`: (text, left x, width). +fn texts(h: &Harness, f: impl Fn(&str) -> bool) -> Vec<(String, f32, f32)> { + h.output() + .shapes + .iter() + .filter_map(|s| match &s.shape { + Shape::Text(t) if f(&t.galley.job.text) => Some((t.galley.job.text.clone(), t.pos.x, t.galley.size().x)), + _ => None, + }) + .collect() +} + +#[test] +fn too_wide_fill_number_shows_hashes_not_a_clipped_value() { + let h = harness(); + let app = h.state(); + let sheet = app.session.active().unwrap().wb.active().unwrap(); + let geo = Geo::new(sheet, app.grid.rect.unwrap(), app.view().scroll); + let a2 = geo.cell_rect(sheet, CellRef::parse("A2").unwrap()); + let b2 = geo.cell_rect(sheet, CellRef::parse("B2").unwrap()); + // A2 is drawn as #### inside its cell. + let hashes = texts(&h, |t| !t.is_empty() && t.chars().all(|c| c == '#')); + assert!(hashes.iter().any(|(_, x, w)| *x >= a2.left() && x + w <= a2.right() + 0.5), "A2 shows ####: {hashes:?}"); + // The only 2,509.00 drawn is B2's, whole and inside column B. + let values = texts(&h, |t| t.contains("509.00")); + assert_eq!(values.len(), 1, "{values:?}"); + let (text, x, w) = &values[0]; + assert!(text.contains("2,509.00 CHF"), "{text:?}"); + assert!(*x >= b2.left() && x + w <= b2.right() + 0.5, "B2's value stays in its cell: {values:?}"); +} + +#[test] +fn fill_spreads_a_fitting_number_across_its_cell() { + let h = harness(); + let app = h.state(); + let sheet = app.session.active().unwrap().wb.active().unwrap(); + let geo = Geo::new(sheet, app.grid.rect.unwrap(), app.view().scroll); + let b1 = geo.cell_rect(sheet, CellRef::parse("B1").unwrap()); + let drawn = texts(&h, |t| t.contains("344.75 CHF")); + let (_, x, w) = drawn.iter().find(|(_, x, _)| *x >= b1.left()).expect("B1 is drawn"); + // The fill pushes the value to the right edge (less the trailing `_-` space). + assert!(b1.right() - (x + w) < 6.0, "B1 ends at its right edge: {drawn:?} in {b1:?}"); + assert!(*x - b1.left() < 6.0, "the fill starts at the left edge: {drawn:?} in {b1:?}"); +} diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 07797d70d..601e19d9d 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-21 · **Change:** minor (Esc and Enter close a data validation alert over the cell editor; Ctrl+Enter fill gives each cell the automatic number format typing gives it; keytip letters typed with Alt held, #1846; Number Stored as Text indicator and Convert to Number) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-22 · **Change:** minor (a too-wide number in a repeat-fill format such as Accounting shows #### instead of losing its leading digits; Esc and Enter close a data validation alert over the cell editor; Ctrl+Enter fill gives each cell the automatic number format typing gives it; keytip letters typed with Alt held, #1846; Number Stored as Text indicator and Convert to Number) · **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 @@ -176,6 +176,7 @@ scrollbar bugs 2–4 · pixel/feel pass against observed Excel 7–12. | Date | Change | Summary | |---|---|---| +| 2026-10-22 | minor | A number in a repeat-fill format (`*`, as in the Accounting format) that is too wide for its column shows #### like any other number, instead of the fill rebuilding the value so its leading digits were cut off (2,509.00 CHF drawn as "509.00 CHF") | | 2026-10-21 | minor | Background error checking, first part (#1994): a constant whose text would be a number if typed (signs, thousands, currency, percent, exponent, parenthesised negative) gets Excel's green corner triangle; the active cell then shows a warning button whose menu names the problem and offers Convert to Number for the selection (`formulas.convertToNumber`). Dates, times and fractions kept as text and formula results are not flagged, as in Excel | | 2026-10-21 | minor | Paste Special laid out as Excel's: Paste and Operation options each in two columns inside a frame, Skip blanks and Transpose side by side, Paste Link as a button that pastes references at once; labels translated (the generic form didn't translate them) and the window no longer runs past the screen | | 2026-10-21 | minor | Form dialogs (conditional-format quick rules, New Name, Remove Duplicates, Text to Columns and every other `Dialog::form`) fit their content: OK and Cancel sit in a row under the last field instead of a right-to-left layout that took the screen's whole height and left the buttons floating halfway down |