mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
dirty regions
This commit is contained in:
@@ -8,6 +8,10 @@ App > Screen {
|
||||
text: on $primary;
|
||||
}
|
||||
|
||||
Widget:hover {
|
||||
outline: solid green;
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
text: #09312e on #3caea3;
|
||||
dock: side;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user