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:?}"); +}