From 8d3b0f22eca8babf95470acf39fc626a1b0be1b4 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 21 Nov 2022 17:33:31 +0100 Subject: [PATCH 01/12] first iteration of spatial map --- src/textual/_spatial_map.py | 101 ++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 src/textual/_spatial_map.py diff --git a/src/textual/_spatial_map.py b/src/textual/_spatial_map.py new file mode 100644 index 000000000..eec69d8fe --- /dev/null +++ b/src/textual/_spatial_map.py @@ -0,0 +1,101 @@ +from __future__ import annotations + +from collections import defaultdict +from itertools import product +from typing import Iterable, Mapping + +from ._layout import WidgetPlacement +from .geometry import Region + + +class SpatialMap: + """An object to return WidgetPlacements within a given region. + + The widget area is split in to a regular grid of buckets. Each placement is assigned to + any bucket it overlaps, which may be 1 or more buckets. + + The `get_placements` function will calculate which buckets overlap the screen area, and combine + the placements from those buckets. This generally means that widgets that aren't overlapping or + near the screen area can be quickly discarded. The result will typically be a superset of visible + placements, which can then be filtered normally. + + """ + + def __init__( + self, + placements: Iterable[WidgetPlacement], + block_width: int = 80, + block_height: int = 80, + ) -> None: + self._placements = placements + self._block_width = block_width + self._block_height = block_height + self._map: defaultdict[tuple[int, int], list[WidgetPlacement]] | None = None + + @property + def placement_map(self) -> Mapping[tuple[int, int], list[WidgetPlacement]]: + """A mapping of block coordinate on to widget placement. + + Returns: + Mapping[tuple[int, int], list[WidgetPlacement]]: Mapping. + """ + if self._map is None: + self._map = self._build_placements(self._placements) + return self._map + return self._map + + def _build_placements( + self, placements: Iterable[WidgetPlacement] + ) -> defaultdict[tuple[int, int], list[WidgetPlacement]]: + """Add placements to map. + + Args: + placements (Iterable[WidgetPlacement]): A number of placements. + """ + map: defaultdict[tuple[int, int], list[WidgetPlacement]] = defaultdict(list) + get_bucket = map.__getitem__ + + block_width = self._block_width + block_height = self._block_height + + for placement in placements: + x1, y1, width, height = placement.region + x2 = x1 + width + y2 = y1 + height + for coord in product( + range(x1 // block_width, x2 // block_width + 1), + range(y1 // block_height, y2 // block_height + 1), + ): + get_bucket(coord).append(placement) + return map + + def get_placements(self, screen_region: Region) -> Iterable[WidgetPlacement]: + """Get placements that may overlap a given region. There may be false positives, + but no false negatives. + + Args: + region (Region): Container region. + + Returns: + set[WidgetPlacement]: Set of Widget placements. + """ + x1, y1, width, height = screen_region + x2 = x1 + width + y2 = y1 + height + block_width = self._block_width + block_height = self._block_height + + placements: set[WidgetPlacement] = set() + extend_placements = placements.update + map = self.placement_map + map_get = map.get + + for coord in product( + range(x1 // block_width, x2 // block_width + 1), + range(y1 // block_height, y2 // block_height + 1), + ): + block_placements = map_get(coord) + if block_placements is not None: + extend_placements(block_placements) + + return placements From 2e698b12002b7067b82915f8ae0c6f291f55d92e Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 21 Nov 2022 17:54:33 +0100 Subject: [PATCH 02/12] promoted to public --- src/textual/{_spatial_map.py => spatial_map.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/textual/{_spatial_map.py => spatial_map.py} (97%) diff --git a/src/textual/_spatial_map.py b/src/textual/spatial_map.py similarity index 97% rename from src/textual/_spatial_map.py rename to src/textual/spatial_map.py index eec69d8fe..416e2e8e1 100644 --- a/src/textual/_spatial_map.py +++ b/src/textual/spatial_map.py @@ -77,7 +77,7 @@ class SpatialMap: region (Region): Container region. Returns: - set[WidgetPlacement]: Set of Widget placements. + Iterable[WidgetPlacement]: A super-set of Widget placements that may be in the screen. """ x1, y1, width, height = screen_region x2 = x1 + width From 01a3838315c8cba3dc702382890e9e655132e008 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 21 Nov 2022 18:04:33 +0100 Subject: [PATCH 03/12] docstring --- src/textual/spatial_map.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/spatial_map.py b/src/textual/spatial_map.py index 416e2e8e1..b8e9e8884 100644 --- a/src/textual/spatial_map.py +++ b/src/textual/spatial_map.py @@ -37,7 +37,7 @@ class SpatialMap: """A mapping of block coordinate on to widget placement. Returns: - Mapping[tuple[int, int], list[WidgetPlacement]]: Mapping. + Mapping[tuple[int, int], list[WidgetPlacement]]: Mapping of coord to list of placements. """ if self._map is None: self._map = self._build_placements(self._placements) From f84313dac7c649405f83e6f15d974c1271735f91 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 23 Nov 2022 22:12:44 +0800 Subject: [PATCH 04/12] integrated spatial map --- src/textual/_compositor.py | 7 ++++- src/textual/_layout.py | 3 +- .../{spatial_map.py => _spatial_map.py} | 31 ++++++++++--------- src/textual/widget.py | 10 ++++-- 4 files changed, 31 insertions(+), 20 deletions(-) rename src/textual/{spatial_map.py => _spatial_map.py} (80%) diff --git a/src/textual/_compositor.py b/src/textual/_compositor.py index ab9d69eca..47305047e 100644 --- a/src/textual/_compositor.py +++ b/src/textual/_compositor.py @@ -381,9 +381,14 @@ class Compositor: if widget.is_container: # Arrange the layout - placements, arranged_widgets, spacing = widget._arrange( + spatial_map, arranged_widgets, spacing = widget._arrange( child_region.size ) + + placements = spatial_map.get_placements( + child_region.reset_offset.translate(widget.scroll_offset) + ) + widgets.update(arranged_widgets) # An offset added to all placements diff --git a/src/textual/_layout.py b/src/textual/_layout.py index fc9f21173..25d2d27c7 100644 --- a/src/textual/_layout.py +++ b/src/textual/_layout.py @@ -8,9 +8,10 @@ from ._typing import TypeAlias if TYPE_CHECKING: from .widget import Widget + from ._spatial_map import SpatialMap ArrangeResult: TypeAlias = "tuple[list[WidgetPlacement], set[Widget]]" -DockArrangeResult: TypeAlias = "tuple[list[WidgetPlacement], set[Widget], Spacing]" +DockArrangeResult: TypeAlias = "SpacialMap, set[Widget], Spacing]" class WidgetPlacement(NamedTuple): diff --git a/src/textual/spatial_map.py b/src/textual/_spatial_map.py similarity index 80% rename from src/textual/spatial_map.py rename to src/textual/_spatial_map.py index b8e9e8884..c55caa9a6 100644 --- a/src/textual/spatial_map.py +++ b/src/textual/_spatial_map.py @@ -2,10 +2,12 @@ from __future__ import annotations from collections import defaultdict from itertools import product -from typing import Iterable, Mapping +from operator import attrgetter +from typing import Iterable, Mapping, Sequence from ._layout import WidgetPlacement from .geometry import Region +from ._partition import partition class SpatialMap: @@ -23,26 +25,23 @@ class SpatialMap: def __init__( self, - placements: Iterable[WidgetPlacement], + placements: Sequence[WidgetPlacement], block_width: int = 80, block_height: int = 80, ) -> None: self._placements = placements self._block_width = block_width self._block_height = block_height + self._fixed: list[WidgetPlacement] = [] self._map: defaultdict[tuple[int, int], list[WidgetPlacement]] | None = None - @property - def placement_map(self) -> Mapping[tuple[int, int], list[WidgetPlacement]]: - """A mapping of block coordinate on to widget placement. + self.placement_map = self._build_placements(placements) - Returns: - Mapping[tuple[int, int], list[WidgetPlacement]]: Mapping of coord to list of placements. - """ - if self._map is None: - self._map = self._build_placements(self._placements) - return self._map - return self._map + def __iter__(self) -> Iterable[WidgetPlacement]: + yield from self._placements + + def __reversed__(self) -> Iterable[WidgetPlacement]: + yield from reversed(self._placements) def _build_placements( self, placements: Iterable[WidgetPlacement] @@ -58,6 +57,8 @@ class SpatialMap: block_width = self._block_width block_height = self._block_height + placements, self._fixed = partition(attrgetter("fixed"), placements) + for placement in placements: x1, y1, width, height = placement.region x2 = x1 + width @@ -69,7 +70,7 @@ class SpatialMap: get_bucket(coord).append(placement) return map - def get_placements(self, screen_region: Region) -> Iterable[WidgetPlacement]: + def get_placements(self, screen_region: Region) -> list[WidgetPlacement]: """Get placements that may overlap a given region. There may be false positives, but no false negatives. @@ -85,7 +86,7 @@ class SpatialMap: block_width = self._block_width block_height = self._block_height - placements: set[WidgetPlacement] = set() + placements: set[WidgetPlacement] = set(self._fixed) extend_placements = placements.update map = self.placement_map map_get = map.get @@ -98,4 +99,4 @@ class SpatialMap: if block_placements is not None: extend_placements(block_placements) - return placements + return list(placements) diff --git a/src/textual/widget.py b/src/textual/widget.py index 3dc821e95..292b670ba 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -1,8 +1,8 @@ from __future__ import annotations -from collections import Counter from asyncio import Event as AsyncEvent from asyncio import Lock, create_task, wait +from collections import Counter from fractions import Fraction from itertools import islice from operator import attrgetter @@ -39,6 +39,7 @@ from ._context import active_app from ._easing import DEFAULT_SCROLL_EASING from ._layout import Layout from ._segment_tools import align_lines +from ._spatial_map import SpatialMap from ._styles_cache import StylesCache from ._types import Lines from .await_remove import AwaitRemove @@ -53,7 +54,6 @@ from .message import Message from .messages import CallbackType from .reactive import Reactive from .render import measure -from .await_remove import AwaitRemove from .walk import walk_depth_first if TYPE_CHECKING: @@ -426,7 +426,11 @@ class Widget(DOMNode): return self._arrangement self._arrangement_cache_key = arrange_cache_key - self._arrangement = arrange(self, self.children, size, self.screen.size) + placements, widgets, spacing = arrange( + self, self.children, size, self.screen.size + ) + arrange_result = SpatialMap(placements), widgets, spacing + self._arrangement = arrange_result return self._arrangement def _clear_arrangement_cache(self) -> None: From 68d74404740a1f69c3ea22cc74ba3532544b8675 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 23 Nov 2022 23:13:58 +0800 Subject: [PATCH 05/12] fix for update region --- src/textual/_compositor.py | 10 ++++------ src/textual/_spatial_map.py | 16 ++++++++++++++-- src/textual/_styles_cache.py | 26 +++++++++++++++++++++----- src/textual/css/_style_properties.py | 2 +- src/textual/css/styles.py | 17 ++++++++++++----- 5 files changed, 52 insertions(+), 19 deletions(-) diff --git a/src/textual/_compositor.py b/src/textual/_compositor.py index 47305047e..75dc2a51f 100644 --- a/src/textual/_compositor.py +++ b/src/textual/_compositor.py @@ -384,6 +384,9 @@ class Compositor: spatial_map, arranged_widgets, spacing = widget._arrange( child_region.size ) + total_region = total_region.union( + spatial_map.total_region.grow(spacing) + ) placements = spatial_map.get_placements( child_region.reset_offset.translate(widget.scroll_offset) @@ -402,16 +405,11 @@ class Compositor: get_layer_index = layers_to_index.get # Add all the widgets - for sub_region, margin, sub_widget, z, fixed in reversed( - placements - ): + for sub_region, _, sub_widget, z, fixed in reversed(placements): # Combine regions with children to calculate the "virtual size" if fixed: widget_region = sub_region + placement_offset else: - total_region = total_region.union( - sub_region.grow(spacing + margin) - ) widget_region = sub_region + placement_scroll_offset widget_order = order + ( diff --git a/src/textual/_spatial_map.py b/src/textual/_spatial_map.py index c55caa9a6..eb2685554 100644 --- a/src/textual/_spatial_map.py +++ b/src/textual/_spatial_map.py @@ -3,10 +3,10 @@ from __future__ import annotations from collections import defaultdict from itertools import product from operator import attrgetter -from typing import Iterable, Mapping, Sequence +from typing import Iterable, Sequence from ._layout import WidgetPlacement -from .geometry import Region +from .geometry import Region, Spacing from ._partition import partition @@ -26,10 +26,12 @@ class SpatialMap: def __init__( self, placements: Sequence[WidgetPlacement], + *, block_width: int = 80, block_height: int = 80, ) -> None: self._placements = placements + self._total_region = Region() self._block_width = block_width self._block_height = block_height self._fixed: list[WidgetPlacement] = [] @@ -37,6 +39,8 @@ class SpatialMap: self.placement_map = self._build_placements(placements) + print("SPATIAL", len(placements)) + def __iter__(self) -> Iterable[WidgetPlacement]: yield from self._placements @@ -57,6 +61,14 @@ class SpatialMap: block_width = self._block_width block_height = self._block_height + self.total_region = Region.from_union( + [ + placement.region.grow(placement.margin) + for placement in placements + if not placement.fixed + ] + ) + placements, self._fixed = partition(attrgetter("fixed"), placements) for placement in placements: diff --git a/src/textual/_styles_cache.py b/src/textual/_styles_cache.py index 8fa39e047..f3b738cce 100644 --- a/src/textual/_styles_cache.py +++ b/src/textual/_styles_cache.py @@ -1,5 +1,7 @@ from __future__ import annotations +from functools import lru_cache +from sys import intern from typing import TYPE_CHECKING, Callable, Iterable, List from rich.segment import Segment @@ -52,6 +54,20 @@ def style_links( return segments +@lru_cache(1024 * 8) +def make_blank(width, style: Style) -> Segment: + """Make a blank segment. + + Args: + width (_type_): Width of blank. + style (Style): Style of blank. + + Returns: + Segment: A single segment + """ + return Segment(intern(" " * width), style) + + class StylesCache: """Responsible for rendering CSS Styles and keeping a cache of rendered lines. @@ -319,20 +335,20 @@ class StylesCache: right_style = from_color(color=(background + border_right_color).rich_color) right = get_box(border_right, inner, outer, right_style)[1][2] if border_left and border_right: - line = [left, Segment(" " * (width - 2), background_style), right] + line = [left, make_blank(width - 2, background_style), right] elif border_left: - line = [left, Segment(" " * (width - 1), background_style)] + line = [left, make_blank(width - 1, background_style)] elif border_right: - line = [Segment(" " * (width - 1), background_style), right] + line = [make_blank(width, background_style), right] else: - line = [Segment(" " * width, background_style)] + line = [make_blank(width, background_style)] else: # Content with border and padding (C) content_y = y - gutter.top if content_y < content_height: line = render_content_line(y - gutter.top) else: - line = [Segment(" " * content_width, inner)] + line = [make_blank(content_width, inner)] if inner: line = Segment.apply_style(line, inner) line = line_pad(line, pad_left, pad_right, inner) diff --git a/src/textual/css/_style_properties.py b/src/textual/css/_style_properties.py index 5770add70..15a1e8474 100644 --- a/src/textual/css/_style_properties.py +++ b/src/textual/css/_style_properties.py @@ -305,7 +305,7 @@ class BoxProperty: current_value: tuple[str, Color] = cast( "tuple[str, Color]", obj.get_rule(self.name) ) - has_edge = current_value and current_value[0] + has_edge = bool(current_value and current_value[0]) new_edge = bool(_type) if obj.set_rule(self.name, new_value): obj.refresh(layout=has_edge != new_edge) diff --git a/src/textual/css/styles.py b/src/textual/css/styles.py index c2bf84364..2fe674a47 100644 --- a/src/textual/css/styles.py +++ b/src/textual/css/styles.py @@ -594,8 +594,10 @@ class Styles(StylesBase): Returns: bool: ``True`` if a rule was cleared, or ``False`` if it was already not set. """ - self._updates += 1 - return self._rules.pop(rule, None) is not None + changed = self._rules.pop(rule, None) is not None + if changed: + self._updates += 1 + return changed def get_rules(self) -> RulesMap: return self._rules.copy() @@ -610,12 +612,17 @@ class Styles(StylesBase): Returns: bool: ``True`` if the rule changed, otherwise ``False``. """ - self._updates += 1 if value is None: - return self._rules.pop(rule, None) is not None + changed = self._rules.pop(rule, None) is not None + if changed: + self._updates += 1 + return changed current = self._rules.get(rule) self._rules[rule] = value - return current != value + changed = current != value + if changed: + self._updates += 1 + return changed def get_rule(self, rule: str, default: object = None) -> object: return self._rules.get(rule, default) From 9dc3e13a3f91040576f28fc1c3581c9b47734c80 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 24 Nov 2022 17:35:00 +0800 Subject: [PATCH 06/12] simplify refresh --- src/textual/_arrange.py | 2 +- src/textual/_compositor.py | 8 ++++---- src/textual/_spatial_map.py | 27 +++++++++++++++++++++------ src/textual/widget.py | 10 ++++------ 4 files changed, 30 insertions(+), 17 deletions(-) diff --git a/src/textual/_arrange.py b/src/textual/_arrange.py index 2bf706e0f..102c17844 100644 --- a/src/textual/_arrange.py +++ b/src/textual/_arrange.py @@ -50,7 +50,7 @@ def arrange( get_dock = attrgetter("styles.dock") styles = widget.styles - for widgets in dock_layers.values(): + for index, widgets in enumerate(dock_layers.values()): layout_widgets, dock_widgets = partition(get_dock, widgets) diff --git a/src/textual/_compositor.py b/src/textual/_compositor.py index 75dc2a51f..689e5b93c 100644 --- a/src/textual/_compositor.py +++ b/src/textual/_compositor.py @@ -388,16 +388,16 @@ class Compositor: spatial_map.total_region.grow(spacing) ) - placements = spatial_map.get_placements( - child_region.reset_offset.translate(widget.scroll_offset) - ) - widgets.update(arranged_widgets) # An offset added to all placements placement_offset = container_region.offset + layout_offset placement_scroll_offset = placement_offset - widget.scroll_offset + placements = spatial_map.get_placements( + (child_region.size.region).translate(widget.scroll_offset) + ) + _layers = widget.layers layers_to_index = { layer_name: index for index, layer_name in enumerate(_layers) diff --git a/src/textual/_spatial_map.py b/src/textual/_spatial_map.py index eb2685554..8f5d9879e 100644 --- a/src/textual/_spatial_map.py +++ b/src/textual/_spatial_map.py @@ -8,6 +8,7 @@ from typing import Iterable, Sequence from ._layout import WidgetPlacement from .geometry import Region, Spacing from ._partition import partition +from ._profile import timer class SpatialMap: @@ -27,8 +28,8 @@ class SpatialMap: self, placements: Sequence[WidgetPlacement], *, - block_width: int = 80, - block_height: int = 80, + block_width: int = 40, + block_height: int = 20, ) -> None: self._placements = placements self._total_region = Region() @@ -39,8 +40,6 @@ class SpatialMap: self.placement_map = self._build_placements(placements) - print("SPATIAL", len(placements)) - def __iter__(self) -> Iterable[WidgetPlacement]: yield from self._placements @@ -92,17 +91,24 @@ class SpatialMap: Returns: Iterable[WidgetPlacement]: A super-set of Widget placements that may be in the screen. """ + return [ + placement + for placement in self._placements + if screen_region.overlaps(placement.region) + ] x1, y1, width, height = screen_region x2 = x1 + width y2 = y1 + height block_width = self._block_width block_height = self._block_height - placements: set[WidgetPlacement] = set(self._fixed) + placements: set[WidgetPlacement] = set() extend_placements = placements.update map = self.placement_map map_get = map.get + overlaps_screen = screen_region.overlaps + for coord in product( range(x1 // block_width, x2 // block_width + 1), range(y1 // block_height, y2 // block_height + 1), @@ -111,4 +117,13 @@ class SpatialMap: if block_placements is not None: extend_placements(block_placements) - return list(placements) + _placements = [ + placement + for placement in self._placements + if placement in placements and not placement.fixed + ] + + visible_placements = self._fixed + [ + placement for placement in _placements if overlaps_screen(placement.region) + ] + return visible_placements diff --git a/src/textual/widget.py b/src/textual/widget.py index 292b670ba..275320558 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -759,14 +759,12 @@ class Widget(DOMNode): def watch_scroll_x(self, new_value: float) -> None: if self.show_horizontal_scrollbar: self.horizontal_scrollbar.position = int(new_value) - self.horizontal_scrollbar.refresh() - self.refresh(layout=True) + self.refresh(layout=True, repaint=False) def watch_scroll_y(self, new_value: float) -> None: if self.show_vertical_scrollbar: self.vertical_scrollbar.position = int(new_value) - self.vertical_scrollbar.refresh() - self.refresh(layout=True) + self.refresh(layout=True, repaint=False) def validate_scroll_x(self, value: float) -> float: return clamp(value, 0, self.max_scroll_x) @@ -2134,12 +2132,12 @@ class Widget(DOMNode): layout (bool, optional): Also layout widgets in the view. Defaults to False. """ - if layout: + if layout and not self._layout_required: self._layout_required = True if isinstance(self._parent, Widget): self._parent._clear_arrangement_cache() - if repaint: + if repaint and not self._repaint_required: self._set_dirty(*regions) self._content_width_cache = (None, 0) self._content_height_cache = (None, 0) From 935020101d719c1821e15c94817d1f35c6017881 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 24 Nov 2022 17:41:24 +0800 Subject: [PATCH 07/12] remove spatial map --- src/textual/_arrange.py | 2 +- src/textual/_compositor.py | 16 ++--- src/textual/_layout.py | 3 +- src/textual/_spatial_map.py | 129 ------------------------------------ src/textual/widget.py | 3 +- 5 files changed, 10 insertions(+), 143 deletions(-) delete mode 100644 src/textual/_spatial_map.py diff --git a/src/textual/_arrange.py b/src/textual/_arrange.py index 102c17844..2bf706e0f 100644 --- a/src/textual/_arrange.py +++ b/src/textual/_arrange.py @@ -50,7 +50,7 @@ def arrange( get_dock = attrgetter("styles.dock") styles = widget.styles - for index, widgets in enumerate(dock_layers.values()): + for widgets in dock_layers.values(): layout_widgets, dock_widgets = partition(get_dock, widgets) diff --git a/src/textual/_compositor.py b/src/textual/_compositor.py index 689e5b93c..d6fd0403c 100644 --- a/src/textual/_compositor.py +++ b/src/textual/_compositor.py @@ -381,12 +381,9 @@ class Compositor: if widget.is_container: # Arrange the layout - spatial_map, arranged_widgets, spacing = widget._arrange( + placements, arranged_widgets, spacing = widget._arrange( child_region.size ) - total_region = total_region.union( - spatial_map.total_region.grow(spacing) - ) widgets.update(arranged_widgets) @@ -394,10 +391,6 @@ class Compositor: placement_offset = container_region.offset + layout_offset placement_scroll_offset = placement_offset - widget.scroll_offset - placements = spatial_map.get_placements( - (child_region.size.region).translate(widget.scroll_offset) - ) - _layers = widget.layers layers_to_index = { layer_name: index for index, layer_name in enumerate(_layers) @@ -405,11 +398,16 @@ class Compositor: get_layer_index = layers_to_index.get # Add all the widgets - for sub_region, _, sub_widget, z, fixed in reversed(placements): + for sub_region, margin, sub_widget, z, fixed in reversed( + placements + ): # Combine regions with children to calculate the "virtual size" if fixed: widget_region = sub_region + placement_offset else: + total_region = total_region.union( + sub_region.grow(spacing + margin) + ) widget_region = sub_region + placement_scroll_offset widget_order = order + ( diff --git a/src/textual/_layout.py b/src/textual/_layout.py index 25d2d27c7..a87061b36 100644 --- a/src/textual/_layout.py +++ b/src/textual/_layout.py @@ -8,10 +8,9 @@ from ._typing import TypeAlias if TYPE_CHECKING: from .widget import Widget - from ._spatial_map import SpatialMap ArrangeResult: TypeAlias = "tuple[list[WidgetPlacement], set[Widget]]" -DockArrangeResult: TypeAlias = "SpacialMap, set[Widget], Spacing]" +DockArrangeResult: TypeAlias = "list[WidgetPlacement], set[Widget], Spacing]" class WidgetPlacement(NamedTuple): diff --git a/src/textual/_spatial_map.py b/src/textual/_spatial_map.py deleted file mode 100644 index 8f5d9879e..000000000 --- a/src/textual/_spatial_map.py +++ /dev/null @@ -1,129 +0,0 @@ -from __future__ import annotations - -from collections import defaultdict -from itertools import product -from operator import attrgetter -from typing import Iterable, Sequence - -from ._layout import WidgetPlacement -from .geometry import Region, Spacing -from ._partition import partition -from ._profile import timer - - -class SpatialMap: - """An object to return WidgetPlacements within a given region. - - The widget area is split in to a regular grid of buckets. Each placement is assigned to - any bucket it overlaps, which may be 1 or more buckets. - - The `get_placements` function will calculate which buckets overlap the screen area, and combine - the placements from those buckets. This generally means that widgets that aren't overlapping or - near the screen area can be quickly discarded. The result will typically be a superset of visible - placements, which can then be filtered normally. - - """ - - def __init__( - self, - placements: Sequence[WidgetPlacement], - *, - block_width: int = 40, - block_height: int = 20, - ) -> None: - self._placements = placements - self._total_region = Region() - self._block_width = block_width - self._block_height = block_height - self._fixed: list[WidgetPlacement] = [] - self._map: defaultdict[tuple[int, int], list[WidgetPlacement]] | None = None - - self.placement_map = self._build_placements(placements) - - def __iter__(self) -> Iterable[WidgetPlacement]: - yield from self._placements - - def __reversed__(self) -> Iterable[WidgetPlacement]: - yield from reversed(self._placements) - - def _build_placements( - self, placements: Iterable[WidgetPlacement] - ) -> defaultdict[tuple[int, int], list[WidgetPlacement]]: - """Add placements to map. - - Args: - placements (Iterable[WidgetPlacement]): A number of placements. - """ - map: defaultdict[tuple[int, int], list[WidgetPlacement]] = defaultdict(list) - get_bucket = map.__getitem__ - - block_width = self._block_width - block_height = self._block_height - - self.total_region = Region.from_union( - [ - placement.region.grow(placement.margin) - for placement in placements - if not placement.fixed - ] - ) - - placements, self._fixed = partition(attrgetter("fixed"), placements) - - for placement in placements: - x1, y1, width, height = placement.region - x2 = x1 + width - y2 = y1 + height - for coord in product( - range(x1 // block_width, x2 // block_width + 1), - range(y1 // block_height, y2 // block_height + 1), - ): - get_bucket(coord).append(placement) - return map - - def get_placements(self, screen_region: Region) -> list[WidgetPlacement]: - """Get placements that may overlap a given region. There may be false positives, - but no false negatives. - - Args: - region (Region): Container region. - - Returns: - Iterable[WidgetPlacement]: A super-set of Widget placements that may be in the screen. - """ - return [ - placement - for placement in self._placements - if screen_region.overlaps(placement.region) - ] - x1, y1, width, height = screen_region - x2 = x1 + width - y2 = y1 + height - block_width = self._block_width - block_height = self._block_height - - placements: set[WidgetPlacement] = set() - extend_placements = placements.update - map = self.placement_map - map_get = map.get - - overlaps_screen = screen_region.overlaps - - for coord in product( - range(x1 // block_width, x2 // block_width + 1), - range(y1 // block_height, y2 // block_height + 1), - ): - block_placements = map_get(coord) - if block_placements is not None: - extend_placements(block_placements) - - _placements = [ - placement - for placement in self._placements - if placement in placements and not placement.fixed - ] - - visible_placements = self._fixed + [ - placement for placement in _placements if overlaps_screen(placement.region) - ] - return visible_placements diff --git a/src/textual/widget.py b/src/textual/widget.py index 275320558..4f9ce86b4 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -39,7 +39,6 @@ from ._context import active_app from ._easing import DEFAULT_SCROLL_EASING from ._layout import Layout from ._segment_tools import align_lines -from ._spatial_map import SpatialMap from ._styles_cache import StylesCache from ._types import Lines from .await_remove import AwaitRemove @@ -429,7 +428,7 @@ class Widget(DOMNode): placements, widgets, spacing = arrange( self, self.children, size, self.screen.size ) - arrange_result = SpatialMap(placements), widgets, spacing + arrange_result = placements, widgets, spacing self._arrangement = arrange_result return self._arrangement From 40dbffc46c988db13c2f232aff596f0191167726 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 24 Nov 2022 17:42:07 +0800 Subject: [PATCH 08/12] Ws --- src/textual/_compositor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/textual/_compositor.py b/src/textual/_compositor.py index d6fd0403c..ab9d69eca 100644 --- a/src/textual/_compositor.py +++ b/src/textual/_compositor.py @@ -384,7 +384,6 @@ class Compositor: placements, arranged_widgets, spacing = widget._arrange( child_region.size ) - widgets.update(arranged_widgets) # An offset added to all placements From ce0773885fa674fbd44bdc12fed65f9b4a3cd4ef Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 24 Nov 2022 17:42:57 +0800 Subject: [PATCH 09/12] fix alias --- src/textual/_layout.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/_layout.py b/src/textual/_layout.py index a87061b36..ca5233104 100644 --- a/src/textual/_layout.py +++ b/src/textual/_layout.py @@ -10,7 +10,7 @@ if TYPE_CHECKING: from .widget import Widget ArrangeResult: TypeAlias = "tuple[list[WidgetPlacement], set[Widget]]" -DockArrangeResult: TypeAlias = "list[WidgetPlacement], set[Widget], Spacing]" +DockArrangeResult: TypeAlias = "tuple[list[WidgetPlacement], set[Widget], Spacing]]" class WidgetPlacement(NamedTuple): From 84f24d947b879279f325834001a55274e47d8b16 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 24 Nov 2022 17:44:16 +0800 Subject: [PATCH 10/12] restore arrangement --- src/textual/widget.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/textual/widget.py b/src/textual/widget.py index 4f9ce86b4..acdac6a9f 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -425,11 +425,7 @@ class Widget(DOMNode): return self._arrangement self._arrangement_cache_key = arrange_cache_key - placements, widgets, spacing = arrange( - self, self.children, size, self.screen.size - ) - arrange_result = placements, widgets, spacing - self._arrangement = arrange_result + self._arrangement = arrange(self, self.children, size, self.screen.size) return self._arrangement def _clear_arrangement_cache(self) -> None: From 4057744c8848176d83264d7a5812ef7d806c1d2d Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 24 Nov 2022 17:49:22 +0800 Subject: [PATCH 11/12] fix blank --- src/textual/_styles_cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/_styles_cache.py b/src/textual/_styles_cache.py index f3b738cce..d502a9a60 100644 --- a/src/textual/_styles_cache.py +++ b/src/textual/_styles_cache.py @@ -339,7 +339,7 @@ class StylesCache: elif border_left: line = [left, make_blank(width - 1, background_style)] elif border_right: - line = [make_blank(width, background_style), right] + line = [make_blank(width - 1, background_style), right] else: line = [make_blank(width, background_style)] else: From edf716b5086aaf023f37c480e7bdc1a6b295f196 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 24 Nov 2022 17:55:14 +0800 Subject: [PATCH 12/12] fix updates --- src/textual/widget.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/textual/widget.py b/src/textual/widget.py index acdac6a9f..70a3b353a 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -2127,12 +2127,12 @@ class Widget(DOMNode): layout (bool, optional): Also layout widgets in the view. Defaults to False. """ - if layout and not self._layout_required: + if layout: self._layout_required = True if isinstance(self._parent, Widget): self._parent._clear_arrangement_cache() - if repaint and not self._repaint_required: + if repaint: self._set_dirty(*regions) self._content_width_cache = (None, 0) self._content_height_cache = (None, 0)