From e87edd79cbb0312ff031c7b1c3d44b6875b4ca28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Tue, 11 Apr 2023 13:29:56 +0100 Subject: [PATCH 1/5] Add test for scroll_to_center method. --- .../snapshot_apps/scroll_to_center.py | 40 +++++++++++++++++++ tests/snapshot_tests/test_snapshots.py | 4 ++ 2 files changed, 44 insertions(+) create mode 100644 tests/snapshot_tests/snapshot_apps/scroll_to_center.py diff --git a/tests/snapshot_tests/snapshot_apps/scroll_to_center.py b/tests/snapshot_tests/snapshot_apps/scroll_to_center.py new file mode 100644 index 0000000000..fdd616cb6d --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/scroll_to_center.py @@ -0,0 +1,40 @@ +from textual.app import App, ComposeResult +from textual.containers import HorizontalScroll, VerticalScroll +from textual.widgets import Label + + +class MyApp(App[None]): + CSS = """ + VerticalScroll, HorizontalScroll { + border: round $primary; + } + #vertical { + height: 21; + } + HorizontalScroll { + height: auto; + } + """ + + def compose(self) -> ComposeResult: + with VerticalScroll(): + yield Label(("SPAM\n" * 25)[:-1]) + with VerticalScroll(): + yield Label(("SPAM\n" * 53)[:-1]) + with VerticalScroll(id="vertical"): + yield Label(("SPAM\n" * 78)[:-1]) + with HorizontalScroll(): + yield Label(("v\n" * 17)[:-1]) + yield Label("@" * 302) + yield Label("[red]>>bullseye<<[/red]", id="bullseye") + yield Label("@" * 99) + yield Label(("SPAM\n" * 49)[:-1]) + yield Label(("SPAM\n" * 51)[:-1]) + yield Label(("SPAM\n" * 59)[:-1]) + + def key_s(self) -> None: + self.query_one("#bullseye").scroll_to_center() + + +if __name__ == "__main__": + MyApp().run() diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index 14226a191f..6e87678859 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -406,3 +406,7 @@ def test_fr_margins(snap_compare): def test_scroll_visible(snap_compare): # https://github.com/Textualize/textual/issues/2181 assert snap_compare(SNAPSHOT_APPS_DIR / "scroll_visible.py", press=["t"]) + + +def test_scroll_to_center(snap_compare): + assert snap_compare(SNAPSHOT_APPS_DIR / "scroll_to_center.py", press=["s"]) From 454254fab4e43a57af192a27b4468ac0e59850f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Tue, 11 Apr 2023 13:32:09 +0100 Subject: [PATCH 2/5] Implement scroll_to_center method. --- CHANGELOG.md | 7 ++ src/textual/widget.py | 98 ++++++++++++++++++++++++++ tests/snapshot_tests/test_snapshots.py | 6 ++ 3 files changed, 111 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c17bbe06c3..2124f2dd3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ 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/). + +## Unreleased + +### Added + +- `Widget.scroll_to_center` now scrolls the widget to the center of the screen https://github.com/Textualize/textual/pull/2255 + ## [0.19.1] - 2023-04-10 ### Fixed diff --git a/src/textual/widget.py b/src/textual/widget.py index fa8d566abe..a628bf94b3 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -2408,6 +2408,104 @@ def scroll_visible( force=force, ) + async def _scroll_to_center_of( + self, + widget: Widget, + animate: bool = True, + *, + speed: float | None = None, + duration: float | None = None, + easing: EasingFunction | str | None = None, + force: bool = False, + ) -> None: + """Scroll a widget to the center of this container. + + Args: + widget: The widget to center. + animate: Whether to animate the scroll. + speed: Speed of scroll if animate is `True`; or `None` to use `duration`. + duration: Duration of animation, if `animate` is `True` and `speed` is `None`. + easing: An easing method for the scrolling animation. + force: Force scrolling even when prohibited by overflow styling. + """ + + central_point = Offset( + widget.virtual_region.x + (1 + widget.virtual_region.width) // 2, + widget.virtual_region.y + (1 + widget.virtual_region.height) // 2, + ) + + container = widget.parent + while isinstance(container, Widget) and widget is not self: + container_virtual_region = container.virtual_region + # The region we want to scroll to must be centered around the central point. + # We make it as big as possible because `scroll_to_region` scrolls as little + # as possible. + target_region = Region( + central_point.x - container_virtual_region.width // 2, + central_point.y - container_virtual_region.height // 2, + container_virtual_region.width, + container_virtual_region.height, + ) + scroll = container.scroll_to_region( + target_region, + animate=animate, + speed=speed, + duration=duration, + easing=easing, + force=force, + ) + + # We scroll `widget` within `container` with the central point written in + # the frame of reference of `container`. However, we need to update it so + # that we are ready to scroll `container` within _its_ container. + # To do this, notice that + # (central_point.y - container.scroll_offset.y - scroll.y) is the number + # of rows of `widget` that are visible within `container`. + # We add that to `container_virtual_region.y` to find the total vertical + # offset of the central point with respect to the container of `container`. + # A similar calculation is made for the horizontal update. + central_point = Offset( + container_virtual_region.x + + central_point.x + - container.scroll_offset.x + - scroll.x, + container_virtual_region.y + + central_point.y + - container.scroll_offset.y + - scroll.y, + ) + widget = container + container = widget.parent + + def scroll_to_center( + self, + animate: bool = True, + *, + speed: float | None = None, + duration: float | None = None, + easing: EasingFunction | str | None = None, + force: bool = False, + ) -> None: + """Scroll this widget to the center of the screen. + + Args: + animate: Whether to animate the scroll. + speed: Speed of scroll if animate is `True`; or `None` to use `duration`. + duration: Duration of animation, if `animate` is `True` and `speed` is `None`. + easing: An easing method for the scrolling animation. + force: Force scrolling even when prohibited by overflow styling. + """ + + self.call_after_refresh( + self.screen._scroll_to_center_of, + widget=self, + animate=animate, + speed=speed, + duration=duration, + easing=easing, + force=force, + ) + def __init_subclass__( cls, can_focus: bool | None = None, diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index 6e87678859..edc151e28d 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -409,4 +409,10 @@ def test_scroll_visible(snap_compare): def test_scroll_to_center(snap_compare): + # READ THIS IF THIS TEST FAILS: + # While https://github.com/Textualize/textual/issues/2254 is open, the snapshot + # this is being compared against is INCORRECT. + # The correct output for this snapshot test would show a couple of containers + # scrolled so that the red string >>bullseye<< is centered on the screen. + # When this snapshot "breaks" because #2254 is fixed, this snapshot can be updated. assert snap_compare(SNAPSHOT_APPS_DIR / "scroll_to_center.py", press=["s"]) From 2588cfdb0d8e9e6bfe84ab9298cc05e89970a25e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Tue, 11 Apr 2023 13:39:24 +0100 Subject: [PATCH 3/5] Update snapshot test. --- .../__snapshots__/test_snapshots.ambr | 480 ++++++------------ 1 file changed, 158 insertions(+), 322 deletions(-) diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index e6c1164852..9e4ea19539 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -17278,328 +17278,6 @@ ''' # --- -# name: test_modal_dialog_bindings - ''' - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ModalApp - - - - - - - - - - ModalApp - Hello - - - - - - - - - - - - - - - - - - - - - -  ⏎  Open Dialog  - - - - - ''' -# --- -# name: test_modal_dialog_bindings_input - ''' - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ModalApp - - - - - - - - - - DialogModalApp - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - hi! - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - OK - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - - - - - - - - - - - -  ⏎  Open Dialog  - - - - - ''' -# --- # name: test_multiple_css ''' @@ -19840,6 +19518,164 @@ ''' # --- +# name: test_scroll_to_center + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MyApp + + + + + + + + + + ────────────────────────────────────────────────────────────────────────────── + SPAM + SPAM + SPAM▂▂ + SPAM + SPAM + SPAM + ────────────────────────────────────────────────────────────────────────── + SPAM + SPAM▆▆ + SPAM + SPAM + SPAM + SPAM▁▁ + SPAM + SPAM + SPAM + SPAM + SPAM + SPAM + SPAM + SPAM + SPAM + ────────────────────────────────────────────────────────────────────────────── + + + + + ''' +# --- # name: test_scroll_visible ''' From 8fe9e97fd77e200980c69906e22c36d6f6cb6cfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Tue, 11 Apr 2023 14:23:45 +0100 Subject: [PATCH 4/5] Restore deleted snapshot tests. --- .../__snapshots__/test_snapshots.ambr | 322 ++++++++++++++++++ 1 file changed, 322 insertions(+) diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index 9e4ea19539..ab9dc85c3d 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -17278,6 +17278,328 @@ ''' # --- +# name: test_modal_dialog_bindings + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ModalApp + + + + + + + + + + ModalApp + Hello + + + + + + + + + + + + + + + + + + + + + +  ⏎  Open Dialog  + + + + + ''' +# --- +# name: test_modal_dialog_bindings_input + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ModalApp + + + + + + + + + + DialogModalApp + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + hi! + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + OK + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + +  ⏎  Open Dialog  + + + + + ''' +# --- # name: test_multiple_css ''' From 349f414dacbb370a2788f89ed1dc22f083886fe9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Tue, 11 Apr 2023 14:51:59 +0100 Subject: [PATCH 5/5] Use Offset operators. --- src/textual/widget.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/textual/widget.py b/src/textual/widget.py index a628bf94b3..a5a422cb1a 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -2464,15 +2464,11 @@ async def _scroll_to_center_of( # We add that to `container_virtual_region.y` to find the total vertical # offset of the central point with respect to the container of `container`. # A similar calculation is made for the horizontal update. - central_point = Offset( - container_virtual_region.x - + central_point.x - - container.scroll_offset.x - - scroll.x, - container_virtual_region.y - + central_point.y - - container.scroll_offset.y - - scroll.y, + central_point = ( + container_virtual_region.offset + + central_point + - container.scroll_offset + - scroll ) widget = container container = widget.parent