Skip to content

Commit

Permalink
enter bubble
Browse files Browse the repository at this point in the history
  • Loading branch information
willmcgugan committed Jul 29, 2024
1 parent 7acab1c commit 2693db7
Show file tree
Hide file tree
Showing 5 changed files with 60 additions and 9 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## Unreleased

### Added

- Added `Widget.is_mouse_over`
- Added node attribute to `events.Enter` and `events.Leave`

### Changed

- `events.Enter` and `events.Leave` events now bubble.

## [0.74.0] - 2024-07-25

### Fixed
Expand Down
6 changes: 3 additions & 3 deletions src/textual/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2304,16 +2304,16 @@ def _set_mouse_over(self, widget: Widget | None) -> None:
if widget is None:
if self.mouse_over is not None:
try:
self.mouse_over.post_message(events.Leave())
self.mouse_over.post_message(events.Leave(self.mouse_over))
finally:
self.mouse_over = None
else:
if self.mouse_over is not widget:
try:
if self.mouse_over is not None:
self.mouse_over.post_message(events.Leave())
self.mouse_over.post_message(events.Leave(self.mouse_over))
if widget is not None:
widget.post_message(events.Enter())
widget.post_message(events.Enter(widget))
finally:
self.mouse_over = widget

Expand Down
31 changes: 27 additions & 4 deletions src/textual/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
MouseEventT = TypeVar("MouseEventT", bound="MouseEvent")

if TYPE_CHECKING:
from .dom import DOMNode
from .timer import Timer as TimerClass
from .timer import TimerCallback
from .widget import Widget
Expand Down Expand Up @@ -548,22 +549,44 @@ def __rich_repr__(self) -> rich.repr.Result:
yield "count", self.count


class Enter(Event, bubble=False, verbose=True):
class Enter(Event, bubble=True, verbose=True):
"""Sent when the mouse is moved over a widget.
- [ ] Bubbles
Note that this widget bubbles, so a widget may receive this event when the mouse
moves over a child widget. Check the `node` attribute for the widget directly under
tne mouse.
- [X] Bubbles
- [X] Verbose
"""

__slots__ = ["node"]

class Leave(Event, bubble=False, verbose=True):
def __init__(self, node: DOMNode) -> None:
self.node = node
"""The node directly under the mouse."""
super().__init__()


class Leave(Event, bubble=True, verbose=True):
"""Sent when the mouse is moved away from a widget, or if a widget is
programmatically disabled while hovered.
- [ ] Bubbles
Note that this widget bubbles, so a widget may receive Leave events for any child widgets.
Check the `node` parameter for the original widget that was previously under the mouse.
- [X] Bubbles
- [X] Verbose
"""

__slots__ = ["node"]

def __init__(self, node: DOMNode) -> None:
self.node = node
"""The node that was previously directly under the mouse."""
super().__init__()


class Focus(Event, bubble=False):
"""Sent when a widget is focussed.
Expand Down
14 changes: 12 additions & 2 deletions src/textual/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,16 @@ def is_anchored(self) -> bool:
"""Is this widget anchored?"""
return self._parent is not None and self._parent is self

@property
def is_mouse_over(self) -> bool:
"""Is the mouse currently over this widget?"""
if not self.screen.is_active:
return False
for widget, _ in self.screen.get_widgets_at(*self.app.mouse_position):
if widget is self:
return True
return False

def anchor(self, *, animate: bool = False) -> None:
"""Anchor the widget, which scrolls it into view (like [scroll_visible][textual.widget.Widget.scroll_visible]),
but also keeps it in view if the widget's size changes, or the size of its container changes.
Expand Down Expand Up @@ -3261,9 +3271,9 @@ def watch_disabled(self, disabled: bool) -> None:
"""Update the styles of the widget and its children when disabled is toggled."""
from .app import ScreenStackError

if disabled and self.mouse_over:
if disabled and self.mouse_over and self.app.mouse_over is not None:
# Ensure widget gets a Leave if it is disabled while hovered
self._message_queue.put_nowait(events.Leave())
self._message_queue.put_nowait(events.Leave(self.app.mouse_over))
try:
screen = self.screen
if (
Expand Down
7 changes: 7 additions & 0 deletions tests/snapshot_tests/test_snapshots.py
Original file line number Diff line number Diff line change
Expand Up @@ -1388,3 +1388,10 @@ def test_progress_gradient(snap_compare):
def test_recompose_in_mount(snap_compare):
"""Regression test for https://github.com/Textualize/textual/issues/4799"""
assert snap_compare(SNAPSHOT_APPS_DIR / "recompose_on_mount.py")


def test_enter_or_leave(snap_compare) -> None:
async def run_before(pilot: Pilot):
await pilot.hover("#foo")

assert snap_compare(SNAPSHOT_APPS_DIR / "enter_or_leave.py", run_before=run_before)

0 comments on commit 2693db7

Please sign in to comment.