combine updates, cache arrangements

This commit is contained in:
Will McGugan
2022-05-30 16:03:10 +01:00
parent c1ad9c5365
commit 55543479ad
4 changed files with 40 additions and 19 deletions

View File

@@ -1,7 +1,6 @@
from __future__ import annotations from __future__ import annotations
from typing import Iterator, overload, TYPE_CHECKING from typing import Iterator, overload, TYPE_CHECKING
from weakref import ref
import rich.repr import rich.repr
@@ -19,7 +18,10 @@ class NodeList:
""" """
def __init__(self) -> None: def __init__(self) -> None:
# The nodes in the list
self._nodes: list[DOMNode] = [] self._nodes: list[DOMNode] = []
# Increments when list is updated (used for caching)
self._updates = 0
def __bool__(self) -> bool: def __bool__(self) -> bool:
return bool(self._nodes) return bool(self._nodes)
@@ -39,9 +41,11 @@ class NodeList:
def _append(self, widget: DOMNode) -> None: def _append(self, widget: DOMNode) -> None:
if widget not in self._nodes: if widget not in self._nodes:
self._nodes.append(widget) self._nodes.append(widget)
self._updates += 1
def _clear(self) -> None: def _clear(self) -> None:
del self._nodes[:] del self._nodes[:]
self._updates += 1
def __iter__(self) -> Iterator[DOMNode]: def __iter__(self) -> Iterator[DOMNode]:
return iter(self._nodes) return iter(self._nodes)

View File

@@ -274,7 +274,11 @@ class MessagePump:
for _cls, method in self._get_dispatch_methods( for _cls, method in self._get_dispatch_methods(
"on_idle", event "on_idle", event
): ):
await invoke(method, event) try:
await invoke(method, event)
except Exception as error:
self.app.on_exception(error)
break
log("CLOSED", self) log("CLOSED", self)

View File

@@ -13,6 +13,7 @@ from .geometry import Offset, Region, Size
from ._compositor import Compositor, MapGeometry from ._compositor import Compositor, MapGeometry
from .reactive import Reactive from .reactive import Reactive
from .renderables.blank import Blank from .renderables.blank import Blank
from ._timer import Timer
from .widget import Widget from .widget import Widget
if sys.version_info >= (3, 8): if sys.version_info >= (3, 8):
@@ -41,11 +42,21 @@ class Screen(Widget):
super().__init__(name=name, id=id) super().__init__(name=name, id=id)
self._compositor = Compositor() self._compositor = Compositor()
self._dirty_widgets: set[Widget] = set() self._dirty_widgets: set[Widget] = set()
self._update_timer: Timer | None = None
@property @property
def is_transparent(self) -> bool: def is_transparent(self) -> bool:
return False return False
@property
def update_timer(self) -> Timer:
"""Timer used to perform updates."""
if self._update_timer is None:
self._update_timer = self.set_interval(
UPDATE_PERIOD, self._on_update, name="screen_update", pause=True
)
return self._update_timer
def watch_dark(self, dark: bool) -> None: def watch_dark(self, dark: bool) -> None:
pass pass
@@ -100,20 +111,24 @@ class Screen(Widget):
def on_idle(self, event: events.Idle) -> None: def on_idle(self, event: events.Idle) -> None:
# Check for any widgets marked as 'dirty' (needs a repaint) # Check for any widgets marked as 'dirty' (needs a repaint)
if self._dirty_widgets: event.prevent_default()
self._update_timer.resume()
if self._layout_required: if self._layout_required:
self._refresh_layout() self._refresh_layout()
self._layout_required = False self._layout_required = False
self._dirty_widgets.clear()
elif self._dirty_widgets:
self.update_timer.resume()
def _on_update(self) -> None: def _on_update(self) -> None:
"""Called by the _update_timer.""" """Called by the _update_timer."""
# Render widgets together # Render widgets together
if self._dirty_widgets: if self._dirty_widgets:
self._compositor.update_widgets(self._dirty_widgets) self._compositor.update_widgets(self._dirty_widgets)
self.app._display(self._compositor.render()) self.app._display(self._compositor.render())
self._dirty_widgets.clear() self._dirty_widgets.clear()
self._update_timer.pause() self.update_timer.pause()
def _refresh_layout(self, size: Size | None = None, full: bool = False) -> None: def _refresh_layout(self, size: Size | None = None, full: bool = False) -> None:
"""Refresh the layout (can change size and positions of widgets).""" """Refresh the layout (can change size and positions of widgets)."""
@@ -122,7 +137,7 @@ class Screen(Widget):
return return
self._compositor.update_widgets(self._dirty_widgets) self._compositor.update_widgets(self._dirty_widgets)
self._update_timer.pause() self.update_timer.pause()
try: try:
hidden, shown, resized = self._compositor.reflow(self, size) hidden, shown, resized = self._compositor.reflow(self, size)
@@ -153,7 +168,6 @@ class Screen(Widget):
except Exception as error: except Exception as error:
self.app.on_exception(error) self.app.on_exception(error)
return return
display_update = self._compositor.render(full=full) display_update = self._compositor.render(full=full)
if display_update is not None: if display_update is not None:
self.app._display(display_update) self.app._display(display_update)
@@ -171,9 +185,8 @@ class Screen(Widget):
self.check_idle() self.check_idle()
def on_mount(self, event: events.Mount) -> None: def on_mount(self, event: events.Mount) -> None:
self._update_timer = self.set_interval( pass
UPDATE_PERIOD, self._on_update, name="screen_update", pause=True # self._refresh_layout()
)
def _screen_resized(self, size: Size): def _screen_resized(self, size: Size):
"""Called by App when the screen is resized.""" """Called by App when the screen is resized."""

