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
14 changes: 14 additions & 0 deletions evidence/viewport-subtree-projection-20261004/SANITY.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"qualification": "native source App and geometry controls only",
"production_head": "9a686b19",
"command": "PYTHONPATH=src python -m pytest -q tests/test_subtree_geometry_cache.py tests/test_viewport_geometry_targets.py tests/test_compositor.py",
"exit_code": 0,
"passed": 26,
"elapsed_seconds": 4.25,
"observed_stdout": ".......................... [100%]\n26 passed in 4.25s\n",
"new_environment": false,
"installed_package_writes": false,
"physical_recording": false,
"provider_inputs": 0,
"remaining": "Affected installed Toad/native qualification under Heisenberg named purpose; no measured CPU/frame-time gain."
}
38 changes: 38 additions & 0 deletions evidence/viewport-subtree-projection-20261004/SCOPE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Complete resources and viewport geometry

The existing `SubtreeGeometry` resource owns complete reusable arrangements.
Its viewport consumer currently publishes every cached descendant before the
compositor sorts and clips that scene. A long live body therefore projects its
offscreen descendants on each scroll even when only a few rows are exposed.

This change makes the original geometry family own viewport projection for
both restored and newly captured resources. Complete capture, ordinary lazy
full-map queries, explicit reader and interaction targets, overlays, covers and
scrollbars retain their original contracts. No second geometry map, cache or
state owner is introduced. Heisenberg confirmed the Toad contract: body roots
and explicit anchor paths need publication; body capture separately acquires
complete geometry. He owns the affected installed qualification.

`SubtreeGeometry.project_into` now owns both cheap source-bounds rejection before
clip/rank/placement transformation and exact clipped publication. Placed and
intrinsic resources implement only their original coordinate transforms. The
cache-hit and fresh-capture publication algorithms are replaced by one call;
enclosing complete captures keep complete descendant resources. Generic
`find_widget`/`full_map` and full subtree captures remain complete.

The one final native batch passed 26 controls in 4.25 seconds using existing
system dependencies. The added actual native App case exercises forward and
reverse scrolling, complete body capture, an offscreen reader target, lazy full
geometry and authored display invalidation. Existing controls cover viewport
targets, scene ordering, clipping, cache retirement and source mutation. See
`SANITY.json` and `owner-consumers.json`; both native source snapshots parse all
249 production modules without omissions, and the Toad context parses 288.

Complete cached membership is still retained and every resource entry is still
visited. The change removes unnecessary offscreen transforms and downstream
scene publication/sorting; it does not establish CPU dominance or frame-time
gain. The affected installed application remains unqualified. This draft has
no build, installed-package, provider or recording purpose.

The separate callback arity source defect remains independent; it does not hold
this geometry change or imply a frame-time attribution.
180 changes: 180 additions & 0 deletions evidence/viewport-subtree-projection-20261004/owner-consumers.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
{
"scope": "Source declarations and syntactic consumers, not dynamic dispatch proof",
"snapshots": [
{
"head": "777232c8b960272cb3ddc445df04c9133e6dd6fa",
"root": "src/textual",
"parsed": 249,
"omissions": [],
"declarations": [
{
"path": "src/textual/_compositor.py",
"class": "SubtreeMapGeometry",
"member": "project",
"line": 172
},
{
"path": "src/textual/_compositor.py",
"class": "SubtreeGeometry",
"member": "complete_arrangement",
"line": 201
},
{
"path": "src/textual/_compositor.py",
"class": "SubtreeGeometry",
"member": "capture",
"line": 205
},
{
"path": "src/textual/_compositor.py",
"class": "SubtreeGeometry",
"member": "restore_into",
"line": 212
},
{
"path": "src/textual/_compositor.py",
"class": "SubtreeGeometry",
"member": "project_into",
"line": 222
},
{
"path": "src/textual/_compositor.py",
"class": "PlacedSubtreeGeometry",
"member": "project_into",
"line": 237
},
{
"path": "src/textual/_compositor.py",
"class": "IntrinsicSubtreeGeometry",
"member": "complete_arrangement",
"line": 248
},
{
"path": "src/textual/_compositor.py",
"class": "IntrinsicSubtreeGeometry",
"member": "capture",
"line": 252
},
{
"path": "src/textual/_compositor.py",
"class": "IntrinsicSubtreeGeometry",
"member": "project_into",
"line": 269
}
],
"consumers": [
{
"path": "src/textual/_compositor.py",
"line": 217,
"callee": "self.project_into"
},
{
"path": "src/textual/_compositor.py",
"line": 1069,
"callee": "cached.restore_into"
}
]
},
{
"head": "9a686b19",
"root": "src/textual",
"parsed": 249,
"omissions": [],
"declarations": [
{
"path": "src/textual/_compositor.py",
"class": "SubtreeMapGeometry",
"member": "region",
"line": 173
},
{
"path": "src/textual/_compositor.py",
"class": "SubtreeMapGeometry",
"member": "project",
"line": 177
},
{
"path": "src/textual/_compositor.py",
"class": "SubtreeGeometry",
"member": "complete_arrangement",
"line": 206
},
{
"path": "src/textual/_compositor.py",
"class": "SubtreeGeometry",
"member": "capture",
"line": 210
},
{
"path": "src/textual/_compositor.py",
"class": "SubtreeGeometry",
"member": "restore_into",
"line": 217
},
{
"path": "src/textual/_compositor.py",
"class": "SubtreeGeometry",
"member": "project_into",
"line": 227
},
{
"path": "src/textual/_compositor.py",
"class": "SubtreeGeometry",
"member": "_project_entry",
"line": 250
},
{
"path": "src/textual/_compositor.py",
"class": "PlacedSubtreeGeometry",
"member": "_project_entry",
"line": 265
},
{
"path": "src/textual/_compositor.py",
"class": "IntrinsicSubtreeGeometry",
"member": "complete_arrangement",
"line": 275
},
{
"path": "src/textual/_compositor.py",
"class": "IntrinsicSubtreeGeometry",
"member": "capture",
"line": 279
},
{
"path": "src/textual/_compositor.py",
"class": "IntrinsicSubtreeGeometry",
"member": "_project_entry",
"line": 296
}
],
"consumers": [
{
"path": "src/textual/_compositor.py",
"line": 222,
"callee": "self.project_into"
},
{
"path": "src/textual/_compositor.py",
"line": 244,
"callee": "self._project_entry"
},
{
"path": "src/textual/_compositor.py",
"line": 1114,
"callee": "resource.restore_into"
}
]
}
],
"ambiguities": [
"External dynamically installed SubtreeGeometry subclasses/hooks are outside the declared repository roots; no compatibility path added.",
"Syntactic attribute-call matching does not resolve arbitrary dynamic aliases."
],
"toad_context": {
"parsed": 288,
"omissions": [],
"head": "c9ae0e5f5ac0f7055680674f1dc9bdd597d96f29",
"contract": "Heisenberg confirmed body-root and explicit anchor targets; complete capture separately borrows published root and render_subtree_strips. No Toad source edits."
}
}
104 changes: 63 additions & 41 deletions src/textual/_compositor.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,11 @@ class SubtreeMapGeometry(NamedTuple):
geometry: MapGeometry
clip_bounds: tuple[Region, ...]

@property
def region(self) -> Region:
"""The original placement bounds, before any destination projection."""
return self.geometry.region

def project(self, original: SubtreeGeometryKey, current: SubtreeGeometryKey,
clip: SceneClip, *, root: bool) -> tuple[MapGeometry, SceneClip]:
for bound in self.clip_bounds:
Expand All @@ -185,7 +190,7 @@ def project(self, original: SubtreeGeometryKey, current: SubtreeGeometryKey,
), clip


GeometryEntry = TypeVar("GeometryEntry")
GeometryEntry = TypeVar("GeometryEntry", MapGeometry, SubtreeMapGeometry)


@dataclass(frozen=True)
Expand All @@ -212,15 +217,38 @@ def matches(self, key: SubtreeGeometryKey) -> bool:
def restore_into(
self, geometry: CompositorMap, widgets: set[Widget], invisible_widgets: set[Widget],
key: SubtreeGeometryKey, clip: SceneClip, clips: dict[Widget, SceneClip],
root: Widget,
root: Widget, *, visible_only: bool, retained: set[Widget], bounds: Region,
) -> None:
self.project_into(geometry, key, clip, clips, root)
self.project_into(geometry, key, clip, clips, root,
visible_only=visible_only, retained=retained, bounds=bounds)
widgets.update(self.widgets)
invisible_widgets.update(self.invisible_widgets)

@abstractmethod
def project_into(self, geometry: CompositorMap, key: SubtreeGeometryKey,
clip: SceneClip, clips: dict[Widget, SceneClip], root: Widget) -> None:
clip: SceneClip, clips: dict[Widget, SceneClip], root: Widget,
*, visible_only: bool, retained: set[Widget], bounds: Region) -> None:
"""Publish the requested scene without retiring complete source geometry.

A viewport needs exposed descendants and its explicit geometry targets.
Complete capture and ordinary full-map acquisition still consume every
entry. Both newly captured and reused resources publish through here.
"""
source_bounds = bounds - (key.region.offset - self.key.region.offset)
for node, entry in self.geometry.items():
required = not visible_only or node is root or node in retained
# Clip intersection can only reduce these original rectangle bounds.
# Reject an offscreen, unrequired entry before resolving its clip
# chain, descendant rank and destination MapGeometry allocation.
if not required and not entry.region.overlaps(source_bounds):
continue
placement, node_clip = self._project_entry(entry, key, clip, root=node is root)
if required or placement.visible_region.overlaps(bounds):
geometry[node] = placement
clips[node] = node_clip

@abstractmethod
def _project_entry(self, entry: GeometryEntry, key: SubtreeGeometryKey,
clip: SceneClip, *, root: bool) -> tuple[MapGeometry, SceneClip]:
...

def references_retired(self, owner: Widget, retired: set[Widget]) -> bool:
Expand All @@ -234,10 +262,9 @@ class PlacedSubtreeGeometry(SubtreeGeometry[MapGeometry]):
def matches(self, key: SubtreeGeometryKey) -> bool:
return self.key == key

def project_into(self, geometry: CompositorMap, key: SubtreeGeometryKey,
clip: SceneClip, clips: dict[Widget, SceneClip], root: Widget) -> None:
geometry.update(self.geometry)
clips.update((node, RootSceneClip(entry.clip)) for node, entry in self.geometry.items())
def _project_entry(self, entry: MapGeometry, key: SubtreeGeometryKey,
clip: SceneClip, *, root: bool) -> tuple[MapGeometry, SceneClip]:
return entry, RootSceneClip(entry.clip)


@dataclass(frozen=True)
Expand Down Expand Up @@ -266,10 +293,9 @@ def capture(cls, key, geometry, widgets, invisible_widgets, clips, clip, screen_
def matches(self, key: SubtreeGeometryKey) -> bool:
return self.key.intrinsic() == key.intrinsic()

def project_into(self, geometry: CompositorMap, key: SubtreeGeometryKey,
clip: SceneClip, clips: dict[Widget, SceneClip], root: Widget) -> None:
for node, entry in self.geometry.items():
geometry[node], clips[node] = entry.project(self.key, key, clip, root=node is root)
def _project_entry(self, entry: SubtreeMapGeometry, key: SubtreeGeometryKey,
clip: SceneClip, *, root: bool) -> tuple[MapGeometry, SceneClip]:
return entry.project(self.key, key, clip, root=root)


class CompositorUpdate:
Expand Down Expand Up @@ -1053,6 +1079,7 @@ def add_widget(widget, virtual_region, region, order, layer_order, clip, visible
complete, inherited_layers) # noqa: F821 -- closure cleared after traversal
return
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.
Expand All @@ -1064,34 +1091,29 @@ def add_widget(widget, virtual_region, region, order, layer_order, clip, visible
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 ())
cached = self._subtree_geometry.get(widget)
if cached is not None and cached.matches(key):
cached.restore_into(map, widgets, invisible_widgets, key, clip, clips, widget._render_widget)
return
# A body needs its complete native arrangement to reveal new rows.
# A scroll-owning viewport changes its own arrangement inputs and
# must continue culling; it cannot demand the entire prepared buffer
# merely to retain an exact-coordinate scene.
# Construct only this native subtree, then publish it into its
# enclosing scene. Copying and subtracting the already-built whole
# scene for each body makes admission depend on unrelated bodies.
parent_map, parent_widgets, parent_invisible = map, widgets, invisible_widgets
map, widgets, invisible_widgets = {}, set(), set()
try:
arrange_widget(widget, virtual_region, region, order, layer_order, clip, visible, dock_gutter,
complete, inherited_layers) # noqa: F821 -- closure cleared after traversal
geometry = map
added_widgets, added_invisible = frozenset(widgets), frozenset(invisible_widgets)
finally:
map, widgets, invisible_widgets = parent_map, parent_widgets, parent_invisible
map.update(geometry)
widgets.update(added_widgets)
invisible_widgets.update(added_invisible)
if (widget not in self._subtree_geometry
and len(self._subtree_geometry) >= self.max_subtree_geometry_entries):
self._subtree_geometry.pop(next(iter(self._subtree_geometry)))
self._subtree_geometry[widget] = resource_type.capture(
key, geometry, added_widgets, added_invisible, clips, clip, screen_coordinates)
resource = self._subtree_geometry.get(widget)
if resource is None or not resource.matches(key):
# Capture complete source geometry before its viewport publication.
# Enclosing complete captures borrow all child entries; ordinary
# viewport publication retains only exposed and required boxes.
parent_map, parent_widgets, parent_invisible = map, widgets, invisible_widgets
map, widgets, invisible_widgets = {}, set(), set()
try:
arrange_widget(widget, virtual_region, region, order, layer_order, clip, visible, dock_gutter,
complete, inherited_layers) # noqa: F821 -- closure cleared after traversal
geometry = map
added_widgets, added_invisible = frozenset(widgets), frozenset(invisible_widgets)
finally:
map, widgets, invisible_widgets = parent_map, parent_widgets, parent_invisible
if (widget not in self._subtree_geometry
and len(self._subtree_geometry) >= self.max_subtree_geometry_entries):
self._subtree_geometry.pop(next(iter(self._subtree_geometry)))
resource = resource_type.capture(
key, geometry, added_widgets, added_invisible, clips, clip, screen_coordinates)
self._subtree_geometry[widget] = resource
resource.restore_into(map, widgets, invisible_widgets, key, clip, clips, widget._render_widget,
visible_only=visible_only and not enclosing_complete,
retained=retained_paths, bounds=root_geometry.region)

# Add top level (root) widget
self._arranging = True
Expand Down
Loading
Loading