From 435f73b60d2cd6fea19c92d45fb7f5648a6ac038 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 18 Nov 2024 10:38:20 +0000 Subject: [PATCH 01/12] optimization --- src/textual/layouts/horizontal.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/textual/layouts/horizontal.py b/src/textual/layouts/horizontal.py index ce032d27a5..63967f7e91 100644 --- a/src/textual/layouts/horizontal.py +++ b/src/textual/layouts/horizontal.py @@ -84,10 +84,10 @@ def arrange( add_placement( _WidgetPlacement( _Region( - int(x), + x.__floor__(), offset_y, - int(next_x - int(x)), - int(content_height), + (next_x - x.__floor__()).__floor__(), + content_height.__floor__(), ), box_margin, widget, From 845840e830f1b8ea4aa6cfd57b28094438ad7e89 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 18 Nov 2024 10:54:16 +0000 Subject: [PATCH 02/12] add offset to WidgetPlacement --- src/textual/_arrange.py | 13 +++++++--- src/textual/_compositor.py | 2 +- src/textual/layout.py | 17 ++++++++++--- src/textual/layouts/grid.py | 3 ++- src/textual/layouts/horizontal.py | 3 ++- src/textual/layouts/vertical.py | 3 ++- tests/test_arrange.py | 41 ++++++++++++++++++++++++------- 7 files changed, 61 insertions(+), 21 deletions(-) diff --git a/src/textual/_arrange.py b/src/textual/_arrange.py index 347e8ce09e..8c2469670f 100644 --- a/src/textual/_arrange.py +++ b/src/textual/_arrange.py @@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, Iterable, Mapping, Sequence from textual._partition import partition -from textual.geometry import Region, Size, Spacing +from textual.geometry import NULL_OFFSET, NULL_SPACING, Region, Size, Spacing from textual.layout import DockArrangeResult, WidgetPlacement if TYPE_CHECKING: @@ -133,7 +133,8 @@ def _arrange_dock_widgets( region_offset = region.offset size = region.size width, height = size - null_spacing = Spacing() + null_spacing = NULL_SPACING + null_offset = NULL_OFFSET top = right = bottom = left = 0 @@ -173,6 +174,7 @@ def _arrange_dock_widgets( append_placement( _WidgetPlacement( dock_region.translate(region_offset), + null_offset, null_spacing, dock_widget, top_z, @@ -202,7 +204,8 @@ def _arrange_split_widgets( placements: list[WidgetPlacement] = [] append_placement = placements.append view_region = size.region - null_spacing = Spacing() + null_spacing = NULL_SPACING + null_offset = NULL_OFFSET for split_widget in split_widgets: split = split_widget.styles.split @@ -226,7 +229,9 @@ def _arrange_split_widgets( raise AssertionError("invalid value for split edge") # pragma: no-cover append_placement( - _WidgetPlacement(split_region, null_spacing, split_widget, 1, True) + _WidgetPlacement( + split_region, null_offset, null_spacing, split_widget, 1, True + ) ) return placements, view_region diff --git a/src/textual/_compositor.py b/src/textual/_compositor.py index 13869c838a..cd382bd235 100644 --- a/src/textual/_compositor.py +++ b/src/textual/_compositor.py @@ -643,7 +643,7 @@ def add_widget( ) # Add all the widgets - for sub_region, _, sub_widget, z, fixed, overlay in reversed( + for sub_region, _, _, sub_widget, z, fixed, overlay in reversed( placements ): layer_index = get_layer_index(sub_widget.layer, 0) diff --git a/src/textual/layout.py b/src/textual/layout.py index adb7ab5c2d..a1c3c197f9 100644 --- a/src/textual/layout.py +++ b/src/textual/layout.py @@ -84,6 +84,7 @@ class WidgetPlacement(NamedTuple): """The position, size, and relative order of a widget within its parent.""" region: Region + offset: Offset margin: Spacing widget: Widget order: int = 0 @@ -92,7 +93,7 @@ class WidgetPlacement(NamedTuple): @classmethod def translate( - cls, placements: list[WidgetPlacement], offset: Offset + cls, placements: list[WidgetPlacement], translate_offset: Offset ) -> list[WidgetPlacement]: """Move all placements by a given offset. @@ -103,10 +104,18 @@ def translate( Returns: Placements with adjusted region, or same instance if offset is null. """ - if offset: + if translate_offset: return [ - cls(region + offset, margin, layout_widget, order, fixed, overlay) - for region, margin, layout_widget, order, fixed, overlay in placements + cls( + region + translate_offset, + offset, + margin, + layout_widget, + order, + fixed, + overlay, + ) + for region, offset, margin, layout_widget, order, fixed, overlay in placements ] return placements diff --git a/src/textual/layouts/grid.py b/src/textual/layouts/grid.py index d7dfc95896..85fff091d0 100644 --- a/src/textual/layouts/grid.py +++ b/src/textual/layouts/grid.py @@ -5,7 +5,7 @@ from textual._resolve import resolve from textual.css.scalar import Scalar -from textual.geometry import Region, Size, Spacing +from textual.geometry import NULL_OFFSET, Region, Size, Spacing from textual.layout import ArrangeResult, Layout, WidgetPlacement if TYPE_CHECKING: @@ -292,6 +292,7 @@ def apply_height_limits(widget: Widget, height: int) -> int: add_placement( WidgetPlacement( region + offset, + NULL_OFFSET, ( margin if gutter_spacing is None diff --git a/src/textual/layouts/horizontal.py b/src/textual/layouts/horizontal.py index 63967f7e91..4346bdc532 100644 --- a/src/textual/layouts/horizontal.py +++ b/src/textual/layouts/horizontal.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING from textual._resolve import resolve_box_models -from textual.geometry import Region, Size +from textual.geometry import NULL_OFFSET, Region, Size from textual.layout import ArrangeResult, Layout, WidgetPlacement if TYPE_CHECKING: @@ -89,6 +89,7 @@ def arrange( (next_x - x.__floor__()).__floor__(), content_height.__floor__(), ), + NULL_OFFSET, box_margin, widget, 0, diff --git a/src/textual/layouts/vertical.py b/src/textual/layouts/vertical.py index a5bf67aa40..04996bd325 100644 --- a/src/textual/layouts/vertical.py +++ b/src/textual/layouts/vertical.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING from textual._resolve import resolve_box_models -from textual.geometry import Region, Size +from textual.geometry import NULL_OFFSET, Region, Size from textual.layout import ArrangeResult, Layout, WidgetPlacement if TYPE_CHECKING: @@ -93,6 +93,7 @@ def arrange( content_width.__floor__(), next_y.__floor__() - y.__floor__(), ), + NULL_OFFSET, box_margin, widget, 0, diff --git a/tests/test_arrange.py b/tests/test_arrange.py index 4b671b8334..b22b8a1459 100644 --- a/tests/test_arrange.py +++ b/tests/test_arrange.py @@ -2,7 +2,7 @@ from textual._arrange import TOP_Z, arrange from textual.app import App -from textual.geometry import Region, Size, Spacing +from textual.geometry import NULL_OFFSET, Region, Size, Spacing from textual.layout import WidgetPlacement from textual.widget import Widget @@ -27,9 +27,11 @@ def test_arrange_dock_top(): assert result.placements == [ WidgetPlacement( - Region(0, 0, 80, 1), Spacing(), header, order=TOP_Z, fixed=True + Region(0, 0, 80, 1), NULL_OFFSET, Spacing(), header, order=TOP_Z, fixed=True + ), + WidgetPlacement( + Region(0, 1, 80, 23), NULL_OFFSET, Spacing(), child, order=0, fixed=False ), - WidgetPlacement(Region(0, 1, 80, 23), Spacing(), child, order=0, fixed=False), ] assert result.widgets == {child, header} @@ -45,9 +47,16 @@ def test_arrange_dock_left(): result = arrange(container, [child, header], Size(80, 24), Size(80, 24)) assert result.placements == [ WidgetPlacement( - Region(0, 0, 10, 24), Spacing(), header, order=TOP_Z, fixed=True + Region(0, 0, 10, 24), + NULL_OFFSET, + Spacing(), + header, + order=TOP_Z, + fixed=True, + ), + WidgetPlacement( + Region(10, 0, 70, 24), NULL_OFFSET, Spacing(), child, order=0, fixed=False ), - WidgetPlacement(Region(10, 0, 70, 24), Spacing(), child, order=0, fixed=False), ] assert result.widgets == {child, header} @@ -63,9 +72,16 @@ def test_arrange_dock_right(): result = arrange(container, [child, header], Size(80, 24), Size(80, 24)) assert result.placements == [ WidgetPlacement( - Region(70, 0, 10, 24), Spacing(), header, order=TOP_Z, fixed=True + Region(70, 0, 10, 24), + NULL_OFFSET, + Spacing(), + header, + order=TOP_Z, + fixed=True, + ), + WidgetPlacement( + Region(0, 0, 70, 24), NULL_OFFSET, Spacing(), child, order=0, fixed=False ), - WidgetPlacement(Region(0, 0, 70, 24), Spacing(), child, order=0, fixed=False), ] assert result.widgets == {child, header} @@ -81,9 +97,16 @@ def test_arrange_dock_bottom(): result = arrange(container, [child, header], Size(80, 24), Size(80, 24)) assert result.placements == [ WidgetPlacement( - Region(0, 23, 80, 1), Spacing(), header, order=TOP_Z, fixed=True + Region(0, 23, 80, 1), + NULL_OFFSET, + Spacing(), + header, + order=TOP_Z, + fixed=True, + ), + WidgetPlacement( + Region(0, 0, 80, 23), NULL_OFFSET, Spacing(), child, order=0, fixed=False ), - WidgetPlacement(Region(0, 0, 80, 23), Spacing(), child, order=0, fixed=False), ] assert result.widgets == {child, header} From 4c805bf4424bf151f924d84f7d800738ffd10a28 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 18 Nov 2024 14:03:53 +0000 Subject: [PATCH 03/12] fix for spatial map and layout --- src/textual/_compositor.py | 37 ++++++++++++++++++------------- src/textual/_spatial_map.py | 10 ++++----- src/textual/layout.py | 3 ++- src/textual/layouts/grid.py | 12 ++++++++-- src/textual/layouts/horizontal.py | 17 +++++++++++--- src/textual/layouts/vertical.py | 15 +++++++++++-- src/textual/widget.py | 6 ++--- tests/test_spatial_map.py | 8 +++---- 8 files changed, 73 insertions(+), 35 deletions(-) diff --git a/src/textual/_compositor.py b/src/textual/_compositor.py index cd382bd235..5a2d6abe5c 100644 --- a/src/textual/_compositor.py +++ b/src/textual/_compositor.py @@ -580,15 +580,9 @@ def add_widget( add_new_widget(widget) else: add_new_invisible_widget(widget) - styles_offset = styles.offset - layout_offset = ( - styles_offset.resolve(region.size, clip.size) - if styles_offset - else ORIGIN - ) # Container region is minus border - container_region = region.shrink(styles.gutter).translate(layout_offset) + container_region = region.shrink(styles.gutter) container_size = container_region.size # Widgets with scrollbars (containers or scroll view) require additional processing @@ -613,9 +607,12 @@ def add_widget( sub_clip = clip.intersection(child_region) if visible_only: - placements = arrange_result.get_visible_placements( + widget_window = ( sub_clip - child_region.offset + widget.scroll_offset ) + placements = arrange_result.get_visible_placements( + widget_window + ) else: placements = arrange_result.placements total_region = total_region.union(arrange_result.total_region) @@ -643,15 +640,25 @@ def add_widget( ) # Add all the widgets - for sub_region, _, _, sub_widget, z, fixed, overlay in reversed( - placements - ): + for ( + sub_region, + sub_region_offset, + _, + sub_widget, + z, + fixed, + overlay, + ) in reversed(placements): layer_index = get_layer_index(sub_widget.layer, 0) # Combine regions with children to calculate the "virtual size" if fixed: - widget_region = sub_region + placement_offset + widget_region = ( + sub_region + sub_region_offset + placement_offset + ) else: - widget_region = sub_region + placement_scroll_offset + widget_region = ( + sub_region + sub_region_offset + placement_scroll_offset + ) widget_order = order + ((layer_index, z, layer_order),) @@ -699,7 +706,7 @@ def add_widget( ) map[widget] = _MapGeometry( - region + layout_offset, + region, order, clip, total_region.size, @@ -711,7 +718,7 @@ def add_widget( elif visible: # Add the widget to the map - widget_region = region + layout_offset + widget_region = region if widget.absolute_offset is not None: margin = styles.margin diff --git a/src/textual/_spatial_map.py b/src/textual/_spatial_map.py index 86debb1ddc..8f6175321a 100644 --- a/src/textual/_spatial_map.py +++ b/src/textual/_spatial_map.py @@ -6,7 +6,7 @@ from typing_extensions import TypeAlias -from textual.geometry import Region +from textual.geometry import Offset, Region ValueType = TypeVar("ValueType") GridCoordinate: TypeAlias = "tuple[int, int]" @@ -57,7 +57,7 @@ def _region_to_grid_coordinates(self, region: Region) -> Iterable[GridCoordinate ) def insert( - self, regions_and_values: Iterable[tuple[Region, bool, bool, ValueType]] + self, regions_and_values: Iterable[tuple[Region, Offset, bool, bool, ValueType]] ) -> None: """Insert values into the Spatial map. @@ -65,19 +65,19 @@ def insert( indicates fixed regions. Fixed regions don't scroll and are always visible. Args: - regions_and_values: An iterable of (REGION, FIXED, OVERLAY, VALUE). + regions_and_values: An iterable of (REGION, OFFSET, FIXED, OVERLAY, VALUE). """ append_fixed = self._fixed.append get_grid_list = self._map.__getitem__ _region_to_grid = self._region_to_grid_coordinates total_region = self.total_region - for region, fixed, overlay, value in regions_and_values: + for region, offset, fixed, overlay, value in regions_and_values: if fixed: append_fixed(value) else: if not overlay: total_region = total_region.union(region) - for grid in _region_to_grid(region): + for grid in _region_to_grid(region + offset): get_grid_list(grid).append(value) self.total_region = total_region diff --git a/src/textual/layout.py b/src/textual/layout.py index a1c3c197f9..8e494fcb74 100644 --- a/src/textual/layout.py +++ b/src/textual/layout.py @@ -39,6 +39,7 @@ def spatial_map(self) -> SpatialMap[WidgetPlacement]: self._spatial_map.insert( ( placement.region.grow(placement.margin), + placement.offset, placement.fixed, placement.overlay, placement, @@ -75,7 +76,7 @@ def get_visible_placements(self, region: Region) -> list[WidgetPlacement]: culled_placements = [ placement for placement in visible_placements - if placement.fixed or overlaps(placement.region) + if placement.fixed or overlaps(placement.region + placement.offset) ] return culled_placements diff --git a/src/textual/layouts/grid.py b/src/textual/layouts/grid.py index 85fff091d0..68772bbe24 100644 --- a/src/textual/layouts/grid.py +++ b/src/textual/layouts/grid.py @@ -284,15 +284,23 @@ def apply_height_limits(widget: Widget, height: int) -> int: if (height > cell_size.height) else Fraction(cell_size.height) ) + region = ( Region(x, y, int(width + margin.width), int(height + margin.height)) .crop_size(cell_size) .shrink(margin) ) + + offset = ( + styles.offset.resolve(cell_size, viewport) + if styles.has_rule("offset") + else NULL_OFFSET + ) + add_placement( WidgetPlacement( - region + offset, - NULL_OFFSET, + region, + offset, ( margin if gutter_spacing is None diff --git a/src/textual/layouts/horizontal.py b/src/textual/layouts/horizontal.py index 4346bdc532..c62fae3c1d 100644 --- a/src/textual/layouts/horizontal.py +++ b/src/textual/layouts/horizontal.py @@ -24,6 +24,7 @@ def arrange( ) -> ArrangeResult: placements: list[WidgetPlacement] = [] add_placement = placements.append + viewport = parent.app.size child_styles = [child.styles for child in children] box_margins: list[Spacing] = [ @@ -52,7 +53,7 @@ def arrange( [styles.width for styles in child_styles], children, size, - parent.app.size, + viewport, resolve_margin, resolve_dimension="width", ) @@ -75,10 +76,20 @@ def arrange( _Region = Region _WidgetPlacement = WidgetPlacement + _Size = Size for widget, (content_width, content_height, box_margin), margin in zip( children, box_models, margins ): - overlay = widget.styles.overlay == "screen" + styles = widget.styles + overlay = styles.overlay == "screen" + offset = ( + styles.offset.resolve( + _Size(content_width.__floor__(), content_height.__floor__()), + viewport, + ) + if styles.has_rule("offset") + else NULL_OFFSET + ) offset_y = box_margin.top next_x = x + content_width add_placement( @@ -89,7 +100,7 @@ def arrange( (next_x - x.__floor__()).__floor__(), content_height.__floor__(), ), - NULL_OFFSET, + offset, box_margin, widget, 0, diff --git a/src/textual/layouts/vertical.py b/src/textual/layouts/vertical.py index 04996bd325..c7b8087f7f 100644 --- a/src/textual/layouts/vertical.py +++ b/src/textual/layouts/vertical.py @@ -22,6 +22,7 @@ def arrange( ) -> ArrangeResult: placements: list[WidgetPlacement] = [] add_placement = placements.append + viewport = parent.app.size child_styles = [child.styles for child in children] box_margins: list[Spacing] = [ @@ -80,11 +81,21 @@ def arrange( _Region = Region _WidgetPlacement = WidgetPlacement + _Size = Size for widget, (content_width, content_height, box_margin), margin in zip( children, box_models, margins ): - overlay = widget.styles.overlay == "screen" + styles = widget.styles + overlay = styles.overlay == "screen" next_y = y + content_height + offset = ( + styles.offset.resolve( + _Size(content_width.__floor__(), content_height.__floor__()), + viewport, + ) + if styles.has_rule("offset") + else NULL_OFFSET + ) add_placement( _WidgetPlacement( _Region( @@ -93,7 +104,7 @@ def arrange( content_width.__floor__(), next_y.__floor__() - y.__floor__(), ), - NULL_OFFSET, + offset, box_margin, widget, 0, diff --git a/src/textual/widget.py b/src/textual/widget.py index 9f758b64cb..5427109b7b 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -437,9 +437,9 @@ def __init__( self._content_width_cache: tuple[object, int] = (None, 0) self._content_height_cache: tuple[object, int] = (None, 0) - self._arrangement_cache: FIFOCache[tuple[Size, int], DockArrangeResult] = ( - FIFOCache(4) - ) + self._arrangement_cache: FIFOCache[ + tuple[Size, int, Widget], DockArrangeResult + ] = FIFOCache(4) self._styles_cache = StylesCache() self._rich_style_cache: dict[tuple[str, ...], tuple[Style, Style]] = {} diff --git a/tests/test_spatial_map.py b/tests/test_spatial_map.py index 6b053633c7..02fb1ea45c 100644 --- a/tests/test_spatial_map.py +++ b/tests/test_spatial_map.py @@ -1,7 +1,7 @@ import pytest from textual._spatial_map import SpatialMap -from textual.geometry import Region +from textual.geometry import Offset, Region @pytest.mark.parametrize( @@ -44,9 +44,9 @@ def test_get_values_in_region() -> None: spatial_map.insert( [ - (Region(10, 5, 5, 5), False, False, "foo"), - (Region(5, 20, 5, 5), False, False, "bar"), - (Region(0, 0, 40, 1), True, False, "title"), + (Region(10, 5, 5, 5), Offset(), False, False, "foo"), + (Region(5, 20, 5, 5), Offset(), False, False, "bar"), + (Region(0, 0, 40, 1), Offset(), True, False, "title"), ] ) From ff061d180d985d127cced8b78cdae3d930e5eedc Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 18 Nov 2024 14:22:23 +0000 Subject: [PATCH 04/12] fix placement --- src/textual/layouts/grid.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/textual/layouts/grid.py b/src/textual/layouts/grid.py index 68772bbe24..89afc4f766 100644 --- a/src/textual/layouts/grid.py +++ b/src/textual/layouts/grid.py @@ -258,6 +258,7 @@ def apply_height_limits(widget: Widget, height: int) -> int: rows = resolve(row_scalars, size.height, gutter_horizontal, size, viewport) placements: list[WidgetPlacement] = [] + _WidgetPlacement = WidgetPlacement add_placement = placements.append widgets: list[Widget] = [] add_widget = widgets.append @@ -291,16 +292,16 @@ def apply_height_limits(widget: Widget, height: int) -> int: .shrink(margin) ) - offset = ( + placement_offset = ( styles.offset.resolve(cell_size, viewport) if styles.has_rule("offset") else NULL_OFFSET ) add_placement( - WidgetPlacement( - region, - offset, + _WidgetPlacement( + region + offset, + placement_offset, ( margin if gutter_spacing is None From 9d8a3586f08482362eabbaef0e77f4961df19ae1 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 18 Nov 2024 14:53:50 +0000 Subject: [PATCH 05/12] typing simplify --- src/textual/_compositor.py | 4 +--- src/textual/layouts/grid.py | 13 ++++++------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/textual/_compositor.py b/src/textual/_compositor.py index 5a2d6abe5c..e59db3af29 100644 --- a/src/textual/_compositor.py +++ b/src/textual/_compositor.py @@ -33,7 +33,7 @@ from textual._cells import cell_len from textual._context import visible_screen_stack from textual._loop import loop_last -from textual.geometry import NULL_OFFSET, NULL_SPACING, Offset, Region, Size, Spacing +from textual.geometry import NULL_SPACING, Offset, Region, Size, Spacing from textual.map_geometry import MapGeometry from textual.strip import Strip, StripRenderable @@ -535,8 +535,6 @@ def _arrange_root( Compositor map and set of widgets. """ - ORIGIN = NULL_OFFSET - map: CompositorMap = {} widgets: set[Widget] = set() add_new_widget = widgets.add diff --git a/src/textual/layouts/grid.py b/src/textual/layouts/grid.py index 89afc4f766..a1b0823d6d 100644 --- a/src/textual/layouts/grid.py +++ b/src/textual/layouts/grid.py @@ -273,21 +273,20 @@ def apply_height_limits(widget: Widget, height: int) -> int: x2, cell_width = columns[min(max_column, column + column_span)] y2, cell_height = rows[min(max_row, row + row_span)] cell_size = Size(cell_width + x2 - x, cell_height + y2 - y) - width, height, margin = widget._get_box_model( + box_width, box_height, margin = widget._get_box_model( cell_size, viewport, Fraction(cell_size.width), Fraction(cell_size.height), ) if self.stretch_height and len(children) > 1: - height = ( - height - if (height > cell_size.height) - else Fraction(cell_size.height) - ) + if box_height <= cell_size.height: + box_height = Fraction(cell_size.height) region = ( - Region(x, y, int(width + margin.width), int(height + margin.height)) + Region( + x, y, int(box_width + margin.width), int(box_height + margin.height) + ) .crop_size(cell_size) .shrink(margin) ) From 073eb0e0ee8d29eb220fc967d3d55c80cf83e71a Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 18 Nov 2024 15:07:04 +0000 Subject: [PATCH 06/12] restore widget region --- src/textual/_compositor.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/textual/_compositor.py b/src/textual/_compositor.py index e59db3af29..e2fe68bc8d 100644 --- a/src/textual/_compositor.py +++ b/src/textual/_compositor.py @@ -716,19 +716,15 @@ def add_widget( elif visible: # Add the widget to the map - widget_region = region - if widget.absolute_offset is not None: margin = styles.margin - widget_region = widget_region.at_offset( - widget.absolute_offset + margin.top_left - ) - widget_region = widget_region.translate( - styles.offset.resolve(widget_region.grow(margin).size, size) + region = region.at_offset(widget.absolute_offset + margin.top_left) + region = region.translate( + styles.offset.resolve(region.grow(margin).size, size) ) has_rule = styles.has_rule if has_rule("constrain_x") or has_rule("constrain_y"): - widget_region = widget_region.constrain( + region = region.constrain( styles.constrain_x, styles.constrain_y, styles.margin, @@ -736,7 +732,7 @@ def add_widget( ) map[widget._render_widget] = _MapGeometry( - widget_region, + region, order, clip, region.size, From 3addcdbf038675a2c1cfee30eb07ace10b7970e3 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 18 Nov 2024 15:38:39 +0000 Subject: [PATCH 07/12] simplify --- src/textual/_compositor.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/textual/_compositor.py b/src/textual/_compositor.py index e2fe68bc8d..7aac04393c 100644 --- a/src/textual/_compositor.py +++ b/src/textual/_compositor.py @@ -605,11 +605,8 @@ def add_widget( sub_clip = clip.intersection(child_region) if visible_only: - widget_window = ( - sub_clip - child_region.offset + widget.scroll_offset - ) placements = arrange_result.get_visible_placements( - widget_window + sub_clip - child_region.offset + widget.scroll_offset ) else: placements = arrange_result.placements From 2a1f74e699587be165dc03ae1e7c306513fbc06c Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 18 Nov 2024 15:54:12 +0000 Subject: [PATCH 08/12] test --- tests/test_compositor.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 tests/test_compositor.py diff --git a/tests/test_compositor.py b/tests/test_compositor.py new file mode 100644 index 0000000000..02b466f1ea --- /dev/null +++ b/tests/test_compositor.py @@ -0,0 +1,39 @@ +from textual.app import App, ComposeResult +from textual.containers import Container +from textual.widgets import Static + + +async def test_compositor_scroll_placements(): + """Regression test for https://github.com/Textualize/textual/issues/5249""" + + class ScrollApp(App): + CSS = """ + Screen { + overflow: scroll; + } + Container { + width: 200vw; + } + #hello { + width: 20; + height: 10; + offset: 50 10; + background: blue; + color: white; + } + """ + + def compose(self) -> ComposeResult: + with Container(): + yield Static("Hello", id="hello") + + def on_mount(self) -> None: + self.query_one("Screen").scroll_to(20, 0, animate=False) + + app = ScrollApp() + async with app.run_test() as pilot: + await pilot.pause() + static = app.query_one("#hello") + widgets = app.screen._compositor.visible_widgets + # The static wasn't scrolled out of view, and should be visible + assert static in widgets From 60638e6b6f08a8eaf2bfc341c059412878d2fab6 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 18 Nov 2024 15:55:21 +0000 Subject: [PATCH 09/12] comment --- tests/test_compositor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_compositor.py b/tests/test_compositor.py index 02b466f1ea..27f23d5e00 100644 --- a/tests/test_compositor.py +++ b/tests/test_compositor.py @@ -36,4 +36,5 @@ def on_mount(self) -> None: static = app.query_one("#hello") widgets = app.screen._compositor.visible_widgets # The static wasn't scrolled out of view, and should be visible + # This wasn't the case <= v0.86.1 assert static in widgets From 890b872513309116d839bff71ce78830683c6396 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 18 Nov 2024 15:58:29 +0000 Subject: [PATCH 10/12] changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 02cbb06664..858b1c0819 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [0.86.2] - Unreleased + +### Fixed + +- Fixed visibility glitch for widgets with an offset https://github.com/Textualize/textual/pull/5253 + ## [0.86.1] - 2024-11-16 ### Fixed From fbb870d17cee50d631aa019998ecf45b409f9f59 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 18 Nov 2024 16:05:09 +0000 Subject: [PATCH 11/12] Update tests/test_compositor.py Co-authored-by: Darren Burns --- tests/test_compositor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_compositor.py b/tests/test_compositor.py index 27f23d5e00..e5dbcdbc6d 100644 --- a/tests/test_compositor.py +++ b/tests/test_compositor.py @@ -4,7 +4,9 @@ async def test_compositor_scroll_placements(): - """Regression test for https://github.com/Textualize/textual/issues/5249""" + """Regression test for https://github.com/Textualize/textual/issues/5249 + The Static should remain visible. + """ class ScrollApp(App): CSS = """ From f91742172741ee7365723980ce2f76efa2ba1671 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 18 Nov 2024 16:07:39 +0000 Subject: [PATCH 12/12] version bump --- CHANGELOG.md | 5 ++++- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 649b368099..2c2f3b2337 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). -## [0.86.2] - Unreleased +## [0.86.2] - 2024-11-18 ### Fixed @@ -2552,6 +2552,9 @@ https://textual.textualize.io/blog/2022/11/08/version-040/#version-040 - New handler system for messages that doesn't require inheritance - Improved traceback handling + +[0.86.2]: https://github.com/Textualize/textual/compare/v0.86.1...v0.86.2 +[0.86.1]: https://github.com/Textualize/textual/compare/v0.86.0...v0.86.1 [0.86.0]: https://github.com/Textualize/textual/compare/v0.85.2...v0.86.0 [0.85.2]: https://github.com/Textualize/textual/compare/v0.85.1...v0.85.2 [0.85.1]: https://github.com/Textualize/textual/compare/v0.85.0...v0.85.1 diff --git a/pyproject.toml b/pyproject.toml index 32baaceca2..45b8f4e573 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "textual" -version = "0.86.1" +version = "0.86.2" homepage = "https://github.com/Textualize/textual" repository = "https://github.com/Textualize/textual" documentation = "https://textual.textualize.io/"