diff --git a/gatelock/gatelock/_scrollable.py b/gatelock/gatelock/_scrollable.py index 334691f..387ffc1 100644 --- a/gatelock/gatelock/_scrollable.py +++ b/gatelock/gatelock/_scrollable.py @@ -344,16 +344,28 @@ class ScrollableSurface: self.canvas.yview_moveto(1.0) def _track_focus(self, parent: tk.Misc) -> None: - """Bind on every descendant, each exactly once. + """Bind and the wheel on every descendant, each exactly once. A repaint destroys and rebuilds the widgets, so this re-runs per screen; the WeakSet is what keeps a second :meth:`finalize` on the *same* widgets from stacking a second handler on each of them, and drops entries as the widgets are destroyed. + + The wheel has to be bound per descendant because Tk dispatches through + *bindtags*, not the parent chain: a child's tags are ``(itself, its + class, the toplevel, "all")``, so an event over a Label never reaches + a binding on ``content``. Content that fills its viewport covers that + frame completely, which means wheel-over-text -- the only way anyone + actually scrolls with a mouse -- would do nothing. The donor this + replaced reached for ``bind_all`` instead, which works but installs + the handler on *every* widget in the application, including other + windows' scroll regions. """ for child in parent.winfo_children(): if child not in self._focus_bound: child.bind("", self._on_descendant_focus, add="+") + child.bind(_WHEEL_UP, self._line_up, add="+") + child.bind(_WHEEL_DOWN, self._line_down, add="+") self._focus_bound.add(child) self._track_focus(child) diff --git a/gatelock/gatelock/tests/test_scrollable.py b/gatelock/gatelock/tests/test_scrollable.py index c5f01a8..6964207 100644 --- a/gatelock/gatelock/tests/test_scrollable.py +++ b/gatelock/gatelock/tests/test_scrollable.py @@ -327,3 +327,37 @@ def test_scroll_into_view_holds_still_for_a_widget_in_the_middle( surface.scroll_into_view(middle) assert _offset(surface) == before + + +def test_the_wheel_scrolls_from_over_the_content(surface: ScrollableSurface) -> None: + """Wheel-over-text scrolls, which is the only way anyone uses a mouse here. + + Tk dispatches through *bindtags*, not the parent chain: a child's tags are + ``(itself, its class, the toplevel, "all")``, so a binding on ``content`` + is never consulted for an event over a Label sitting on top of it. Content + that fills the viewport covers that frame entirely, so binding the wheel + only on the canvas and the content frame makes wheel scrolling dead + everywhere the user would actually point. + """ + child = surface.content.winfo_children()[10] + before = surface.canvas.yview()[0] + + child.event_generate("") + surface.canvas.update() + + assert surface.canvas.yview()[0] > before + + +def test_the_wheel_scrolls_back_up_from_over_the_content( + surface: ScrollableSurface, +) -> None: + """The up notch is bound on descendants too, not just the down one.""" + child = surface.content.winfo_children()[10] + child.event_generate("") + surface.canvas.update() + scrolled = surface.canvas.yview()[0] + + child.event_generate("") + surface.canvas.update() + + assert surface.canvas.yview()[0] < scrolled