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:
Rodrigo Girão Serrão
2024-03-28 15:30:59 +00:00
parent 69cc80b8f9
commit bf8c837a35
4 changed files with 90 additions and 13 deletions

View File

@@ -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:

View File

@@ -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

View File

@@ -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.

View File

@@ -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)