geometry tests

This commit is contained in:
Will McGugan
2022-06-27 10:11:57 +01:00
parent 82358fdb7b
commit 86fdc96ab3
8 changed files with 103 additions and 59 deletions

View File

@@ -15,6 +15,10 @@
scrollbar-size-vertical: 2;
}
*:hover {
tint: red 30%;
}
App > Screen {
layout: dock;
docks: side=left/1;
@@ -23,7 +27,9 @@ App > Screen {
}
DataTable {
margin: 2;
border: solid red;
margin: 1 1;
height: 12;
}

View File

@@ -137,17 +137,15 @@ class BasicApp(App, css_path="basic.css"):
Widget(classes="content"),
),
)
table.add_column("Foo", width=80)
table.add_column("Bar", width=50)
table.add_column("Baz", width=40)
table.add_column("Foo", width=20)
table.add_column("Bar", width=20)
table.add_column("Baz", width=20)
table.add_column("Foo", width=20)
table.add_column("Bar", width=20)
table.add_column("Baz", width=20)
table.zebra_stripes = True
for n in range(100):
table.add_row(
f"{n} This is an example of a [b]DataTable widget[/b] within a larger [bold magenta]Textual UI",
"Cells may contain just about any kind of data",
"Where there is a Will there is a Way",
height=1,
)
table.add_row(*[f"Cell ([b]{n}[/b], {col})" for col in range(6)])
async def on_key(self, event) -> None:
await self.dispatch_key(event)

View File

@@ -578,9 +578,8 @@ class Compositor:
if not region:
continue
if region in clip:
yield region, clip, widget.render_lines(
Region(0, 0, region.width, region.height)
)
lines = widget.render_lines(Region(0, 0, region.width, region.height))
yield region, clip, lines
elif overlaps(clip, region):
clipped_region = intersection(region, clip)
if not clipped_region:
@@ -706,12 +705,12 @@ class Compositor:
regions: list[Region] = []
add_region = regions.append
for widget in self.regions.keys() & widgets:
(x, y, _, _), clip = self.regions[widget]
region, clip = self.regions[widget]
offset = region.offset
intersection = clip.intersection
for dirty_region in widget.get_dirty_regions():
update_region = intersection(dirty_region.translate(x, y))
update_region = intersection(dirty_region.translate(offset))
if update_region:
add_region(update_region)
self._dirty_regions.update(regions)
# self.add_dirty_regions(regions)

View File

@@ -243,8 +243,8 @@ class Region(NamedTuple):
return cls(x, y, width, height)
@classmethod
def translate_inside(cls, window_region: Region, region: Region) -> Offset:
"""Calculate the smallest offset required to translate a region so that is is within
def get_scroll_to_visible(cls, window_region: Region, region: Region) -> Offset:
"""Calculate the smallest offset required to translate a window so that it contains
another region.
This method is used to calculate the required offset to scroll something in to view.
@@ -507,33 +507,19 @@ class Region(NamedTuple):
and (y2 >= oy2 >= y1)
)
def translate(self, x: int = 0, y: int = 0) -> Region:
def translate(self, offset: tuple[int, int]) -> Region:
"""Move the offset of the Region.
Args:
translate_x (int): Value to add to x coordinate.
translate_y (int): Value to add to y coordinate.
translate (tuple[int, int]): Offset to add to region.
Returns:
Region: A new region shifted by x, y
Region: A new region shifted by (x, y)
"""
self_x, self_y, width, height = self
return Region(self_x + x, self_y + y, width, height)
def translate_negative(self, x: int = 0, y: int = 0) -> Region:
"""Move the offset of the Region in the opposite direction.
Args:
translate_x (int): Value to subtract to x coordinate.
translate_y (int): Value to subtract to y coordinate.
Returns:
Region: A new region shifted by x, y
"""
self_x, self_y, width, height = self
return Region(self_x - x, self_y - y, width, height)
offset_x, offset_y = offset
return Region(self_x + offset_x, self_y + offset_y, width, height)
@lru_cache(maxsize=4096)
def __contains__(self, other: Any) -> bool:

