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 ### Changed
- Simplified events. Remove Startup event (use Mount) - 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 ## [0.1.8] - 2021-07-17

View File

@@ -5,7 +5,7 @@ from typing import Awaitable, Callable, Type, TYPE_CHECKING, TypeVar
import rich.repr import rich.repr
from rich.style import Style from rich.style import Style
from .geometry import Offset, Dimensions from .geometry import Offset, Size
from .message import Message from .message import Message
from ._types import MessageTarget from ._types import MessageTarget
from .keys import Keys from .keys import Keys
@@ -106,8 +106,8 @@ class Resize(Event):
return isinstance(message, Resize) return isinstance(message, Resize)
@property @property
def size(self) -> Dimensions: def size(self) -> Size:
return Dimensions(self.width, self.height) return Size(self.width, self.height)
def __rich_repr__(self) -> rich.repr.RichReprResult: def __rich_repr__(self) -> rich.repr.RichReprResult:
yield self.width yield self.width

View File

@@ -65,7 +65,7 @@ class Offset(NamedTuple):
return Offset(int(x1 + (x2 - x1) * factor), int((y1 + (y2 - y1) * factor))) 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.""" """An area defined by its width and height."""
width: int width: int
@@ -194,9 +194,9 @@ class Region(NamedTuple):
return Offset(self.x, self.y) return Offset(self.x, self.y)
@property @property
def size(self) -> Dimensions: def size(self) -> Size:
"""Get the size of the region.""" """Get the size of the region."""
return Dimensions(self.width, self.height) return Size(self.width, self.height)
@property @property
def corners(self) -> tuple[int, int, int, int]: 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 ._lines import crop_lines
from ._types import 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) PY38 = sys.version_info >= (3, 8)
@@ -106,7 +106,7 @@ class Layout(ABC):
map = self.generate_map( map = self.generate_map(
console, console,
Dimensions(width, height), Size(width, height),
Region(0, 0, width, height), Region(0, 0, width, height),
scroll, scroll,
) )
@@ -164,7 +164,7 @@ class Layout(ABC):
@abstractmethod @abstractmethod
def generate_map( def generate_map(
self, console: Console, size: Dimensions, viewport: Region, scroll: Offset self, console: Console, size: Size, viewport: Region, scroll: Offset
) -> LayoutMap: ) -> LayoutMap:
"""Generate a layout map that defines where on the screen the widgets will be drawn. """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 region, clip, lines = region_lines
else: else:
lines = render(widget, region.width, region.height) lines = render(widget, region.width, region.height)
log("RENDERING", widget)
if region in clip: if region in clip:
self.renders[widget] = (region, clip, lines) self.renders[widget] = (region, clip, lines)
yield 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 typing import ItemsView, KeysView, ValuesView, NamedTuple
from .geometry import Region, Dimensions from .geometry import Region, Size
from .widget import Widget from .widget import Widget
@@ -16,13 +16,13 @@ class RenderRegion(NamedTuple):
class LayoutMap: class LayoutMap:
def __init__(self, size: Dimensions) -> None: def __init__(self, size: Size) -> None:
self.size = size self.size = size
self.contents_region = Region(0, 0, 0, 0) self.contents_region = Region(0, 0, 0, 0)
self.widgets: dict[Widget, RenderRegion] = {} self.widgets: dict[Widget, RenderRegion] = {}
@property @property
def virtual_size(self) -> Dimensions: def virtual_size(self) -> Size:
return self.contents_region.size return self.contents_region.size
def __getitem__(self, widget: Widget) -> RenderRegion: 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 rich.console import Console
from .._layout_resolve import layout_resolve from .._layout_resolve import layout_resolve
from ..geometry import Offset, Region, Dimensions from ..geometry import Offset, Region, Size
from ..layout import Layout from ..layout import Layout
from ..layout_map import LayoutMap from ..layout_map import LayoutMap
@@ -49,7 +49,7 @@ class DockLayout(Layout):
yield from dock.widgets yield from dock.widgets
def generate_map( def generate_map(
self, console: Console, size: Dimensions, viewport: Region, scroll: Offset self, console: Console, size: Size, viewport: Region, scroll: Offset
) -> LayoutMap: ) -> LayoutMap:
map: LayoutMap = LayoutMap(size) map: LayoutMap = LayoutMap(size)

View File

@@ -11,7 +11,7 @@ from typing import Iterable, NamedTuple
from rich.console import Console from rich.console import Console
from .._layout_resolve import layout_resolve from .._layout_resolve import layout_resolve
from ..geometry import Dimensions, Offset, Region from ..geometry import Size, Offset, Region
from ..layout import Layout from ..layout import Layout
from ..layout_map import LayoutMap from ..layout_map import LayoutMap
from ..widget import Widget from ..widget import Widget
@@ -238,8 +238,8 @@ class GridLayout(Layout):
def _align( def _align(
cls, cls,
region: Region, region: Region,
grid_size: Dimensions, grid_size: Size,
container: Dimensions, container: Size,
col_align: GridAlign, col_align: GridAlign,
row_align: GridAlign, row_align: GridAlign,
) -> Region: ) -> Region:
@@ -264,7 +264,7 @@ class GridLayout(Layout):
return self.widgets.keys() return self.widgets.keys()
def generate_map( def generate_map(
self, console: Console, size: Dimensions, viewport: Region, scroll: Offset self, console: Console, size: Size, viewport: Region, scroll: Offset
) -> LayoutMap: ) -> LayoutMap:
"""Generate a map that associates widgets with their location on screen. """Generate a map that associates widgets with their location on screen.
@@ -338,9 +338,7 @@ class GridLayout(Layout):
# ) # )
# map.update(sub_map) # map.update(sub_map)
container = Dimensions( container = Size(width - self.column_gutter * 2, height - self.row_gutter * 2)
width - self.column_gutter * 2, height - self.row_gutter * 2
)
column_names, column_tracks, column_count, column_size = resolve_tracks( column_names, column_tracks, column_count, column_size = resolve_tracks(
[ [
options options
@@ -357,7 +355,7 @@ class GridLayout(Layout):
self.row_gap, self.row_gap,
self.row_repeat, self.row_repeat,
) )
grid_size = Dimensions(column_size, row_size) grid_size = Size(column_size, row_size)
widget_areas = ( widget_areas = (
(widget, area) (widget, area)

View File

@@ -5,7 +5,7 @@ from typing import Iterable
from rich.console import Console from rich.console import Console
from ..geometry import Offset, Region, Dimensions from ..geometry import Offset, Region, Size
from ..layout import Layout from ..layout import Layout
from ..layout_map import LayoutMap from ..layout_map import LayoutMap
from ..widget import Widget from ..widget import Widget
@@ -28,7 +28,7 @@ class VerticalLayout(Layout):
return self._widgets return self._widgets
def generate_map( def generate_map(
self, console: Console, size: Dimensions, viewport: Region, scroll: Offset self, console: Console, size: Size, viewport: Region, scroll: Offset
) -> LayoutMap: ) -> LayoutMap:
index = 0 index = 0
width, height = size width, height = size
@@ -50,6 +50,7 @@ class VerticalLayout(Layout):
renderable, console.options.update_width(render_width) renderable, console.options.update_width(render_width)
) )
region = Region(x, y, render_width, len(lines)) region = Region(x, y, render_width, len(lines))
self.renders[widget] = (region - scroll, viewport, lines)
add_widget(widget, region - scroll, viewport) add_widget(widget, region - scroll, viewport)
else: else:
add_widget( add_widget(

View File

@@ -7,7 +7,7 @@ from rich.padding import Padding, PaddingDimensions
from rich.segment import Segment from rich.segment import Segment
from rich.style import StyleType from rich.style import StyleType
from .geometry import Dimensions, Offset from .geometry import Size, Offset
from .message import Message from .message import Message
from .widget import Widget, Reactive from .widget import Widget, Reactive
@@ -38,7 +38,7 @@ class PageRender:
self.offset = Offset(0, 0) self.offset = Offset(0, 0)
self._render_width: int | None = None self._render_width: int | None = None
self._render_height: int | None = None self._render_height: int | None = None
self.size = Dimensions(0, 0) self.size = Size(0, 0)
self._lines: list[list[Segment]] = [] self._lines: list[list[Segment]] = []
def move_to(self, x: int = 0, y: int = 0) -> None: def move_to(self, x: int = 0, y: int = 0) -> None:
@@ -61,7 +61,7 @@ class PageRender:
if self.padding: if self.padding:
renderable = Padding(renderable, self.padding) renderable = Padding(renderable, self.padding)
self._lines[:] = console.render_lines(renderable, options, style=style) 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)) self.page.emit_no_wait(PageUpdate(self.page))
def __rich_console__( def __rich_console__(
@@ -126,7 +126,7 @@ class Page(Widget):
self.require_repaint() self.require_repaint()
@property @property
def virtual_size(self) -> Dimensions: def virtual_size(self) -> Size:
return self._page.size return self._page.size
def render(self) -> RenderableType: def render(self) -> RenderableType:

View File

@@ -10,7 +10,7 @@ from rich.style import Style
from . import events from . import events
from . import log from . import log
from .layout import Layout, NoWidget from .layout import Layout, NoWidget
from .geometry import Dimensions, Offset, Region from .geometry import Size, Offset, Region
from .messages import UpdateMessage, LayoutMessage from .messages import UpdateMessage, LayoutMessage
from .reactive import Reactive, watch from .reactive import Reactive, watch
@@ -30,7 +30,7 @@ class View(Widget):
self.layout: Layout = layout or self.layout_factory() self.layout: Layout = layout or self.layout_factory()
self.mouse_over: Widget | None = None self.mouse_over: Widget | None = None
self.focused: Widget | None = None self.focused: Widget | None = None
self.size = Dimensions(0, 0) self.size = Size(0, 0)
self.widgets: set[Widget] = set() self.widgets: set[Widget] = set()
self.named_widgets: dict[str, Widget] = {} self.named_widgets: dict[str, Widget] = {}
self._mouse_style: Style = Style() self._mouse_style: Style = Style()
@@ -56,7 +56,7 @@ class View(Widget):
def scroll(self) -> Offset: def scroll(self) -> Offset:
return Offset(self.scroll_x, self.scroll_y) 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 # @property
# def virtual_size(self) -> Dimensions: # def virtual_size(self) -> Dimensions:
@@ -160,7 +160,7 @@ class View(Widget):
self.console, width, height, self.scroll self.console, width, height, self.scroll
) )
self.virtual_size = self.layout.map.virtual_size self.virtual_size = self.layout.map.virtual_size
self.app.refresh() # self.app.refresh()
for widget in hidden: for widget in hidden:
widget.post_message_no_wait(events.Hide(self)) widget.post_message_no_wait(events.Hide(self))
@@ -177,7 +177,7 @@ class View(Widget):
) )
async def on_resize(self, event: events.Resize) -> None: 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() await self.refresh_layout()
def get_widget_at(self, x: int, y: int) -> tuple[Widget, Region]: 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 rich.console import RenderableType
from ..geometry import Offset, Dimensions from .. import events
from ..geometry import Offset, Size
from ..layouts.vertical import VerticalLayout from ..layouts.vertical import VerticalLayout
from ..view import View from ..view import View
from ..message import Message from ..message import Message
@@ -24,16 +25,22 @@ class WindowView(View, layout=VerticalLayout):
) -> None: ) -> None:
self.gutter = gutter self.gutter = gutter
layout = VerticalLayout() 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) super().__init__(name=name, layout=layout)
async def update(self, widget: Widget | RenderableType) -> None: async def update(self, widget: Widget | RenderableType) -> None:
layout = self.layout layout = self.layout
assert isinstance(layout, VerticalLayout) assert isinstance(layout, VerticalLayout)
layout.clear() 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() 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)) 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 . import events
from ._animator import BoundAnimator from ._animator import BoundAnimator
from ._context import active_app from ._context import active_app
from .geometry import Dimensions from .geometry import Size
from .message import Message from .message import Message
from .message_pump import MessagePump from .message_pump import MessagePump
from .messages import LayoutMessage, UpdateMessage from .messages import LayoutMessage, UpdateMessage
@@ -47,7 +47,7 @@ class Widget(MessagePump):
self.name = name or f"{class_name}#{_count}" self.name = name or f"{class_name}#{_count}"
self.size = Dimensions(0, 0) self.size = Size(0, 0)
self.size_changed = False self.size_changed = False
self._repaint_required = False self._repaint_required = False
self._layout_required = False self._layout_required = False
@@ -175,7 +175,7 @@ class Widget(MessagePump):
async def on_event(self, event: events.Event) -> None: async def on_event(self, event: events.Event) -> None:
if isinstance(event, events.Resize): if isinstance(event, events.Resize):
new_size = Dimensions(event.width, event.height) new_size = Size(event.width, event.height)
if self.size != new_size: if self.size != new_size:
self.size = new_size self.size = new_size
self.require_repaint() self.require_repaint()

View File

@@ -9,7 +9,7 @@ from .. import events
from ..layouts.grid import GridLayout from ..layouts.grid import GridLayout
from ..message import Message from ..message import Message
from ..scrollbar import ScrollTo, ScrollBar from ..scrollbar import ScrollTo, ScrollBar
from ..geometry import clamp, Offset, Dimensions from ..geometry import clamp, Offset, Size
from ..page import Page from ..page import Page
from ..reactive import watch from ..reactive import watch
from ..view import View from ..view import View
@@ -52,16 +52,24 @@ class ScrollView(View):
target_y: Reactive[float] = Reactive(0, repaint=False) target_y: Reactive[float] = Reactive(0, repaint=False)
def validate_x(self, value: float) -> float: 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: 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: 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: 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: async def watch_x(self, new_value: float) -> None:
self.window.scroll_x = round(new_value) 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") self.animate("y", self.target_y, speed=150, easing="out_cubic")
async def message_virtual_size_change(self, message: Message) -> None: async def message_virtual_size_change(self, message: Message) -> None:
self.log(self.y)
return
virtual_size = self.window.virtual_size virtual_size = self.window.virtual_size
# self.log("VIRTUAL_SIZE", self.size, virtual_size) # self.log("VIRTUAL_SIZE", self.size, virtual_size)
# self.x = self.validate_x(self.x) self.x = self.validate_x(self.x)
# self.y = self.validate_y(self.y) self.y = self.validate_y(self.y)
self.log(self.y) self.log(self.y)
self.vscroll.virtual_size = virtual_size.height self.vscroll.virtual_size = virtual_size.height
self.vscroll.window_size = self.size.height self.vscroll.window_size = self.size.height

View File

@@ -1,57 +1,57 @@
import pytest import pytest
from textual.geometry import clamp, Offset, Dimensions, Region from textual.geometry import clamp, Offset, Size, Region
def test_dimensions_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(): def test_dimensions_contains():
assert Dimensions(10, 10).contains(5, 5) assert Size(10, 10).contains(5, 5)
assert Dimensions(10, 10).contains(9, 9) assert Size(10, 10).contains(9, 9)
assert Dimensions(10, 10).contains(0, 0) assert Size(10, 10).contains(0, 0)
assert not Dimensions(10, 10).contains(10, 9) assert not Size(10, 10).contains(10, 9)
assert not Dimensions(10, 10).contains(9, 10) assert not Size(10, 10).contains(9, 10)
assert not Dimensions(10, 10).contains(-1, 0) assert not Size(10, 10).contains(-1, 0)
assert not Dimensions(10, 10).contains(0, -1) assert not Size(10, 10).contains(0, -1)
def test_dimensions_contains_point(): def test_dimensions_contains_point():
assert Dimensions(10, 10).contains_point(Offset(5, 5)) assert Size(10, 10).contains_point(Offset(5, 5))
assert Dimensions(10, 10).contains_point(Offset(9, 9)) assert Size(10, 10).contains_point(Offset(9, 9))
assert Dimensions(10, 10).contains_point(Offset(0, 0)) assert Size(10, 10).contains_point(Offset(0, 0))
assert not Dimensions(10, 10).contains_point(Offset(10, 9)) assert not Size(10, 10).contains_point(Offset(10, 9))
assert not Dimensions(10, 10).contains_point(Offset(9, 10)) assert not Size(10, 10).contains_point(Offset(9, 10))
assert not Dimensions(10, 10).contains_point(Offset(-1, 0)) assert not Size(10, 10).contains_point(Offset(-1, 0))
assert not Dimensions(10, 10).contains_point(Offset(0, -1)) assert not Size(10, 10).contains_point(Offset(0, -1))
def test_dimensions_contains_special(): def test_dimensions_contains_special():
with pytest.raises(TypeError): 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 (5, 5) in Size(10, 10)
assert (9, 9) in Dimensions(10, 10) assert (9, 9) in Size(10, 10)
assert (0, 0) in Dimensions(10, 10) assert (0, 0) in Size(10, 10)
assert (10, 9) not in Dimensions(10, 10) assert (10, 9) not in Size(10, 10)
assert (9, 10) not in Dimensions(10, 10) assert (9, 10) not in Size(10, 10)
assert (-1, 0) not in Dimensions(10, 10) assert (-1, 0) not in Size(10, 10)
assert (0, -1) not in Dimensions(10, 10) assert (0, -1) not in Size(10, 10)
def test_dimensions_bool(): def test_dimensions_bool():
assert Dimensions(1, 1) assert Size(1, 1)
assert Dimensions(3, 4) assert Size(3, 4)
assert not Dimensions(0, 1) assert not Size(0, 1)
assert not Dimensions(1, 0) assert not Size(1, 0)
def test_dimensions_area(): def test_dimensions_area():
assert Dimensions(0, 0).area == 0 assert Size(0, 0).area == 0
assert Dimensions(1, 0).area == 0 assert Size(1, 0).area == 0
assert Dimensions(1, 1).area == 1 assert Size(1, 1).area == 1
assert Dimensions(4, 5).area == 20 assert Size(4, 5).area == 20
def test_clamp(): def test_clamp():
@@ -97,8 +97,8 @@ def test_region_area():
def test_region_size(): def test_region_size():
assert isinstance(Region(3, 4, 5, 6).size, Dimensions) assert isinstance(Region(3, 4, 5, 6).size, Size)
assert Region(3, 4, 5, 6).size == Dimensions(5, 6) assert Region(3, 4, 5, 6).size == Size(5, 6)
def test_region_origin(): def test_region_origin():