From 0a03c3a7183b28a72da6fbb7cdc4d330ed8e00d2 Mon Sep 17 00:00:00 2001 From: xfy Date: Fri, 17 Jul 2026 17:41:39 +0800 Subject: [PATCH] fix(ui): block scroll-wheel passthrough to orbit camera over the panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wheel-scrolling over the right-side egui panel was zooming the orbit camera instead of scrolling the panel. Root cause: ui_system set WantsPointer from ctx.egui_wants_pointer_input(), which delegates to is_pointer_over_egui() -> layer_id_at(). That lookup only walks egui's `areas` table. But this SidePanel is shown on the background layer of a hand-rolled root `Ui` (the bevy_egui 0.41 single-context SidePanel pattern) and never registers an Area, so layer_id_at() returns None over the panel and egui_wants_pointer_input() returns false even with the cursor squarely over it. WantsPointer stayed false, so orbit_controller let every MouseWheel event through. Confirmed via diagnostics: `over=false` on every frame while hovering the panel. First attempt — claiming the panel interior with ui.interact(Sense::hover()) — failed: interact registers a *widget*, not an *area*, so layer_id_at() still found nothing (verified: over stayed false). Fix: bypass egui's area heuristic entirely. Capture the SidePanel's InnerResponse and test the pointer position against its response.rect directly (a deterministic geometric check). Keep egui_is_using_pointer() so an in-progress slider/resize drag still claims the pointer even if the cursor momentarily exits the rect. wants.0 = pointer_in(panel_rect) || egui_is_using_pointer() --- src/ui/mod.rs | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 04dda25..949e8fc 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -180,7 +180,7 @@ pub fn ui_system( .layer_id(LayerId::background()) .max_rect(ctx.viewport_rect()), ); - egui::Panel::right("controls") + let panel_response = egui::Panel::right("controls") .default_size(300.0) .size_range(260.0..=400.0) .resizable(true) @@ -253,7 +253,19 @@ pub fn ui_system( }); }); - // egui captures pointer when the cursor is over a window or being - // interacted with. MUST stay last — load-bearing for orbit camera. - wants.0 = ctx.egui_wants_pointer_input(); + // Decide whether egui should own the pointer this frame, so the orbit + // camera can stand down. We can't rely on `ctx.egui_wants_pointer_input()`: + // this SidePanel is rendered on the background layer of a hand-rolled root + // `Ui` (the bevy_egui 0.41 single-context pattern) and never registers an + // `Area`, so egui's `is_pointer_over_egui()` / `layer_id_at()` return `false` + // even while the cursor is squarely over the panel — wheel scroll then leaks + // straight through to the camera. Instead we test the pointer against the + // panel's own response rect (a deterministic geometric check), and keep + // `egui_is_using_pointer()` so dragging a slider/resize-handle still claims + // the pointer even if the cursor momentarily leaves the rect. + let panel_rect = panel_response.response.rect; + let pointer_pos = ctx.input(|i| i.pointer.interact_pos()); + let over_panel = pointer_pos.is_some_and(|p| panel_rect.contains(p)); + let using = ctx.egui_is_using_pointer(); + wants.0 = over_panel || using; }