View File

@@ -253,10 +253,12 @@ class GridLayout(Layout):
offset = (container - size) // 2
return offset
offset_x = align(grid_size.width, container.width, col_align)
offset_y = align(grid_size.height, container.height, row_align)
offset = Offset(
align(grid_size.width, container.width, col_align),
align(grid_size.height, container.height, row_align),
)
region = region.translate(offset_x, offset_y)
region = region.translate(offset)
return region
def get_widgets(self) -> Iterable[Widget]:

View File

@@ -435,12 +435,13 @@ class Widget(DOMNode):
# self._dirty_regions.append(self.size.region)
if regions:
content_offset = self.content_offset
self._dirty_regions.update(regions)
else:
self._dirty_regions.clear()
# TODO: Does this need to be content region?
# self._dirty_regions.append(self.size.region)
self._dirty_regions.add(self.content_region.size.region)
self._dirty_regions.add(self.size.region)
def get_dirty_regions(self) -> Collection[Region]:
regions = self._dirty_regions.copy()
@@ -589,6 +590,7 @@ class Widget(DOMNode):
bool: True if any scrolling has occurred in any descendant, otherwise False.
"""
# TODO: Update this to use scroll_to_region
scrolls = set()
node = widget.parent
@@ -657,7 +659,7 @@ class Widget(DOMNode):
window = self.region.at_offset(self.scroll_offset)
if spacing is not None:
window = window.shrink(spacing)
delta = Region.translate_inside(window, region)
delta = Region.get_scroll_to_visible(window, region)
if delta:
self.scroll_relative(
delta.x or None,
@@ -665,7 +667,6 @@ class Widget(DOMNode):
animate=animate,
duration=0.2,
)
return delta
def __init_subclass__(
@@ -769,18 +770,18 @@ class Widget(DOMNode):
def watch(self, attribute_name, callback: Callable[[Any], Awaitable[None]]) -> None:
watch(self, attribute_name, callback)
def render_styled(self) -> RenderableType:
"""Applies style attributes to the default renderable.
def _style_renderable(self, renderable: RenderableType) -> RenderableType:
"""Applies CSS styles to a renderable by wrapping it in another renderable.
Args:
renderable (RenderableType): Renderable to apply styles to.
Returns:
RenderableType: A new renderable.
RenderableType: An updated renderable.
"""
(base_background, base_color), (background, color) = self.colors
styles = self.styles
renderable = self.render()
content_align = (styles.content_align_horizontal, styles.content_align_vertical)
if content_align != ("left", "top"):
horizontal, vertical = content_align
@@ -816,6 +817,17 @@ class Widget(DOMNode):
return renderable
def render_styled(self) -> RenderableType:
"""Applies style attributes to the default renderable.
Returns:
RenderableType: A new renderable.
"""
renderable = self.render()
renderable = self._style_renderable(renderable)
return renderable
@property
def size(self) -> Size:
return self._size
@@ -843,6 +855,16 @@ class Widget(DOMNode):
except errors.NoWidget:
return Region()
@property
def window_region(self) -> Region:
"""The region within the scrollable area that is currently visible.
Returns:
Region: New region.
"""
window_region = self.region.at_offset(self.scroll_offset)
return window_region
@property
def scroll_offset(self) -> Offset:
return Offset(int(self.scroll_x), int(self.scroll_y))
@@ -937,7 +959,8 @@ class Widget(DOMNode):
def _crop_lines(self, lines: Lines, x1, x2) -> Lines:
width = self.size.width
if (x1, x2) != (0, width):
lines = [line_crop(line, x1, x2, width) for line in lines]
_line_crop = line_crop
lines = [_line_crop(line, x1, x2, width) for line in lines]
return lines
def render_lines(self, crop: Region) -> Lines:

View File

