|
20 | 20 | Generic, |
21 | 21 | Iterable, |
22 | 22 | Iterator, |
| 23 | + NamedTuple, |
23 | 24 | Optional, |
24 | 25 | TypeVar, |
25 | 26 | Union, |
|
83 | 84 | """Type of a screen result callback function.""" |
84 | 85 |
|
85 | 86 |
|
| 87 | +class HoverWidgets(NamedTuple): |
| 88 | + """Result of [get_hover_widget_at][textual.screen.Screen.get_hover_widget_at]""" |
| 89 | + |
| 90 | + mouse_over: tuple[Widget, Region] |
| 91 | + """Widget and region directly under the mouse.""" |
| 92 | + hover_over: tuple[Widget, Region] | None |
| 93 | + """Widget with a hover style under the mouse, or `None` for no hover style widget.""" |
| 94 | + |
| 95 | + @property |
| 96 | + def widgets(self) -> tuple[Widget, Widget | None]: |
| 97 | + """Just the widgets.""" |
| 98 | + return ( |
| 99 | + self.mouse_over[0], |
| 100 | + None if self.hover_over is None else self.hover_over[0], |
| 101 | + ) |
| 102 | + |
| 103 | + |
86 | 104 | @rich.repr.auto |
87 | 105 | class ResultCallback(Generic[ScreenResultType]): |
88 | 106 | """Holds the details of a callback.""" |
@@ -584,6 +602,33 @@ def get_widget_at(self, x: int, y: int) -> tuple[Widget, Region]: |
584 | 602 | """ |
585 | 603 | return self._compositor.get_widget_at(x, y) |
586 | 604 |
|
| 605 | + def get_hover_widgets_at(self, x: int, y: int) -> HoverWidgets: |
| 606 | + """Get the widget, and its region directly under the mouse, and the first |
| 607 | + widget, region pair with a hover style. |
| 608 | +
|
| 609 | + Args: |
| 610 | + x: X Coordinate. |
| 611 | + y: Y Coordinate. |
| 612 | +
|
| 613 | + Returns: |
| 614 | + A pair of (WIDGET, REGION) tuples for the top most and first hover style respectively. |
| 615 | +
|
| 616 | + Raises: |
| 617 | + NoWidget: If there is no widget under the screen coordinate. |
| 618 | +
|
| 619 | + """ |
| 620 | + widgets_under_coordinate = iter(self._compositor.get_widgets_at(x, y)) |
| 621 | + try: |
| 622 | + top_widget, top_region = next(widgets_under_coordinate) |
| 623 | + except StopIteration: |
| 624 | + raise errors.NoWidget(f"No hover widget under screen coordinate ({x}, {y})") |
| 625 | + if not top_widget._has_hover_style: |
| 626 | + for widget, region in widgets_under_coordinate: |
| 627 | + if widget._has_hover_style: |
| 628 | + return HoverWidgets((top_widget, top_region), (widget, region)) |
| 629 | + return HoverWidgets((top_widget, top_region), None) |
| 630 | + return HoverWidgets((top_widget, top_region), (top_widget, top_region)) |
| 631 | + |
587 | 632 | def get_widgets_at(self, x: int, y: int) -> Iterable[tuple[Widget, Region]]: |
588 | 633 | """Get all widgets under a given coordinate. |
589 | 634 |
|
@@ -1380,7 +1425,7 @@ def _on_screen_suspend(self) -> None: |
1380 | 1425 | """Screen has suspended.""" |
1381 | 1426 | if self.app.SUSPENDED_SCREEN_CLASS: |
1382 | 1427 | self.add_class(self.app.SUSPENDED_SCREEN_CLASS) |
1383 | | - self.app._set_mouse_over(None) |
| 1428 | + self.app._set_mouse_over(None, None) |
1384 | 1429 | self._clear_tooltip() |
1385 | 1430 | self.stack_updates += 1 |
1386 | 1431 |
|
@@ -1492,24 +1537,26 @@ def _handle_tooltip_timer(self, widget: Widget) -> None: |
1492 | 1537 | tooltip.update(tooltip_content) |
1493 | 1538 |
|
1494 | 1539 | def _handle_mouse_move(self, event: events.MouseMove) -> None: |
| 1540 | + hover_widget: Widget | None = None |
1495 | 1541 | try: |
1496 | 1542 | if self.app.mouse_captured: |
1497 | 1543 | widget = self.app.mouse_captured |
1498 | 1544 | region = self.find_widget(widget).region |
1499 | 1545 | else: |
1500 | | - widget, region = self.get_widget_at(event.x, event.y) |
| 1546 | + (widget, region), hover = self.get_hover_widgets_at(event.x, event.y) |
| 1547 | + if hover is not None: |
| 1548 | + hover_widget = hover[0] |
1501 | 1549 | except errors.NoWidget: |
1502 | | - self.app._set_mouse_over(None) |
| 1550 | + self.app._set_mouse_over(None, None) |
1503 | 1551 | if self._tooltip_timer is not None: |
1504 | 1552 | self._tooltip_timer.stop() |
1505 | 1553 | if not self.app._disable_tooltips: |
1506 | 1554 | try: |
1507 | 1555 | self.get_child_by_type(Tooltip).display = False |
1508 | 1556 | except NoMatches: |
1509 | 1557 | pass |
1510 | | - |
1511 | 1558 | else: |
1512 | | - self.app._set_mouse_over(widget) |
| 1559 | + self.app._set_mouse_over(widget, hover_widget) |
1513 | 1560 | widget.hover_style = event.style |
1514 | 1561 | if widget is self: |
1515 | 1562 | self.post_message(event) |
|
0 commit comments