ui: a too-wide number in a repeat-fill format shows ####

A number too wide for its column is drawn as ####. With a repeat fill
(`*` in the format, as in the Accounting format) the fill step then
rebuilt the text from the formatted value, so it overflowed and, being
right-aligned, lost its leading digits: 2,509.00 CHF in a narrow column
was drawn as "509.00 CHF". The fill now leaves #### alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Dominik Strebel
2026-10-22 15:00:06 +02:00
co-authored by Claude Opus 5.5
parent a1c84547e3
commit 843c9b8404
3 changed files with 84 additions and 3 deletions
+5 -2
View File
@@ -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;
+77
View File
@@ -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:?}");
}
+2 -1
View File
@@ -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 |