From 02ceedee173b6b1e6c063e8e4a7f5ec0924e2d83 Mon Sep 17 00:00:00 2001 From: Camilo Agudelo Date: Mon, 19 Oct 2026 10:12:17 -0500 Subject: [PATCH] In-cell editor: a Wrap Text cell edits wrapped at the column width The in-cell editor ignored Wrap Text: it widened to the text's one-line width, up to the grid's right edge, so a long wrapped cell became one unmanageable line while edited. With Wrap Text on it now keeps the column's width, wraps, and grows down to show every line, as Excel's does. The multiline field is one row tall at least, not four, so a short edit doesn't reach over the rows below. --- crates/ui-egui/src/formula_bar.rs | 8 ++- crates/ui-egui/src/grid.rs | 16 ++++-- crates/ui-egui/tests/in_cell_editor_wrap.rs | 59 +++++++++++++++++++++ 3 files changed, 78 insertions(+), 5 deletions(-) create mode 100644 crates/ui-egui/tests/in_cell_editor_wrap.rs diff --git a/crates/ui-egui/src/formula_bar.rs b/crates/ui-egui/src/formula_bar.rs index 7f1ad87fc..848ba5458 100644 --- a/crates/ui-egui/src/formula_bar.rs +++ b/crates/ui-egui/src/formula_bar.rs @@ -279,7 +279,13 @@ pub fn editor_widget(app: &mut SheetApp, ui: &mut egui::Ui, id: egui::Id, font: job.wrap.max_width = if multiline { wrap_width } else { f32::INFINITY }; ui.fonts_mut(|f| f.layout_job(job)) }; - let edit = if multiline || ed.text.contains('\n') { TextEdit::multiline(&mut ed.text) } else { TextEdit::singleline(&mut ed.text) }; + // The in-cell editor is as tall as its lines (a multiline field reserves four rows). + let in_cell = id == egui::Id::new("gridcraft.cell_editor"); + let edit = if multiline || ed.text.contains('\n') { + TextEdit::multiline(&mut ed.text).desired_rows(if in_cell { 1 } else { 4 }) + } else { + TextEdit::singleline(&mut ed.text) + }; let mut output = edit .id(id) .frame(egui::Frame::NONE) diff --git a/crates/ui-egui/src/grid.rs b/crates/ui-egui/src/grid.rs index 90a39cb06..6cd49691c 100644 --- a/crates/ui-egui/src/grid.rs +++ b/crates/ui-egui/src/grid.rs @@ -2707,10 +2707,18 @@ fn in_cell_editor(app: &mut SheetApp, ui: &mut egui::Ui, geo: &Geo, sh: &Sheet, let st = wb.styles.get(sh.style_id(cell)).clone(); let size = st.font.size * 96.0 / 72.0 * geo.z; let font = FontId::new(size, theme::cell_family(&st.font.name, st.font.bold, st.font.italic)); - let text_w = ui.painter().layout_no_wrap(ed.text.clone(), font.clone(), Color32::BLACK).size().x; let lines = ed.text.lines().count().max(1) as f32 + if ed.text.ends_with('\n') { 1.0 } else { 0.0 }; - let w = (text_w + 14.0).max(rect.width()).min(geo.cells.right() - rect.left()); - let h = (size * 1.3 * lines + 4.0).max(rect.height()); + // Wrap Text: the editor keeps the column's width and wraps, growing down as Excel's does. + // Otherwise it widens to the text's one-line width. + let wrap = st.align.wrap; + let (w, h) = if wrap { + // The field's own margins (4 px a side) and the 2 px inset leave this much for the text. + let wrapped = ui.painter().layout(ed.text.clone(), font.clone(), Color32::BLACK, (rect.width() - 12.0).max(1.0)); + (rect.width(), (wrapped.size().y.max(size * 1.3 * lines) + 4.0).max(rect.height())) + } else { + let text_w = ui.painter().layout_no_wrap(ed.text.clone(), font.clone(), Color32::BLACK).size().x; + ((text_w + 14.0).max(rect.width()).min(geo.cells.right() - rect.left()), (size * 1.3 * lines + 4.0).max(rect.height())) + }; // Grows to the left on a right-to-left sheet. let erect = geo.mirror_rect(Rect::from_min_size(rect.min, vec2(w, h))); let fill = match st.fill.pattern { @@ -2729,7 +2737,7 @@ fn in_cell_editor(app: &mut SheetApp, ui: &mut egui::Ui, geo: &Geo, sh: &Sheet, let id = egui::Id::new("gridcraft.cell_editor"); let mut child = ui.new_child(egui::UiBuilder::new().max_rect(erect.shrink2(vec2(2.0, 1.0)))); child.set_clip_rect(clip); - crate::formula_bar::editor_widget(app, &mut child, id, font, false, erect.width() - 4.0); + crate::formula_bar::editor_widget(app, &mut child, id, font, wrap, erect.width() - 4.0); } /// Test the popup itself, not whether the pointer is over any part of the app. diff --git a/crates/ui-egui/tests/in_cell_editor_wrap.rs b/crates/ui-egui/tests/in_cell_editor_wrap.rs new file mode 100644 index 000000000..7ebbf79ea --- /dev/null +++ b/crates/ui-egui/tests/in_cell_editor_wrap.rs @@ -0,0 +1,59 @@ +//! The in-cell editor of a Wrap Text cell keeps the column's width and wraps, growing down, as +//! Excel's does; it used to widen to one long line. +use egui::{Context, Id, RawInput, Rect, pos2, vec2}; +use gridcraft_engine::Session; +use gridcraft_ui_egui::SheetApp; +use serde_json::json; + +const TEXT: &str = "Since the 1960s, NASA has left hardware across the solar system, on the Moon, on Mars, and in deep space."; + +/// Runs a few frames of the app in a 1200 × 800 window. +fn frames(ctx: &Context, app: &mut SheetApp) { + for _ in 0..4 { + let mut output = + ctx.run_ui(RawInput { screen_rect: Some(Rect::from_min_size(pos2(0.0, 0.0), vec2(1200.0, 800.0))), ..Default::default() }, |ui| { + let ctx = ui.ctx().clone(); + app.logic(&ctx); + app.ui(ui); + }); + output.textures_delta.clear(); + } +} + +/// The editor's text field rect while A1 (narrow, wrap on or off) is edited with [`TEXT`]. +fn editor_rect(wrap: bool) -> Rect { + let ctx = Context::default(); + let mut session = Session::new(); + session.new_workbook(); + session.execute("home.columnWidth", json!({"cols": "A:A", "chars": 12})).unwrap(); + session.execute("home.wrapText", json!({"range": "A1", "on": wrap})).unwrap(); + let mut app = SheetApp::new(session, Default::default()); + frames(&ctx, &mut app); + app.begin_edit(Some(TEXT.into()), false); + frames(&ctx, &mut app); + ctx.read_response(Id::new("gridcraft.cell_editor")).expect("the in-cell editor is shown").rect +} + +#[test] +fn a_wrap_text_cell_edits_wrapped_at_the_column_width() { + let wrapped = editor_rect(true); + let one_line = editor_rect(false); + assert!(one_line.width() > 400.0, "without Wrap Text the editor widens to the text: {one_line:?}"); + assert!(wrapped.width() < 120.0, "with Wrap Text the editor keeps the column's width: {wrapped:?}"); + assert!(wrapped.height() > 4.0 * one_line.height(), "and grows down to show every line: {wrapped:?} vs {one_line:?}"); +} + +/// A short Wrap Text edit stays one line tall: the field doesn't reserve rows over the cells below. +#[test] +fn a_short_wrap_text_edit_stays_one_line_tall() { + let ctx = Context::default(); + let mut session = Session::new(); + session.new_workbook(); + session.execute("home.wrapText", json!({"range": "A1", "on": true})).unwrap(); + let mut app = SheetApp::new(session, Default::default()); + frames(&ctx, &mut app); + app.begin_edit(Some("short".into()), false); + frames(&ctx, &mut app); + let field = ctx.read_response(Id::new("gridcraft.cell_editor")).expect("the in-cell editor is shown").rect; + assert!(field.height() < 30.0, "a one-line edit doesn't reach over the rows below: {field:?}"); +}