mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Merge pull request #855 from Textualize/regions-optimize
Regions optimize
This commit is contained in:
@@ -1,14 +0,0 @@
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.widgets import Button
|
||||
|
||||
|
||||
class ButtonsApp(App):
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Button("Paul")
|
||||
yield Button("Duncan")
|
||||
yield Button("Chani")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = ButtonsApp()
|
||||
app.run()
|
||||
@@ -16,7 +16,7 @@ from __future__ import annotations
|
||||
import sys
|
||||
from itertools import chain
|
||||
from operator import itemgetter
|
||||
from typing import TYPE_CHECKING, Callable, Iterable, Iterator, NamedTuple, cast
|
||||
from typing import TYPE_CHECKING, Callable, Iterable, NamedTuple, cast
|
||||
|
||||
import rich.repr
|
||||
from rich.console import Console, ConsoleOptions, RenderableType, RenderResult
|
||||
@@ -182,8 +182,8 @@ class Compositor:
|
||||
# Note this may be a superset of self.map.keys() as some widgets may be invisible for various reasons
|
||||
self.widgets: set[Widget] = set()
|
||||
|
||||
# A lazy cache of visible (on screen) widgets
|
||||
self._visible_widgets: set[Widget] | None = set()
|
||||
# Mapping of visible widgets on to their region, and clip region
|
||||
self._visible_widgets: dict[Widget, tuple[Region, Region]] | None = None
|
||||
|
||||
# The top level widget
|
||||
self.root: Widget | None = None
|
||||
@@ -191,10 +191,6 @@ class Compositor:
|
||||
# Dimensions of the arrangement
|
||||
self.size = Size(0, 0)
|
||||
|
||||
# A mapping of Widget on to region, and clip region
|
||||
# The clip region can be considered the window through which a widget is viewed
|
||||
self.regions: dict[Widget, tuple[Region, Region]] = {}
|
||||
|
||||
# The points in each line where the line bisects the left and right edges of the widget
|
||||
self._cuts: list[list[int]] | None = None
|
||||
|
||||
@@ -259,6 +255,7 @@ class Compositor:
|
||||
self._cuts = None
|
||||
self._layers = None
|
||||
self._layers_visible = None
|
||||
self._visible_widgets = None
|
||||
self.root = parent
|
||||
self.size = size
|
||||
|
||||
@@ -277,12 +274,8 @@ class Compositor:
|
||||
# Replace map and widgets
|
||||
self.map = map
|
||||
self.widgets = widgets
|
||||
self._visible_widgets = None
|
||||
|
||||
# Get a map of regions
|
||||
self.regions = {
|
||||
widget: (region, clip) for widget, (region, _order, clip, *_) in map.items()
|
||||
}
|
||||
screen = size.region
|
||||
|
||||
# Widgets with changed size
|
||||
resized_widgets = {
|
||||
@@ -293,7 +286,6 @@ class Compositor:
|
||||
|
||||
# Gets pairs of tuples of (Widget, MapGeometry) which have changed
|
||||
# i.e. if something is moved / deleted / added
|
||||
screen = size.region
|
||||
|
||||
if screen not in self._dirty_regions:
|
||||
crop_screen = screen.intersection
|
||||
@@ -315,18 +307,26 @@ class Compositor:
|
||||
)
|
||||
|
||||
@property
|
||||
def visible_widgets(self) -> set[Widget]:
|
||||
"""Get a set of visible widgets.
|
||||
def visible_widgets(self) -> dict[Widget, tuple[Region, Region]]:
|
||||
"""Get a mapping of widgets on to region and clip.
|
||||
|
||||
Returns:
|
||||
set[Widget]: Widgets in the screen.
|
||||
dict[Widget, tuple[Region, Region]]: visible widget mapping.
|
||||
"""
|
||||
if self._visible_widgets is None:
|
||||
in_screen = self.size.region.__contains__
|
||||
screen = self.size.region
|
||||
in_screen = screen.overlaps
|
||||
overlaps = Region.overlaps
|
||||
|
||||
# Widgets and regions in render order
|
||||
visible_widgets = [
|
||||
(order, widget, region, clip)
|
||||
for widget, (region, order, clip, _, _, _) in self.map.items()
|
||||
if in_screen(region) and overlaps(clip, region)
|
||||
]
|
||||
visible_widgets.sort(key=itemgetter(0), reverse=True)
|
||||
self._visible_widgets = {
|
||||
widget
|
||||
for widget, (region, clip) in self.regions.items()
|
||||
if in_screen(region)
|
||||
widget: (region, clip) for _, widget, region, clip in visible_widgets
|
||||
}
|
||||
return self._visible_widgets
|
||||
|
||||
@@ -480,41 +480,23 @@ class Compositor:
|
||||
@property
|
||||
def layers_visible(self) -> list[list[tuple[Widget, Region, Region]]]:
|
||||
"""Visible widgets and regions in layers order."""
|
||||
screen_height = self.size.height
|
||||
|
||||
if self._layers_visible is None:
|
||||
layers_visible: list[list[tuple[Widget, Region, Region]]]
|
||||
layers_visible = [[] for y in range(self.size.height)]
|
||||
layers_visible_appends = [layer.append for layer in layers_visible]
|
||||
intersection = Region.intersection
|
||||
_range = range
|
||||
for widget, (region, _, clip, _, _, _) in self.layers:
|
||||
_x, y, _width, height = region
|
||||
if -height <= y < screen_height:
|
||||
cropped_region = intersection(region, clip)
|
||||
_x, region_y, _width, region_height = cropped_region
|
||||
if region_height:
|
||||
widget_location = (widget, cropped_region, region)
|
||||
for y in _range(region_y, region_y + region_height):
|
||||
layers_visible_appends[y](widget_location)
|
||||
for widget, (region, clip) in self.visible_widgets.items():
|
||||
cropped_region = intersection(region, clip)
|
||||
_x, region_y, _width, region_height = cropped_region
|
||||
if region_height:
|
||||
widget_location = (widget, cropped_region, region)
|
||||
for y in _range(region_y, region_y + region_height):
|
||||
layers_visible_appends[y](widget_location)
|
||||
self._layers_visible = layers_visible
|
||||
return self._layers_visible
|
||||
|
||||
def __iter__(self) -> Iterator[tuple[Widget, Region, Size, Size]]:
|
||||
"""Iterate map with information regarding each widget and is position
|
||||
|
||||
Yields:
|
||||
Iterator[tuple[Widget, Region, Region, Size, Size]]: Iterates a tuple of
|
||||
Widget, region, virtual size, and container size.
|
||||
"""
|
||||
layers = self.layers
|
||||
for widget, (region, _order, clip, virtual_size, container_size, _) in layers:
|
||||
yield (
|
||||
widget,
|
||||
region,
|
||||
virtual_size,
|
||||
container_size,
|
||||
)
|
||||
|
||||
def get_offset(self, widget: Widget) -> Offset:
|
||||
"""Get the offset of a widget."""
|
||||
try:
|
||||
@@ -537,9 +519,10 @@ class Compositor:
|
||||
"""
|
||||
|
||||
contains = Region.contains
|
||||
for widget, cropped_region, region in self.layers_visible[y]:
|
||||
if contains(cropped_region, x, y) and widget.visible:
|
||||
return widget, region
|
||||
if len(self.layers_visible) > y >= 0:
|
||||
for widget, cropped_region, region in self.layers_visible[y]:
|
||||
if contains(cropped_region, x, y) and widget.visible:
|
||||
return widget, region
|
||||
raise errors.NoWidget(f"No widget under screen coordinate ({x}, {y})")
|
||||
|
||||
def get_widgets_at(self, x: int, y: int) -> Iterable[tuple[Widget, Region]]:
|
||||
@@ -571,7 +554,7 @@ class Compositor:
|
||||
widget, region = self.get_widget_at(x, y)
|
||||
except errors.NoWidget:
|
||||
return Style.null()
|
||||
if widget not in self.regions:
|
||||
if widget not in self.visible_widgets:
|
||||
return Style.null()
|
||||
|
||||
x -= region.x
|
||||
@@ -627,7 +610,7 @@ class Compositor:
|
||||
intersection = Region.intersection
|
||||
extend = list.extend
|
||||
|
||||
for region, order, clip, *_ in self.map.values():
|
||||
for region, clip in self.visible_widgets.values():
|
||||
region = intersection(region, clip)
|
||||
if region and (region in screen_region):
|
||||
x, y, region_width, region_height = region
|
||||
@@ -652,6 +635,9 @@ class Compositor:
|
||||
# up to this point.
|
||||
_rich_traceback_guard = True
|
||||
|
||||
if not self.map:
|
||||
return
|
||||
|
||||
def is_visible(widget: Widget) -> bool:
|
||||
"""Return True if the widget is (literally) visible by examining various
|
||||
properties which affect whether it can be seen or not."""
|
||||
@@ -661,45 +647,42 @@ class Compositor:
|
||||
and widget.styles.opacity > 0
|
||||
)
|
||||
|
||||
if self.map:
|
||||
if crop:
|
||||
overlaps = crop.overlaps
|
||||
mapped_regions = [
|
||||
(widget, region, order, clip)
|
||||
for widget, (region, order, clip, *_) in self.map.items()
|
||||
if is_visible(widget) and overlaps(crop)
|
||||
]
|
||||
else:
|
||||
mapped_regions = [
|
||||
(widget, region, order, clip)
|
||||
for widget, (region, order, clip, *_) in self.map.items()
|
||||
if is_visible(widget)
|
||||
]
|
||||
_Region = Region
|
||||
|
||||
widget_regions = sorted(mapped_regions, key=itemgetter(2), reverse=True)
|
||||
visible_widgets = self.visible_widgets
|
||||
|
||||
if crop:
|
||||
crop_overlaps = crop.overlaps
|
||||
widget_regions = [
|
||||
(widget, region, clip)
|
||||
for widget, (region, clip) in visible_widgets.items()
|
||||
if crop_overlaps(clip) and is_visible(widget)
|
||||
]
|
||||
else:
|
||||
widget_regions = []
|
||||
widget_regions = [
|
||||
(widget, region, clip)
|
||||
for widget, (region, clip) in visible_widgets.items()
|
||||
if is_visible(widget)
|
||||
]
|
||||
|
||||
intersection = Region.intersection
|
||||
overlaps = Region.overlaps
|
||||
intersection = _Region.intersection
|
||||
contains_region = _Region.contains_region
|
||||
|
||||
for widget, region, _order, clip in widget_regions:
|
||||
if not region:
|
||||
continue
|
||||
if region in clip:
|
||||
lines = widget.render_lines(Region(0, 0, region.width, region.height))
|
||||
yield region, clip, lines
|
||||
elif overlaps(clip, region):
|
||||
for widget, region, clip in widget_regions:
|
||||
if contains_region(clip, region):
|
||||
yield region, clip, widget.render_lines(
|
||||
_Region(0, 0, region.width, region.height)
|
||||
)
|
||||
else:
|
||||
clipped_region = intersection(region, clip)
|
||||
if not clipped_region:
|
||||
continue
|
||||
new_x, new_y, new_width, new_height = clipped_region
|
||||
delta_x = new_x - region.x
|
||||
delta_y = new_y - region.y
|
||||
lines = widget.render_lines(
|
||||
Region(delta_x, delta_y, new_width, new_height)
|
||||
yield region, clip, widget.render_lines(
|
||||
_Region(delta_x, delta_y, new_width, new_height)
|
||||
)
|
||||
yield region, clip, lines
|
||||
|
||||
@classmethod
|
||||
def _assemble_chops(
|
||||
@@ -813,8 +796,9 @@ class Compositor:
|
||||
"""
|
||||
regions: list[Region] = []
|
||||
add_region = regions.append
|
||||
for widget in self.regions.keys() & widgets:
|
||||
region, clip = self.regions[widget]
|
||||
get_widget = self.visible_widgets.__getitem__
|
||||
for widget in self.visible_widgets.keys() & widgets:
|
||||
region, clip = get_widget(widget)
|
||||
offset = region.offset
|
||||
intersection = clip.intersection
|
||||
for dirty_region in widget._exchange_repaint_regions():
|
||||
|
||||
@@ -46,7 +46,7 @@ from ..geometry import Spacing, SpacingDimensions, clamp
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .._layout import Layout
|
||||
from .styles import DockGroup, Styles, StylesBase
|
||||
from .styles import Styles, StylesBase
|
||||
|
||||
from .types import DockEdge, EdgeType, AlignHorizontal, AlignVertical
|
||||
|
||||
@@ -409,12 +409,37 @@ class BorderProperty:
|
||||
"""
|
||||
_rich_traceback_omit = True
|
||||
top, right, bottom, left = self._properties
|
||||
|
||||
border_spacing = Edges(
|
||||
getattr(obj, top),
|
||||
getattr(obj, right),
|
||||
getattr(obj, bottom),
|
||||
getattr(obj, left),
|
||||
).spacing
|
||||
|
||||
def check_refresh() -> None:
|
||||
"""Check if an update requires a layout"""
|
||||
if not self._layout:
|
||||
obj.refresh()
|
||||
else:
|
||||
layout = (
|
||||
Edges(
|
||||
getattr(obj, top),
|
||||
getattr(obj, right),
|
||||
getattr(obj, bottom),
|
||||
getattr(obj, left),
|
||||
).spacing
|
||||
!= border_spacing
|
||||
)
|
||||
obj.refresh(layout=layout)
|
||||
|
||||
if border is None:
|
||||
clear_rule = obj.clear_rule
|
||||
clear_rule(top)
|
||||
clear_rule(right)
|
||||
clear_rule(bottom)
|
||||
clear_rule(left)
|
||||
check_refresh()
|
||||
return
|
||||
if isinstance(border, tuple) and len(border) == 2:
|
||||
_border = normalize_border_value(border)
|
||||
@@ -422,6 +447,7 @@ class BorderProperty:
|
||||
setattr(obj, right, _border)
|
||||
setattr(obj, bottom, _border)
|
||||
setattr(obj, left, _border)
|
||||
check_refresh()
|
||||
return
|
||||
|
||||
count = len(border)
|
||||
@@ -456,7 +482,7 @@ class BorderProperty:
|
||||
"expected 1, 2, or 4 values",
|
||||
help_text=border_property_help_text(self.name, context="inline"),
|
||||
)
|
||||
obj.refresh(layout=self._layout)
|
||||
check_refresh()
|
||||
|
||||
|
||||
class SpacingProperty:
|
||||
|
||||
@@ -643,7 +643,7 @@ class Styles(StylesBase):
|
||||
speed: float | None,
|
||||
easing: EasingFunction,
|
||||
on_complete: CallbackType | None = None,
|
||||
) -> Animation | None:
|
||||
) -> ScalarAnimation | None:
|
||||
# from ..widget import Widget
|
||||
# node = self.node
|
||||
# assert isinstance(self.node, Widget)
|
||||
|
||||
@@ -346,7 +346,8 @@ class Region(NamedTuple):
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
"""A Region is considered False when it has no area."""
|
||||
return bool(self.width and self.height)
|
||||
_, _, width, height = self
|
||||
return width * height > 0
|
||||
|
||||
@property
|
||||
def column_span(self) -> tuple[int, int]:
|
||||
@@ -603,6 +604,7 @@ class Region(NamedTuple):
|
||||
raise TypeError(f"a tuple of two integers is required, not {point!r}")
|
||||
return (x2 > ox >= x1) and (y2 > oy >= y1)
|
||||
|
||||
@lru_cache(maxsize=1024)
|
||||
def contains_region(self, other: Region) -> bool:
|
||||
"""Check if a region is entirely contained within this region.
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ class ScrollView(Widget):
|
||||
self._size = size
|
||||
virtual_size = self.virtual_size
|
||||
self._scroll_update(virtual_size)
|
||||
self._container_size = size - self.gutter.totals
|
||||
self._container_size = size - self.styles.gutter.totals
|
||||
self.scroll_to(self.scroll_x, self.scroll_y, animate=False)
|
||||
self.refresh()
|
||||
|
||||
|
||||
@@ -1680,21 +1680,22 @@ class Widget(DOMNode):
|
||||
return lines
|
||||
|
||||
def get_style_at(self, x: int, y: int) -> Style:
|
||||
"""Get the Rich style at a given screen offset.
|
||||
"""Get the Rich style in a widget at a given relative offset.
|
||||
|
||||
Args:
|
||||
x (int): X coordinate relative to the screen.
|
||||
y (int): Y coordinate relative to the screen.
|
||||
x (int): X coordinate relative to the widget.
|
||||
y (int): Y coordinate relative to the widget.
|
||||
|
||||
Returns:
|
||||
Style: A rich Style object.
|
||||
"""
|
||||
widget, region = self.screen.get_widget_at(x, y)
|
||||
offset = Offset(x, y)
|
||||
screen_offset = offset + self.region.offset
|
||||
|
||||
widget, _ = self.screen.get_widget_at(*screen_offset)
|
||||
if widget is not self:
|
||||
return Style()
|
||||
offset_x, offset_y = region.offset
|
||||
# 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(*screen_offset)
|
||||
|
||||
async def _forward_event(self, event: events.Event) -> None:
|
||||
event._set_forwarded()
|
||||
@@ -1733,8 +1734,6 @@ class Widget(DOMNode):
|
||||
self._content_height_cache = (None, 0)
|
||||
self._rich_style_cache.clear()
|
||||
self._repaint_required = True
|
||||
if isinstance(self.parent, Widget) and self.styles.auto_dimensions:
|
||||
self.parent.refresh(layout=True)
|
||||
|
||||
self.check_idle()
|
||||
|
||||
|
||||
@@ -269,9 +269,10 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
|
||||
column.content_width = max(column.content_width, content_width)
|
||||
|
||||
total_width = sum(column.render_width for column in self.columns)
|
||||
header_height = self.header_height if self.show_header else 0
|
||||
self.virtual_size = Size(
|
||||
total_width,
|
||||
max(len(self._y_offsets), (self.header_height if self.show_header else 0)),
|
||||
len(self._y_offsets) + header_height,
|
||||
)
|
||||
|
||||
def _get_cell_region(self, row_index: int, column_index: int) -> Region:
|
||||
|
||||
@@ -126,7 +126,7 @@ class AppTest(App):
|
||||
widget, region = self.get_widget_at(x, y)
|
||||
except errors.NoWidget:
|
||||
return ""
|
||||
if widget not in self.screen._compositor.regions:
|
||||
if widget not in self.screen._compositor.visible_widgets:
|
||||
return ""
|
||||
|
||||
x -= region.x
|
||||
|
||||
Reference in New Issue
Block a user