@@ -284,9 +284,10 @@ class DataTable(ScrollView, Generic[CellType]):
if row_index < 0 or column_index < 0:
return
region = self._get_cell_region(row_index, column_index)
region = region.translate_negative(*self.scroll_offset)
if region:
self.refresh(region)
if not self.window_region.overlaps(region):
return
region = region.translate(-self.scroll_offset)
self.refresh(region)
# refresh_region = self.content_region.intersection(region)
# if refresh_region:
# self.refresh(refresh_region)
@@ -491,8 +492,8 @@ class DataTable(ScrollView, Generic[CellType]):
Returns:
Lines: A list of segments for every line within crop region.
"""
scroll_x, scroll_y = self.scroll_offset
x1, y1, x2, y2 = crop.translate(scroll_x, scroll_y).corners
scroll_y = self.scroll_offset.y
x1, y1, x2, y2 = crop.translate(self.scroll_offset).corners
base_style = self.rich_style
@@ -511,6 +512,7 @@ class DataTable(ScrollView, Generic[CellType]):
for line_index, y in enumerate(range(y1, y2)):
if y - scroll_y < fixed_top_row_count:
lines[line_index] = fixed_lines[line_index]
return lines
def on_mouse_move(self, event: events.MouseMove):

View File

@@ -89,6 +89,11 @@ def test_offset_sub():
Offset(1, 1) - "foo"
def test_offset_neg():
assert Offset(0, 0) == Offset(0, 0)
assert -Offset(2, -3) == Offset(-2, 3)
def test_offset_mul():
assert Offset(2, 1) * 2 == Offset(4, 2)
assert Offset(2, 1) * -2 == Offset(-4, -2)
@@ -125,10 +130,23 @@ def test_region_from_union():
assert Region.from_union(regions) == Region(10, 20, 40, 40)
def test_region_from_origin():
def test_region_from_offset():
assert Region.from_offset(Offset(3, 4), (5, 6)) == Region(3, 4, 5, 6)
@pytest.mark.parametrize(
"window,region,scroll",
[
(Region(0, 0, 200, 100), Region(0, 0, 200, 100), Offset(0, 0)),
(Region(0, 0, 200, 100), Region(0, -100, 10, 10), Offset(0, -100)),
(Region(10, 15, 20, 10), Region(0, 0, 50, 50), Offset(-10, -15)),
],
)
def test_get_scroll_to_visible(window, region, scroll):
assert Region.get_scroll_to_visible(window, region) == scroll
assert region.overlaps(window + scroll)
def test_region_area():
assert Region(3, 4, 0, 0).area == 0
assert Region(3, 4, 5, 6).area == 30
@@ -167,6 +185,16 @@ def test_region_sub():
Region(1, 2, 3, 4) - "foo"
def test_region_at_offset():
assert Region(10, 10, 30, 40).at_offset((0, 0)) == Region(0, 0, 30, 40)
assert Region(10, 10, 30, 40).at_offset((-15, 30)) == Region(-15, 30, 30, 40)
def test_crop_size():
assert Region(10, 20, 100, 200).crop_size((50, 40)) == Region(10, 20, 50, 40)
assert Region(10, 20, 100, 200).crop_size((500, 40)) == Region(10, 20, 100, 40)
def test_region_overlaps():
assert Region(10, 10, 30, 20).overlaps(Region(0, 0, 20, 20))
assert not Region(10, 10, 5, 5).overlaps(Region(15, 15, 20, 20))
@@ -201,8 +229,8 @@ def test_region_contains_region():
def test_region_translate():
assert Region(1, 2, 3, 4).translate(10, 20) == Region(11, 22, 3, 4)
assert Region(1, 2, 3, 4).translate(y=20) == Region(1, 22, 3, 4)
assert Region(1, 2, 3, 4).translate((10, 20)) == Region(11, 22, 3, 4)
assert Region(1, 2, 3, 4).translate((0, 20)) == Region(1, 22, 3, 4)
def test_region_contains_special():
@@ -274,7 +302,7 @@ def test_region_y_range():
assert Region(5, 10, 20, 30).y_range == range(10, 40)
def test_region_reset_origin():
def test_region_reset_offset():
assert Region(5, 10, 20, 30).reset_offset == Region(0, 0, 20, 30)