From 3edcfdacc0b644596d64898603aa932e56a0c29b Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 30 Jul 2021 11:50:32 +0100 Subject: [PATCH] scroll bug --- CHANGELOG.md | 1 + src/textual/events.py | 6 +-- src/textual/geometry.py | 6 +-- src/textual/layout.py | 7 +-- src/textual/layout_map.py | 6 +-- src/textual/layouts/dock.py | 4 +- src/textual/layouts/grid.py | 14 +++--- src/textual/layouts/vertical.py | 5 ++- src/textual/page.py | 8 ++-- src/textual/view.py | 10 ++--- src/textual/views/_window_view.py | 17 +++++--- src/textual/widget.py | 6 +-- src/textual/widgets/_scroll_view.py | 25 +++++++---- tests/test_geometry.py | 68 ++++++++++++++--------------- 14 files changed, 99 insertions(+), 84 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d5070c8d..24b7047c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Changed - Simplified events. Remove Startup event (use Mount) +- Changed geometry.Point to geometry.Offset and geometry.Dimensions to geometry.Size ## [0.1.8] - 2021-07-17 diff --git a/src/textual/events.py b/src/textual/events.py index cdb3b9b83..5d5974662 100644 --- a/src/textual/events.py +++ b/src/textual/events.py @@ -5,7 +5,7 @@ from typing import Awaitable, Callable, Type, TYPE_CHECKING, TypeVar import rich.repr from rich.style import Style -from .geometry import Offset, Dimensions +from .geometry import Offset, Size from .message import Message from ._types import MessageTarget from .keys import Keys @@ -106,8 +106,8 @@ class Resize(Event): return isinstance(message, Resize) @property - def size(self) -> Dimensions: - return Dimensions(self.width, self.height) + def size(self) -> Size: + return Size(self.width, self.height) def __rich_repr__(self) -> rich.repr.RichReprResult: yield self.width diff --git a/src/textual/geometry.py b/src/textual/geometry.py index e1749fbf1..85fe35fac 100644 --- a/src/textual/geometry.py +++ b/src/textual/geometry.py @@ -65,7 +65,7 @@ class Offset(NamedTuple): return Offset(int(x1 + (x2 - x1) * factor), int((y1 + (y2 - y1) * factor))) -class Dimensions(NamedTuple): +class Size(NamedTuple): """An area defined by its width and height.""" width: int @@ -194,9 +194,9 @@ class Region(NamedTuple): return Offset(self.x, self.y) @property - def size(self) -> Dimensions: + def size(self) -> Size: """Get the size of the region.""" - return Dimensions(self.width, self.height) + return Size(self.width, self.height) @property def corners(self) -> tuple[int, int, int, int]: diff --git a/src/textual/layout.py b/src/textual/layout.py index bf39801ce..1d91bcceb 100644 --- a/src/textual/layout.py +++ b/src/textual/layout.py @@ -21,7 +21,7 @@ from .layout_map import LayoutMap from ._lines import crop_lines from ._types import Lines -from .geometry import clamp, Region, Offset, Dimensions +from .geometry import clamp, Region, Offset, Size PY38 = sys.version_info >= (3, 8) @@ -106,7 +106,7 @@ class Layout(ABC): map = self.generate_map( console, - Dimensions(width, height), + Size(width, height), Region(0, 0, width, height), scroll, ) @@ -164,7 +164,7 @@ class Layout(ABC): @abstractmethod def generate_map( - self, console: Console, size: Dimensions, viewport: Region, scroll: Offset + self, console: Console, size: Size, viewport: Region, scroll: Offset ) -> LayoutMap: """Generate a layout map that defines where on the screen the widgets will be drawn. @@ -297,6 +297,7 @@ class Layout(ABC): region, clip, lines = region_lines else: lines = render(widget, region.width, region.height) + log("RENDERING", widget) if region in clip: self.renders[widget] = (region, clip, lines) yield region, clip, lines diff --git a/src/textual/layout_map.py b/src/textual/layout_map.py index be42e610d..a32c633d9 100644 --- a/src/textual/layout_map.py +++ b/src/textual/layout_map.py @@ -4,7 +4,7 @@ from rich.console import Console from typing import ItemsView, KeysView, ValuesView, NamedTuple -from .geometry import Region, Dimensions +from .geometry import Region, Size from .widget import Widget @@ -16,13 +16,13 @@ class RenderRegion(NamedTuple): class LayoutMap: - def __init__(self, size: Dimensions) -> None: + def __init__(self, size: Size) -> None: self.size = size self.contents_region = Region(0, 0, 0, 0) self.widgets: dict[Widget, RenderRegion] = {} @property - def virtual_size(self) -> Dimensions: + def virtual_size(self) -> Size: return self.contents_region.size def __getitem__(self, widget: Widget) -> RenderRegion: diff --git a/src/textual/layouts/dock.py b/src/textual/layouts/dock.py index 9bbba9cce..8616c1fea 100644 --- a/src/textual/layouts/dock.py +++ b/src/textual/layouts/dock.py @@ -8,7 +8,7 @@ from typing import Iterable, TYPE_CHECKING, Sequence from rich.console import Console from .._layout_resolve import layout_resolve -from ..geometry import Offset, Region, Dimensions +from ..geometry import Offset, Region, Size from ..layout import Layout from ..layout_map import LayoutMap @@ -49,7 +49,7 @@ class DockLayout(Layout): yield from dock.widgets def generate_map( - self, console: Console, size: Dimensions, viewport: Region, scroll: Offset + self, console: Console, size: Size, viewport: Region, scroll: Offset ) -> LayoutMap: map: LayoutMap = LayoutMap(size) diff --git a/src/textual/layouts/grid.py b/src/textual/layouts/grid.py index 5a1d7c31e..25a7056a9 100644 --- a/src/textual/layouts/grid.py +++ b/src/textual/layouts/grid.py @@ -11,7 +11,7 @@ from typing import Iterable, NamedTuple from rich.console import Console from .._layout_resolve import layout_resolve -from ..geometry import Dimensions, Offset, Region +from ..geometry import Size, Offset, Region from ..layout import Layout from ..layout_map import LayoutMap from ..widget import Widget @@ -238,8 +238,8 @@ class GridLayout(Layout): def _align( cls, region: Region, - grid_size: Dimensions, - container: Dimensions, + grid_size: Size, + container: Size, col_align: GridAlign, row_align: GridAlign, ) -> Region: @@ -264,7 +264,7 @@ class GridLayout(Layout): return self.widgets.keys() def generate_map( - self, console: Console, size: Dimensions, viewport: Region, scroll: Offset + self, console: Console, size: Size, viewport: Region, scroll: Offset ) -> LayoutMap: """Generate a map that associates widgets with their location on screen. @@ -338,9 +338,7 @@ class GridLayout(Layout): # ) # map.update(sub_map) - container = Dimensions( - width - self.column_gutter * 2, height - self.row_gutter * 2 - ) + container = Size(width - self.column_gutter * 2, height - self.row_gutter * 2) column_names, column_tracks, column_count, column_size = resolve_tracks( [ options @@ -357,7 +355,7 @@ class GridLayout(Layout): self.row_gap, self.row_repeat, ) - grid_size = Dimensions(column_size, row_size) + grid_size = Size(column_size, row_size) widget_areas = ( (widget, area) diff --git a/src/textual/layouts/vertical.py b/src/textual/layouts/vertical.py index d0d46d217..d44e7226c 100644 --- a/src/textual/layouts/vertical.py +++ b/src/textual/layouts/vertical.py @@ -5,7 +5,7 @@ from typing import Iterable from rich.console import Console -from ..geometry import Offset, Region, Dimensions +from ..geometry import Offset, Region, Size from ..layout import Layout from ..layout_map import LayoutMap from ..widget import Widget @@ -28,7 +28,7 @@ class VerticalLayout(Layout): return self._widgets def generate_map( - self, console: Console, size: Dimensions, viewport: Region, scroll: Offset + self, console: Console, size: Size, viewport: Region, scroll: Offset ) -> LayoutMap: index = 0 width, height = size @@ -50,6 +50,7 @@ class VerticalLayout(Layout): renderable, console.options.update_width(render_width) ) region = Region(x, y, render_width, len(lines)) + self.renders[widget] = (region - scroll, viewport, lines) add_widget(widget, region - scroll, viewport) else: add_widget( diff --git a/src/textual/page.py b/src/textual/page.py index 6816055c9..b45eb7ec1 100644 --- a/src/textual/page.py +++ b/src/textual/page.py @@ -7,7 +7,7 @@ from rich.padding import Padding, PaddingDimensions from rich.segment import Segment from rich.style import StyleType -from .geometry import Dimensions, Offset +from .geometry import Size, Offset from .message import Message from .widget import Widget, Reactive @@ -38,7 +38,7 @@ class PageRender: self.offset = Offset(0, 0) self._render_width: int | None = None self._render_height: int | None = None - self.size = Dimensions(0, 0) + self.size = Size(0, 0) self._lines: list[list[Segment]] = [] def move_to(self, x: int = 0, y: int = 0) -> None: @@ -61,7 +61,7 @@ class PageRender: if self.padding: renderable = Padding(renderable, self.padding) self._lines[:] = console.render_lines(renderable, options, style=style) - self.size = Dimensions(width, len(self._lines)) + self.size = Size(width, len(self._lines)) self.page.emit_no_wait(PageUpdate(self.page)) def __rich_console__( @@ -126,7 +126,7 @@ class Page(Widget): self.require_repaint() @property - def virtual_size(self) -> Dimensions: + def virtual_size(self) -> Size: return self._page.size def render(self) -> RenderableType: diff --git a/src/textual/view.py b/src/textual/view.py index 358fc109e..b095afd29 100644 --- a/src/textual/view.py +++ b/src/textual/view.py @@ -10,7 +10,7 @@ from rich.style import Style from . import events from . import log from .layout import Layout, NoWidget -from .geometry import Dimensions, Offset, Region +from .geometry import Size, Offset, Region from .messages import UpdateMessage, LayoutMessage from .reactive import Reactive, watch @@ -30,7 +30,7 @@ class View(Widget): self.layout: Layout = layout or self.layout_factory() self.mouse_over: Widget | None = None self.focused: Widget | None = None - self.size = Dimensions(0, 0) + self.size = Size(0, 0) self.widgets: set[Widget] = set() self.named_widgets: dict[str, Widget] = {} self._mouse_style: Style = Style() @@ -56,7 +56,7 @@ class View(Widget): def scroll(self) -> Offset: return Offset(self.scroll_x, self.scroll_y) - virtual_size: Reactive[Dimensions] = Reactive(Dimensions(0, 0)) + virtual_size: Reactive[Size] = Reactive(Size(0, 0)) # @property # def virtual_size(self) -> Dimensions: @@ -160,7 +160,7 @@ class View(Widget): self.console, width, height, self.scroll ) self.virtual_size = self.layout.map.virtual_size - self.app.refresh() + # self.app.refresh() for widget in hidden: widget.post_message_no_wait(events.Hide(self)) @@ -177,7 +177,7 @@ class View(Widget): ) async def on_resize(self, event: events.Resize) -> None: - self.size = Dimensions(event.width, event.height) + self.size = Size(event.width, event.height) await self.refresh_layout() def get_widget_at(self, x: int, y: int) -> tuple[Widget, Region]: diff --git a/src/textual/views/_window_view.py b/src/textual/views/_window_view.py index 9ba580846..fcc78bd88 100644 --- a/src/textual/views/_window_view.py +++ b/src/textual/views/_window_view.py @@ -2,7 +2,8 @@ from __future__ import annotations from rich.console import RenderableType -from ..geometry import Offset, Dimensions +from .. import events +from ..geometry import Offset, Size from ..layouts.vertical import VerticalLayout from ..view import View from ..message import Message @@ -24,16 +25,22 @@ class WindowView(View, layout=VerticalLayout): ) -> None: self.gutter = gutter layout = VerticalLayout() - layout.add(widget if isinstance(widget, Widget) else Static(widget)) + self.widget = widget if isinstance(widget, Widget) else Static(widget) + layout.add(self.widget) super().__init__(name=name, layout=layout) async def update(self, widget: Widget | RenderableType) -> None: layout = self.layout assert isinstance(layout, VerticalLayout) layout.clear() - layout.add(widget if isinstance(widget, Widget) else Static(widget)) + self.widget = widget if isinstance(widget, Widget) else Static(widget) + layout.add(self.widget) await self.refresh_layout() - # self.require_layout() + self.require_layout() - async def watch_virtual_size(self, size: Dimensions) -> None: + async def watch_virtual_size(self, size: Size) -> None: await self.emit(VirtualSizeChange(self)) + + # async def on_resize(self, event: events.Resize) -> None: + # self.layout.renders.pop(self.widget) + # self.require_repaint() diff --git a/src/textual/widget.py b/src/textual/widget.py index d3ceed38e..acf1429fb 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -20,7 +20,7 @@ from rich.style import Style from . import events from ._animator import BoundAnimator from ._context import active_app -from .geometry import Dimensions +from .geometry import Size from .message import Message from .message_pump import MessagePump from .messages import LayoutMessage, UpdateMessage @@ -47,7 +47,7 @@ class Widget(MessagePump): self.name = name or f"{class_name}#{_count}" - self.size = Dimensions(0, 0) + self.size = Size(0, 0) self.size_changed = False self._repaint_required = False self._layout_required = False @@ -175,7 +175,7 @@ class Widget(MessagePump): async def on_event(self, event: events.Event) -> None: if isinstance(event, events.Resize): - new_size = Dimensions(event.width, event.height) + new_size = Size(event.width, event.height) if self.size != new_size: self.size = new_size self.require_repaint() diff --git a/src/textual/widgets/_scroll_view.py b/src/textual/widgets/_scroll_view.py index 4c1400662..8bb6b12fd 100644 --- a/src/textual/widgets/_scroll_view.py +++ b/src/textual/widgets/_scroll_view.py @@ -9,7 +9,7 @@ from .. import events from ..layouts.grid import GridLayout from ..message import Message from ..scrollbar import ScrollTo, ScrollBar -from ..geometry import clamp, Offset, Dimensions +from ..geometry import clamp, Offset, Size from ..page import Page from ..reactive import watch from ..view import View @@ -52,16 +52,24 @@ class ScrollView(View): target_y: Reactive[float] = Reactive(0, repaint=False) def validate_x(self, value: float) -> float: - return clamp(value, 0, self.window.virtual_size.width - self.size.width) + return clamp(value, 0, self.max_scroll_x) def validate_target_x(self, value: float) -> float: - return clamp(value, 0, self.window.virtual_size.width - self.size.width) + return clamp(value, 0, self.max_scroll_x) def validate_y(self, value: float) -> float: - return clamp(value, 0, self.window.virtual_size.height - self.size.height) + return clamp(value, 0, self.max_scroll_y) def validate_target_y(self, value: float) -> float: - return clamp(value, 0, self.window.virtual_size.height - self.size.height) + return clamp(value, 0, self.max_scroll_y) + + @property + def max_scroll_y(self) -> float: + return max(0, self.window.virtual_size.height - self.size.height) + + @property + def max_scroll_x(self) -> float: + return max(0, self.window.virtual_size.width - self.size.width) async def watch_x(self, new_value: float) -> None: self.window.scroll_x = round(new_value) @@ -175,12 +183,11 @@ class ScrollView(View): self.animate("y", self.target_y, speed=150, easing="out_cubic") async def message_virtual_size_change(self, message: Message) -> None: - self.log(self.y) - return + virtual_size = self.window.virtual_size # self.log("VIRTUAL_SIZE", self.size, virtual_size) - # self.x = self.validate_x(self.x) - # self.y = self.validate_y(self.y) + self.x = self.validate_x(self.x) + self.y = self.validate_y(self.y) self.log(self.y) self.vscroll.virtual_size = virtual_size.height self.vscroll.window_size = self.size.height diff --git a/tests/test_geometry.py b/tests/test_geometry.py index cfe94fc74..71c16317b 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -1,57 +1,57 @@ import pytest -from textual.geometry import clamp, Offset, Dimensions, Region +from textual.geometry import clamp, Offset, Size, Region def test_dimensions_region(): - assert Dimensions(30, 40).region == Region(0, 0, 30, 40) + assert Size(30, 40).region == Region(0, 0, 30, 40) def test_dimensions_contains(): - assert Dimensions(10, 10).contains(5, 5) - assert Dimensions(10, 10).contains(9, 9) - assert Dimensions(10, 10).contains(0, 0) - assert not Dimensions(10, 10).contains(10, 9) - assert not Dimensions(10, 10).contains(9, 10) - assert not Dimensions(10, 10).contains(-1, 0) - assert not Dimensions(10, 10).contains(0, -1) + assert Size(10, 10).contains(5, 5) + assert Size(10, 10).contains(9, 9) + assert Size(10, 10).contains(0, 0) + assert not Size(10, 10).contains(10, 9) + assert not Size(10, 10).contains(9, 10) + assert not Size(10, 10).contains(-1, 0) + assert not Size(10, 10).contains(0, -1) def test_dimensions_contains_point(): - assert Dimensions(10, 10).contains_point(Offset(5, 5)) - assert Dimensions(10, 10).contains_point(Offset(9, 9)) - assert Dimensions(10, 10).contains_point(Offset(0, 0)) - assert not Dimensions(10, 10).contains_point(Offset(10, 9)) - assert not Dimensions(10, 10).contains_point(Offset(9, 10)) - assert not Dimensions(10, 10).contains_point(Offset(-1, 0)) - assert not Dimensions(10, 10).contains_point(Offset(0, -1)) + assert Size(10, 10).contains_point(Offset(5, 5)) + assert Size(10, 10).contains_point(Offset(9, 9)) + assert Size(10, 10).contains_point(Offset(0, 0)) + assert not Size(10, 10).contains_point(Offset(10, 9)) + assert not Size(10, 10).contains_point(Offset(9, 10)) + assert not Size(10, 10).contains_point(Offset(-1, 0)) + assert not Size(10, 10).contains_point(Offset(0, -1)) def test_dimensions_contains_special(): with pytest.raises(TypeError): - (1, 2, 3) in Dimensions(10, 10) + (1, 2, 3) in Size(10, 10) - assert (5, 5) in Dimensions(10, 10) - assert (9, 9) in Dimensions(10, 10) - assert (0, 0) in Dimensions(10, 10) - assert (10, 9) not in Dimensions(10, 10) - assert (9, 10) not in Dimensions(10, 10) - assert (-1, 0) not in Dimensions(10, 10) - assert (0, -1) not in Dimensions(10, 10) + assert (5, 5) in Size(10, 10) + assert (9, 9) in Size(10, 10) + assert (0, 0) in Size(10, 10) + assert (10, 9) not in Size(10, 10) + assert (9, 10) not in Size(10, 10) + assert (-1, 0) not in Size(10, 10) + assert (0, -1) not in Size(10, 10) def test_dimensions_bool(): - assert Dimensions(1, 1) - assert Dimensions(3, 4) - assert not Dimensions(0, 1) - assert not Dimensions(1, 0) + assert Size(1, 1) + assert Size(3, 4) + assert not Size(0, 1) + assert not Size(1, 0) def test_dimensions_area(): - assert Dimensions(0, 0).area == 0 - assert Dimensions(1, 0).area == 0 - assert Dimensions(1, 1).area == 1 - assert Dimensions(4, 5).area == 20 + assert Size(0, 0).area == 0 + assert Size(1, 0).area == 0 + assert Size(1, 1).area == 1 + assert Size(4, 5).area == 20 def test_clamp(): @@ -97,8 +97,8 @@ def test_region_area(): def test_region_size(): - assert isinstance(Region(3, 4, 5, 6).size, Dimensions) - assert Region(3, 4, 5, 6).size == Dimensions(5, 6) + assert isinstance(Region(3, 4, 5, 6).size, Size) + assert Region(3, 4, 5, 6).size == Size(5, 6) def test_region_origin():