From 83c7a3c2f5b582388b7436b8c48be76e5790a019 Mon Sep 17 00:00:00 2001 From: webdevtodayjason <115739170+webdevtodayjason@users.noreply.github.com> Date: Sat, 10 Oct 2026 22:56:15 +0000 Subject: [PATCH 1/2] Contributors: add Jason Brashear display name --- contributors/people.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contributors/people.toml b/contributors/people.toml index 78683ae..9b0d8aa 100644 --- a/contributors/people.toml +++ b/contributors/people.toml @@ -28,3 +28,7 @@ sync_github_name = false [people.edurbs] sync_github_name = true + +[people.webdevtodayjason] +display_name = "Jason Brashear" +sync_github_name = false From a788c41faa5b94ab280cfa40c9e1e43d4b731ce5 Mon Sep 17 00:00:00 2001 From: webdevtodayjason <115739170+webdevtodayjason@users.noreply.github.com> Date: Sat, 10 Oct 2026 22:56:15 +0000 Subject: [PATCH 2/2] #200: keep formula helpers inside the worksheet Fixes #200 --- crates/ui-egui/src/formula_bar.rs | 6 +- crates/ui-egui/src/grid.rs | 12 ++- crates/ui-egui/tests/formula_popup.rs | 144 ++++++++++++++++++++++++++ docs/ui-parity.md | 5 +- 4 files changed, 161 insertions(+), 6 deletions(-) create mode 100644 crates/ui-egui/tests/formula_popup.rs diff --git a/crates/ui-egui/src/formula_bar.rs b/crates/ui-egui/src/formula_bar.rs index 6cca110..63abc6f 100644 --- a/crates/ui-egui/src/formula_bar.rs +++ b/crates/ui-egui/src/formula_bar.rs @@ -336,9 +336,11 @@ pub fn editor_widget(app: &mut SheetApp, ui: &mut egui::Ui, id: egui::Id, font: ui.painter().galley(pos, g, Color32::WHITE); } // Autocomplete list and argument hint under the editor. - if mine && (!ed.autocomplete.is_empty() || ed.current_function().is_some()) { + if mine && ui.is_rect_visible(resp.rect) && (!ed.autocomplete.is_empty() || ed.current_function().is_some()) { let below = resp.rect.left_bottom() + vec2(0.0, 4.0); - egui::Area::new(id.with("ac")).fixed_pos(below).order(egui::Order::Tooltip).show(ui.ctx(), |ui| { + // Areas have their own clip rect: keep cell helpers inside the worksheet. + let bounds = if ed.from_formula_bar { ui.ctx().content_rect() } else { ui.clip_rect() }; + egui::Area::new(id.with("ac")).fixed_pos(below).constrain_to(bounds).order(egui::Order::Tooltip).show(ui.ctx(), |ui| { egui::Frame::popup(ui.style()).inner_margin(4.0).show(ui, |ui| { if !ed.autocomplete.is_empty() { let mut pick = None; diff --git a/crates/ui-egui/src/grid.rs b/crates/ui-egui/src/grid.rs index 72d33e2..8cad6c6 100644 --- a/crates/ui-egui/src/grid.rs +++ b/crates/ui-egui/src/grid.rs @@ -1756,10 +1756,18 @@ fn in_cell_editor(app: &mut SheetApp, ui: &mut egui::Ui, geo: &Geo, sh: &Sheet, PatternType::Solid => st.fill.fg.resolve(&wb.theme).map(theme::color32).unwrap_or(Color32::WHITE), _ => Color32::WHITE, }; - ui.painter().rect_filled(erect, 0.0, fill); - ui.painter().rect_stroke(erect.expand(1.0), 0.0, Stroke::new(2.0, Tokens::get(ui.ctx()).sel_border), StrokeKind::Middle); + let split = pos2(geo.cells.left() + geo.frozen_w, geo.cells.top() + geo.frozen_h); + let quadrant = Rect::from_min_max( + pos2(if cell.col >= geo.fc { split.x } else { geo.cells.left() }, if cell.row >= geo.fr { split.y } else { geo.cells.top() }), + pos2(if cell.col >= geo.fc { geo.cells.right() } else { split.x }, if cell.row >= geo.fr { geo.cells.bottom() } else { split.y }), + ); + let clip = ui.clip_rect().intersect(quadrant); + let painter = ui.painter().with_clip_rect(clip); + painter.rect_filled(erect, 0.0, fill); + painter.rect_stroke(erect.expand(1.0), 0.0, Stroke::new(2.0, Tokens::get(ui.ctx()).sel_border), StrokeKind::Middle); 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); } diff --git a/crates/ui-egui/tests/formula_popup.rs b/crates/ui-egui/tests/formula_popup.rs new file mode 100644 index 0000000..6734d30 --- /dev/null +++ b/crates/ui-egui/tests/formula_popup.rs @@ -0,0 +1,144 @@ +//! Formula helpers must stay in the worksheet while its editor scrolls. +use egui::{Context, Id, LayerId, Order, RawInput, Rect, pos2, vec2}; +use gridcraft_engine::Session; +use gridcraft_ui_egui::SheetApp; + +fn frame(ctx: &Context, app: &mut SheetApp) -> egui::FullOutput { + 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(); + output +} + +fn frames(ctx: &Context, app: &mut SheetApp) { + for _ in 0..4 { + frame(ctx, app); + } +} + +fn visible(ctx: &Context, editor: &str) -> bool { + ctx.memory(|m| m.areas().is_visible(&LayerId::new(Order::Tooltip, Id::new(editor).with("ac")))) +} + +#[test] +fn cell_formula_helpers_hide_when_scrolled_out_and_return_with_the_editor() { + for text in ["=SU", "=SUM("] { + let ctx = Context::default(); + let mut session = Session::new(); + session.new_workbook(); + let mut app = SheetApp::new(session, Default::default()); + frames(&ctx, &mut app); + app.begin_edit(Some(text.into()), false); + if text == "=SU" { + app.editor.as_mut().unwrap().update_autocomplete(&["SUM".into(), "SUMIF".into()]); + } + frames(&ctx, &mut app); + assert!(visible(&ctx, "gridcraft.cell_editor"), "visible cell shows {text} help"); + app.view_mut().unwrap().scroll.y = 400.0; + frames(&ctx, &mut app); + assert!(!visible(&ctx, "gridcraft.cell_editor"), "scrolled-out cell must hide {text} help"); + assert_eq!(app.editor.as_ref().unwrap().text, text, "scrolling preserves the edit"); + app.view_mut().unwrap().scroll.y = 0.0; + frames(&ctx, &mut app); + assert!(visible(&ctx, "gridcraft.cell_editor"), "scrolling back restores {text} help"); + } +} + +#[test] +fn formula_bar_hint_remains_visible_when_the_worksheet_scrolls() { + let ctx = Context::default(); + let mut session = Session::new(); + session.new_workbook(); + let mut app = SheetApp::new(session, Default::default()); + frames(&ctx, &mut app); + app.begin_edit(Some("=SUM(".into()), true); + frames(&ctx, &mut app); + assert!(visible(&ctx, "gridcraft.formula_bar")); + app.view_mut().unwrap().scroll.y = 400.0; + frames(&ctx, &mut app); + assert!(visible(&ctx, "gridcraft.formula_bar")); +} + +#[test] +fn cell_argument_hint_is_clipped_to_the_worksheet() { + let ctx = Context::default(); + let mut session = Session::new(); + session.new_workbook(); + let mut app = SheetApp::new(session, Default::default()); + frames(&ctx, &mut app); + app.begin_edit(Some("=SUM(".into()), false); + frames(&ctx, &mut app); + app.view_mut().unwrap().scroll.y = 10.0; + frames(&ctx, &mut app); + let output = frame(&ctx, &mut app); + let cells = app.grid.cells_rect.unwrap(); + let hint = output.shapes.iter().find(|s| matches!(&s.shape, egui::Shape::Text(t) if t.galley.text() == "SUM(")).expect("argument hint painted"); + assert!(cells.contains_rect(hint.clip_rect), "helper must not paint over the headers: {:?}", hint.clip_rect); +} + +#[test] +fn scrolled_out_cell_editor_still_handles_escape() { + let ctx = Context::default(); + let mut session = Session::new(); + session.new_workbook(); + let mut app = SheetApp::new(session, Default::default()); + frames(&ctx, &mut app); + app.begin_edit(Some("=SUM(".into()), false); + frames(&ctx, &mut app); + app.view_mut().unwrap().scroll.y = 400.0; + frames(&ctx, &mut app); + let mut output = ctx.run_ui( + RawInput { + screen_rect: Some(Rect::from_min_size(pos2(0.0, 0.0), vec2(1200.0, 800.0))), + events: vec![egui::Event::Key { + key: egui::Key::Escape, + physical_key: None, + pressed: true, + repeat: false, + modifiers: egui::Modifiers::NONE, + }], + ..Default::default() + }, + |ui| app.ui(ui), + ); + output.textures_delta.clear(); + assert!(app.editor.is_none(), "scrolling must not disable editor keys"); +} + +#[test] +fn cell_editor_and_helper_stay_below_the_frozen_top_row() { + let ctx = Context::default(); + let mut session = Session::new(); + session.new_workbook(); + session.run("view.freezeTopRow", serde_json::json!({})).unwrap(); + session.run("selection.set", serde_json::json!({"range": "B5"})).unwrap(); + let mut app = SheetApp::new(session, Default::default()); + frames(&ctx, &mut app); + let sh = app.session.active().unwrap().wb.active().unwrap(); + let frozen_h = sh.row_height(0); + let scroll = sh.row_top(4) as f32; + app.begin_edit(Some("=SUM(".into()), false); + frames(&ctx, &mut app); + assert!(visible(&ctx, "gridcraft.cell_editor")); + app.view_mut().unwrap().scroll.y = scroll - frozen_h / 2.0; + frames(&ctx, &mut app); + let output = frame(&ctx, &mut app); + let frozen_bottom = app.grid.cells_rect.unwrap().top() + frozen_h * gridcraft_ui_egui::grid::DISPLAY_SCALE; + let editor = output + .shapes + .iter() + .find(|s| s.clip_rect.bottom() > frozen_bottom && matches!(&s.shape, egui::Shape::Text(t) if t.galley.text() == "=SUM(")) + .expect("editor painted"); + assert!(editor.clip_rect.top() >= frozen_bottom, "editor must be clipped below the frozen row: {:?}", editor.clip_rect); + app.view_mut().unwrap().scroll.y = scroll; + frames(&ctx, &mut app); + assert!(!visible(&ctx, "gridcraft.cell_editor"), "cell hidden under the frozen row must hide its helper"); + assert_eq!(app.editor.as_ref().unwrap().text, "=SUM("); + app.view_mut().unwrap().scroll.y = 0.0; + frames(&ctx, &mut app); + assert!(visible(&ctx, "gridcraft.cell_editor"), "scrolling back restores the helper"); +} diff --git a/docs/ui-parity.md b/docs/ui-parity.md index e85bca1..689a580 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-10 · **Change:** major (created from a source audit and the issue tracker) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** minor (formula helpers stay inside the worksheet, #200) · **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 @@ -36,7 +36,7 @@ Sources: `crates/ui-egui/src` (ribbon, grid, editor, formula bar, dialogs, panes | Drag-move and drag-copy cells (border drag, Ctrl/Option, Shift to insert) | done (M3) | | In-cell editing, Enter/Tab movement, Alt/Option+Enter line breaks | done | | Point mode (click/drag/arrow references), reference colouring, F4 | done | -| AutoComplete in columns, function autocomplete and argument tips | done | +| AutoComplete in columns, function autocomplete and argument tips | done; cell helpers clip to the worksheet and hide when the editor scrolls out (#200) | | Header resize and double-click AutoFit | done (#42, #88 for header dividers) | | Freeze panes, split panes | freeze done; split to verify | | Copy as HTML table for other apps | done (#36, 10-10) | @@ -91,5 +91,6 @@ scrollbar bugs 2–4 · pixel/feel pass against observed Excel 7–12. | Date | Change | Summary | |---|---|---| +| 2026-10-10 | minor | Keep formula helpers inside the worksheet when scrolling (#200) | | 2026-10-10 | minor | Chart resize bug (#190) | | 2026-10-10 | major | Created: chrome, grid, keyboard, dialogs, accessibility; key tips, Page Layout view and accessibility tree flagged |