diff --git a/CHANGELOG.md b/CHANGELOG.md
index e550842bba..326ba48e2a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,12 @@ 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/).
+## [0.42.2] - 2023-11-29
+
+### Fixed
+
+- Fixed NoWidget error https://github.com/Textualize/textual/pull/3779
+
## [0.43.1] - 2023-11-29
### Fixed
@@ -1465,6 +1471,7 @@ https://textual.textualize.io/blog/2022/11/08/version-040/#version-040
- New handler system for messages that doesn't require inheritance
- Improved traceback handling
+[0.43.2]: https://github.com/Textualize/textual/compare/v0.43.1...v0.43.2
[0.43.1]: https://github.com/Textualize/textual/compare/v0.43.0...v0.43.1
[0.43.0]: https://github.com/Textualize/textual/compare/v0.42.0...v0.43.0
[0.42.0]: https://github.com/Textualize/textual/compare/v0.41.0...v0.42.0
diff --git a/pyproject.toml b/pyproject.toml
index b95f190d2d..2087d691e8 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "textual"
-version = "0.43.1"
+version = "0.43.2"
homepage = "https://github.com/Textualize/textual"
repository = "https://github.com/Textualize/textual"
documentation = "https://textual.textualize.io/"
diff --git a/src/textual/app.py b/src/textual/app.py
index e76b96a847..108dd4daba 100644
--- a/src/textual/app.py
+++ b/src/textual/app.py
@@ -79,6 +79,7 @@
from .dom import DOMNode
from .driver import Driver
from .drivers.headless_driver import HeadlessDriver
+from .errors import NoWidget
from .features import FeatureFlag, parse_features
from .file_monitor import FileMonitor
from .geometry import Offset, Region, Size
@@ -444,6 +445,9 @@ def __init__(
self._animate = self._animator.bind(self)
self.mouse_position = Offset(0, 0)
+ self._mouse_down_widget: Widget | None = None
+ """The widget that was most recently mouse downed (used to create click events)."""
+
self.cursor_position = Offset(0, 0)
"""The position of the terminal cursor in screen-space.
@@ -2671,7 +2675,7 @@ async def on_event(self, event: events.Event) -> None:
# Handle input events that haven't been forwarded
# If the event has been forwarded it may have bubbled up back to the App
if isinstance(event, events.Compose):
- screen = Screen(id=f"_default")
+ screen: Screen[Any] = Screen(id=f"_default")
self._register(self, screen)
self._screen_stack.append(screen)
screen.post_message(events.ScreenResume())
@@ -2683,7 +2687,26 @@ async def on_event(self, event: events.Event) -> None:
if isinstance(event, events.MouseEvent):
# Record current mouse position on App
self.mouse_position = Offset(event.x, event.y)
+
+ if isinstance(event, events.MouseDown):
+ try:
+ self._mouse_down_widget, _ = self.get_widget_at(
+ event.x, event.y
+ )
+ except NoWidget:
+ # Shouldn't occur, since at the very least this will find the Screen
+ self._mouse_down_widget = None
+
self.screen._forward_event(event)
+
+ if isinstance(event, events.MouseUp):
+ if self._mouse_down_widget is not None and (
+ self.get_widget_at(event.x, event.y)[0]
+ is self._mouse_down_widget
+ ):
+ click_event = events.Click.from_event(event)
+ self.screen._forward_event(click_event)
+
elif isinstance(event, events.Key):
if not await self.check_bindings(event.key, priority=True):
forward_target = self.focused or self.screen
diff --git a/src/textual/driver.py b/src/textual/driver.py
index 5e472f5bc6..7cada2a473 100644
--- a/src/textual/driver.py
+++ b/src/textual/driver.py
@@ -9,7 +9,6 @@
if TYPE_CHECKING:
from .app import App
- from .widget import Widget
class Driver(ABC):
@@ -33,7 +32,6 @@ def __init__(
self._debug = debug
self._size = size
self._loop = asyncio.get_running_loop()
- self._mouse_down_widget: Widget | None = None
self._down_buttons: list[int] = []
self._last_move_event: events.MouseMove | None = None
@@ -58,17 +56,15 @@ def process_event(self, event: events.Event) -> None:
Args:
event: An event to send.
"""
+ # NOTE: This runs in a thread.
+ # Avoid calling methods on the app.
event._set_sender(self._app)
if isinstance(event, events.MouseDown):
- self._mouse_down_widget = self._app.get_widget_at(event.x, event.y)[0]
if event.button:
self._down_buttons.append(event.button)
elif isinstance(event, events.MouseUp):
- if event.button:
- try:
- self._down_buttons.remove(event.button)
- except ValueError:
- pass
+ if event.button and event.button in self._down_buttons:
+ self._down_buttons.remove(event.button)
elif isinstance(event, events.MouseMove):
if (
self._down_buttons
@@ -99,13 +95,6 @@ def process_event(self, event: events.Event) -> None:
self.send_event(event)
- if (
- isinstance(event, events.MouseUp)
- and self._app.get_widget_at(event.x, event.y)[0] is self._mouse_down_widget
- ):
- click_event = events.Click.from_event(event)
- self.send_event(click_event)
-
@abstractmethod
def write(self, data: str) -> None:
"""Write data to the output device.
diff --git a/src/textual/pilot.py b/src/textual/pilot.py
index c3bcbe2978..07873ab36f 100644
--- a/src/textual/pilot.py
+++ b/src/textual/pilot.py
@@ -323,12 +323,12 @@ async def _post_mouse_events(
# Get the widget under the mouse before the event because the app might
# react to the event and move things around. We override on each iteration
# because we assume the final event in `events` is the actual event we care
- # about and that all the preceeding events are just setup.
- # E.g., the click event is preceeded by MouseDown/MouseUp to emulate how
+ # about and that all the preceding events are just setup.
+ # E.g., the click event is preceded by MouseDown/MouseUp to emulate how
# the driver works and emits a click event.
widget_at, _ = app.get_widget_at(*offset)
event = mouse_event_cls(**message_arguments)
- app.post_message(event)
+ app.screen._forward_event(event)
await self.pause()
return selector is None or widget_at is target_widget
diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
index 84857386ae..a1da279f1c 100644
--- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
+++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
@@ -8002,136 +8002,136 @@
font-weight: 700;
}
- .terminal-3953145704-matrix {
+ .terminal-3762053931-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
- .terminal-3953145704-title {
+ .terminal-3762053931-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
- .terminal-3953145704-r1 { fill: #c5c8c6 }
- .terminal-3953145704-r2 { fill: #008000 }
- .terminal-3953145704-r3 { fill: #e8e0e7 }
- .terminal-3953145704-r4 { fill: #eae3e5 }
- .terminal-3953145704-r5 { fill: #ede6e6 }
- .terminal-3953145704-r6 { fill: #efe9e4 }
- .terminal-3953145704-r7 { fill: #efeedf }
+ .terminal-3762053931-r1 { fill: #c5c8c6 }
+ .terminal-3762053931-r2 { fill: #008000 }
+ .terminal-3762053931-r3 { fill: #e8e0e7 }
+ .terminal-3762053931-r4 { fill: #eae3e5 }
+ .terminal-3762053931-r5 { fill: #1e1e1e }
+ .terminal-3762053931-r6 { fill: #ede6e6 }
+ .terminal-3762053931-r7 { fill: #efeedf }
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
- KeylineApp
+ KeylineApp
-
-
-
-
-
- ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓
- ┃┃┃
- ┃┃┃
- ┃#foo┃┃
- ┃┃┃
- ┃┃┃
- ┣━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┫#bar┃
- ┃┃┃┃
- ┃┃┃┃
- ┃Placeholder┃Placeholder┃┃
- ┃┃┃┃
- ┃┃┃┃
- ┣━━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━┫
- ┃┃
- ┃┃
- ┃#baz┃
- ┃┃
- ┃┃
- ┃┃
- ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
-
+
+
+
+
+
+ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓
+ ┃┃┃
+ ┃┃┃
+ ┃#foo┃┃
+ ┃┃┃
+ ┃┃┃
+ ┣━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┫#bar┃
+ ┃┃┃┃
+ ┃┃┃┃
+ ┃Placeholder┃┃┃
+ ┃┃┃┃
+ ┃┃┃┃
+ ┣━━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━┫
+ ┃┃
+ ┃┃
+ ┃#baz┃
+ ┃┃
+ ┃┃
+ ┃┃
+ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+