mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
scroll bug
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user