diff --git a/src/textual/_compositor.py b/src/textual/_compositor.py index 8dc03857de..3e85996124 100644 --- a/src/textual/_compositor.py +++ b/src/textual/_compositor.py @@ -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) @@ -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 = [] @@ -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