View File

@@ -104,7 +104,7 @@ class Widget(DOMNode):
self._content_height_cache: tuple[object, int] = (None, 0) self._content_height_cache: tuple[object, int] = (None, 0)
self._arrangement: ArrangeResult | None = None self._arrangement: ArrangeResult | None = None
self._arrangement_size: Size = Size() self._arrangement_cache_key: tuple[int, Size] = (-1, Size())
super().__init__(name=name, id=id, classes=classes) super().__init__(name=name, id=id, classes=classes)
self.add_children(*children) self.add_children(*children)
@@ -122,10 +122,14 @@ class Widget(DOMNode):
show_horizontal_scrollbar = Reactive(False, layout=True) show_horizontal_scrollbar = Reactive(False, layout=True)
def _arrange(self, size: Size) -> ArrangeResult: def _arrange(self, size: Size) -> ArrangeResult:
if self._arrangement is not None and size == self._arrangement_size: arrange_cache_key = (self.children._updates, size)
if (
self._arrangement is not None
and arrange_cache_key == self._arrangement_cache_key
):
return self._arrangement return self._arrangement
self._arrangement = self.layout.arrange(self, size) self._arrangement = self.layout.arrange(self, size)
self._arrangement_size = size self._arrangement_cache_key = (self.children._updates, size)
return self._arrangement return self._arrangement
def watch_show_horizontal_scrollbar(self, value: bool) -> None: def watch_show_horizontal_scrollbar(self, value: bool) -> None:
@@ -855,10 +859,6 @@ class Widget(DOMNode):
lines = self._render_cache.lines[start:end] lines = self._render_cache.lines[start:end]
return lines return lines
def check_layout(self) -> bool:
"""Check if a layout has been requested."""
return self._layout_required
def get_style_at(self, x: int, y: int) -> Style: def get_style_at(self, x: int, y: int) -> Style:
offset_x, offset_y = self.screen.get_offset(self) offset_x, offset_y = self.screen.get_offset(self)
return self.screen.get_style_at(x + offset_x, y + offset_y) return self.screen.get_style_at(x + offset_x, y + offset_y)
@@ -917,12 +917,12 @@ class Widget(DOMNode):
event (events.Idle): Idle event. event (events.Idle): Idle event.
""" """
if self.check_layout(): if self._layout_required:
self._layout_required = False self._layout_required = False
self.screen.post_message_no_wait(messages.Layout(self)) self.screen.post_message_no_wait(messages.Layout(self))
elif self._repaint_required: elif self._repaint_required:
self.emit_no_wait(messages.Update(self, self)) self.emit_no_wait(messages.Update(self, self))
self._repaint_required = False self._repaint_required = False
def focus(self) -> None: def focus(self) -> None:
"""Give input focus to this widget.""" """Give input focus to this widget."""