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; text: on $primary;
} }
Widget:hover {
outline: solid green;
}
#sidebar { #sidebar {
text: #09312e on #3caea3; text: #09312e on #3caea3;
dock: side; dock: side;

View File

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

View File

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

View File

@@ -77,10 +77,8 @@ class Reactive(Generic[ReactiveType]):
setattr(obj, self.internal_name, value) setattr(obj, self.internal_name, value)
self.check_watchers(obj, name, current_value) self.check_watchers(obj, name, current_value)
if self.layout: if self.layout or self.repaint:
obj.refresh(layout=True) obj.refresh(repaint=self.repaint, layout=self.layout)
elif self.repaint:
obj.refresh()
@classmethod @classmethod
def check_watchers(cls, obj: Reactable, name: str, old_value: Any) -> None: 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: def __init__(self, name: str | None = None, id: str | None = None) -> None:
super().__init__(name=name, id=id) super().__init__(name=name, id=id)
self._compositor = Compositor() self._compositor = Compositor()
self._dirty_widgets: list[Widget] = []
@property @property
def is_transparent(self) -> bool: def is_transparent(self) -> bool:
@@ -81,11 +82,21 @@ class Screen(Widget):
""" """
return self._compositor.get_widget_region(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: if not self.size:
return return
try: try:
hidden, shown, resized = self._compositor.reflow(self, self.size) hidden, shown, resized = self._compositor.reflow(self, self.size)
@@ -100,7 +111,7 @@ class Screen(Widget):
for ( for (
widget, widget,
region, _region,
unclipped_region, unclipped_region,
virtual_size, virtual_size,
container_size, container_size,
@@ -112,20 +123,15 @@ class Screen(Widget):
self, unclipped_region.size, virtual_size, container_size self, unclipped_region.size, virtual_size, container_size
) )
) )
except Exception: except Exception:
self.app.panic() self.app.panic()
self.app.refresh() self.app.refresh()
async def handle_update(self, message: messages.Update) -> None: async def handle_update(self, message: messages.Update) -> None:
message.stop() message.stop()
widget = message.widget widget = message.widget
assert isinstance(widget, Widget) assert isinstance(widget, Widget)
self._dirty_widgets.append(widget)
display_update = self._compositor.update_widget(self.console, widget)
if display_update is not None:
self.app.display(display_update)
async def handle_layout(self, message: messages.Layout) -> None: async def handle_layout(self, message: messages.Layout) -> None:
message.stop() message.stop()

View File

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