From ba00f86f30ef0813251d2067cb33aa2ed04c7ddd Mon Sep 17 00:00:00 2001 From: BFlat Date: Tue, 3 Nov 2026 21:05:27 +0700 Subject: [PATCH] The command palette stays where it's scrolled (#658) The command palette follows its selection only when the arrow keys or typing move it, so the wheel's scroll isn't pulled back to the top on the next frame (fixes #584). --- crates/ui-egui/src/palette.rs | 6 +++++- crates/ui-egui/tests/smoke.rs | 27 +++++++++++++++++++++++++++ docs/ROADMAP.md | 2 ++ docs/pitfalls.md | 5 +++++ 4 files changed, 39 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/palette.rs b/crates/ui-egui/src/palette.rs index a81aed41..c5edb466 100644 --- a/crates/ui-egui/src/palette.rs +++ b/crates/ui-egui/src/palette.rs @@ -101,6 +101,9 @@ pub fn show(app: &mut CraftApp, ctx: &egui::Context) { p.selected = p.selected.saturating_sub(1); } p.selected = p.selected.min(n.saturating_sub(1)); + // The list follows the selection only when the keys or typing moved it: every frame would pull + // the wheel's scroll back to it (#584). + let mut follow = down || up; let mut chosen: Option = None; let screen = ctx.content_rect(); let width = 620.0_f32.min(screen.width() - 40.0); @@ -128,6 +131,7 @@ pub fn show(app: &mut CraftApp, ctx: &egui::Context) { r.request_focus(); if r.changed() { app.state.palette.selected = 0; + follow = true; } }); ui.add_space(6.0); @@ -142,7 +146,7 @@ pub fn show(app: &mut CraftApp, ctx: &egui::Context) { if on || r.hovered() { ui.painter().rect_filled(rect, t.radius_sm, if on { t.accent_soft } else { t.hover }); } - if on { + if on && follow { r.scroll_to_me(None); } let (icon, title, sub, tag) = match e { diff --git a/crates/ui-egui/tests/smoke.rs b/crates/ui-egui/tests/smoke.rs index c6ac8e09..2119c683 100644 --- a/crates/ui-egui/tests/smoke.rs +++ b/crates/ui-egui/tests/smoke.rs @@ -960,6 +960,33 @@ fn command_palette_opens_and_lists_tools() { assert!(!h.state().state.palette.open); } +/// #584: the palette's list jumped back to the top after every scroll, because the selected row +/// asked to be scrolled into view on every frame. +#[test] +fn the_command_palette_stays_where_it_was_scrolled() { + let mut h = harness(|| demo::app(demo::engine(Mode::Experienced, true))); + h.run_steps(3); + h.key_press_modifiers(egui::Modifiers::COMMAND, egui::Key::K); + h.run_steps(3); + let before = h.get_by_label("Session status").rect(); + // Over the list's first rows (this one may be below what the list shows). + h.event(egui::Event::PointerMoved(egui::pos2(before.center().x, 250.0))); + h.event(egui::Event::MouseWheel { + unit: egui::MouseWheelUnit::Point, + delta: egui::vec2(0.0, -300.0), + phase: egui::TouchPhase::Move, + modifiers: egui::Modifiers::NONE, + }); + h.run_steps(20); + let after = h.get_by_label("Session status").rect(); + assert!(after.top() < before.top() - 100.0, "it scrolled and stayed scrolled: {before:?} → {after:?}"); + // The arrow keys still bring the selection back into view. + h.key_press(egui::Key::ArrowDown); + h.key_press(egui::Key::ArrowUp); + h.run_steps(20); + assert_eq!(h.get_by_label("Session status").rect().top(), before.top()); +} + /// A stand-in model server whose every answer lists an embedding model, then `qwen3:8b`. Its /// OpenAI-style address. fn model_server() -> String { diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 1d02c4c2..c83576b6 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -501,6 +501,8 @@ Gaps found while using the app for real projects. check against DNS rebinding), streams large files instead of reading them into memory, sends no body for HEAD on a missing file, and caps open connections and the time a request may take to arrive (#597, #598, #600, #601, by Afloat16) +- [x] The command palette stays where the wheel scrolls it; it follows the selection only when the + arrow keys or typing move it (#584) ### R2.10 Pinned builds Owner: maintainers · Status: in progress diff --git a/docs/pitfalls.md b/docs/pitfalls.md index 0fdf45f1..b061a339 100644 --- a/docs/pitfalls.md +++ b/docs/pitfalls.md @@ -564,3 +564,8 @@ parser between JSON recovery and ReAct: count the opener, require a standalone c least that long, and reject an earlier matching close followed by more text. Shorter backtick lines inside a longer fence are content. Test both paths and the next HTTP request, including malformed suffixes and non-call JSON examples whose strings contain protocol markers. + +**A view that asked to be scrolled on every frame.** The command palette called `scroll_to_me` on +the selected row each frame, so the wheel's scroll was pulled back to it at once and the list +kept jumping to the top (#584). Scroll something into view on the frame it changed (a key, a new +query), never as a standing request.