diff --git a/crates/engine/src/display.rs b/crates/engine/src/display.rs index a604f0b..9770518 100644 --- a/crates/engine/src/display.rs +++ b/crates/engine/src/display.rs @@ -31,6 +31,37 @@ pub fn format(v: &Value, code: &str, wb: &Workbook) -> Formatted { format_value(v, &parsed(code), wb.date_system) } +/// What the cell editor and the formula bar start from. Same as `Cell::input_text`, except that a +/// constant number in a date or time format is shown as a date (as the grid shows it), not as its +/// serial. When the formatted text would not read back as the same date (for example +/// `dddd, mmmm d`), an ISO `yyyy-mm-dd` (with `hh:mm:ss` for a time of day; just `hh:mm:ss` +/// for a time alone) is used instead, and if even that would not read back, the serial. +pub fn edit_text(wb: &Workbook, sheet: &Sheet, c: CellRef) -> String { + let raw = sheet.input_text(c); + let Some(cell) = sheet.cell(c) else { return raw }; + let Value::Number(n) = &cell.value else { return raw }; + let code = wb.styles.get(cell.style).num_fmt.as_str(); + if cell.formula.is_some() || !parsed(code).is_date() { + return raw; + } + let reads_back = |s: &str| matches!(gridcraft_core::parse::parse_input(s, wb.date_system).value, Value::Number(m) if (m - n).abs() < 1e-9); + let shown = format(&cell.value, code, wb).text; + if reads_back(&shown) { + return shown; + } + let Some(dt) = gridcraft_core::date::datetime_from_serial(wb.date_system, *n) else { return raw }; + let time = format!("{:02}:{:02}:{:02}", dt.hour, dt.minute, dt.second); + let date = format!("{:04}-{:02}-{:02}", dt.year, dt.month, dt.day); + let iso = if *n < 1.0 { + time + } else if n.fract() == 0.0 { + date + } else { + format!("{date} {time}") + }; + if reads_back(&iso) { iso } else { raw } +} + /// Whether `c` has a formula that must not be shown: its cell is formatted Hidden on a /// protected sheet. A spilled cell follows its anchor. pub fn formula_hidden(wb: &Workbook, sheet: &Sheet, c: CellRef) -> bool { diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index cf24ab3..40a0851 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -2095,3 +2095,31 @@ fn copying_areas_that_do_not_line_up_is_refused_with_a_message() { assert!(s.execute("edit.pasteSpecial", json!({"what": "all", "link": true, "at": "E1"})).is_err()); assert!(v(&s, "E1").is_empty()); } + +#[test] +fn editing_a_date_cell_shows_a_date_not_the_serial() { + let mut s = s(); + let at = CellRef::parse("A1").unwrap(); + s.execute("cell.set", json!({"cell": "A1", "input": "2026-03-04"})).unwrap(); + { + let d = s.doc().unwrap(); + let sh = d.wb.active().unwrap(); + assert_eq!(crate::display::edit_text(&d.wb, sh, at), "2026-03-04"); + } + // A format whose text cannot be typed back falls back to ISO. + s.execute("home.formatCells", json!({"range": "A1", "style": {"num_fmt": "dddd, mmmm d"}})).unwrap(); + let d = s.doc().unwrap(); + let sh = d.wb.active().unwrap(); + assert_eq!(crate::display::edit_text(&d.wb, sh, at), "2026-03-04"); + // A time alone falls back to a time, not to day 0 of 1900 ("9 AM" would lose the 36 minutes). + let mut s3 = self::s(); + s3.execute("cell.set", json!({"cell": "A1", "input": "0.4"})).unwrap(); + s3.execute("home.formatCells", json!({"range": "A1", "style": {"num_fmt": "h AM/PM"}})).unwrap(); + let d = s3.doc().unwrap(); + assert_eq!(crate::display::edit_text(&d.wb, d.wb.active().unwrap(), at), "09:36:00"); + // Plain numbers are untouched. + let mut s2 = self::s(); + s2.execute("cell.set", json!({"cell": "A1", "input": "42"})).unwrap(); + let d = s2.doc().unwrap(); + assert_eq!(crate::display::edit_text(&d.wb, d.wb.active().unwrap(), at), "42"); +} diff --git a/crates/ui-egui/src/formula_bar.rs b/crates/ui-egui/src/formula_bar.rs index 40a25c4..7404a61 100644 --- a/crates/ui-egui/src/formula_bar.rs +++ b/crates/ui-egui/src/formula_bar.rs @@ -78,7 +78,7 @@ fn active_input(app: &SheetApp) -> String { // A legacy array formula shows in braces (`{=A1:A2*2}`), as `Cell::bar_text` does. if f.array.is_some() { format!("{{{shown}}}") } else { shown } } else { - c.input_text() + gridcraft_engine::display::edit_text(&d.wb, sh, a) }; } // A spilled cell shows the anchor's formula greyed out in Excel; we show it plainly. diff --git a/crates/ui-egui/src/keymap.rs b/crates/ui-egui/src/keymap.rs index 1dc90e4..54ab319 100644 --- a/crates/ui-egui/src/keymap.rs +++ b/crates/ui-egui/src/keymap.rs @@ -184,7 +184,7 @@ pub(crate) fn dispatch(app: &mut SheetApp, ctx: &egui::Context, key: Key, physic Shortcut::FormulaAbove | Shortcut::ValueAbove => { let Some(above) = active.row.checked_sub(1).map(|r| CellRef::new(r, active.col)) else { return true }; let text = if shortcut == Shortcut::FormulaAbove { - sh.cell(above).map(|c| c.input_text()).unwrap_or_default() + app.session.active().map(|d| gridcraft_engine::display::edit_text(&d.wb, sh, above)).unwrap_or_default() } else { app.session .active() diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index d174397..e4fa4ba 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -550,11 +550,10 @@ impl SheetApp { let current = if sh.cell_pictures.contains_key(&at) { String::new() } else { - sh.cell(at) - .map( - |c| if c.formula.is_some() { formula_locale::display(&c.input_text(), locale, &d.wb, d.wb.active_sheet) } else { c.input_text() }, - ) - .unwrap_or_default() + match sh.cell(at) { + Some(c) if c.formula.is_some() => formula_locale::display(&c.input_text(), locale, &d.wb, d.wb.active_sheet), + _ => gridcraft_engine::display::edit_text(&d.wb, sh, at), + } }; let (text, replace) = match text { Some(t) => (t, true),