Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 29 additions & 18 deletions src/textual/_compositor.py
Original file line number Diff line number Diff line change
Expand Up @@ -923,8 +923,8 @@ def _arrange_root(
for owner in mutation_roots:
mutation_paths.update(owner.walk_ancestors(with_self=True))
if mutation_roots:
for node, geometry in previous.items():
for owner in mutation_roots.intersection(node.walk_ancestors(with_self=True)):
for node, geometry, owners in self._geometry_for_roots(mutation_roots):
for owner in owners:
held[owner][node] = geometry
for owner in mutation_paths - mutation_roots:
self._subtree_geometry.pop(owner, None)
Expand Down Expand Up @@ -1667,21 +1667,17 @@ def _get_renders(
),
)

def deferred_regions(self, roots: Iterable[Widget]) -> tuple[Region, ...]:
"""Derive held paint bounds from committed geometry, never lazy layout.
def _geometry_for_roots(
self, roots: Iterable[Widget],
) -> Iterator[tuple[Widget, MapGeometry, frozenset[Widget]]]:
"""Read committed placements with their original current root membership.

A mounted root without a placement holds its nearest placed ancestor:
an unknown destination cannot authorize painting through that subtree.
Overflowing descendants retain their own original clipped bounds.
Share parent answers only during this synchronous traversal. Reading
children instead would omit covers and native virtual widgets.
"""
roots = set(roots)
if not roots:
return ()
regions: set[Region] = set()
placed: set[Widget] = set()
# This synchronous read shares parent answers only for this invocation.
# Read the original parent path, including covers and virtual widgets;
# DOM child traversal is not equivalent to committed-map membership.
return
membership = {}
for widget, geometry in self._published_map.items():
path = []
Expand All @@ -1691,15 +1687,30 @@ def deferred_regions(self, roots: Iterable[Widget]) -> tuple[Region, ...]:
break
path.append(ancestor)
else:
held = False
held = frozenset()
for ancestor in reversed(path):
if ancestor in roots:
placed.add(ancestor)
held = True
held = held | {ancestor}
membership[ancestor] = held
if held:
if region := geometry.visible_region.intersection(self.size.region):
regions.add(region)
yield widget, geometry, held

def deferred_regions(self, roots: Iterable[Widget]) -> tuple[Region, ...]:
"""Derive held paint bounds from committed geometry, never lazy layout.

A mounted root without a placement holds its nearest placed ancestor:
an unknown destination cannot authorize painting through that subtree.
Overflowing descendants retain their own original clipped bounds.
"""
roots = set(roots)
if not roots:
return ()
regions: set[Region] = set()
placed: set[Widget] = set()
for _, geometry, owners in self._geometry_for_roots(roots):
placed.update(owners)
if region := geometry.visible_region.intersection(self.size.region):
regions.add(region)
for root in roots - placed:
if not root.is_attached:
continue
Expand Down
Loading