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