faster layout in stream

This commit is contained in:
Will McGugan
2025-10-08 15:50:50 +01:00
parent 46cc18879e
commit 665610ec0f
5 changed files with 38 additions and 11 deletions

View File

@@ -53,7 +53,6 @@ def arrange(
Returns:
Widget arrangement information.
"""
print("ARRANGE", widget)
placements: list[WidgetPlacement] = []
scroll_spacing = NULL_SPACING
styles = widget.styles

View File

@@ -1,5 +1,6 @@
from __future__ import annotations
from itertools import zip_longest
from typing import TYPE_CHECKING
from textual.geometry import NULL_OFFSET, Region, Size
@@ -30,6 +31,9 @@ class StreamLayout(Layout):
name = "stream"
def __init__(self) -> None:
self._cache: dict[object, list[WidgetPlacement]] = {}
def arrange(
self, parent: Widget, children: list[Widget], size: Size, greedy: bool = True
) -> ArrangeResult:
@@ -38,6 +42,10 @@ class StreamLayout(Layout):
return []
viewport = parent.app.viewport_size
cache_key = (size, viewport)
previous_results = self._cache.get(cache_key, None) or []
layout_widgets = parent.screen._layout_widgets.get(parent, [])
_Region = Region
_WidgetPlacement = WidgetPlacement
@@ -48,7 +56,18 @@ class StreamLayout(Layout):
previous_margin = first_child_styles.margin.top
null_offset = NULL_OFFSET
for widget in children:
pre_populate = bool(previous_results and layout_widgets)
for widget, placement in zip_longest(children, previous_results):
if pre_populate and placement is not None and widget == placement.widget:
if widget in layout_widgets:
pre_populate = False
else:
placements.append(placement)
y = placement.region.bottom
continue
if widget is None:
break
styles = widget.styles._base_styles
margin = styles.margin
gutter_width, gutter_height = styles.gutter.totals
@@ -85,6 +104,7 @@ class StreamLayout(Layout):
)
y += height
self._cache[cache_key] = placements
return placements
def get_content_width(self, widget: Widget, container: Size, viewport: Size) -> int:

View File

@@ -11,19 +11,15 @@ if TYPE_CHECKING:
from textual.geometry import Spacing
from textual.widget import Widget
from textual._profile import timer
class VerticalLayout(Layout):
"""Used to layout Widgets vertically on screen, from top to bottom."""
name = "vertical"
@timer("Vertical.arrange")
def arrange(
self, parent: Widget, children: list[Widget], size: Size, greedy: bool = True
) -> ArrangeResult:
print(parent, size, greedy, len(children))
parent.pre_layout(self)
placements: list[WidgetPlacement] = []
add_placement = placements.append

View File

@@ -362,8 +362,6 @@ class Reactive(Generic[ReactiveType]):
# Refresh according to descriptor flags
if self._layout or self._repaint or self._recompose:
if self._layout:
print(self, "layout", obj)
obj.refresh(
repaint=self._repaint,
layout=self._layout,

View File

@@ -321,7 +321,7 @@ class Screen(Generic[ScreenResultType], Widget):
self._css_update_count = -1
"""Track updates to CSS."""
self._layout_widgets: set[Widget] = set()
self._layout_widgets: dict[DOMNode, set[Widget]] = {}
"""Widgets whose layout may have changed."""
@property
@@ -1353,8 +1353,22 @@ class Screen(Generic[ScreenResultType], Widget):
async def _on_layout(self, message: messages.Layout) -> None:
message.stop()
message.prevent_default()
if message.widget not in self._layout_widgets:
self._layout_widgets.add(message.widget)
layout_required = True
widget: DOMNode = message.widget
for ancestor in message.widget.ancestors:
if not isinstance(ancestor, Widget):
break
if ancestor not in self._layout_widgets:
self._layout_widgets[ancestor] = set()
# assert isinstance(widget, Widget)
self._layout_widgets[ancestor].add(widget)
layout_required = True
if not ancestor.styles.auto_dimensions:
break
widget = ancestor
if layout_required:
self._layout_required = True
self.check_idle()