mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
faster layout
This commit is contained in:
@@ -53,7 +53,6 @@ def arrange(
|
||||
Returns:
|
||||
Widget arrangement information.
|
||||
"""
|
||||
|
||||
placements: list[WidgetPlacement] = []
|
||||
scroll_spacing = NULL_SPACING
|
||||
styles = widget.styles
|
||||
@@ -65,8 +64,10 @@ def arrange(
|
||||
|
||||
for widgets in layers.values():
|
||||
# Partition widgets into split widgets and non-split widgets
|
||||
|
||||
non_split_widgets, split_widgets = partition(_get_split, widgets)
|
||||
if split_widgets:
|
||||
|
||||
_split_placements, dock_region = _arrange_split_widgets(
|
||||
split_widgets, size, viewport
|
||||
)
|
||||
|
||||
@@ -1248,8 +1248,7 @@ class Compositor:
|
||||
offset = region.offset
|
||||
intersection = clip.intersection
|
||||
for dirty_region in widget._exchange_repaint_regions():
|
||||
update_region = intersection(dirty_region.translate(offset))
|
||||
if update_region:
|
||||
if update_region := intersection(dirty_region.translate(offset)):
|
||||
add_region(update_region)
|
||||
|
||||
self._dirty_regions.update(regions)
|
||||
|
||||
@@ -11,15 +11,19 @@ if TYPE_CHECKING:
|
||||
from textual.geometry import Spacing
|
||||
from textual.widget import Widget
|
||||
|
||||
from textual._profile import timer
|
||||
|
||||
|
||||
class VerticalLayout(Layout):
|
||||
"""Used to layout Widgets vertically on screen, from top to bottom."""
|
||||
|
||||
name = "vertical"
|
||||
|
||||
@timer("Vertical.arrange")
|
||||
def arrange(
|
||||
self, parent: Widget, children: list[Widget], size: Size, greedy: bool = True
|
||||
) -> ArrangeResult:
|
||||
print(parent, size, greedy, len(children))
|
||||
parent.pre_layout(self)
|
||||
placements: list[WidgetPlacement] = []
|
||||
add_placement = placements.append
|
||||
@@ -50,15 +54,16 @@ class VerticalLayout(Layout):
|
||||
else:
|
||||
resolve_margin = Size(0, 0)
|
||||
|
||||
box_models = resolve_box_models(
|
||||
[styles.height for styles in child_styles],
|
||||
children,
|
||||
size,
|
||||
parent.app.size,
|
||||
resolve_margin,
|
||||
resolve_dimension="height",
|
||||
greedy=greedy,
|
||||
)
|
||||
with timer("resolve_box_models"):
|
||||
box_models = resolve_box_models(
|
||||
[styles.height for styles in child_styles],
|
||||
children,
|
||||
size,
|
||||
parent.app.size,
|
||||
resolve_margin,
|
||||
resolve_dimension="height",
|
||||
greedy=greedy,
|
||||
)
|
||||
|
||||
margins = [
|
||||
(
|
||||
|
||||
@@ -52,6 +52,10 @@ class Update(Message, verbose=True):
|
||||
class Layout(Message, verbose=True):
|
||||
"""Sent by Textual when a layout is required."""
|
||||
|
||||
def __init__(self, widget: Widget) -> None:
|
||||
super().__init__()
|
||||
self.widget = widget
|
||||
|
||||
def can_replace(self, message: Message) -> bool:
|
||||
return isinstance(message, Layout)
|
||||
|
||||
|
||||
@@ -362,6 +362,8 @@ class Reactive(Generic[ReactiveType]):
|
||||
|
||||
# Refresh according to descriptor flags
|
||||
if self._layout or self._repaint or self._recompose:
|
||||
if self._layout:
|
||||
print(self, "layout", obj)
|
||||
obj.refresh(
|
||||
repaint=self._repaint,
|
||||
layout=self._layout,
|
||||
|
||||
@@ -321,6 +321,9 @@ class Screen(Generic[ScreenResultType], Widget):
|
||||
self._css_update_count = -1
|
||||
"""Track updates to CSS."""
|
||||
|
||||
self._layout_widgets: set[Widget] = set()
|
||||
"""Widgets whose layout may have changed."""
|
||||
|
||||
@property
|
||||
def is_modal(self) -> bool:
|
||||
"""Is the screen modal?"""
|
||||
@@ -486,11 +489,12 @@ class Screen(Generic[ScreenResultType], Widget):
|
||||
|
||||
return bindings_map
|
||||
|
||||
def arrange(self, size: Size) -> DockArrangeResult:
|
||||
def arrange(self, size: Size, _optimal: bool = False) -> DockArrangeResult:
|
||||
"""Arrange children.
|
||||
|
||||
Args:
|
||||
size: Size of container.
|
||||
optimal: Ignored on screen.
|
||||
|
||||
Returns:
|
||||
Widget locations.
|
||||
@@ -1270,7 +1274,7 @@ class Screen(Generic[ScreenResultType], Widget):
|
||||
ResizeEvent = events.Resize
|
||||
|
||||
try:
|
||||
if scroll:
|
||||
if scroll and not self._layout_widgets:
|
||||
exposed_widgets = self._compositor.reflow_visible(self, size)
|
||||
if exposed_widgets:
|
||||
layers = self._compositor.layers
|
||||
@@ -1295,6 +1299,7 @@ class Screen(Generic[ScreenResultType], Widget):
|
||||
|
||||
else:
|
||||
hidden, shown, resized = self._compositor.reflow(self, size)
|
||||
self._layout_widgets.clear()
|
||||
Hide = events.Hide
|
||||
Show = events.Show
|
||||
|
||||
@@ -1348,8 +1353,10 @@ class Screen(Generic[ScreenResultType], Widget):
|
||||
async def _on_layout(self, message: messages.Layout) -> None:
|
||||
message.stop()
|
||||
message.prevent_default()
|
||||
self._layout_required = True
|
||||
self.check_idle()
|
||||
if message.widget not in self._layout_widgets:
|
||||
self._layout_widgets.add(message.widget)
|
||||
self._layout_required = True
|
||||
self.check_idle()
|
||||
|
||||
async def _on_update_scroll(self, message: messages.UpdateScroll) -> None:
|
||||
message.stop()
|
||||
|
||||
@@ -60,7 +60,7 @@ from textual._types import AnimationLevel
|
||||
from textual.actions import SkipAction
|
||||
from textual.await_remove import AwaitRemove
|
||||
from textual.box_model import BoxModel
|
||||
from textual.cache import FIFOCache
|
||||
from textual.cache import FIFOCache, LRUCache
|
||||
from textual.color import Color
|
||||
from textual.compose import compose
|
||||
from textual.content import Content, ContentType
|
||||
@@ -427,6 +427,7 @@ class Widget(DOMNode):
|
||||
self._size = _null_size
|
||||
self._container_size = _null_size
|
||||
self._layout_required = False
|
||||
self._layout_updates = 0
|
||||
self._repaint_required = False
|
||||
self._scroll_required = False
|
||||
self._recompose_required = False
|
||||
@@ -455,6 +456,8 @@ class Widget(DOMNode):
|
||||
# Regions which need to be transferred from cache to screen
|
||||
self._repaint_regions: set[Region] = set()
|
||||
|
||||
self._box_model_cache: LRUCache[object, BoxModel] = LRUCache(16)
|
||||
|
||||
# Cache the auto content dimensions
|
||||
self._content_width_cache: tuple[object, int] = (None, 0)
|
||||
self._content_height_cache: tuple[object, int] = (None, 0)
|
||||
@@ -1642,6 +1645,19 @@ class Widget(DOMNode):
|
||||
Returns:
|
||||
The size and margin for this widget.
|
||||
"""
|
||||
cache_key = (
|
||||
container,
|
||||
viewport,
|
||||
width_fraction,
|
||||
height_fraction,
|
||||
greedy,
|
||||
constrain_width,
|
||||
self._layout_updates,
|
||||
self.styles._cache_key,
|
||||
)
|
||||
if cached_box_model := self._box_model_cache.get(cache_key):
|
||||
return cached_box_model
|
||||
|
||||
styles = self.styles
|
||||
is_border_box = styles.box_sizing == "border-box"
|
||||
gutter = styles.gutter # Padding plus border
|
||||
@@ -1750,6 +1766,7 @@ class Widget(DOMNode):
|
||||
model = BoxModel(
|
||||
content_width + gutter.width, content_height + gutter.height, margin
|
||||
)
|
||||
self._box_model_cache[cache_key] = model
|
||||
return model
|
||||
|
||||
def get_content_width(self, container: Size, viewport: Size) -> int:
|
||||
@@ -3999,10 +4016,10 @@ class Widget(DOMNode):
|
||||
if self._size != size:
|
||||
self._set_dirty()
|
||||
self._size = size
|
||||
if layout:
|
||||
self.virtual_size = virtual_size
|
||||
else:
|
||||
self.set_reactive(Widget.virtual_size, virtual_size)
|
||||
# if layout:
|
||||
# self.virtual_size = virtual_size
|
||||
# else:
|
||||
self.set_reactive(Widget.virtual_size, virtual_size)
|
||||
self._container_size = container_size
|
||||
if self.is_scrollable:
|
||||
self._scroll_update(virtual_size)
|
||||
@@ -4200,14 +4217,15 @@ class Widget(DOMNode):
|
||||
Returns:
|
||||
The `Widget` instance.
|
||||
"""
|
||||
if layout:
|
||||
if layout and not self._layout_required:
|
||||
self._layout_required = True
|
||||
for ancestor in self.ancestors:
|
||||
if not isinstance(ancestor, Widget):
|
||||
break
|
||||
ancestor._clear_arrangement_cache()
|
||||
if not ancestor.styles.auto_dimensions:
|
||||
break
|
||||
self._layout_updates += 1
|
||||
# for ancestor in self.ancestors:
|
||||
# if not isinstance(ancestor, Widget):
|
||||
# break
|
||||
# ancestor._clear_arrangement_cache()
|
||||
# if not ancestor.styles.auto_dimensions:
|
||||
# break
|
||||
|
||||
if recompose:
|
||||
self._recompose_required = True
|
||||
@@ -4422,7 +4440,14 @@ class Widget(DOMNode):
|
||||
screen.post_message(messages.Update(self))
|
||||
if self._layout_required:
|
||||
self._layout_required = False
|
||||
screen.post_message(messages.Layout())
|
||||
for ancestor in self.ancestors:
|
||||
if not isinstance(ancestor, Widget):
|
||||
break
|
||||
ancestor._clear_arrangement_cache()
|
||||
ancestor._layout_updates += 1
|
||||
if not ancestor.styles.auto_dimensions:
|
||||
break
|
||||
screen.post_message(messages.Layout(self))
|
||||
|
||||
def focus(self, scroll_visible: bool = True) -> Self:
|
||||
"""Give focus to this widget.
|
||||
|
||||
@@ -228,7 +228,7 @@ class Button(Widget, can_focus=True):
|
||||
|
||||
BINDINGS = [Binding("enter", "press", "Press button", show=False)]
|
||||
|
||||
label: reactive[ContentText] = reactive[ContentText](Content.empty)
|
||||
label: reactive[ContentText] = reactive[ContentText](Content.empty())
|
||||
"""The text label that appears within the button."""
|
||||
|
||||
variant = reactive("default", init=False)
|
||||
@@ -293,11 +293,12 @@ class Button(Widget, can_focus=True):
|
||||
if label is None:
|
||||
label = self.css_identifier_styled
|
||||
|
||||
self.label = Content.from_text(label)
|
||||
self.variant = variant
|
||||
self.action = action
|
||||
self.compact = compact
|
||||
self.flat = flat
|
||||
self.compact = compact
|
||||
self.set_reactive(Button.label, Content.from_text(label))
|
||||
|
||||
self.action = action
|
||||
self.active_effect_duration = 0.2
|
||||
"""Amount of time in seconds the button 'press' animation lasts."""
|
||||
|
||||
|
||||
@@ -133,7 +133,6 @@ def test_height():
|
||||
styles.margin = 2
|
||||
|
||||
box_model = widget._get_box_model(Size(60, 20), Size(80, 24), one, one)
|
||||
print(box_model)
|
||||
assert box_model == BoxModel(Fraction(56), Fraction(10), Spacing(2, 2, 2, 2))
|
||||
|
||||
styles.margin = 1, 2, 3, 4
|
||||
|
||||
Reference in New Issue
Block a user