mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Implement Widget.render_delta_lines
The method `Widget.render_delta_lines` provides a mechanism for a widget to render itself and only issue strips that correspond to lines that actually changed.
This commit is contained in:
@@ -899,12 +899,18 @@ class Compositor:
|
||||
return self._cuts
|
||||
|
||||
def _get_renders(
|
||||
self, crop: Region | None = None
|
||||
self,
|
||||
crop: Region | None = None,
|
||||
allow_delta_updates: bool = False,
|
||||
) -> Iterable[tuple[Region, Region, list[Strip]]]:
|
||||
"""Get rendered widgets (lists of segments) in the composition.
|
||||
|
||||
Args:
|
||||
crop: Region to crop to, or `None` for entire screen.
|
||||
allow_delta_updates: Whether we can issue partial updates for widgets or
|
||||
not. If `True`, widgets with `_enable_delta_updates` will produce only
|
||||
updates for lines inside `crop` that have changed. If `False`, each
|
||||
widget produces the full render for the region inside `crop`.
|
||||
|
||||
Returns:
|
||||
An iterable of <region>, <clip region>, and <strips>
|
||||
@@ -935,17 +941,16 @@ class Compositor:
|
||||
|
||||
for widget, region, clip in widget_regions:
|
||||
if contains_region(clip, region):
|
||||
yield region, clip, widget.render_lines(
|
||||
_Region(0, 0, region.width, region.height)
|
||||
)
|
||||
region_to_render = _Region(0, 0, region.width, region.height)
|
||||
else:
|
||||
new_x, new_y, new_width, new_height = intersection(region, clip)
|
||||
if new_width and new_height:
|
||||
yield region, clip, widget.render_lines(
|
||||
_Region(
|
||||
new_x - region.x, new_y - region.y, new_width, new_height
|
||||
)
|
||||
)
|
||||
region_to_render = _Region(
|
||||
new_x - region.x, new_y - region.y, new_width, new_height
|
||||
)
|
||||
if allow_delta_updates and widget._enable_delta_updates:
|
||||
yield from widget.render_delta_lines(region, clip, region_to_render)
|
||||
else:
|
||||
yield region, clip, widget.render_lines(region_to_render)
|
||||
|
||||
def render_update(
|
||||
self, full: bool = False, screen_stack: list[Screen] | None = None
|
||||
@@ -995,7 +1000,7 @@ class Compositor:
|
||||
is_rendered_line = {y for y, _, _ in spans}.__contains__
|
||||
else:
|
||||
return None
|
||||
chops = self._render_chops(crop, is_rendered_line)
|
||||
chops = self._render_chops(crop, is_rendered_line, allow_delta_updates=True)
|
||||
chop_ends = [cut_set[1:] for cut_set in self.cuts]
|
||||
return ChopsUpdate(chops, spans, chop_ends)
|
||||
|
||||
@@ -1013,12 +1018,17 @@ class Compositor:
|
||||
self,
|
||||
crop: Region,
|
||||
is_rendered_line: Callable[[int], bool],
|
||||
allow_delta_updates: bool = False,
|
||||
) -> Sequence[Mapping[int, Strip | None]]:
|
||||
"""Render update 'chops'.
|
||||
|
||||
Args:
|
||||
crop: Region to crop to.
|
||||
is_rendered_line: Callable to check if line should be rendered.
|
||||
allow_delta_updates: Whether we can issue partial updates for widgets or
|
||||
not. If `True`, widgets with `_enable_delta_updates` will produce only
|
||||
updates for lines inside `crop` that have changed. If `False`, each
|
||||
widget produces the full render for the region inside `crop`.
|
||||
|
||||
Returns:
|
||||
Chops structure.
|
||||
@@ -1031,7 +1041,7 @@ class Compositor:
|
||||
cut_strips: Iterable[Strip]
|
||||
|
||||
# Go through all the renders in reverse order and fill buckets with no render
|
||||
renders = self._get_renders(crop)
|
||||
renders = self._get_renders(crop, allow_delta_updates=allow_delta_updates)
|
||||
intersection = Region.intersection
|
||||
|
||||
for region, clip, strips in renders:
|
||||
|
||||
@@ -234,6 +234,7 @@ class ScrollBar(Widget):
|
||||
self.grabbed_position: float = 0
|
||||
super().__init__(name=name)
|
||||
self.auto_links = False
|
||||
self._enable_delta_updates = False
|
||||
|
||||
window_virtual_size: Reactive[int] = Reactive(100)
|
||||
window_size: Reactive[int] = Reactive(0)
|
||||
@@ -366,6 +367,7 @@ class ScrollBarCorner(Widget):
|
||||
|
||||
def __init__(self, name: str | None = None):
|
||||
super().__init__(name=name)
|
||||
self._enable_delta_updates = False
|
||||
|
||||
def render(self) -> RenderableType:
|
||||
assert self.parent is not None
|
||||
|
||||
@@ -8,7 +8,7 @@ from asyncio import Lock, create_task, wait
|
||||
from collections import Counter
|
||||
from contextlib import asynccontextmanager
|
||||
from fractions import Fraction
|
||||
from itertools import islice
|
||||
from itertools import groupby, islice
|
||||
from types import TracebackType
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
@@ -361,6 +361,13 @@ class Widget(DOMNode):
|
||||
|
||||
self._styles_cache = StylesCache()
|
||||
self._rich_style_cache: dict[str, tuple[Style, Style]] = {}
|
||||
self._enable_delta_updates: bool = True
|
||||
"""Get the compositor to only rerender lines that changed between consecutive
|
||||
updates.
|
||||
"""
|
||||
self._delta_updates_cache: tuple[Region, Region, Region, list[Strip]] | None = (
|
||||
None
|
||||
)
|
||||
|
||||
self._tooltip: RenderableType | None = None
|
||||
"""The tooltip content."""
|
||||
@@ -3265,9 +3272,58 @@ class Widget(DOMNode):
|
||||
Returns:
|
||||
A list of list of segments.
|
||||
"""
|
||||
self._delta_updates_cache = None
|
||||
strips = self._styles_cache.render_widget(self, crop)
|
||||
return strips
|
||||
|
||||
def render_delta_lines(
|
||||
self,
|
||||
region: Region,
|
||||
clip: Region,
|
||||
region_to_render: Region,
|
||||
) -> Generator[tuple[Region, Region, list[Strip]], None, None]:
|
||||
"""Render the lines of the widget that changed since the last render.
|
||||
|
||||
When a widget is rendered consecutively in the same region
|
||||
"""
|
||||
print(self)
|
||||
strips = self._styles_cache.render_widget(self, region_to_render)
|
||||
|
||||
if self._delta_updates_cache is None:
|
||||
self._delta_updates_cache = (region, clip, region_to_render, strips)
|
||||
yield region, clip, strips
|
||||
return
|
||||
|
||||
cached_region, cached_clip, cached_region_to_render, cached_strips = (
|
||||
self._delta_updates_cache
|
||||
)
|
||||
self._delta_updates_cache = (region, clip, region_to_render, strips)
|
||||
if (
|
||||
cached_region != region
|
||||
or cached_clip != clip
|
||||
or cached_region_to_render != region_to_render
|
||||
):
|
||||
yield region, clip, strips
|
||||
return
|
||||
|
||||
cached_strips_iter = iter(cached_strips)
|
||||
y_offset = 0
|
||||
for matches_cache, new_strips in groupby(
|
||||
strips, key=lambda strip: strip == next(cached_strips_iter)
|
||||
):
|
||||
new_strips = list(new_strips)
|
||||
if matches_cache:
|
||||
y_offset += len(new_strips)
|
||||
continue
|
||||
reg = Region(
|
||||
region_to_render.x + region.x,
|
||||
region_to_render.y + region.y + y_offset,
|
||||
region.width,
|
||||
len(new_strips),
|
||||
)
|
||||
y_offset += len(new_strips)
|
||||
yield reg, clip, new_strips
|
||||
|
||||
def get_style_at(self, x: int, y: int) -> Style:
|
||||
"""Get the Rich style in a widget at a given relative offset.
|
||||
|
||||
|
||||
@@ -1008,6 +1008,15 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True):
|
||||
self.cursor_line = -1
|
||||
self.refresh()
|
||||
|
||||
def render_delta_lines(
|
||||
self,
|
||||
region: Region,
|
||||
clip: Region,
|
||||
region_to_render: Region,
|
||||
) -> tuple[Region, Region, list[Strip]]:
|
||||
self._pseudo_class_state = self.get_pseudo_class_state()
|
||||
return super().render_delta_lines(region, clip, region_to_render)
|
||||
|
||||
def render_lines(self, crop: Region) -> list[Strip]:
|
||||
self._pseudo_class_state = self.get_pseudo_class_state()
|
||||
return super().render_lines(crop)
|
||||
|
||||
Reference in New Issue
Block a user