From 12c553d1953fe8cd539bab33ed8af3670f2a2311 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sat, 29 Oct 2022 13:29:32 +0100 Subject: [PATCH] shutdown scrollbas --- CHANGELOG.md | 5 +++++ sandbox/will/scroll_remove.py | 18 ++++++++++++++++++ src/textual/app.py | 5 +---- src/textual/cli/cli.py | 8 +++++++- src/textual/widget.py | 17 ++++++++++++++++- tests/test_auto_pilot.py | 23 +++++++++++++++++++++++ tests/test_test_runner.py | 21 +++++++++++++++++++++ 7 files changed, 91 insertions(+), 6 deletions(-) create mode 100644 sandbox/will/scroll_remove.py create mode 100644 tests/test_auto_pilot.py create mode 100644 tests/test_test_runner.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 81cbdbf06..5d605f010 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [0.2.2] - Unreleased +### Fixed + +- Fixed issue where scrollbars weren't being unmounted + ### Changed - DOMQuery now raises InvalidQueryFormat in response to invalid query strings, rather than cryptic CSS error @@ -19,6 +23,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Added App.run_async method - Added App.run_test context manager - Added auto_pilot to App.run and App.run_async +- Added Widget._get_virtual_dom to get scrollbars ## [0.2.1] - 2022-10-23 diff --git a/sandbox/will/scroll_remove.py b/sandbox/will/scroll_remove.py new file mode 100644 index 000000000..abad13947 --- /dev/null +++ b/sandbox/will/scroll_remove.py @@ -0,0 +1,18 @@ +from textual.app import App, ComposeResult + +from textual.containers import Container + + +class ScrollApp(App): + + def compose(self) -> ComposeResult: + yield Container( + Container(), Container(), + id="top") + + def key_r(self) -> None: + self.query_one("#top").remove() + +if __name__ == "__main__": + app = ScrollApp() + app.run() diff --git a/src/textual/app.py b/src/textual/app.py index 11f481789..14b46c1aa 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -1605,9 +1605,6 @@ class App(Generic[ReturnType], DOMNode): await self._prune_node(widget) - # for child in remove_widgets: - # await child._close_messages() - # self._unregister(child) if parent is not None: parent.refresh(layout=True) @@ -1625,7 +1622,7 @@ class App(Generic[ReturnType], DOMNode): while stack: widget = pop() if widget.children: - yield list(widget.children) + yield [*widget.children, *widget._get_virtual_dom()] for child in widget.children: push(child) diff --git a/src/textual/cli/cli.py b/src/textual/cli/cli.py index 3b1df1f4b..414575d82 100644 --- a/src/textual/cli/cli.py +++ b/src/textual/cli/cli.py @@ -4,6 +4,7 @@ from __future__ import annotations import click from importlib_metadata import version +from textual.pilot import Pilot from textual._import_app import import_app, AppFail @@ -84,7 +85,12 @@ def run_app(import_name: str, dev: bool, press: str) -> None: sys.exit(1) press_keys = press.split(",") if press else None - result = app.run(press=press_keys) + + async def run_press_keys(pilot: Pilot) -> None: + if press_keys is not None: + await pilot.press(*press_keys) + + result = app.run(auto_pilot=run_press_keys) if result is not None: from rich.console import Console diff --git a/src/textual/widget.py b/src/textual/widget.py index d83403482..e43c52f17 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -359,6 +359,20 @@ class Widget(DOMNode): """Clear arrangement cache, forcing a new arrange operation.""" self._arrangement = None + def _get_virtual_dom(self) -> Iterable[Widget]: + """Get widgets not part of the DOM. + + Returns: + Iterable[Widget]: An iterable of Widgets. + + """ + if self._horizontal_scrollbar is not None: + yield self._horizontal_scrollbar + if self._vertical_scrollbar is not None: + yield self._vertical_scrollbar + if self._scrollbar_corner is not None: + yield self._scrollbar_corner + def mount(self, *anon_widgets: Widget, **widgets: Widget) -> AwaitMount: """Mount child widgets (making this widget a container). @@ -587,6 +601,7 @@ class Widget(DOMNode): Returns: ScrollBar: ScrollBar Widget. """ + from .scrollbar import ScrollBar if self._horizontal_scrollbar is not None: @@ -600,7 +615,7 @@ class Widget(DOMNode): def _refresh_scrollbars(self) -> None: """Refresh scrollbar visibility.""" - if not self.is_scrollable: + if not self.is_scrollable or not self.container_size: return styles = self.styles diff --git a/tests/test_auto_pilot.py b/tests/test_auto_pilot.py new file mode 100644 index 000000000..dde2ad18c --- /dev/null +++ b/tests/test_auto_pilot.py @@ -0,0 +1,23 @@ +from textual.app import App +from textual.pilot import Pilot +from textual import events + + +def test_auto_pilot() -> None: + + keys_pressed: list[str] = [] + + class TestApp(App): + def on_key(self, event: events.Key) -> None: + keys_pressed.append(event.key) + + async def auto_pilot(pilot: Pilot) -> None: + + await pilot.press("tab", *"foo") + await pilot.pause(1 / 100) + await pilot.exit("bar") + + app = TestApp() + result = app.run(headless=True, auto_pilot=auto_pilot) + assert result == "bar" + assert keys_pressed == ["tab", "f", "o", "o"] diff --git a/tests/test_test_runner.py b/tests/test_test_runner.py new file mode 100644 index 000000000..515f87341 --- /dev/null +++ b/tests/test_test_runner.py @@ -0,0 +1,21 @@ +from textual.app import App +from textual import events + + +async def test_run_test() -> None: + """Test the run_test context manager.""" + keys_pressed: list[str] = [] + + class TestApp(App[str]): + def on_key(self, event: events.Key) -> None: + keys_pressed.append(event.key) + + app = TestApp() + async with app.run_test() as pilot: + assert str(pilot) == "" + await pilot.press("tab", *"foo") + await pilot.pause(1 / 100) + await pilot.exit("bar") + + assert app.return_value == "bar" + assert keys_pressed == ["tab", "f", "o", "o"]