scroll bug

This commit is contained in:
Will McGugan
2021-07-30 11:50:32 +01:00
parent 8bdbfffe71
commit 3edcfdacc0
14 changed files with 99 additions and 84 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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]:

View File

@@ -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

View File

@@ -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:

View File

@@ -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)

View File

@@ -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)

View File

@@ -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(

View File

@@ -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:

View File

@@ -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]:

View File

@@ -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()

View File

@@ -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()

View File

@@ -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

View File

@@ -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():