layout_map refactor

This commit is contained in:
Will McGugan
2021-07-27 14:23:28 +01:00
parent a5faf4a07e
commit 534f7b4dc1
13 changed files with 405 additions and 177 deletions

View File

@@ -26,11 +26,12 @@ class SmoothApp(App):
"""Build layout here."""
footer = Footer()
self.bar = Placeholder(name="left")
self.bar.layout_offset_x = -40
await self.view.dock(footer, edge="bottom")
await self.view.dock(Placeholder(), Placeholder(), edge="top")
await self.view.dock(self.bar, edge="left", size=40, z=1)
self.bar.layout_offset_x = -40
SmoothApp.run()
SmoothApp.run(log="textual.log")

View File

@@ -42,6 +42,7 @@ class MyApp(App):
)
self.app.sub_title = os.path.basename(message.path)
await self.body.update(syntax)
# self.body.layout_offset_y = -5
self.body.home()

View File

@@ -9,7 +9,8 @@ class GridTest(App):
async def on_mount(self, event: events.Mount) -> None:
grid = await self.view.dock_grid()
grid = await self.view.dock_grid(edge="left", size=70, name="left")
left = self.view["left"]
grid.add_column(fraction=1, name="left", min_size=20)
grid.add_column(size=30, name="center")
@@ -26,11 +27,17 @@ class GridTest(App):
area4="right,top-start|middle-end",
)
def make_placeholder(name: str) -> Placeholder:
p = Placeholder(name=name)
p.layout_offset_x = 10
p.layout_offset_y = 0
return p
grid.place(
area1=Placeholder(name="area1"),
area2=Placeholder(name="area2"),
area3=Placeholder(name="area3"),
area4=Placeholder(name="area4"),
area1=make_placeholder(name="area1"),
area2=make_placeholder(name="area2"),
area3=make_placeholder(name="area3"),
area4=make_placeholder(name="area4"),
)

View File

@@ -84,7 +84,7 @@ class App(MessagePump):
driver_class (Type[Driver], optional): Driver class, or None to use default. Defaults to None.
title (str, optional): Title of the application. Defaults to "Textual Application".
"""
self.console = console or get_console()
self.console = console or Console()
self.error_console = Console(stderr=True)
self._screen = screen
self.driver_class = driver_class or LinuxDriver

View File

@@ -76,8 +76,19 @@ class Dimensions(NamedTuple):
@property
def area(self) -> int:
"""Get the area of the dimensions.
Returns:
int: Area in cells.
"""
return self.width * self.height
@property
def region(self) -> Region:
"""Get a region of the same size."""
width, height = self
return Region(0, 0, width, height)
def contains(self, x: int, y: int) -> bool:
"""Check if a point is in the region.
@@ -138,8 +149,23 @@ class Region(NamedTuple):
"""
return cls(x1, y1, x2 - x1, y2 - y1)
@classmethod
def from_origin(cls, origin: tuple[int, int], size: tuple[int, int]) -> Region:
"""Create a region from origin and size.
Args:
origin (Point): [description]
size (tuple[int, int]): [description]
Returns:
Region: [description]
"""
x, y = origin
width, height = size
return Region(x, y, width, height)
def __bool__(self) -> bool:
return self.width != 0 and self.height != 0
return bool(self.width and self.height)
@property
def area(self) -> int:
@@ -151,17 +177,6 @@ class Region(NamedTuple):
"""Get the start point of the region."""
return Point(self.x, self.y)
@property
def limit(self) -> Point:
x, y, width, height = self
return Point(x + width, y + height)
@property
def limit_inclusive(self) -> Point:
"""Get the end point of the region."""
x, y, width, height = self
return Point(x + width - 1, y + height - 1)
@property
def size(self) -> Dimensions:
"""Get the size of the region."""
@@ -251,7 +266,7 @@ class Region(NamedTuple):
x2 >= ox2 >= x1 and y2 >= oy2 >= y1
)
def translate(self, translate_x: int, translate_y: int) -> Region:
def translate(self, x: int = 0, y: int = 0) -> Region:
"""Move the origin of the Region.
Args:
@@ -262,8 +277,8 @@ class Region(NamedTuple):
Region: A new region shifted by x, y
"""
x, y, width, height = self
return Region(x + translate_x, y + translate_y, width, height)
self_x, self_y, width, height = self
return Region(self_x + x, self_y + y, width, height)
def __contains__(self, other: Any) -> bool:
"""Check if a point is in this region."""
@@ -287,16 +302,17 @@ class Region(NamedTuple):
"""
x1, y1, x2, y2 = self.corners
_clamp = clamp
new_region = Region.from_corners(
clamp(x1, 0, width),
clamp(y1, 0, height),
clamp(x2, 0, width),
clamp(y2, 0, height),
_clamp(x1, 0, width),
_clamp(y1, 0, height),
_clamp(x2, 0, width),
_clamp(y2, 0, height),
)
return new_region
def clip_region(self, region: Region) -> Region:
"""Clip this region to fit within another region.
def intersection(self, region: Region) -> Region:
"""Get that covers both regions.
Args:
region ([type]): A region that overlaps this region.
@@ -307,10 +323,11 @@ class Region(NamedTuple):
x1, y1, x2, y2 = self.corners
cx1, cy1, cx2, cy2 = region.corners
_clamp = clamp
new_region = Region.from_corners(
clamp(x1, cx1, cx2),
clamp(y1, cy1, cy2),
clamp(x2, cx2, cx2),
clamp(y2, cy2, cy2),
_clamp(x1, cx1, cx2),
_clamp(y1, cy1, cy2),
_clamp(x2, cx1, cx2),
_clamp(y2, cy1, cy2),
)
return new_region

