mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
layout_map refactor
This commit is contained in:
@@ -26,11 +26,12 @@ class SmoothApp(App):
|
|||||||
"""Build layout here."""
|
"""Build layout here."""
|
||||||
footer = Footer()
|
footer = Footer()
|
||||||
self.bar = Placeholder(name="left")
|
self.bar = Placeholder(name="left")
|
||||||
self.bar.layout_offset_x = -40
|
|
||||||
|
|
||||||
await self.view.dock(footer, edge="bottom")
|
await self.view.dock(footer, edge="bottom")
|
||||||
await self.view.dock(Placeholder(), Placeholder(), edge="top")
|
await self.view.dock(Placeholder(), Placeholder(), edge="top")
|
||||||
await self.view.dock(self.bar, edge="left", size=40, z=1)
|
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")
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ class MyApp(App):
|
|||||||
)
|
)
|
||||||
self.app.sub_title = os.path.basename(message.path)
|
self.app.sub_title = os.path.basename(message.path)
|
||||||
await self.body.update(syntax)
|
await self.body.update(syntax)
|
||||||
|
# self.body.layout_offset_y = -5
|
||||||
self.body.home()
|
self.body.home()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ class GridTest(App):
|
|||||||
|
|
||||||
async def on_mount(self, event: events.Mount) -> None:
|
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(fraction=1, name="left", min_size=20)
|
||||||
grid.add_column(size=30, name="center")
|
grid.add_column(size=30, name="center")
|
||||||
@@ -26,11 +27,17 @@ class GridTest(App):
|
|||||||
area4="right,top-start|middle-end",
|
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(
|
grid.place(
|
||||||
area1=Placeholder(name="area1"),
|
area1=make_placeholder(name="area1"),
|
||||||
area2=Placeholder(name="area2"),
|
area2=make_placeholder(name="area2"),
|
||||||
area3=Placeholder(name="area3"),
|
area3=make_placeholder(name="area3"),
|
||||||
area4=Placeholder(name="area4"),
|
area4=make_placeholder(name="area4"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ class App(MessagePump):
|
|||||||
driver_class (Type[Driver], optional): Driver class, or None to use default. Defaults to None.
|
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".
|
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.error_console = Console(stderr=True)
|
||||||
self._screen = screen
|
self._screen = screen
|
||||||
self.driver_class = driver_class or LinuxDriver
|
self.driver_class = driver_class or LinuxDriver
|
||||||
|
|||||||
@@ -76,8 +76,19 @@ class Dimensions(NamedTuple):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def area(self) -> int:
|
def area(self) -> int:
|
||||||
|
"""Get the area of the dimensions.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: Area in cells.
|
||||||
|
"""
|
||||||
return self.width * self.height
|
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:
|
def contains(self, x: int, y: int) -> bool:
|
||||||
"""Check if a point is in the region.
|
"""Check if a point is in the region.
|
||||||
|
|
||||||
@@ -138,8 +149,23 @@ class Region(NamedTuple):
|
|||||||
"""
|
"""
|
||||||
return cls(x1, y1, x2 - x1, y2 - y1)
|
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:
|
def __bool__(self) -> bool:
|
||||||
return self.width != 0 and self.height != 0
|
return bool(self.width and self.height)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def area(self) -> int:
|
def area(self) -> int:
|
||||||
@@ -151,17 +177,6 @@ class Region(NamedTuple):
|
|||||||
"""Get the start point of the region."""
|
"""Get the start point of the region."""
|
||||||
return Point(self.x, self.y)
|
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
|
@property
|
||||||
def size(self) -> Dimensions:
|
def size(self) -> Dimensions:
|
||||||
"""Get the size of the region."""
|
"""Get the size of the region."""
|
||||||
@@ -251,7 +266,7 @@ class Region(NamedTuple):
|
|||||||
x2 >= ox2 >= x1 and y2 >= oy2 >= y1
|
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.
|
"""Move the origin of the Region.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -262,8 +277,8 @@ class Region(NamedTuple):
|
|||||||
Region: A new region shifted by x, y
|
Region: A new region shifted by x, y
|
||||||
"""
|
"""
|
||||||
|
|
||||||
x, y, width, height = self
|
self_x, self_y, width, height = self
|
||||||
return Region(x + translate_x, y + translate_y, width, height)
|
return Region(self_x + x, self_y + y, width, height)
|
||||||
|
|
||||||
def __contains__(self, other: Any) -> bool:
|
def __contains__(self, other: Any) -> bool:
|
||||||
"""Check if a point is in this region."""
|
"""Check if a point is in this region."""
|
||||||
@@ -287,16 +302,17 @@ class Region(NamedTuple):
|
|||||||
"""
|
"""
|
||||||
x1, y1, x2, y2 = self.corners
|
x1, y1, x2, y2 = self.corners
|
||||||
|
|
||||||
|
_clamp = clamp
|
||||||
new_region = Region.from_corners(
|
new_region = Region.from_corners(
|
||||||
clamp(x1, 0, width),
|
_clamp(x1, 0, width),
|
||||||
clamp(y1, 0, height),
|
_clamp(y1, 0, height),
|
||||||
clamp(x2, 0, width),
|
_clamp(x2, 0, width),
|
||||||
clamp(y2, 0, height),
|
_clamp(y2, 0, height),
|
||||||
)
|
)
|
||||||
return new_region
|
return new_region
|
||||||
|
|
||||||
def clip_region(self, region: Region) -> Region:
|
def intersection(self, region: Region) -> Region:
|
||||||
"""Clip this region to fit within another region.
|
"""Get that covers both regions.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
region ([type]): A region that overlaps this region.
|
region ([type]): A region that overlaps this region.
|
||||||
@@ -307,10 +323,11 @@ class Region(NamedTuple):
|
|||||||
x1, y1, x2, y2 = self.corners
|
x1, y1, x2, y2 = self.corners
|
||||||
cx1, cy1, cx2, cy2 = region.corners
|
cx1, cy1, cx2, cy2 = region.corners
|
||||||
|
|
||||||
|
_clamp = clamp
|
||||||
new_region = Region.from_corners(
|
new_region = Region.from_corners(
|
||||||
clamp(x1, cx1, cx2),
|
_clamp(x1, cx1, cx2),
|
||||||
clamp(y1, cy1, cy2),
|
_clamp(y1, cy1, cy2),
|
||||||
clamp(x2, cx2, cx2),
|
_clamp(x2, cx1, cx2),
|
||||||
clamp(y2, cy2, cy2),
|
_clamp(y2, cy1, cy2),
|
||||||
)
|
)
|
||||||
return new_region
|
return new_region
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from rich.style import Style
|
|||||||
|
|
||||||
from . import log
|
from . import log
|
||||||
from ._loop import loop_last
|
from ._loop import loop_last
|
||||||
|
from .layout_map import LayoutMap
|
||||||
from ._types import Lines
|
from ._types import Lines
|
||||||
|
|
||||||
from .geometry import clamp, Region, Point, Dimensions
|
from .geometry import clamp, Region, Point, Dimensions
|
||||||
@@ -34,27 +35,6 @@ class NoWidget(Exception):
|
|||||||
pass
|
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):
|
class OrderedRegion(NamedTuple):
|
||||||
region: Region
|
region: Region
|
||||||
order: tuple[int, int]
|
order: tuple[int, int]
|
||||||
@@ -92,10 +72,10 @@ class Layout(ABC):
|
|||||||
"""Responsible for arranging Widgets in a view."""
|
"""Responsible for arranging Widgets in a view."""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._layout_map: dict[Widget, RenderRegion] = {}
|
self._layout_map: LayoutMap | None = None
|
||||||
self.width = 0
|
self.width = 0
|
||||||
self.height = 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._cuts: list[list[int]] | None = None
|
||||||
self._require_update: bool = True
|
self._require_update: bool = True
|
||||||
self.background = ""
|
self.background = ""
|
||||||
@@ -113,31 +93,33 @@ class Layout(ABC):
|
|||||||
self._cuts = None
|
self._cuts = None
|
||||||
if self._require_update:
|
if self._require_update:
|
||||||
self.renders.clear()
|
self.renders.clear()
|
||||||
self._layout_map.clear()
|
self._layout_map = None
|
||||||
|
|
||||||
def reflow(
|
def reflow(
|
||||||
self, console: Console, width: int, height: int, viewport: Region
|
self, console: Console, width: int, height: int, viewport: Region
|
||||||
) -> ReflowResult:
|
) -> ReflowResult:
|
||||||
self.reset()
|
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
|
self._require_update = False
|
||||||
|
|
||||||
map = {
|
# log(map.widgets)
|
||||||
widget: OrderedRegion(region + offset, order)
|
# map = {
|
||||||
for widget, (region, order, offset) in map.items()
|
# widget: OrderedRegion(region + offset, order)
|
||||||
}
|
# for widget, (region, order, offset) in map.items()
|
||||||
|
# }
|
||||||
|
|
||||||
# Filter out widgets that are off screen or zero area
|
# 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())
|
new_widgets = set(map.keys())
|
||||||
# Newly visible widgets
|
# Newly visible widgets
|
||||||
shown_widgets = new_widgets - old_widgets
|
shown_widgets = new_widgets - old_widgets
|
||||||
@@ -150,8 +132,8 @@ class Layout(ABC):
|
|||||||
|
|
||||||
# Copy renders if the size hasn't changed
|
# Copy renders if the size hasn't changed
|
||||||
new_renders = {
|
new_renders = {
|
||||||
widget: (region, self.renders[widget][1])
|
widget: (region, clip, self.renders[widget][2])
|
||||||
for widget, (region, _order) in map.items()
|
for widget, (region, _order, clip) in map.items()
|
||||||
if (
|
if (
|
||||||
widget in self.renders
|
widget in self.renders
|
||||||
and self.renders[widget][0].size == region.size
|
and self.renders[widget][0].size == region.size
|
||||||
@@ -163,7 +145,7 @@ class Layout(ABC):
|
|||||||
# Widgets with changed size
|
# Widgets with changed size
|
||||||
resized_widgets = {
|
resized_widgets = {
|
||||||
widget
|
widget
|
||||||
for widget, (region, _order) in map.items()
|
for widget, (region, *_) in map.items()
|
||||||
if widget in old_widgets and widget.size != region.size
|
if widget in old_widgets and widget.size != region.size
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,35 +159,34 @@ class Layout(ABC):
|
|||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def generate_map(
|
def generate_map(
|
||||||
self,
|
self, console: Console, size: Dimensions, viewport: Region
|
||||||
console: Console,
|
) -> LayoutMap:
|
||||||
size: Dimensions,
|
|
||||||
offset: Point,
|
|
||||||
) -> dict[Widget, RenderRegion]:
|
|
||||||
...
|
...
|
||||||
|
|
||||||
async def mount_all(self, view: "View") -> None:
|
async def mount_all(self, view: "View") -> None:
|
||||||
await view.mount(*self.get_widgets())
|
await view.mount(*self.get_widgets())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def map(self) -> dict[Widget, RenderRegion]:
|
def map(self) -> LayoutMap | None:
|
||||||
return self._layout_map
|
return self._layout_map
|
||||||
|
|
||||||
def __iter__(self) -> Iterator[tuple[Widget, Region]]:
|
def __iter__(self) -> Iterator[tuple[Widget, Region]]:
|
||||||
layers = sorted(
|
if self.map is not None:
|
||||||
self._layout_map.items(), key=lambda item: item[1].order, reverse=True
|
layers = sorted(
|
||||||
)
|
self.map.widgets.items(), key=lambda item: item[1].order, reverse=True
|
||||||
for widget, (region, _) in layers:
|
)
|
||||||
yield widget, region
|
for widget, (region, order, clip) in layers:
|
||||||
|
yield widget, region.intersection(clip)
|
||||||
|
|
||||||
def __reversed__(self) -> Iterable[tuple[Widget, Region]]:
|
def __reversed__(self) -> Iterable[tuple[Widget, Region]]:
|
||||||
layers = sorted(self._layout_map.items(), key=lambda item: item[1].order)
|
if self.map is not None:
|
||||||
for widget, (region, _) in layers:
|
layers = sorted(self.map.items(), key=lambda item: item[1].order)
|
||||||
yield widget, region
|
for widget, (region, _order, clip) in layers:
|
||||||
|
yield widget, region.intersection(clip)
|
||||||
|
|
||||||
def get_offset(self, widget: Widget) -> Point:
|
def get_offset(self, widget: Widget) -> Point:
|
||||||
try:
|
try:
|
||||||
return self._layout_map[widget].region.origin
|
return self.map[widget].region.origin
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise NoWidget("Widget is not in layout")
|
raise NoWidget("Widget is not in layout")
|
||||||
|
|
||||||
@@ -221,7 +202,9 @@ class Layout(ABC):
|
|||||||
widget, region = self.get_widget_at(x, y)
|
widget, region = self.get_widget_at(x, y)
|
||||||
except NoWidget:
|
except NoWidget:
|
||||||
return Style.null()
|
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
|
x -= region.x
|
||||||
y -= region.y
|
y -= region.y
|
||||||
line = lines[y]
|
line = lines[y]
|
||||||
@@ -234,7 +217,7 @@ class Layout(ABC):
|
|||||||
|
|
||||||
def get_widget_region(self, widget: Widget) -> Region:
|
def get_widget_region(self, widget: Widget) -> Region:
|
||||||
try:
|
try:
|
||||||
region, _ = self._layout_map[widget]
|
region, *_ = self.map[widget]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise NoWidget("Widget is not in layout")
|
raise NoWidget("Widget is not in layout")
|
||||||
else:
|
else:
|
||||||
@@ -256,28 +239,35 @@ class Layout(ABC):
|
|||||||
screen_region = Region(0, 0, width, height)
|
screen_region = Region(0, 0, width, height)
|
||||||
cuts_sets = [{0, width} for _ in range(height)]
|
cuts_sets = [{0, width} for _ in range(height)]
|
||||||
|
|
||||||
for region, order in self._layout_map.values():
|
if self.map is not None:
|
||||||
region = region.clip(width, height)
|
for region, order, clip in self.map.values():
|
||||||
if region and (region in screen_region): # type: ignore
|
region = region.intersection(clip)
|
||||||
for y in range(region.y, region.y + region.height):
|
if region and (region in screen_region): # type: ignore
|
||||||
cuts_sets[y].update({region.x, region.x + region.width})
|
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
|
# Sort the cuts for each line
|
||||||
self._cuts = [sorted(cut_set) for cut_set in cuts_sets]
|
self._cuts = [sorted(cut_set) for cut_set in cuts_sets]
|
||||||
return self._cuts
|
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
|
_rich_traceback_guard = True
|
||||||
width = self.width
|
width = self.width
|
||||||
height = self.height
|
height = self.height
|
||||||
screen_region = Region(0, 0, width, height)
|
screen_region = Region(0, 0, width, height)
|
||||||
layout_map = self._layout_map
|
layout_map = self.map
|
||||||
|
|
||||||
widget_regions = sorted(
|
if layout_map:
|
||||||
((widget, region, order) for widget, (region, order) in layout_map.items()),
|
widget_regions = sorted(
|
||||||
key=itemgetter(2),
|
(
|
||||||
reverse=True,
|
(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:
|
def render(widget: Widget, width: int, height: int) -> Lines:
|
||||||
lines = console.render_lines(
|
lines = console.render_lines(
|
||||||
@@ -285,7 +275,7 @@ class Layout(ABC):
|
|||||||
)
|
)
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
for widget, region, _order in widget_regions:
|
for widget, region, _order, clip in widget_regions:
|
||||||
|
|
||||||
if not widget.is_visual:
|
if not widget.is_visual:
|
||||||
continue
|
continue
|
||||||
@@ -295,23 +285,22 @@ class Layout(ABC):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
lines = render(widget, region.width, region.height)
|
lines = render(widget, region.width, region.height)
|
||||||
if region in screen_region:
|
if region in clip:
|
||||||
self.renders[widget] = (region, lines)
|
self.renders[widget] = (region, clip, lines)
|
||||||
yield region, lines
|
yield region, clip, lines
|
||||||
elif screen_region.overlaps(region):
|
elif clip.overlaps(region):
|
||||||
new_region = region.clip(width, height)
|
new_region = region.intersection(clip)
|
||||||
delta_x = new_region.x - region.x
|
delta_x = new_region.x - region.x
|
||||||
delta_y = new_region.y - region.y
|
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
|
divide = Segment.divide
|
||||||
lines = [
|
lines = [
|
||||||
list(divide(line, splits))[1]
|
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, clip, lines
|
||||||
yield region, lines
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _assemble_chops(
|
def _assemble_chops(
|
||||||
@@ -361,7 +350,10 @@ class Layout(ABC):
|
|||||||
]
|
]
|
||||||
# Go through all the renders in reverse order and fill buckets with no render
|
# Go through all the renders in reverse order and fill buckets with no render
|
||||||
renders = self._get_renders(console)
|
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):
|
for y, line in enumerate(lines, region.y):
|
||||||
if clip_y > y > clip_y2:
|
if clip_y > y > clip_y2:
|
||||||
continue
|
continue
|
||||||
@@ -391,12 +383,12 @@ class Layout(ABC):
|
|||||||
if widget not in self.renders:
|
if widget not in self.renders:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
region, lines = self.renders[widget]
|
region, clip, lines = self.renders[widget]
|
||||||
new_lines = console.render_lines(
|
new_lines = console.render_lines(
|
||||||
widget, console.options.update_dimensions(region.width, region.height)
|
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
|
update_lines = self.render(console, region).lines
|
||||||
return LayoutUpdate(update_lines, region.x, region.y)
|
return LayoutUpdate(update_lines, region.x, region.y)
|
||||||
|
|||||||
61
src/textual/layout_map.py
Normal file
61
src/textual/layout_map.py
Normal 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)
|
||||||
@@ -9,7 +9,8 @@ from rich.console import Console
|
|||||||
|
|
||||||
from .._layout_resolve import layout_resolve
|
from .._layout_resolve import layout_resolve
|
||||||
from ..geometry import Region, Point, Dimensions
|
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):
|
if sys.version_info >= (3, 8):
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
@@ -48,25 +49,16 @@ class DockLayout(Layout):
|
|||||||
yield from dock.widgets
|
yield from dock.widgets
|
||||||
|
|
||||||
def generate_map(
|
def generate_map(
|
||||||
self, console: Console, size: Dimensions, offset: Point
|
self, console: Console, size: Dimensions, viewport: Region
|
||||||
) -> dict[Widget, RenderRegion]:
|
) -> LayoutMap:
|
||||||
from ..view import View
|
|
||||||
|
|
||||||
map: dict[Widget, RenderRegion] = {}
|
map: LayoutMap = LayoutMap(size)
|
||||||
width, height = size
|
width, height = size
|
||||||
layout_region = Region(0, 0, width, height)
|
layout_region = Region(0, 0, width, height)
|
||||||
layers: dict[int, Region] = defaultdict(lambda: layout_region)
|
layers: dict[int, Region] = defaultdict(lambda: layout_region)
|
||||||
|
|
||||||
def add_widget(widget: Widget, region: Region, order: tuple[int, int]):
|
def add_widget(widget: Widget, region: Region, order: tuple[int, ...]):
|
||||||
region = region + widget.layout_offset
|
map.add_widget(console, widget, region, order, viewport)
|
||||||
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)
|
|
||||||
|
|
||||||
for index, dock in enumerate(self.docks):
|
for index, dock in enumerate(self.docks):
|
||||||
dock_options = [
|
dock_options = [
|
||||||
@@ -88,16 +80,16 @@ class DockLayout(Layout):
|
|||||||
render_y = y
|
render_y = y
|
||||||
remaining = region.height
|
remaining = region.height
|
||||||
total = 0
|
total = 0
|
||||||
for widget, size in zip(dock.widgets, sizes):
|
for widget, layout_size in zip(dock.widgets, sizes):
|
||||||
if not widget.visible:
|
if not widget.visible:
|
||||||
continue
|
continue
|
||||||
size = min(remaining, size)
|
layout_size = min(remaining, layout_size)
|
||||||
if not size:
|
if not layout_size:
|
||||||
break
|
break
|
||||||
total += size
|
total += layout_size
|
||||||
add_widget(widget, Region(x, render_y, width, size), order)
|
add_widget(widget, Region(x, render_y, width, layout_size), order)
|
||||||
render_y += size
|
render_y += layout_size
|
||||||
remaining = max(0, remaining - size)
|
remaining = max(0, remaining - layout_size)
|
||||||
region = Region(x, y + total, width, height - total)
|
region = Region(x, y + total, width, height - total)
|
||||||
|
|
||||||
elif dock.edge == "bottom":
|
elif dock.edge == "bottom":
|
||||||
@@ -105,16 +97,20 @@ class DockLayout(Layout):
|
|||||||
render_y = y + height
|
render_y = y + height
|
||||||
remaining = region.height
|
remaining = region.height
|
||||||
total = 0
|
total = 0
|
||||||
for widget, size in zip(dock.widgets, sizes):
|
for widget, layout_size in zip(dock.widgets, sizes):
|
||||||
if not widget.visible:
|
if not widget.visible:
|
||||||
continue
|
continue
|
||||||
size = min(remaining, size)
|
layout_size = min(remaining, layout_size)
|
||||||
if not size:
|
if not layout_size:
|
||||||
break
|
break
|
||||||
total += size
|
total += layout_size
|
||||||
add_widget(widget, Region(x, render_y - size, width, size), order)
|
add_widget(
|
||||||
render_y -= size
|
widget,
|
||||||
remaining = max(0, remaining - size)
|
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)
|
region = Region(x, y, width, height - total)
|
||||||
|
|
||||||
elif dock.edge == "left":
|
elif dock.edge == "left":
|
||||||
@@ -122,16 +118,16 @@ class DockLayout(Layout):
|
|||||||
render_x = x
|
render_x = x
|
||||||
remaining = region.width
|
remaining = region.width
|
||||||
total = 0
|
total = 0
|
||||||
for widget, size in zip(dock.widgets, sizes):
|
for widget, layout_size in zip(dock.widgets, sizes):
|
||||||
if not widget.visible:
|
if not widget.visible:
|
||||||
continue
|
continue
|
||||||
size = min(remaining, size)
|
layout_size = min(remaining, layout_size)
|
||||||
if not size:
|
if not layout_size:
|
||||||
break
|
break
|
||||||
total += size
|
total += layout_size
|
||||||
add_widget(widget, Region(render_x, y, size, height), order)
|
add_widget(widget, Region(render_x, y, layout_size, height), order)
|
||||||
render_x += size
|
render_x += layout_size
|
||||||
remaining = max(0, remaining - size)
|
remaining = max(0, remaining - layout_size)
|
||||||
region = Region(x + total, y, width - total, height)
|
region = Region(x + total, y, width - total, height)
|
||||||
|
|
||||||
elif dock.edge == "right":
|
elif dock.edge == "right":
|
||||||
@@ -139,16 +135,20 @@ class DockLayout(Layout):
|
|||||||
render_x = x + width
|
render_x = x + width
|
||||||
remaining = region.width
|
remaining = region.width
|
||||||
total = 0
|
total = 0
|
||||||
for widget, size in zip(dock.widgets, sizes):
|
for widget, layout_size in zip(dock.widgets, sizes):
|
||||||
if not widget.visible:
|
if not widget.visible:
|
||||||
continue
|
continue
|
||||||
size = min(remaining, size)
|
layout_size = min(remaining, layout_size)
|
||||||
if not size:
|
if not layout_size:
|
||||||
break
|
break
|
||||||
total += size
|
total += layout_size
|
||||||
add_widget(widget, Region(render_x - size, y, size, height), order)
|
add_widget(
|
||||||
render_x -= size
|
widget,
|
||||||
remaining = max(0, remaining - size)
|
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)
|
region = Region(x, y, width - total, height)
|
||||||
|
|
||||||
layers[dock.z] = region
|
layers[dock.z] = region
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ from rich.console import Console
|
|||||||
|
|
||||||
from .._layout_resolve import layout_resolve
|
from .._layout_resolve import layout_resolve
|
||||||
from ..geometry import Dimensions, Point, Region
|
from ..geometry import Dimensions, Point, Region
|
||||||
from ..layout import Layout, RenderRegion
|
from ..layout import Layout
|
||||||
from ..view import View
|
from ..layout_map import LayoutMap
|
||||||
from ..widget import Widget
|
from ..widget import Widget
|
||||||
|
|
||||||
if sys.version_info >= (3, 8):
|
if sys.version_info >= (3, 8):
|
||||||
@@ -264,8 +264,8 @@ class GridLayout(Layout):
|
|||||||
return self.widgets.keys()
|
return self.widgets.keys()
|
||||||
|
|
||||||
def generate_map(
|
def generate_map(
|
||||||
self, console: Console, size: Dimensions, offset: Point
|
self, console: Console, size: Dimensions, viewport: Region
|
||||||
) -> dict[Widget, RenderRegion]:
|
) -> LayoutMap:
|
||||||
"""Generate a map that associates widgets with their location on screen.
|
"""Generate a map that associates widgets with their location on screen.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -276,6 +276,7 @@ class GridLayout(Layout):
|
|||||||
Returns:
|
Returns:
|
||||||
dict[Widget, OrderedRegion]: [description]
|
dict[Widget, OrderedRegion]: [description]
|
||||||
"""
|
"""
|
||||||
|
map: LayoutMap = LayoutMap(size)
|
||||||
width, height = size
|
width, height = size
|
||||||
|
|
||||||
def resolve(
|
def resolve(
|
||||||
@@ -327,13 +328,14 @@ class GridLayout(Layout):
|
|||||||
return names, tracks, len(spans), max_size
|
return names, tracks, len(spans), max_size
|
||||||
|
|
||||||
def add_widget(widget: Widget, region: Region, order: tuple[int, int]):
|
def add_widget(widget: Widget, region: Region, order: tuple[int, int]):
|
||||||
region = region + widget.layout_offset
|
map.add_widget(console, widget, region, order, viewport)
|
||||||
map[widget] = RenderRegion(region, order, offset)
|
# region = region + widget.layout_offset
|
||||||
if isinstance(widget, View):
|
# map[widget] = RenderRegion(region, order, offset)
|
||||||
sub_map = widget.layout.generate_map(
|
# if isinstance(widget, View):
|
||||||
region.width, region.height, region.origin + offset
|
# sub_map = widget.layout.generate_map(
|
||||||
)
|
# region.width, region.height, region.origin + offset
|
||||||
map.update(sub_map)
|
# )
|
||||||
|
# map.update(sub_map)
|
||||||
|
|
||||||
container = Dimensions(
|
container = Dimensions(
|
||||||
width - self.column_gutter * 2, height - self.row_gutter * 2
|
width - self.column_gutter * 2, height - self.row_gutter * 2
|
||||||
@@ -365,8 +367,6 @@ class GridLayout(Layout):
|
|||||||
free_slots = {
|
free_slots = {
|
||||||
(col, row) for col, row in product(range(column_count), range(row_count))
|
(col, row) for col, row in product(range(column_count), range(row_count))
|
||||||
}
|
}
|
||||||
|
|
||||||
map: dict[Widget, RenderRegion] = {}
|
|
||||||
order = 1
|
order = 1
|
||||||
from_corners = Region.from_corners
|
from_corners = Region.from_corners
|
||||||
gutter = Point(self.column_gutter, self.row_gutter)
|
gutter = Point(self.column_gutter, self.row_gutter)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from rich.console import Console
|
|||||||
|
|
||||||
|
|
||||||
from ..geometry import Point, Region, Dimensions
|
from ..geometry import Point, Region, Dimensions
|
||||||
from ..layout import Layout, RenderRegion, WidgetMap
|
from ..layout import Layout
|
||||||
from ..widget import Widget
|
from ..widget import Widget
|
||||||
from ..view import View
|
from ..view import View
|
||||||
|
|
||||||
@@ -19,8 +19,9 @@ class VerticalLayout(Layout):
|
|||||||
self._widgets.append(widget)
|
self._widgets.append(widget)
|
||||||
|
|
||||||
def generate_map(
|
def generate_map(
|
||||||
self, console: Console, size: Dimensions, offset: Point
|
self, console: Console, size: Dimensions, viewport: Region
|
||||||
) -> WidgetMap:
|
) -> WidgetMap:
|
||||||
|
offset = viewport.origin
|
||||||
width, height = size
|
width, height = size
|
||||||
gutter_width, gutter_height = self.gutter
|
gutter_width, gutter_height = self.gutter
|
||||||
render_width = width - gutter_width * 2
|
render_width = width - gutter_width * 2
|
||||||
|
|||||||
@@ -62,6 +62,12 @@ class View(Widget):
|
|||||||
(virtual_height if virtual_height is not None else self.size.height),
|
(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
|
@property
|
||||||
def offset(self) -> Point:
|
def offset(self) -> Point:
|
||||||
return Point(self.offset_x, self.offset_y)
|
return Point(self.offset_x, self.offset_y)
|
||||||
@@ -133,7 +139,7 @@ class View(Widget):
|
|||||||
|
|
||||||
async def refresh_layout(self) -> None:
|
async def refresh_layout(self) -> None:
|
||||||
await self.layout.mount_all(self)
|
await self.layout.mount_all(self)
|
||||||
if not self.size or not self.is_root_view:
|
if not self.size:
|
||||||
return
|
return
|
||||||
|
|
||||||
width, height = self.console.size
|
width, height = self.console.size
|
||||||
|
|||||||
@@ -61,8 +61,8 @@ class Widget(MessagePump):
|
|||||||
layout_size: Reactive[int | None] = Reactive(None, layout=True)
|
layout_size: Reactive[int | None] = Reactive(None, layout=True)
|
||||||
layout_fraction: Reactive[int] = Reactive(1, layout=True)
|
layout_fraction: Reactive[int] = Reactive(1, layout=True)
|
||||||
layout_min_size: 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_x: Reactive[float] = Reactive(0.0, layout=True)
|
||||||
layout_offset_y: Reactive[int] = Reactive(0, layout=True)
|
layout_offset_y: Reactive[float] = Reactive(0.0, layout=True)
|
||||||
|
|
||||||
def validate_layout_offset_x(self, value) -> int:
|
def validate_layout_offset_x(self, value) -> int:
|
||||||
return int(value)
|
return int(value)
|
||||||
|
|||||||
@@ -3,6 +3,57 @@ import pytest
|
|||||||
from textual.geometry import clamp, Point, Dimensions, Region
|
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():
|
def test_clamp():
|
||||||
assert clamp(5, 0, 10) == 5
|
assert clamp(5, 0, 10) == 5
|
||||||
assert clamp(-1, 0, 10) == 0
|
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), 0) == Point(1, 2)
|
||||||
assert Point(1, 2).blend(Point(3, 4), 1) == Point(3, 4)
|
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)
|
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))
|
||||||
|
|||||||
Reference in New Issue
Block a user