Screen processes mouse move events after forwarding them to the child widget.

Signed-off-by: Michael Seifert <m.seifert@digitalernachschub.de>
This commit is contained in:
Michael Seifert
2023-07-22 14:22:17 +02:00
parent 40ba3347e5
commit 8b5f4fd03d
5 changed files with 91 additions and 19 deletions

View File

@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Fixed a crash when a `SelectionList` had a prompt wider than itself https://github.com/Textualize/textual/issues/2900
- Fixed a bug where `Click` events were bubbling up from `Switch` widgets https://github.com/Textualize/textual/issues/2366
- Fixed a crash when using empty CSS variables https://github.com/Textualize/textual/issues/1849
- `MouseMove` events bubble up from widgets. `App` and `Screen` receive `MouseMove` events even if there's no Widget under the cursor. https://github.com/Textualize/textual/issues/2905
## [0.30.0] - 2023-07-17

View File

@@ -2,7 +2,7 @@
The `MouseMove` event is sent to a widget when the mouse pointer is moved over a widget.
- [ ] Bubbles
- [x] Bubbles
- [x] Verbose
## Attributes

View File

@@ -435,10 +435,10 @@ class MouseEvent(InputEvent, bubble=True):
@rich.repr.auto
class MouseMove(MouseEvent, bubble=False, verbose=True):
class MouseMove(MouseEvent, bubble=True, verbose=True):
"""Sent when the mouse cursor moves.
- [ ] Bubbles
- [X] Bubbles
- [X] Verbose
"""

View File

@@ -818,22 +818,13 @@ class Screen(Generic[ScreenResultType], Widget):
else:
self.app._set_mouse_over(widget)
mouse_event = events.MouseMove(
event.x - region.x,
event.y - region.y,
event.delta_x,
event.delta_y,
event.button,
event.shift,
event.meta,
event.ctrl,
screen_x=event.screen_x,
screen_y=event.screen_y,
style=event.style,
)
widget.hover_style = event.style
mouse_event._set_forwarded()
widget._forward_event(mouse_event)
if widget is self:
self.post_message(event)
else:
mouse_event = self._translate_mouse_move_event(event, region)
mouse_event._set_forwarded()
widget._forward_event(mouse_event)
if not self.app._disable_tooltips:
try:
@@ -856,6 +847,28 @@ class Screen(Generic[ScreenResultType], Widget):
else:
tooltip.display = False
@staticmethod
def _translate_mouse_move_event(
event: events.MouseMove, region: Region
) -> events.MouseMove:
"""
Returns a mouse move event whose relative coordinates are translated to
the origin of the specified region.
"""
return events.MouseMove(
event.x - region.x,
event.y - region.y,
event.delta_x,
event.delta_y,
event.button,
event.shift,
event.meta,
event.ctrl,
screen_x=event.screen_x,
screen_y=event.screen_y,
style=event.style,
)
def _forward_event(self, event: events.Event) -> None:
if event.is_forwarded:
return

View File

@@ -4,7 +4,9 @@ import threading
import pytest
from textual.app import App, ScreenStackError
from textual.app import App, ScreenStackError, ComposeResult
from textual.events import MouseMove
from textual.geometry import Offset
from textual.screen import Screen
from textual.widgets import Button, Input, Label
@@ -350,3 +352,59 @@ async def test_switch_screen_updates_results_callback_stack():
app.switch_screen("b")
assert len(app.screen._result_callbacks) == 1
assert app.screen._result_callbacks[-1].callback is None
async def test_screen_receives_mouse_move_events():
class MouseMoveRecordingScreen(Screen):
mouse_events = []
def on_mouse_move(self, event: MouseMove) -> None:
MouseMoveRecordingScreen.mouse_events.append(event)
class SimpleApp(App[None]):
SCREENS = {"a": MouseMoveRecordingScreen()}
def on_mount(self):
self.push_screen("a")
mouse_offset = Offset(1, 1)
async with SimpleApp().run_test() as pilot:
await pilot.hover(None, mouse_offset)
assert len(MouseMoveRecordingScreen.mouse_events) == 1
mouse_event = MouseMoveRecordingScreen.mouse_events[0]
assert mouse_event.x, mouse_event.y == mouse_offset
async def test_mouse_move_event_bubbles_to_screen_from_widget():
class MouseMoveRecordingScreen(Screen):
mouse_events = []
DEFAULT_CSS = """
Label {
offset: 10 10;
}
"""
def compose(self) -> ComposeResult:
yield Label("Any label")
def on_mouse_move(self, event: MouseMove) -> None:
MouseMoveRecordingScreen.mouse_events.append(event)
class SimpleApp(App[None]):
SCREENS = {"a": MouseMoveRecordingScreen()}
def on_mount(self):
self.push_screen("a")
label_offset = Offset(10, 10)
mouse_offset = Offset(1, 1)
async with SimpleApp().run_test() as pilot:
await pilot.hover(Label, mouse_offset)
assert len(MouseMoveRecordingScreen.mouse_events) == 1
mouse_event = MouseMoveRecordingScreen.mouse_events[0]
assert mouse_event.x, mouse_event.y == (label_offset.x + mouse_offset.x, label_offset.y + mouse_offset.y)