diff --git a/src/textual/geometry.py b/src/textual/geometry.py index b1b2b6ef1..cf07808f4 100644 --- a/src/textual/geometry.py +++ b/src/textual/geometry.py @@ -485,7 +485,7 @@ class Spacing(NamedTuple): left: int = 0 def __bool__(self) -> bool: - return self == (0, 0, 0, 0) + return self != (0, 0, 0, 0) @property def width(self) -> int: @@ -507,16 +507,6 @@ class Spacing(NamedTuple): """Bottom right space.""" return (self.right, self.bottom) - @property - def packed(self) -> str: - top, right, bottom, left = self - if top == right == bottom == left: - return f"{top}" - if (top, right) == (bottom, left): - return f"{top}, {right}" - else: - return f"{top}, {right}, {bottom}, {left}" - @property def css(self) -> str: top, right, bottom, left = self @@ -532,16 +522,17 @@ class Spacing(NamedTuple): """Unpack padding specified in CSS style.""" if isinstance(pad, int): return cls(pad, pad, pad, pad) - if len(pad) == 1: + pad_len = len(pad) + if pad_len == 1: _pad = pad[0] return cls(_pad, _pad, _pad, _pad) - if len(pad) == 2: + if pad_len == 2: pad_top, pad_right = cast(Tuple[int, int], pad) return cls(pad_top, pad_right, pad_top, pad_right) - if len(pad) == 4: + if pad_len == 4: top, right, bottom, left = cast(Tuple[int, int, int, int], pad) return cls(top, right, bottom, left) - raise ValueError(f"1, 2 or 4 integers required for spacing; {len(pad)} given") + raise ValueError(f"1, 2 or 4 integers required for spacing; {pad_len} given") def __add__(self, other: object) -> Spacing: if isinstance(other, tuple): diff --git a/src/textual/layout.py b/src/textual/layout.py index 00faf088a..cd5549bbf 100644 --- a/src/textual/layout.py +++ b/src/textual/layout.py @@ -27,12 +27,7 @@ if TYPE_CHECKING: class NoWidget(Exception): - pass - - -class OrderedRegion(NamedTuple): - region: Region - order: tuple[int, int] + """Raised when there is no widget at the requested coordinate.""" class ReflowResult(NamedTuple): @@ -44,9 +39,10 @@ class ReflowResult(NamedTuple): class WidgetPlacement(NamedTuple): + """The position, size, and relative order of a widget within its parent.""" region: Region - widget: Widget | None = None + widget: Widget | None = None # A widget of None means empty space order: int = 0 def apply_margin(self) -> "WidgetPlacement": @@ -61,7 +57,7 @@ class WidgetPlacement(NamedTuple): region, widget, order = self if widget is not None: styles = widget.styles - if any(styles.margin): + if styles.margin: return WidgetPlacement( region=region.shrink(styles.margin), widget=widget, @@ -72,6 +68,8 @@ class WidgetPlacement(NamedTuple): @rich.repr.auto class LayoutUpdate: + """A renderable containing the result of a render for a given region.""" + def __init__(self, lines: Lines, region: Region) -> None: self.lines = lines self.region = region @@ -79,12 +77,12 @@ class LayoutUpdate: def __rich_console__( self, console: Console, options: ConsoleOptions ) -> RenderResult: - yield Control.home().segment + yield Control.home() x = self.region.x new_line = Segment.line() move_to = Control.move_to for last, (y, line) in loop_last(enumerate(self.lines, self.region.y)): - yield move_to(x, y).segment + yield move_to(x, y) yield from line if not last: yield new_line @@ -187,7 +185,8 @@ class Layout(ABC): view.mount(*widgets) @property - def map(self) -> LayoutMap | None: + def map(self) -> LayoutMap: + assert self._layout_map is not None return self._layout_map def __iter__(self) -> Iterator[tuple[Widget, Region, Region]]: diff --git a/src/textual/widget.py b/src/textual/widget.py index d98d5af3f..fbaf4fcc0 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -99,44 +99,6 @@ class Widget(DOMNode): yield "hover" # TODO: focus - def get_child_by_id(self, id: str) -> Widget: - """Get a child with a given id. - - Args: - id (str): A Widget id. - - Raises: - errors.MissingWidget: If the widget was not found. - - Returns: - Widget: A child widget. - """ - - for widget in self.children: - if widget.id == id: - return cast(Widget, widget) - raise errors.MissingWidget(f"Widget with id=={id!r} was not found in {self}") - - def get_child_by_name(self, name: str) -> Widget: - """Get a child widget with a given name. - - Args: - name (str): A name. Defaults to None. - - Raises: - errors.MissingWidget: If no Widget is found. - - Returns: - Widget: A Widget with the given name. - """ - - for widget in self.children: - if widget.name == name: - return cast(Widget, widget) - raise errors.MissingWidget( - f"Widget with name=={name!r} was not found in {self}" - ) - def watch(self, attribute_name, callback: Callable[[Any], Awaitable[None]]) -> None: watch(self, attribute_name, callback) diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 2bfbc695d..d0305941a 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -63,31 +63,52 @@ def test_clamp(): assert clamp(5, 10, 0) == 5 -def test_point_is_origin(): +def test_offset_bool(): + assert Offset(1, 0) + assert Offset(0, 1) + assert Offset(0, -1) + assert not Offset(0, 0) + + +def test_offset_is_origin(): assert Offset(0, 0).is_origin assert not Offset(1, 0).is_origin -def test_point_add(): +def test_offset_add(): assert Offset(1, 1) + Offset(2, 2) == Offset(3, 3) assert Offset(1, 2) + Offset(3, 4) == Offset(4, 6) with pytest.raises(TypeError): Offset(1, 1) + "foo" -def test_point_sub(): +def test_offset_sub(): assert Offset(1, 1) - Offset(2, 2) == Offset(-1, -1) assert Offset(3, 4) - Offset(2, 1) == Offset(1, 3) with pytest.raises(TypeError): Offset(1, 1) - "foo" -def test_point_blend(): +def test_offset_mul(): + assert Offset(2, 1) * 2 == Offset(4, 2) + assert Offset(2, 1) * -2 == Offset(-4, -2) + assert Offset(2, 1) * 0 == Offset(0, 0) + with pytest.raises(TypeError): + Offset(10, 20) * "foo" + + +def test_offset_blend(): assert Offset(1, 2).blend(Offset(3, 4), 0) == Offset(1, 2) assert Offset(1, 2).blend(Offset(3, 4), 1) == Offset(3, 4) assert Offset(1, 2).blend(Offset(3, 4), 0.5) == Offset(2, 3) +def test_offset_get_distance_to(): + assert Offset(20, 30).get_distance_to(Offset(20, 30)) == 0 + assert Offset(0, 0).get_distance_to(Offset(1, 0)) == 1.0 + assert Offset(2, 1).get_distance_to(Offset(5, 5)) == 5.0 + + def test_region_null(): assert Region() == Region(0, 0, 0, 0) assert not Region() @@ -196,10 +217,14 @@ def test_region_union(): def test_size_add(): assert Size(5, 10) + Size(2, 3) == Size(7, 13) + with pytest.raises(TypeError): + Size(1, 2) + "foo" def test_size_sub(): assert Size(5, 10) - Size(2, 3) == Size(3, 7) + with pytest.raises(TypeError): + Size(1, 2) - "foo" def test_region_x_extents(): @@ -224,3 +249,61 @@ def test_region_x_range(): def test_region_y_range(): assert Region(5, 10, 20, 30).y_range == range(10, 40) + + +def test_region_expand(): + assert Region(50, 10, 10, 5).expand((2, 3)) == Region(48, 7, 14, 11) + + +def test_spacing_bool(): + assert Spacing(1, 0, 0, 0) + assert Spacing(0, 1, 0, 0) + assert Spacing(0, 1, 0, 0) + assert Spacing(0, 0, 1, 0) + assert Spacing(0, 0, 0, 1) + assert not Spacing(0, 0, 0, 0) + + +def test_spacing_width(): + assert Spacing(2, 3, 4, 5).width == 8 + + +def test_spacing_height(): + assert Spacing(2, 3, 4, 5).height == 6 + + +def test_spacing_top_left(): + assert Spacing(2, 3, 4, 5).top_left == (5, 2) + + +def test_spacing_bottom_right(): + assert Spacing(2, 3, 4, 5).bottom_right == (3, 4) + + +def test_spacing_css(): + assert Spacing(1, 1, 1, 1).css == "1" + assert Spacing(1, 2, 1, 2).css == "1 2" + assert Spacing(1, 2, 3, 4).css == "1 2 3 4" + + +def test_spacing_unpack(): + assert Spacing.unpack(1) == Spacing(1, 1, 1, 1) + assert Spacing.unpack((1,)) == Spacing(1, 1, 1, 1) + assert Spacing.unpack((1, 2)) == Spacing(1, 2, 1, 2) + assert Spacing.unpack((1, 2, 3, 4)) == Spacing(1, 2, 3, 4) + + with pytest.raises(ValueError): + assert Spacing.unpack(()) == Spacing(1, 2, 1, 2) + + with pytest.raises(ValueError): + assert Spacing.unpack((1, 2, 3)) == Spacing(1, 2, 1, 2) + + with pytest.raises(ValueError): + assert Spacing.unpack((1, 2, 3, 4, 5)) == Spacing(1, 2, 1, 2) + + +def test_spacing_add(): + assert Spacing(1, 2, 3, 4) + Spacing(5, 6, 7, 8) == Spacing(6, 8, 10, 12) + + with pytest.raises(TypeError): + Spacing(1, 2, 3, 4) + "foo"