dirty regions

This commit is contained in:
Will McGugan
2022-03-27 17:50:26 +01:00
parent d004c88f77
commit 0441a5d838
6 changed files with 54 additions and 57 deletions

View File

@@ -8,6 +8,10 @@ App > Screen {
text: on $primary;
}
Widget:hover {
outline: solid green;
}
#sidebar {
text: #09312e on #3caea3;
dock: side;

View File

@@ -325,7 +325,7 @@ class Compositor:
return Style.null()
if widget not in self.regions:
return Style.null()
lines = widget._get_lines()
lines = widget.get_render_lines()
x -= region.x
y -= region.y
if y > len(lines):
@@ -421,9 +421,9 @@ class Compositor:
# log(widget, region)
continue
if region in clip:
yield region, clip, widget._get_lines()
yield region, clip, widget.get_render_lines()
elif overlaps(clip, region):
lines = widget._get_lines()
lines = widget.get_render_lines()
new_x, new_y, new_width, new_height = intersection(region, clip)
delta_x = new_x - region.x
delta_y = new_y - region.y
@@ -538,14 +538,12 @@ class Compositor:
"""
if widget not in self.regions:
return None
region, clip = self.regions[widget]
if not region:
return None
update_region = region.intersection(clip)
if not update_region:
return None
widget.clear_render_cache()
update_lines = self.render(crop=update_region).lines
update = LayoutUpdate(update_lines, update_region)
return update

View File

@@ -220,7 +220,7 @@ class MessagePump:
if not self._closed:
event = events.Idle(self)
for method in self._get_dispatch_methods("on_idle", event):
await method(event)
await invoke(method, event)
log("CLOSED", self)

View File

@@ -77,10 +77,8 @@ class Reactive(Generic[ReactiveType]):
setattr(obj, self.internal_name, value)
self.check_watchers(obj, name, current_value)
if self.layout:
obj.refresh(layout=True)
elif self.repaint:
obj.refresh()
if self.layout or self.repaint:
obj.refresh(repaint=self.repaint, layout=self.layout)
@classmethod
def check_watchers(cls, obj: Reactable, name: str, old_value: Any) -> None:

View File

@@ -27,6 +27,7 @@ class Screen(Widget):
def __init__(self, name: str | None = None, id: str | None = None) -> None:
super().__init__(name=name, id=id)
self._compositor = Compositor()
self._dirty_widgets: list[Widget] = []
@property
def is_transparent(self) -> bool:
@@ -81,11 +82,21 @@ class Screen(Widget):
"""
return self._compositor.get_widget_region(widget)
async def refresh_layout(self) -> None:
def on_idle(self, event: events.Idle) -> None:
# Check for any widgets marked as 'dirty' (needs a repaint)
if self._dirty_widgets:
for widget in self._dirty_widgets:
# Repaint widgets
display_update = self._compositor.update_widget(self.console, widget)
if display_update is not None:
self.app.display(display_update)
# Reset dirty list
self._dirty_widgets.clear()
async def refresh_layout(self) -> None:
"""Refresh the layout (can change size and positions of widgets)."""
if not self.size:
return
try:
hidden, shown, resized = self._compositor.reflow(self, self.size)
@@ -100,7 +111,7 @@ class Screen(Widget):
for (
widget,
region,
_region,
unclipped_region,
virtual_size,
container_size,
@@ -112,20 +123,15 @@ class Screen(Widget):
self, unclipped_region.size, virtual_size, container_size
)
)
except Exception:
self.app.panic()
self.app.refresh()
async def handle_update(self, message: messages.Update) -> None:
message.stop()
widget = message.widget
assert isinstance(widget, Widget)
display_update = self._compositor.update_widget(self.console, widget)
if display_update is not None:
self.app.display(display_update)
self._dirty_widgets.append(widget)
async def handle_layout(self, message: messages.Layout) -> None:
message.stop()

View File

