fix(ui): block scroll-wheel passthrough to orbit camera over the panel

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()
This commit is contained in:
xfy 2026-07-17 17:41:39 +08:00
parent e779ad7bb7
commit 0a03c3a718

View File

@ -180,7 +180,7 @@ pub fn ui_system(
.layer_id(LayerId::background()) .layer_id(LayerId::background())
.max_rect(ctx.viewport_rect()), .max_rect(ctx.viewport_rect()),
); );
egui::Panel::right("controls") let panel_response = egui::Panel::right("controls")
.default_size(300.0) .default_size(300.0)
.size_range(260.0..=400.0) .size_range(260.0..=400.0)
.resizable(true) .resizable(true)
@ -253,7 +253,19 @@ pub fn ui_system(
}); });
}); });
// egui captures pointer when the cursor is over a window or being // Decide whether egui should own the pointer this frame, so the orbit
// interacted with. MUST stay last — load-bearing for orbit camera. // camera can stand down. We can't rely on `ctx.egui_wants_pointer_input()`:
wants.0 = 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;
} }