mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
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:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
"""
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user