Skip to content

Map acts on wheel/zoom input without consuming it - double-handling when nested in a ScrollArea #544

Description

@stergiotis

walkers version: 0.56.0
egui version: 0.35
co-author: Antropic Fabel 5

Summary

Map reads pointer-gesture input from the global InputState and never
consumes it:

  • handle_gestures reads ui.input(|i| i.smooth_scroll_delta) for
    wheel/two-finger panning (map.rs, the panning_enabled block), and
  • zoom_delta reads ui.input(|input| input.zoom_delta()).

Both reads are gated only by ui_contains_pointer(). Nothing marks the
scroll as used, so when the map is nested inside anything that also reacts
to smooth_scroll_delta — most commonly an egui::ScrollArea whose content
overflows — one wheel event is applied twice: the parent scrolls the panel
and the map pans. During zoom sessions (trackpad pinch with scroll
residue, or wheel ticks with ctrl released between notches) the map widget
bounces inside the scrolling parent while the map content moves under the
pointer. Visually this reads as violent flicker.

Note the ordering trap: the map runs inside the ScrollArea's content
closure, i.e. before the ScrollArea finishes its own scroll handling for
the frame — so even a parent that consumed the delta at the end of show
could not prevent the inner map from having already acted on it. Only the
inner widget (the map) can resolve the conflict, by consuming what it uses.

This is easy to hit in practice: egui_dock wraps every tab body in
ScrollArea::new([true, true]) by default, so a map inside a dock tab
whose body overflows gets the double-handling out of the box. (Same nesting
family as #516, which is about the projector vs. clipped-tiles anchor
offset in a partially scrolled ScrollArea.)

Reproduction

  1. Put a Map (with HttpTiles) inside a ScrollArea together with
    enough other content that the scroll area overflows:

    egui::ScrollArea::vertical().show(ui, |ui| {
        ui.label("some controls above the map");
        ui.add_sized(
            egui::vec2(960.0, 560.0),
            walkers::Map::new(Some(&mut tiles), &mut map_memory, my_position),
        );
        ui.label("content below, tall enough to overflow");
    });
  2. Hover the map and scroll with the wheel (no ctrl).

Expected: one consumer — either the map pans (it acted on the input) or
the scroll area scrolls, not both.

Observed: the scroll area scrolls the panel and the map pans by the
same delta in the same frame.

With zoom_with_ctrl (default true) the wheel-pan path is always active
(panning_enabled = panning && (any_touches || zoom_with_ctrl)), so plain
wheel over the map is always double-handled in a scrollable parent.

Suggested direction

When the map decides to act on scroll/zoom input (pointer over the map,
gesture enabled), consume the input it used, e.g.:

let scroll_delta = ui.input_mut(|i| std::mem::take(&mut i.smooth_scroll_delta));

(and analogously zero the zoom component it applies), or gate the reads
through the Response-based helpers so egui's usual containment rules
apply. If unconditional consumption is considered too opinionated, an
opt-in builder flag (.consume_scroll(true) or similar) would let nested
users fix the conflict.

Downstream workaround (for reference)

We disable the wrapping ScrollArea for dock tabs that host a map
(egui_dock::TabViewer::scroll_bars[false, false]), so the map is the
only scroll consumer under the pointer. That works, but costs the tab its
scrollability for overflow, which the map didn't need but sibling controls
might.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions