Skip to content
Merged
Show file tree
Hide file tree
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
48 changes: 48 additions & 0 deletions evidence/complete-placed-geometry-20261007/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Complete placed geometry supplies narrower requests

Native PlacedSubtreeGeometry previously required the capture visible_only flag
to equal the request flag. It therefore discarded an unchanged complete source
when the next transaction requested only visible geometry. The compositor also
bypassed any partial-mode resource when acquiring a retained path, even if its
original capture was complete and already owned that path.

The existing resource matches method now accepts require_complete. Placed
geometry permits complete-to-visible requests only when every other key field
matches. Partial-to-full remains a miss. Retained requests require the original
complete capture, while IntrinsicSubtreeGeometry retains its existing complete
source and original intrinsic matching. No new state, cache, capture format or
projection behavior. The compositor delegates to this one answer; an incomplete
or changed retained source still uses the original uncached path acquisition.

Facts independently forcing placement remain geometry/child revision, width and
height, viewport size, region, rank, clip, visibility, dock gutter, inherited
layers and scroll offset. Authored mutations and held-root invalidation remain
unchanged. Full acquisition/viewport projection are different requests against
one complete source, not different source identities. Partial captures cannot
invent offscreen geometry. Existing spatial projection handles actual entries.

Source audit with original Package:250 production modules, zero omissions;
SubtreeGeometry plus both concrete matches implementations migrated, one caller
in Compositor.add_widget. No external dynamic resource override is inferred from
lexical source. Public custom arrangement and coordinate behavior remain bound
to the original CACHE_SUBTREE_GEOMETRY opt-in and native key contract.

Three real native App checks passed1.19s. Complete capture to visible plus an
actual offscreen retained row performs zero subtree arrangements; cache-disabled
native reference agrees on visible geometry and retained coordinates. Changed
scroll, partial-to-retained/full and changed child arrange. Existing fixed,
viewport/overlay, resize, full-map and rendering controls remain correct.

One original history_check.py App run used readonly installed selected-memberships
delivery runtime Toad/Core, replacing only Textual source2d85b8e9. Imports/command
stdout retained under /home/ts/.cache/agent-scratch/native99-channel-history-20261007.
HISTORY_PROFILE=0;160 authored initial private wire messages,20 incoming and30
PageUp. No provider/public inputs, package writes, native build or pin change.
Terminal0, empty stderr, no App exception. Result:53 mounted rows/oldest113,
p95 loop16.20ms, maximum126.83ms,6 intervals above50ms. Actual runtime/Toad changes
and different final workload invalidate comparison against the earlier120-row
run. The driver asserts App health, not complete pagination or exact reader
position; no full paging or overall speedup claim. Significant pauses remain.
The original runtime_fixture joins teardown; controller joined and no driver
process remained. Exact child births were not recorded. Parent owns Toad
consumer followthrough and durable delivery; no extra App rerun performed.
11 changes: 11 additions & 0 deletions evidence/complete-placed-geometry-20261007/app-result.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"main_loop_samples": 2621,
"median_ms": 5.766002926975489,
"p95_ms": 16.204836894758046,
"max_ms": 126.82982999831438,
"gaps_above_50ms": 6,
"mounted_rows": 53,
"oldest_sequence": 113,
"app_error": null,
"profiling": false
}
30 changes: 19 additions & 11 deletions src/textual/_compositor.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ def _spatial_map(self) -> SpatialMap[tuple[int, Widget]]:
return spatial_map

@abstractmethod
def matches(self, key: SubtreeGeometryKey) -> bool:
def matches(self, key: SubtreeGeometryKey, *, require_complete: bool = False) -> bool:
...

