From 094d433dfb6d1e720ea18c1bcc312af71093688a Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 26 Jun 2025 14:23:41 +0100 Subject: [PATCH 1/7] Input should release mouse on screen change --- src/textual/app.py | 2 ++ src/textual/screen.py | 1 + src/textual/widget.py | 3 ++- src/textual/widgets/_input.py | 8 +++++++- 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/textual/app.py b/src/textual/app.py index eecddc1d4d..a0af455f4d 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -2736,6 +2736,7 @@ def push_screen( else: future = loop.create_future() + self.app.capture_mouse(None) if self._screen_stack: self.screen.post_message(events.ScreenSuspend()) self.screen.refresh() @@ -2804,6 +2805,7 @@ def switch_screen(self, screen: Screen | str) -> AwaitComplete: self.log.system(f"Screen {screen} is already current.") return AwaitComplete.nothing() + self.app.capture_mouse(None) top_screen = self._screen_stack.pop() top_screen._pop_result_callback() diff --git a/src/textual/screen.py b/src/textual/screen.py index 73369c6a46..f7723c5f22 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -1607,6 +1607,7 @@ def _forward_event(self, event: events.Event) -> None: except errors.NoWidget: self.set_focus(None) else: + if isinstance(event, events.MouseDown): focusable_widget = self.get_focusable_widget_at(event.x, event.y) if focusable_widget: diff --git a/src/textual/widget.py b/src/textual/widget.py index 05d99addc3..4b9936c629 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -4341,7 +4341,8 @@ def release_mouse(self) -> None: Mouse events will only be sent when the mouse is over the widget. """ - self.app.capture_mouse(None) + if self.app.mouse_captured is self: + self.app.capture_mouse(None) def text_select_all(self) -> None: """Select the entire widget.""" diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index 326e535958..b3da680ebb 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -769,12 +769,18 @@ async def _on_mouse_down(self, event: events.MouseDown) -> None: self._selecting = True self.capture_mouse() - async def _on_mouse_up(self, event: events.MouseUp) -> None: + def _end_selecting(self): if self._selecting: self._selecting = False self.release_mouse() self._restart_blink() + async def _on_mouse_release(self, _event: events.MouseRelease) -> None: + self._end_selecting() + + async def _on_mouse_up(self, _event: events.MouseUp) -> None: + self._end_selecting() + async def _on_mouse_move(self, event: events.MouseMove) -> None: if self._selecting: # As we drag the mouse, we update the end position of the selection, From 872f6b54c37df6d4935581c4642a47e6ccdee834 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 26 Jun 2025 14:25:57 +0100 Subject: [PATCH 2/7] whitespace --- src/textual/screen.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/textual/screen.py b/src/textual/screen.py index f7723c5f22..73369c6a46 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -1607,7 +1607,6 @@ def _forward_event(self, event: events.Event) -> None: except errors.NoWidget: self.set_focus(None) else: - if isinstance(event, events.MouseDown): focusable_widget = self.get_focusable_widget_at(event.x, event.y) if focusable_widget: From 241646663af2f1cbd135a099c4226a9c038885ab Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 26 Jun 2025 14:27:37 +0100 Subject: [PATCH 3/7] changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63bf7dd9df..2f3ecd40b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Fixed -- Fixed issue with the "transparent" CSS value not being transparent when set using python +- Fixed issue with the "transparent" CSS value not being transparent when set using python https://github.com/Textualize/textual/pull/5890 +- Fixed issue with pushing screens when Input has mouse captured https://github.com/Textualize/textual/pull/5900 ## [3.5.0] - 2025-06-20 From 55fb731b85909799c470c45eb48f3cd77fd92f78 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 26 Jun 2025 14:30:52 +0100 Subject: [PATCH 4/7] release on pop --- src/textual/app.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/textual/app.py b/src/textual/app.py index a0af455f4d..15ec877432 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -2896,6 +2896,7 @@ def pop_screen(self) -> AwaitComplete: "Can't pop screen; there must be at least one screen on the stack" ) + self.capture_mouse(None) previous_screen = screen_stack.pop() previous_screen._pop_result_callback() self.screen.post_message(events.ScreenResume()) From 9d9e566dd1db17410316e6b9a2a0afe09a6f4540 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 26 Jun 2025 14:46:27 +0100 Subject: [PATCH 5/7] capture mouse on replace --- src/textual/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/app.py b/src/textual/app.py index 15ec877432..01996615ff 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -2680,6 +2680,7 @@ async def _replace_screen(self, screen: Screen) -> Screen: if not self.is_screen_installed(screen) and all( screen not in stack for stack in self._screen_stacks.values() ): + self.capture_mouse(None) await screen.remove() self.log.system(f"{screen} REMOVED") return screen @@ -2896,7 +2897,6 @@ def pop_screen(self) -> AwaitComplete: "Can't pop screen; there must be at least one screen on the stack" ) - self.capture_mouse(None) previous_screen = screen_stack.pop() previous_screen._pop_result_callback() self.screen.post_message(events.ScreenResume()) From 9b133ff05bbb3d59143ea1db99c5112431ea6d00 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 26 Jun 2025 14:49:06 +0100 Subject: [PATCH 6/7] Changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f3ecd40b4..a76f222901 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Fixed issue with the "transparent" CSS value not being transparent when set using python https://github.com/Textualize/textual/pull/5890 - Fixed issue with pushing screens when Input has mouse captured https://github.com/Textualize/textual/pull/5900 +## Changed + +- Widget.release_mouse will now only release the mouse, if it was captured by self https://github.com/Textualize/textual/pull/5900 + ## [3.5.0] - 2025-06-20 ### Changed From bf089e0983a42d1ed4f085dd3b7033f928fb10ce Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 26 Jun 2025 16:48:12 +0100 Subject: [PATCH 7/7] docstring --- src/textual/widgets/_input.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index b3da680ebb..715ab0f5f0 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -769,7 +769,8 @@ async def _on_mouse_down(self, event: events.MouseDown) -> None: self._selecting = True self.capture_mouse() - def _end_selecting(self): + def _end_selecting(self) -> None: + """End selecting if it is currently active.""" if self._selecting: self._selecting = False self.release_mouse()