mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Merge pull request #2255 from Textualize/scroll_to_center
Add scroll_to_center method.
This commit is contained in:
@@ -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/)
|
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
||||||
and this project adheres to [Semantic Versioning](http://semver.org/).
|
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
|
## [0.19.1] - 2023-04-10
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -2408,6 +2408,100 @@ class Widget(DOMNode):
|
|||||||
force=force,
|
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 = (
|
||||||
|
container_virtual_region.offset
|
||||||
|
+ central_point
|
||||||
|
- container.scroll_offset
|
||||||
|
- scroll
|
||||||
|
)
|
||||||
|
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__(
|
def __init_subclass__(
|
||||||
cls,
|
cls,
|
||||||
can_focus: bool | None = None,
|
can_focus: bool | None = None,
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
40
tests/snapshot_tests/snapshot_apps/scroll_to_center.py
Normal file
40
tests/snapshot_tests/snapshot_apps/scroll_to_center.py
Normal file
@@ -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()
|
||||||
@@ -406,3 +406,13 @@ def test_fr_margins(snap_compare):
|
|||||||
def test_scroll_visible(snap_compare):
|
def test_scroll_visible(snap_compare):
|
||||||
# https://github.com/Textualize/textual/issues/2181
|
# https://github.com/Textualize/textual/issues/2181
|
||||||
assert snap_compare(SNAPSHOT_APPS_DIR / "scroll_visible.py", press=["t"])
|
assert snap_compare(SNAPSHOT_APPS_DIR / "scroll_visible.py", press=["t"])
|
||||||
|
|
||||||
|
|
||||||
|
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"])
|
||||||
|
|||||||
Reference in New Issue
Block a user