@@ -81,26 +81,27 @@ class Widget(DOMNode):
self._size = Size(0, 0)
self._virtual_size = Size(0, 0)
self._container_size = Size(0, 0)
self._repaint_required = False
self._layout_required = False
self._animate: BoundAnimator | None = None
self._reactive_watches: dict[str, Callable] = {}
self._mouse_over: bool = False
self.render_cache: RenderCache | None = None
self.highlight_style: Style | None = None
self._vertical_scrollbar: ScrollBar | None = None
self._horizontal_scrollbar: ScrollBar | None = None
self._render_cache = RenderCache(Size(0, 0), [])
self._dirty_regions: list[Region] = []
super().__init__(name=name, id=id, classes=classes)
self.add_children(*children)
has_focus = Reactive(False)
mouse_over = Reactive(False)
scroll_x = Reactive(0.0)
scroll_y = Reactive(0.0)
scroll_target_x = Reactive(0.0)
scroll_target_y = Reactive(0.0)
scroll_x = Reactive(0.0, repaint=False)
scroll_y = Reactive(0.0, repaint=False)
scroll_target_x = Reactive(0.0, repaint=False)
scroll_target_y = Reactive(0.0, repaint=False)
show_vertical_scrollbar = Reactive(False, layout=True)
show_horizontal_scrollbar = Reactive(False, layout=True)
@@ -208,6 +209,11 @@ class Widget(DOMNode):
enabled = self.show_vertical_scrollbar, self.show_horizontal_scrollbar
return enabled
def set_dirty(self) -> None:
"""Set the Widget as 'dirty' (requiring re-render)."""
self._dirty_regions.clear()
self._dirty_regions.append(self.size.region)
def scroll_to(
self,
x: float | None = None,
@@ -469,7 +475,7 @@ class Widget(DOMNode):
self.app.update_styles()
def on_style_change(self) -> None:
self.clear_render_cache()
self.set_dirty()
def size_updated(
self, size: Size, virtual_size: Size, container_size: Size
@@ -494,36 +500,26 @@ class Widget(DOMNode):
self.refresh()
self.call_later(self._refresh_scrollbars)
def render_lines(self) -> None:
def _render_lines(self) -> None:
width, height = self.size
renderable = self.render_styled()
options = self.console.options.update_dimensions(width, height)
lines = self.console.render_lines(renderable, options)
self.render_cache = RenderCache(self.size, lines)
self._render_cache = RenderCache(self.size, lines)
self._dirty_regions.clear()
def _get_lines(self) -> Lines:
def get_render_lines(self) -> Lines:
"""Get segment lines to render the widget."""
if self.render_cache is None:
self.render_lines()
assert self.render_cache is not None
lines = self.render_cache.lines
if self._dirty_regions:
self._render_lines()
lines = self._render_cache.lines
return lines
def clear_render_cache(self) -> None:
self.render_cache = None
def check_repaint(self) -> bool:
"""Check if a repaint has been requested."""
return self._repaint_required
def check_layout(self) -> bool:
"""Check if a layout has been requested."""
return self._layout_required
def reset_check_repaint(self) -> None:
self._repaint_required = False
def reset_check_layout(self) -> None:
self._layout_required = False
@@ -549,11 +545,9 @@ class Widget(DOMNode):
layout (bool, optional): Also layout widgets in the view. Defaults to False.
"""
if layout:
self.clear_render_cache()
self._layout_required = True
elif repaint:
self.clear_render_cache()
self._repaint_required = True
if repaint:
self.set_dirty()
self.check_idle()
def render(self) -> RenderableType:
@@ -566,7 +560,6 @@ class Widget(DOMNode):
# Default displays a pretty repr in the center of the screen
label = self.css_identifier_styled
return Align.center(label, vertical="middle")
async def action(self, action: str, *params) -> None:
@@ -579,24 +572,22 @@ class Widget(DOMNode):
self.log(self, f"IS NOT RUNNING, {message!r} not sent")
return await super().post_message(message)
async def on_idle(self, event: events.Idle) -> None:
def on_idle(self, event: events.Idle) -> None:
"""Called when there are no more events on the queue.
Args:
event (events.Idle): Idle event.
"""
# Check if the styles have chained
# Check if the styles have changed
repaint, layout = self.styles.check_refresh()
if self._dirty_regions:
repaint = True
if layout or self.check_layout():
# self.render_cache = None
self.reset_check_repaint()
self.reset_check_layout()
await self.screen.post_message(messages.Layout(self))
elif repaint or self.check_repaint():
# self.render_cache = None
self.reset_check_repaint()
await self.emit(messages.Update(self, self))
self.screen.post_message_no_wait(messages.Layout(self))
elif repaint:
self.emit_no_wait(messages.Update(self, self))
async def focus(self) -> None:
"""Give input focus to this widget."""