mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
combine updates, cache arrangements
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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."""
|
||||||
|
|||||||
@@ -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."""
|
||||||
|
|||||||
Reference in New Issue
Block a user