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;
|
text: on $primary;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget:hover {
|
||||||
|
outline: solid green;
|
||||||
|
}
|
||||||
|
|
||||||
#sidebar {
|
#sidebar {
|
||||||
text: #09312e on #3caea3;
|
text: #09312e on #3caea3;
|
||||||
dock: side;
|
dock: side;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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."""
|
||||||
|
|||||||
Reference in New Issue
Block a user