diff --git a/src/textual/_compositor.py b/src/textual/_compositor.py index 74023b89c..9a623fd13 100644 --- a/src/textual/_compositor.py +++ b/src/textual/_compositor.py @@ -14,6 +14,7 @@ without having to render the entire screen. from __future__ import annotations from itertools import chain +from collections import defaultdict from operator import itemgetter import sys from typing import Callable, cast, Iterator, Iterable, NamedTuple, TYPE_CHECKING @@ -203,6 +204,8 @@ class Compositor: # Regions that require an update self._dirty_regions: set[Region] = set() + self._layers_visible: dict[int, list[tuple[Widget, Region]]] | None = None + @classmethod def _regions_to_spans( cls, regions: Iterable[Region] @@ -257,6 +260,7 @@ class Compositor: """ self._cuts = None self._layers = None + self._layers_visible = None self.root = parent self.size = size @@ -328,6 +332,7 @@ class Compositor: } return self._visible_widgets + @timer("arrange") def _arrange_root( self, root: Widget, size: Size ) -> tuple[CompositorMap, set[Widget]]: @@ -475,6 +480,27 @@ class Compositor: ) return self._layers + @property + def layers_visible(self) -> dict[int, list[tuple[Widget, Region]]]: + """Visible widgets and regions in layers order.""" + + if self._layers_visible is None: + with timer("CALC LAYERS"): + layers_visible: dict[int, list[tuple[Widget, Region]]] + screen_region = self.size.region + _, screen_height = self.size + layers_visible = {y: [] for y in screen_region.line_range} + + visible_intersection = screen_region.intersection + + for widget, region, *_ in self: + (_x, y, _width, height) = region + if y + height > 0 and y < screen_height: + for y in visible_intersection(region).line_range: + layers_visible[y].append((widget, region)) + self._layers_visible = layers_visible + return self._layers_visible + def __iter__(self) -> Iterator[tuple[Widget, Region, Region, Size, Size]]: """Iterate map with information regarding each widget and is position @@ -514,10 +540,16 @@ class Compositor: tuple[Widget, Region]: A tuple of the widget and its region. """ # TODO: Optimize with some line based lookup + contains = Region.contains - for widget, cropped_region, region, *_ in self: - if contains(cropped_region, x, y) and widget.visible: + for widget, region in self.layers_visible.get(y, []): + if contains(region, x, y) and widget.visible: return widget, region + + # contains = Region.contains + # for widget, cropped_region, region, *_ in self: + # if contains(cropped_region, x, y) and widget.visible: + # return widget, region raise errors.NoWidget(f"No widget under screen coordinate ({x}, {y})") def get_widgets_at(self, x: int, y: int) -> Iterable[tuple[Widget, Region]]: @@ -531,10 +563,15 @@ class Compositor: Iterable[tuple[Widget, Region]]: Sequence of (WIDGET, REGION) tuples. """ contains = Region.contains - for widget, cropped_region, region, *_ in self: - if contains(cropped_region, x, y) and widget.visible: + for widget, region in self.layers_visible.get(y, []): + if contains(region, x, y) and widget.visible: yield widget, region + # contains = Region.contains + # for widget, cropped_region, region, *_ in self: + # if contains(cropped_region, x, y) and widget.visible: + # yield widget, region + def get_style_at(self, x: int, y: int) -> Style: """Get the Style at the given cell or Style.null() diff --git a/src/textual/widgets/_static.py b/src/textual/widgets/_static.py index 3e75adad8..01af1d953 100644 --- a/src/textual/widgets/_static.py +++ b/src/textual/widgets/_static.py @@ -86,12 +86,13 @@ class Static(Widget): """ return self._renderable - def update(self, renderable: RenderableType = "") -> None: + def update(self, renderable: RenderableType = "", *, layout: bool = False) -> None: """Update the widget's content area with new text or Rich renderable. Args: renderable (RenderableType, optional): A new rich renderable. Defaults to empty renderable; + layout (bool, optional): Perform a layout. Defaults to True. """ _check_renderable(renderable) self.renderable = renderable - self.refresh(layout=True) + self.refresh(layout=layout)