def restore_into(
Expand Down Expand Up @@ -287,8 +287,13 @@ def references_retired(self, owner: Widget, retired: set[Widget]) -> bool:
class PlacedSubtreeGeometry(SubtreeGeometry[MapGeometry]):
"""A culled or screen-dependent arrangement keeps its exact placement."""

def matches(self, key: SubtreeGeometryKey) -> bool:
return self.key == key
def matches(self, key: SubtreeGeometryKey, *, require_complete: bool = False) -> bool:
if self.key.visible_only and require_complete:
return False
return self.key == key or (
not self.key.visible_only and key.visible_only
and self.key._replace(visible_only=True) == key
)

def _project_entry(self, entry: MapGeometry, key: SubtreeGeometryKey,
clip: SceneClip, *, root: bool) -> tuple[MapGeometry, SceneClip]:
Expand Down Expand Up @@ -319,7 +324,7 @@ def capture(cls, key, geometry, widgets, invisible_widgets, clips, clip, screen_
return super().capture(key, intrinsic, widgets, invisible_widgets,
clips, clip, screen_coordinates)

def matches(self, key: SubtreeGeometryKey) -> bool:
def matches(self, key: SubtreeGeometryKey, *, require_complete: bool = False) -> bool:
return self.key.intrinsic() == key.intrinsic()

def _project_entry(self, entry: SubtreeMapGeometry, key: SubtreeGeometryKey,
Expand Down Expand Up @@ -1156,18 +1161,21 @@ def add_widget(widget, virtual_region, region, order, layer_order, clip, visible
resource_type = widget.subtree_geometry_resource()
enclosing_complete = complete
complete = resource_type.complete_arrangement(complete)
# Retention requires an uncropped path only for partial resources.
# Complete native arrangements already own every descendant box.
if widget in retained_paths and not complete:
arrange_widget(widget, virtual_region, region, order, layer_order, clip, visible, dock_gutter,
complete, inherited_layers) # noqa: F821 -- closure cleared after traversal
return
key = SubtreeGeometryKey.from_widget(widget,
virtual_region, region, order, layer_order, clip.region, visible, dock_gutter,
size, visible_only and not complete,
tuple(inherited_layers.items()) if inherited_layers is not None else ())
resource = self._subtree_geometry.get(widget)
if resource is None or not resource.matches(key):
require_complete = widget in retained_paths and not complete
matches = resource is not None and resource.matches(key, require_complete=require_complete)
# A complete captured source already owns every requested path.
# A partial or changed source still acquires the original path;
# do not retain a partial capture whose key omits that request.
if require_complete and not matches:
arrange_widget(widget, virtual_region, region, order, layer_order, clip, visible, dock_gutter,
complete, inherited_layers) # noqa: F821 -- closure cleared after traversal
return
if not matches:
# Capture complete source geometry before its viewport publication.
# Enclosing complete captures borrow all child entries; ordinary
# viewport publication retains only exposed and required boxes.
Expand Down
71 changes: 71 additions & 0 deletions tests/test_complete_placed_geometry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
from textual._compositor import Compositor, PlacedSubtreeGeometry
from textual.app import App, ComposeResult
from textual.containers import VerticalScroll
from textual.widgets import Static


class CapturedScroll(VerticalScroll):
CACHE_SUBTREE_GEOMETRY = True
arrangement_calls = 0

def arrange(self, size, optimal=False):
self.arrangement_calls += 1
return super().arrange(size, optimal=optimal)


class CompleteSceneApp(App):
CSS = "CapturedScroll { height: 8; } Static { height: 2; }"

def compose(self) -> ComposeResult:
with CapturedScroll():
for index in range(25):
yield Static(f"Original row {index}", id=f"row{index}")


async def test_complete_placed_source_serves_viewport_and_retained_paths():
app = CompleteSceneApp()
async with app.run_test(size=(40, 12)) as pilot:
await pilot.pause()
history = app.query_one(CapturedScroll)
target = app.query_one("#row23")
compositor = app.screen._compositor
compositor.reflow(app.screen, app.size)
complete = compositor._subtree_geometry[history]
assert isinstance(complete, PlacedSubtreeGeometry)
assert not complete.key.visible_only and target in complete.geometry

history.arrangement_calls = 0
compositor.reflow_visible(app.screen, app.size, retain_geometry=(target,))
assert history.arrangement_calls == 0
assert compositor._subtree_geometry[history] is complete
target_geometry = compositor._visible_map[target]
visible = dict(compositor.visible_widgets)
reference = Compositor(max_subtree_geometry_entries=0)
reference.reflow_visible(app.screen, app.size, retain_geometry=(target,))
assert reference.visible_widgets == visible
assert reference._visible_map[target] == target_geometry

# A changed scroll scope still needs fresh placement and clipping.
history.scroll_to(y=6, animate=False, immediate=True)
history.arrangement_calls = 0
compositor.reflow_visible(app.screen, app.size, retain_geometry=())
assert history.arrangement_calls > 0
partial = compositor._subtree_geometry[history]
assert partial.key.visible_only

# This partial source cannot grant a previously unacquired path.
history.arrangement_calls = 0
compositor.reflow_visible(app.screen, app.size, retain_geometry=(target,))
assert history.arrangement_calls > 0
reference.reflow_visible(app.screen, app.size, retain_geometry=(target,))
assert reference.visible_widgets == compositor.visible_widgets
assert reference._visible_map[target] == compositor._visible_map[target]

# Neither full acquisition nor a changed child can reuse partial data.
history.arrangement_calls = 0
compositor.reflow(app.screen, app.size)
assert history.arrangement_calls > 0
app.query_one("#row0", Static).styles.height = 4
history.arrangement_calls = 0
compositor.reflow_visible(app.screen, app.size, retain_geometry=())
assert history.arrangement_calls > 0
Loading