Skip to content

Commit 1a76b62

Browse files
authored
fix threading issue (#3779)
* fix threading issue * remote debug * changelog * version bump * changelog * docstring * fix snapshot test
1 parent eed7a94 commit 1a76b62

File tree

6 files changed

+99
-80
lines changed

6 files changed

+99
-80
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](http://keepachangelog.com/)
66
and this project adheres to [Semantic Versioning](http://semver.org/).
77

8+
## [0.42.2] - 2023-11-29
9+
10+
### Fixed
11+
12+
- Fixed NoWidget error https://github.com/Textualize/textual/pull/3779
13+
814
## [0.43.1] - 2023-11-29
915

1016
### Fixed
@@ -1465,6 +1471,7 @@ https://textual.textualize.io/blog/2022/11/08/version-040/#version-040
14651471
- New handler system for messages that doesn't require inheritance
14661472
- Improved traceback handling
14671473

1474+
[0.43.2]: https://github.com/Textualize/textual/compare/v0.43.1...v0.43.2
14681475
[0.43.1]: https://github.com/Textualize/textual/compare/v0.43.0...v0.43.1
14691476
[0.43.0]: https://github.com/Textualize/textual/compare/v0.42.0...v0.43.0
14701477
[0.42.0]: https://github.com/Textualize/textual/compare/v0.41.0...v0.42.0

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "textual"
3-
version = "0.43.1"
3+
version = "0.43.2"
44
homepage = "https://github.com/Textualize/textual"
55
repository = "https://github.com/Textualize/textual"
66
documentation = "https://textual.textualize.io/"

src/textual/app.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
from .dom import DOMNode
8080
from .driver import Driver
8181
from .drivers.headless_driver import HeadlessDriver
82+
from .errors import NoWidget
8283
from .features import FeatureFlag, parse_features
8384
from .file_monitor import FileMonitor
8485
from .geometry import Offset, Region, Size
@@ -444,6 +445,9 @@ def __init__(
444445
self._animate = self._animator.bind(self)
445446
self.mouse_position = Offset(0, 0)
446447

448+
self._mouse_down_widget: Widget | None = None
449+
"""The widget that was most recently mouse downed (used to create click events)."""
450+
447451
self.cursor_position = Offset(0, 0)
448452
"""The position of the terminal cursor in screen-space.
449453
@@ -2671,7 +2675,7 @@ async def on_event(self, event: events.Event) -> None:
26712675
# Handle input events that haven't been forwarded
26722676
# If the event has been forwarded it may have bubbled up back to the App
26732677
if isinstance(event, events.Compose):
2674-
screen = Screen(id=f"_default")
2678+
screen: Screen[Any] = Screen(id=f"_default")
26752679
self._register(self, screen)
26762680
self._screen_stack.append(screen)
26772681
screen.post_message(events.ScreenResume())
@@ -2683,7 +2687,26 @@ async def on_event(self, event: events.Event) -> None:
26832687
if isinstance(event, events.MouseEvent):
26842688
# Record current mouse position on App
26852689
self.mouse_position = Offset(event.x, event.y)
2690+
2691+
if isinstance(event, events.MouseDown):
2692+
try:
2693+
self._mouse_down_widget, _ = self.get_widget_at(
2694+
event.x, event.y
2695+
)
2696+
except NoWidget:
2697+
# Shouldn't occur, since at the very least this will find the Screen
2698+
self._mouse_down_widget = None
2699+
26862700
self.screen._forward_event(event)
2701+
2702+
if isinstance(event, events.MouseUp):
2703+
if self._mouse_down_widget is not None and (
2704+
self.get_widget_at(event.x, event.y)[0]
2705+
is self._mouse_down_widget
2706+
):
2707+
click_event = events.Click.from_event(event)
2708+
self.screen._forward_event(click_event)
2709+
26872710
elif isinstance(event, events.Key):
26882711
if not await self.check_bindings(event.key, priority=True):
26892712
forward_target = self.focused or self.screen

src/textual/driver.py

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99

1010
if TYPE_CHECKING:
1111
from .app import App
12-
from .widget import Widget
1312

1413

1514
class Driver(ABC):
@@ -33,7 +32,6 @@ def __init__(
3332
self._debug = debug
3433
self._size = size
3534
self._loop = asyncio.get_running_loop()
36-
self._mouse_down_widget: Widget | None = None
3735
self._down_buttons: list[int] = []
3836
self._last_move_event: events.MouseMove | None = None
3937

@@ -58,17 +56,15 @@ def process_event(self, event: events.Event) -> None:
5856
Args:
5957
event: An event to send.
6058
"""
59+
# NOTE: This runs in a thread.
60+
# Avoid calling methods on the app.
6161
event._set_sender(self._app)
6262
if isinstance(event, events.MouseDown):
63-
self._mouse_down_widget = self._app.get_widget_at(event.x, event.y)[0]
6463
if event.button:
6564
self._down_buttons.append(event.button)
6665
elif isinstance(event, events.MouseUp):
67-
if event.button:
68-
try:
69-
self._down_buttons.remove(event.button)
70-
except ValueError:
71-
pass
66+
if event.button and event.button in self._down_buttons:
67+
self._down_buttons.remove(event.button)
7268
elif isinstance(event, events.MouseMove):
7369
if (
7470
self._down_buttons
@@ -99,13 +95,6 @@ def process_event(self, event: events.Event) -> None:
9995

10096
self.send_event(event)
10197

102-
if (
103-
isinstance(event, events.MouseUp)
104-
and self._app.get_widget_at(event.x, event.y)[0] is self._mouse_down_widget
105-
):
106-
click_event = events.Click.from_event(event)
107-
self.send_event(click_event)
108-
10998
@abstractmethod
11099
def write(self, data: str) -> None:
111100
"""Write data to the output device.

src/textual/pilot.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -323,12 +323,12 @@ async def _post_mouse_events(
323323
# Get the widget under the mouse before the event because the app might
324324
# react to the event and move things around. We override on each iteration
325325
# because we assume the final event in `events` is the actual event we care
326-
# about and that all the preceeding events are just setup.
327-
# E.g., the click event is preceeded by MouseDown/MouseUp to emulate how
326+
# about and that all the preceding events are just setup.
327+
# E.g., the click event is preceded by MouseDown/MouseUp to emulate how
328328
# the driver works and emits a click event.
329329
widget_at, _ = app.get_widget_at(*offset)
330330
event = mouse_event_cls(**message_arguments)
331-
app.post_message(event)
331+
app.screen._forward_event(event)
332332
await self.pause()
333333

334334
return selector is None or widget_at is target_widget

tests/snapshot_tests/__snapshots__/test_snapshots.ambr

Lines changed: 60 additions & 60 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)