View File

@@ -17,6 +17,7 @@ from rich.style import Style
from . import log
from ._loop import loop_last
from .layout_map import LayoutMap
from ._types import Lines
from .geometry import clamp, Region, Point, Dimensions
@@ -34,27 +35,6 @@ class NoWidget(Exception):
pass
@rich.repr.auto
class RenderRegion(NamedTuple):
region: Region
order: tuple[int, int]
offset: Point
def translate(self, offset: Point) -> RenderRegion:
region, order, self_offset = self
return RenderRegion(region, order, self_offset + offset)
def __rich_repr__(self) -> rich.repr.RichReprResult:
yield "region", self.region
yield "order", self.order
@dataclass
class WidgetMap:
virtual_size: Dimensions
widgets: dict[Widget, RenderRegion]
class OrderedRegion(NamedTuple):
region: Region
order: tuple[int, int]
@@ -92,10 +72,10 @@ class Layout(ABC):
"""Responsible for arranging Widgets in a view."""
def __init__(self) -> None:
self._layout_map: dict[Widget, RenderRegion] = {}
self._layout_map: LayoutMap | None = None
self.width = 0
self.height = 0
self.renders: dict[Widget, tuple[Region, Lines]] = {}
self.renders: dict[Widget, tuple[Region, Region, Lines]] = {}
self._cuts: list[list[int]] | None = None
self._require_update: bool = True
self.background = ""
@@ -113,31 +93,33 @@ class Layout(ABC):
self._cuts = None
if self._require_update:
self.renders.clear()
self._layout_map.clear()
self._layout_map = None
def reflow(
self, console: Console, width: int, height: int, viewport: Region
) -> ReflowResult:
self.reset()
map = self.generate_map(console, Dimensions(width, height), Point(0, 0))
map = self.generate_map(
console, Dimensions(width, height), Region(0, 0, width, height)
)
self._require_update = False
map = {
widget: OrderedRegion(region + offset, order)
for widget, (region, order, offset) in map.items()
}
# log(map.widgets)
# map = {
# widget: OrderedRegion(region + offset, order)
# for widget, (region, order, offset) in map.items()
# }
# Filter out widgets that are off screen or zero area
log("VIEWPORT", viewport)
log(map)
map = {
widget: map_region
for widget, map_region in map.items()
if map_region.region and viewport.overlaps(map_region.region)
}
old_widgets = set(self._layout_map.keys())
# map = {
# widget: map_region
# for widget, map_region in map.items()
# if map_region.region and viewport.overlaps(map_region.region)
# }
old_widgets = set() if self.map is None else set(self.map.keys())
new_widgets = set(map.keys())
# Newly visible widgets
shown_widgets = new_widgets - old_widgets
@@ -150,8 +132,8 @@ class Layout(ABC):
# Copy renders if the size hasn't changed
new_renders = {
widget: (region, self.renders[widget][1])
for widget, (region, _order) in map.items()
widget: (region, clip, self.renders[widget][2])
for widget, (region, _order, clip) in map.items()
if (
widget in self.renders
and self.renders[widget][0].size == region.size
@@ -163,7 +145,7 @@ class Layout(ABC):
# Widgets with changed size
resized_widgets = {
widget
for widget, (region, _order) in map.items()
for widget, (region, *_) in map.items()
if widget in old_widgets and widget.size != region.size
}
@@ -177,35 +159,34 @@ class Layout(ABC):
@abstractmethod
def generate_map(
self,
console: Console,
size: Dimensions,
offset: Point,
) -> dict[Widget, RenderRegion]:
self, console: Console, size: Dimensions, viewport: Region
) -> LayoutMap:
...
async def mount_all(self, view: "View") -> None:
await view.mount(*self.get_widgets())
@property
def map(self) -> dict[Widget, RenderRegion]:
def map(self) -> LayoutMap | None:
return self._layout_map
def __iter__(self) -> Iterator[tuple[Widget, Region]]:
layers = sorted(
self._layout_map.items(), key=lambda item: item[1].order, reverse=True
)
for widget, (region, _) in layers:
yield widget, region
if self.map is not None:
layers = sorted(
self.map.widgets.items(), key=lambda item: item[1].order, reverse=True
)
for widget, (region, order, clip) in layers:
yield widget, region.intersection(clip)
def __reversed__(self) -> Iterable[tuple[Widget, Region]]:
layers = sorted(self._layout_map.items(), key=lambda item: item[1].order)
for widget, (region, _) in layers:
yield widget, region
if self.map is not None:
layers = sorted(self.map.items(), key=lambda item: item[1].order)
for widget, (region, _order, clip) in layers:
yield widget, region.intersection(clip)
def get_offset(self, widget: Widget) -> Point:
try:
return self._layout_map[widget].region.origin
return self.map[widget].region.origin
except KeyError:
raise NoWidget("Widget is not in layout")
@@ -221,7 +202,9 @@ class Layout(ABC):
widget, region = self.get_widget_at(x, y)
except NoWidget:
return Style.null()
_region, lines = self.renders[widget]
if widget not in self.renders:
return Style.null()
_region, clip, lines = self.renders[widget]
x -= region.x
y -= region.y
line = lines[y]
@@ -234,7 +217,7 @@ class Layout(ABC):
def get_widget_region(self, widget: Widget) -> Region:
try:
region, _ = self._layout_map[widget]
region, *_ = self.map[widget]
except KeyError:
raise NoWidget("Widget is not in layout")
else:
@@ -256,28 +239,35 @@ class Layout(ABC):
screen_region = Region(0, 0, width, height)
cuts_sets = [{0, width} for _ in range(height)]
for region, order in self._layout_map.values():
region = region.clip(width, height)
if region and (region in screen_region): # type: ignore
for y in range(region.y, region.y + region.height):
cuts_sets[y].update({region.x, region.x + region.width})
if self.map is not None:
for region, order, clip in self.map.values():
region = region.intersection(clip)
if region and (region in screen_region): # type: ignore
for y in range(region.y, region.y + region.height):
cuts_sets[y].update({region.x, region.x + region.width})
# Sort the cuts for each line
self._cuts = [sorted(cut_set) for cut_set in cuts_sets]
return self._cuts
def _get_renders(self, console: Console) -> Iterable[tuple[Region, Lines]]:
def _get_renders(self, console: Console) -> Iterable[tuple[Region, Region, Lines]]:
_rich_traceback_guard = True
width = self.width
height = self.height
screen_region = Region(0, 0, width, height)
layout_map = self._layout_map
layout_map = self.map
widget_regions = sorted(
((widget, region, order) for widget, (region, order) in layout_map.items()),
key=itemgetter(2),
reverse=True,
)
if layout_map:
widget_regions = sorted(
(
(widget, region, order, clip)
for widget, (region, order, clip) in layout_map.items()
),
key=itemgetter(2),
reverse=True,
)
else:
widget_regions = []
def render(widget: Widget, width: int, height: int) -> Lines:
lines = console.render_lines(
@@ -285,7 +275,7 @@ class Layout(ABC):
)
return lines
for widget, region, _order in widget_regions:
for widget, region, _order, clip in widget_regions:
if not widget.is_visual:
continue
@@ -295,23 +285,22 @@ class Layout(ABC):
continue
lines = render(widget, region.width, region.height)
if region in screen_region:
self.renders[widget] = (region, lines)
yield region, lines
elif screen_region.overlaps(region):
new_region = region.clip(width, height)
if region in clip:
self.renders[widget] = (region, clip, lines)
yield region, clip, lines
elif clip.overlaps(region):
new_region = region.intersection(clip)
delta_x = new_region.x - region.x
delta_y = new_region.y - region.y
region = new_region
self.renders[widget] = (region, clip, lines)
splits = [delta_x, delta_x + new_region.width]
splits = [delta_x, delta_x + region.width]
divide = Segment.divide
lines = [
list(divide(line, splits))[1]
for line in lines[delta_y : delta_y + region.height]
for line in lines[delta_y : delta_y + new_region.height]
]
self.renders[widget] = (region, lines)
yield region, lines
yield region, clip, lines
@classmethod
def _assemble_chops(
@@ -361,7 +350,10 @@ class Layout(ABC):
]
# Go through all the renders in reverse order and fill buckets with no render
renders = self._get_renders(console)
for region, lines in chain(renders, [(screen, background_render)]):
for region, clip, lines in chain(
renders, [(screen, screen, background_render)]
):
# region = region.intersection(clip)
for y, line in enumerate(lines, region.y):
if clip_y > y > clip_y2:
continue
@@ -391,12 +383,12 @@ class Layout(ABC):
if widget not in self.renders:
return None
region, lines = self.renders[widget]
region, clip, lines = self.renders[widget]
new_lines = console.render_lines(
widget, console.options.update_dimensions(region.width, region.height)
)
self.renders[widget] = (region, new_lines)
self.renders[widget] = (region, clip, new_lines)
update_lines = self.render(console, region).lines
return LayoutUpdate(update_lines, region.x, region.y)

61
src/textual/layout_map.py Normal file
View File

@@ -0,0 +1,61 @@
from __future__ import annotations
from rich.console import Console
from typing import ItemsView, KeysView, ValuesView, NamedTuple
from .geometry import Region, Dimensions
from .widget import Widget
class Order(NamedTuple):
layer: int
z: int
class RenderRegion(NamedTuple):
region: Region
order: tuple[int, ...]
clip: Region
class LayoutMap:
def __init__(self, size: Dimensions) -> None:
self.size = size
self.widgets: dict[Widget, RenderRegion] = {}
def __getitem__(self, widget: Widget) -> RenderRegion:
return self.widgets[widget]
def items(self) -> ItemsView:
return self.widgets.items()
def keys(self) -> KeysView:
return self.widgets.keys()
def values(self) -> ValuesView:
return self.widgets.values()
def clear(self) -> None:
self.widgets.clear()
def add_widget(
self,
console: Console,
widget: Widget,
region: Region,
order: tuple[int, ...],
clip: Region,
) -> None:
from .view import View
region += widget.layout_offset
self.widgets[widget] = RenderRegion(region, order, clip)
if isinstance(widget, View):
sub_map = widget.layout.generate_map(console, region.size, region)
for widget, (sub_region, sub_order, sub_clip) in sub_map.items():
sub_region += region.origin
sub_clip = sub_clip.intersection(clip)
self.add_widget(console, widget, sub_region, sub_order, sub_clip)

View File

@@ -9,7 +9,8 @@ from rich.console import Console
from .._layout_resolve import layout_resolve
from ..geometry import Region, Point, Dimensions
from ..layout import Layout, RenderRegion
from ..layout import Layout
from ..layout_map import LayoutMap, Order
if sys.version_info >= (3, 8):
from typing import Literal
@@ -48,25 +49,16 @@ class DockLayout(Layout):
yield from dock.widgets
def generate_map(
self, console: Console, size: Dimensions, offset: Point
) -> dict[Widget, RenderRegion]:
from ..view import View
self, console: Console, size: Dimensions, viewport: Region
) -> LayoutMap:
map: dict[Widget, RenderRegion] = {}
map: LayoutMap = LayoutMap(size)
width, height = size
layout_region = Region(0, 0, width, height)
layers: dict[int, Region] = defaultdict(lambda: layout_region)
def add_widget(widget: Widget, region: Region, order: tuple[int, int]):
region = region + widget.layout_offset
map[widget] = RenderRegion(region, order, offset)
if isinstance(widget, View):
sub_map = widget.layout.generate_map(
console,
Dimensions(region.width, region.height),
region.origin + offset,
)
map.update(sub_map)
def add_widget(widget: Widget, region: Region, order: tuple[int, ...]):
map.add_widget(console, widget, region, order, viewport)
for index, dock in enumerate(self.docks):
dock_options = [
@@ -88,16 +80,16 @@ class DockLayout(Layout):
render_y = y
remaining = region.height
total = 0
for widget, size in zip(dock.widgets, sizes):
for widget, layout_size in zip(dock.widgets, sizes):
if not widget.visible:
continue
size = min(remaining, size)
if not size:
layout_size = min(remaining, layout_size)
if not layout_size:
break
total += size
add_widget(widget, Region(x, render_y, width, size), order)
render_y += size
remaining = max(0, remaining - size)
total += layout_size
add_widget(widget, Region(x, render_y, width, layout_size), order)
render_y += layout_size
remaining = max(0, remaining - layout_size)
region = Region(x, y + total, width, height - total)
elif dock.edge == "bottom":
@@ -105,16 +97,20 @@ class DockLayout(Layout):
render_y = y + height
remaining = region.height
total = 0
for widget, size in zip(dock.widgets, sizes):
for widget, layout_size in zip(dock.widgets, sizes):
if not widget.visible:
continue
size = min(remaining, size)
if not size:
layout_size = min(remaining, layout_size)
if not layout_size:
break
total += size
add_widget(widget, Region(x, render_y - size, width, size), order)
render_y -= size
remaining = max(0, remaining - size)
total += layout_size
add_widget(
widget,
Region(x, render_y - layout_size, width, layout_size),
order,
)
render_y -= layout_size
remaining = max(0, remaining - layout_size)
region = Region(x, y, width, height - total)
elif dock.edge == "left":
@@ -122,16 +118,16 @@ class DockLayout(Layout):
render_x = x
remaining = region.width
total = 0
for widget, size in zip(dock.widgets, sizes):
for widget, layout_size in zip(dock.widgets, sizes):
if not widget.visible:
continue
size = min(remaining, size)
if not size:
layout_size = min(remaining, layout_size)
if not layout_size:
break
total += size
add_widget(widget, Region(render_x, y, size, height), order)
render_x += size
remaining = max(0, remaining - size)
total += layout_size
add_widget(widget, Region(render_x, y, layout_size, height), order)
render_x += layout_size
remaining = max(0, remaining - layout_size)
region = Region(x + total, y, width - total, height)
elif dock.edge == "right":
@@ -139,16 +135,20 @@ class DockLayout(Layout):
render_x = x + width
remaining = region.width
total = 0
for widget, size in zip(dock.widgets, sizes):
for widget, layout_size in zip(dock.widgets, sizes):
if not widget.visible:
continue
size = min(remaining, size)
if not size:
layout_size = min(remaining, layout_size)
if not layout_size:
break
total += size
add_widget(widget, Region(render_x - size, y, size, height), order)
render_x -= size
remaining = max(0, remaining - size)
total += layout_size
add_widget(
widget,
Region(render_x - layout_size, y, layout_size, height),
order,
)
render_x -= layout_size
remaining = max(0, remaining - layout_size)
region = Region(x, y, width - total, height)
layers[dock.z] = region

View File

@@ -12,8 +12,8 @@ from rich.console import Console
from .._layout_resolve import layout_resolve
from ..geometry import Dimensions, Point, Region
from ..layout import Layout, RenderRegion
from ..view import View
from ..layout import Layout
from ..layout_map import LayoutMap
from ..widget import Widget
if sys.version_info >= (3, 8):
@@ -264,8 +264,8 @@ class GridLayout(Layout):
return self.widgets.keys()
def generate_map(
self, console: Console, size: Dimensions, offset: Point
) -> dict[Widget, RenderRegion]:
self, console: Console, size: Dimensions, viewport: Region
) -> LayoutMap:
"""Generate a map that associates widgets with their location on screen.
Args:
@@ -276,6 +276,7 @@ class GridLayout(Layout):
Returns:
dict[Widget, OrderedRegion]: [description]
"""
map: LayoutMap = LayoutMap(size)
width, height = size
def resolve(
@@ -327,13 +328,14 @@ class GridLayout(Layout):
return names, tracks, len(spans), max_size
def add_widget(widget: Widget, region: Region, order: tuple[int, int]):
region = region + widget.layout_offset
map[widget] = RenderRegion(region, order, offset)
if isinstance(widget, View):
sub_map = widget.layout.generate_map(
region.width, region.height, region.origin + offset
)
map.update(sub_map)
map.add_widget(console, widget, region, order, viewport)
# region = region + widget.layout_offset
# map[widget] = RenderRegion(region, order, offset)
# if isinstance(widget, View):
# sub_map = widget.layout.generate_map(
# region.width, region.height, region.origin + offset
# )
# map.update(sub_map)
container = Dimensions(
width - self.column_gutter * 2, height - self.row_gutter * 2
@@ -365,8 +367,6 @@ class GridLayout(Layout):
free_slots = {
(col, row) for col, row in product(range(column_count), range(row_count))
}
map: dict[Widget, RenderRegion] = {}
order = 1
from_corners = Region.from_corners
gutter = Point(self.column_gutter, self.row_gutter)

View File

@@ -4,7 +4,7 @@ from rich.console import Console
from ..geometry import Point, Region, Dimensions
from ..layout import Layout, RenderRegion, WidgetMap
from ..layout import Layout
from ..widget import Widget
from ..view import View
@@ -19,8 +19,9 @@ class VerticalLayout(Layout):
self._widgets.append(widget)
def generate_map(
self, console: Console, size: Dimensions, offset: Point
self, console: Console, size: Dimensions, viewport: Region
) -> WidgetMap:
offset = viewport.origin
width, height = size
gutter_width, gutter_height = self.gutter
render_width = width - gutter_width * 2

View File

@@ -62,6 +62,12 @@ class View(Widget):
(virtual_height if virtual_height is not None else self.size.height),
)
@virtual_size.setter
def virtual_size(self, size: tuple[int, int]) -> None:
width, height = size
self.virtual_width = width
self.virtual_height = height
@property
def offset(self) -> Point:
return Point(self.offset_x, self.offset_y)
@@ -133,7 +139,7 @@ class View(Widget):
async def refresh_layout(self) -> None:
await self.layout.mount_all(self)
if not self.size or not self.is_root_view:
if not self.size:
return
width, height = self.console.size

View File

@@ -61,8 +61,8 @@ class Widget(MessagePump):
layout_size: Reactive[int | None] = Reactive(None, layout=True)
layout_fraction: Reactive[int] = Reactive(1, layout=True)
layout_min_size: Reactive[int] = Reactive(1, layout=True)
layout_offset_x: Reactive[int] = Reactive(0, layout=True)
layout_offset_y: Reactive[int] = Reactive(0, layout=True)
layout_offset_x: Reactive[float] = Reactive(0.0, layout=True)
layout_offset_y: Reactive[float] = Reactive(0.0, layout=True)
def validate_layout_offset_x(self, value) -> int:
return int(value)

View File

@@ -3,6 +3,57 @@ import pytest
from textual.geometry import clamp, Point, Dimensions, Region
def test_dimensions_region():
assert Dimensions(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)
def test_dimensions_contains_point():
assert Dimensions(10, 10).contains_point(Point(5, 5))
assert Dimensions(10, 10).contains_point(Point(9, 9))
assert Dimensions(10, 10).contains_point(Point(0, 0))
assert not Dimensions(10, 10).contains_point(Point(10, 9))
assert not Dimensions(10, 10).contains_point(Point(9, 10))
assert not Dimensions(10, 10).contains_point(Point(-1, 0))
assert not Dimensions(10, 10).contains_point(Point(0, -1))
def test_dimensions_contains_special():
with pytest.raises(TypeError):
(1, 2, 3) in Dimensions(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)
def test_dimensions_bool():
assert Dimensions(1, 1)
assert Dimensions(3, 4)
assert not Dimensions(0, 1)
assert not Dimensions(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
def test_clamp():
assert clamp(5, 0, 10) == 5
assert clamp(-1, 0, 10) == 0
@@ -34,3 +85,94 @@ def test_point_blend():
assert Point(1, 2).blend(Point(3, 4), 0) == Point(1, 2)
assert Point(1, 2).blend(Point(3, 4), 1) == Point(3, 4)
assert Point(1, 2).blend(Point(3, 4), 0.5) == Point(2, 3)
def test_region_from_origin():
assert Region.from_origin(Point(3, 4), (5, 6)) == Region(3, 4, 5, 6)
def test_region_area():
assert Region(3, 4, 0, 0).area == 0
assert Region(3, 4, 5, 6).area == 30
def test_region_size():
assert isinstance(Region(3, 4, 5, 6).size, Dimensions)
assert Region(3, 4, 5, 6).size == Dimensions(5, 6)
def test_region_origin():
assert Region(1, 2, 3, 4).origin == Point(1, 2)
def test_region_add():
assert Region(1, 2, 3, 4) + (10, 20) == Region(11, 22, 3, 4)
with pytest.raises(TypeError):
Region(1, 2, 3, 4) + "foo"
def test_region_sub():
assert Region(11, 22, 3, 4) - (10, 20) == Region(1, 2, 3, 4)
with pytest.raises(TypeError):
Region(1, 2, 3, 4) - "foo"
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))
assert not Region(10, 10, 5, 5).overlaps(Region(0, 0, 50, 10))
assert Region(10, 10, 5, 5).overlaps(Region(0, 0, 50, 11))
assert not Region(10, 10, 5, 5).overlaps(Region(0, 15, 50, 10))
assert Region(10, 10, 5, 5).overlaps(Region(0, 14, 50, 10))
def test_region_contains():
assert Region(10, 10, 20, 30).contains(10, 10)
assert Region(10, 10, 20, 30).contains(29, 39)
assert not Region(10, 10, 20, 30).contains(30, 40)
def test_region_contains_point():
assert Region(10, 10, 20, 30).contains_point((10, 10))
assert Region(10, 10, 20, 30).contains_point((29, 39))
assert not Region(10, 10, 20, 30).contains_point((30, 40))
with pytest.raises(TypeError):
Region(10, 10, 20, 30).contains_point((1, 2, 3))
def test_region_contains_region():
assert Region(10, 10, 20, 30).contains_region(Region(10, 10, 5, 5))
assert not Region(10, 10, 20, 30).contains_region(Region(10, 9, 5, 5))
assert not Region(10, 10, 20, 30).contains_region(Region(9, 10, 5, 5))
assert Region(10, 10, 20, 30).contains_region(Region(10, 10, 20, 30))
assert not Region(10, 10, 20, 30).contains_region(Region(10, 10, 21, 30))
assert not Region(10, 10, 20, 30).contains_region(Region(10, 10, 20, 31))
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)
def test_region_contains_special():
assert (10, 10) in Region(10, 10, 20, 30)
assert (9, 10) not in Region(10, 10, 20, 30)
assert Region(10, 10, 5, 5) in Region(10, 10, 20, 30)
assert Region(5, 5, 5, 5) not in Region(10, 10, 20, 30)
assert "foo" not in Region(0, 0, 10, 10)
def test_clip():
assert Region(10, 10, 20, 30).clip(20, 25) == Region(10, 10, 10, 15)
def test_region_intersection():
assert Region(0, 0, 100, 50).intersection(Region(10, 10, 10, 10)) == Region(
10, 10, 10, 10
)
assert Region(10, 10, 30, 20).intersection(Region(20, 15, 60, 40)) == Region(
20, 15, 20, 15
)
assert not Region(10, 10, 20, 30).intersection(Region(50, 50, 100, 200))