Merge pull request #228 from webdevtodayjason/fix/200-formula-helpers

Keep formula helpers inside the worksheet when scrolling (#200)

# Conflicts:
#	contributors/people.toml
#	docs/ui-parity.md
This commit is contained in:
Brandon Thomas 2026-10-11 11:44:56 -04:00
commit ea0e971e27
5 changed files with 164 additions and 5 deletions

View File

@ -38,3 +38,7 @@ sync_github_name = true
[people.alexanderalber]
real_name = "Alexander Alber"
[people.webdevtodayjason]
display_name = "Jason Brashear"
sync_github_name = false

View File

@ -340,9 +340,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;

View File

@ -1847,10 +1847,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);
}

View File

@ -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");
}

View File

@ -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) |
@ -93,5 +93,6 @@ scrollbar bugs 2–4 · pixel/feel pass against observed Excel 7–12.
| Date | Change | Summary |
|---|---|---|
| 2026-10-11 | minor | Delete and Backspace now remove the selected chart, picture or shape (#213) |
| 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 |