From 3a8f6f5aa505396d7486eb8ef9c7743d4732222a Mon Sep 17 00:00:00 2001 From: tydude001 Date: Sun, 11 Oct 2026 19:04:55 -0500 Subject: [PATCH] AutoFilter: the value list scrolls across the menu's full width (Fixes #432) The value list kept egui's default auto_shrink, so its scroll bar sat mid-menu after the longest value and the wheel only scrolled it with the pointer over a value. Keep the height shrinking but not the width: the scroll bar is at the menu's right edge and the wheel works anywhere over the list. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/grid.rs | 4 +- crates/ui-egui/tests/filter_list_scroll.rs | 61 ++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 crates/ui-egui/tests/filter_list_scroll.rs diff --git a/crates/ui-egui/src/grid.rs b/crates/ui-egui/src/grid.rs index d0eef85..aaf6885 100644 --- a/crates/ui-egui/src/grid.rs +++ b/crates/ui-egui/src/grid.rs @@ -2547,7 +2547,9 @@ fn filter_menu(app: &mut SheetApp, ui: &mut egui::Ui, geo: &Geo, was_open: bool) c.1 = all_new; } } - egui::ScrollArea::vertical().max_height(220.0).show(ui, |ui| { + // Full width: shrunk to the longest value, the scroll bar sat mid-menu and the wheel only + // worked over the values themselves (#432). + egui::ScrollArea::vertical().max_height(220.0).auto_shrink([false, true]).show(ui, |ui| { for (v, on) in checks.iter_mut().filter(|(v, _)| matches(v)) { ui.checkbox(on, v.as_str()); } diff --git a/crates/ui-egui/tests/filter_list_scroll.rs b/crates/ui-egui/tests/filter_list_scroll.rs new file mode 100644 index 0000000..35406fa --- /dev/null +++ b/crates/ui-egui/tests/filter_list_scroll.rs @@ -0,0 +1,61 @@ +//! The AutoFilter value list scrolls with the pointer anywhere over it, not only over the values +//! themselves (issue #432): the list takes the menu's full width, so its scroll bar and wheel area do too. + +use egui::{Event, Modifiers, MouseWheelUnit, PointerButton, TouchPhase, pos2, vec2}; +use egui_kittest::kittest::Queryable; +use gridcraft_engine::Session; +use gridcraft_engine::core::CellRef; +use gridcraft_ui_egui::{SheetApp, grid}; +use serde_json::json; + +type Harness = egui_kittest::Harness<'static, SheetApp>; + +/// A1:A81 holds a header and 80 distinct values, with the AutoFilter's menu open on column A. +fn filter_menu_on_a_long_list() -> Harness { + let mut session = Session::new(); + session.new_workbook(); + let mut values = vec![vec![json!("Code")]]; + values.extend((1..=80).map(|i| vec![json!(format!("ITEM-{i:03}"))])); + session.execute("range.setValues", json!({"range": "A1", "values": values})).unwrap(); + session.execute("data.filter", json!({"range": "A1:A81"})).unwrap(); + let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1200.0, 800.0)).build_ui_state( + |ui, app: &mut SheetApp| { + app.logic(ui.ctx()); + app.ui(ui); + }, + SheetApp::new(session, Default::default()), + ); + h.run_steps(4); + let button = { + let app = h.state(); + let sh = app.session.active().unwrap().wb.active().unwrap(); + let geo = grid::Geo::new(sh, app.grid.rect.unwrap(), app.view().scroll); + grid::filter_button_rect(&geo, sh, CellRef::parse("A1").unwrap()).center() + }; + h.input_mut().events.push(Event::PointerMoved(button)); + for pressed in [true, false] { + h.input_mut().events.push(Event::PointerButton { pos: button, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE }); + } + h.run_steps(4); + assert!(h.state().grid.filter_menu.is_some(), "the filter menu opens"); + h +} + +#[test] +fn filter_list_scrolls_with_the_pointer_right_of_the_values() { + let mut h = filter_menu_on_a_long_list(); + let first = h.get_by_label("ITEM-001").rect(); + // The menu is 230 points wide; the value labels end well before this point. + let pointer = pos2(first.left() + 200.0, first.center().y); + h.input_mut().events.push(Event::PointerMoved(pointer)); + h.run_steps(2); + h.input_mut().events.push(Event::MouseWheel { + unit: MouseWheelUnit::Line, + delta: vec2(0.0, -3.0), + phase: TouchPhase::Move, + modifiers: Modifiers::NONE, + }); + h.run_steps(20); + let now = h.query_by_label("ITEM-001").map(|n| n.rect().top()); + assert!(now.is_none_or(|y| y < first.top() - 20.0), "the wheel scrolls the list: ITEM-001 was at {}, now {now:?}", first.top()); +}