diff --git a/evidence/immutable-subtree-window-acquisition-20261005/NORMAL-MAIN-JOIN.json b/evidence/immutable-subtree-window-acquisition-20261005/NORMAL-MAIN-JOIN.json new file mode 100644 index 0000000000..4863254de7 --- /dev/null +++ b/evidence/immutable-subtree-window-acquisition-20261005/NORMAL-MAIN-JOIN.json @@ -0,0 +1,17 @@ +{ + "qualified_source": "230289d89d23195a005aadaa645686cebdcf7c04", + "normal_main": "fc17c5f6f046d5fadad47da71842a78432731f63", + "merge_head": "f244a641e1aece5ec385c1a81e3908ae5a5eb68e", + "all_textual_assets": 266, + "git_local_frozen_wheel_byte_equal": true, + "build_inputs_byte_equal": [ + "README.md", + "pyproject.toml" + ], + "src_delta": [], + "retained_wheel": "/home/ts/wt/textual-native-subtree-strips-20261002/.artifacts/native65-filewheel-20261005/textual-8.2.8-py3-none-any.whl", + "wheel_sha256": "e253066f005a2d9d67aeae2279ee4852b92f86d13b03acf32b60f16307d79355", + "wheel_metadata_sha256": "fa4831c4001c94a8d8177cccd05099d8fa021575c81a3bd53eae3b995eb22f1e", + "merge_conflicts": false, + "qualification": "Normal current-main integration only. Original native controls and standalone e253 artifact preserved; no new source checks/build/runtime/installed/frame qualification." +} diff --git a/evidence/immutable-subtree-window-acquisition-20261005/SANITY.json b/evidence/immutable-subtree-window-acquisition-20261005/SANITY.json new file mode 100644 index 0000000000..a1b5650d77 --- /dev/null +++ b/evidence/immutable-subtree-window-acquisition-20261005/SANITY.json @@ -0,0 +1,32 @@ +{ + "source": "0ab59a0466cff6fd2bd8c3747b1be7e2fd660586", + "production_byte_equal_native_batch": "5a569464bcc567b5f8e85120a1983f52db1cf82e", + "native_source_batch": { + "passed": 32, + "failed": 1, + "seconds": 5.195401245029643, + "original_log": "/home/ts/wt/textual-native-subtree-strips-20261002/.artifacts/immutable-subtree-window-acquisition-20261005/negative01-native-source-batch.log", + "original_log_sha256": "543bdbdecaa15d6150b4d9c582097a33d013432e21a3aeca9a8c0d48200528f9" + }, + "corrected_overlay_source_control": { + "source": "0ab59a0466cff6fd2bd8c3747b1be7e2fd660586", + "argv": [ + "python", + "-m", + "pytest", + "-q", + "tests/test_viewport_geometry_targets.py::test_cached_overlay_and_fixed_children_match_the_original_scene" + ], + "exit_code": 0, + "seconds": 1.4935176220024005, + "assessment": "Original negative01: 32 qualified controls and one incorrect overlay oracle. Diagnostic identified source-row-9 at y10 outside10row viewport; visible_region had zeroheight but Region.overlaps(wholeviewport) reportedtrue. Oracle now consumes original Compositor._paint_regions, exactly terminal paint admission plus original explicit bodytarget. Native production unchanged from source batch5a569; passed32 source cases not repeated.", + "qualification": "Actual native App source only; no installed Toad/frame performance." + }, + "interpretation": "32 original qualified controls plus corrected affected overlay control PASS on byte-equal native production. Original 1-failure receipt retained. No repeated unchanged controls. Source-App scope only, no installed or frame gain qualification.", + "remaining_cost": "Complete widgets/invisible membership unions remain O(n); spatial grid construction is O(source extent/entries) once per immutable resource; coarse query false positives still need exact clipping. No CPU/smoothness proof.", + "no_new_env": true, + "no_installed_access": true, + "no_build": true, + "no_provider": true, + "no_film": true +} diff --git a/evidence/immutable-subtree-window-acquisition-20261005/SCOPE.md b/evidence/immutable-subtree-window-acquisition-20261005/SCOPE.md new file mode 100644 index 0000000000..9a77e3e3dd --- /dev/null +++ b/evidence/immutable-subtree-window-acquisition-20261005/SCOPE.md @@ -0,0 +1,79 @@ +# Immutable subtree window acquisition + +Base: published frozen combined Text63/64 08e3dd6b3c7839baafcac6ff9ea61eb510ff9573. +The qualified source controls and 487b standalone wheel remain unchanged. + +The existing immutable SubtreeGeometry owns source placement, membership, clip +scope and source order. Its viewport projection still scans every stored entry +before rejecting offscreen rectangles; restore also flattens complete native +membership on each use. Preserve complete capture, full-map queries, explicit +reader paths, overlay/screen-dependent fallback and bounded resource retirement. + +Reuse the existing native spatial acquisition mechanism only as a derived index +of the original immutable resource, not a second scene or mutable geometry +registry. Migrate Placed and Intrinsic capture/publication together. Investigate +membership consumers before changing the published considered-widget contract. + +Claim: native _compositor.py subtree capture/projection/restore and necessary +original membership consumers. No Toad, installed prefix, SDK, keeper, App or +recording lease. Heis owns the Toad workflow and original453 qualification. + +Source ownership reasoning and implementation precede the final proportional +native App source controls. No CPU, frame latency or smoothness claim follows +from source deletion or a spatial-query operation count. + +## Frozen implementation and source qualification + +Production source: 0ab59a0466cff6fd2bd8c3747b1be7e2fd660586 (native product +byte-equal to the actual 5a569464 source batch). One production file: 33 added, +6 deleted lines compared with frozen08e3; no other native production changes. + +`SubtreeGeometry.capture` now binds the original source ordinal alongside each +entry in its one immutable geometry mapping. Placed capture and Intrinsic capture +both consume this shared algorithm. Its private cached spatial resource uses +EXISTING SpatialMap and contains widget/ordinal references derived only from that +mapping. No second geometry map, ordinal registry, mutable update path or extra +lifetime owner was introduced. The original cache capacity, source key and +retirement own the index's lifetime. + +Viewport publication queries that resource in original source coordinates, adds +root/explicit retained paths by direct lookup, and projects candidates in original +insertion order. This replaces the every-entry viewport scan. Complete full-map +and body capture consume all original entries and do not require the spatial +index. Screen-dependent/overlay clips still select Placed geometry at capture; +the index uses those actual absolute rectangles, not layout's fixed/overlay flags. +Exact region and clip admission remains unchanged after the candidate query. + +Retirement now uses original geometry-key and membership-set disjointness rather +than allocating an expanded tuple of every referenced widget. Complete widget and +invisible-widget membership remain their original immutable sets, and restore +still unions them: this O(n) work is a required current contract, not claimed gone. +Index construction is source-size/extent work once per retained resource; coarse +spatial candidates still require exact clipping. No asymptotic/time claim applies +to the complete rendering workflow. + +One final native App/source batch yielded 32 PASS and one failed NEW overlay +expectation. The original log and negative receipt are retained. Its expected set +used an empty intersection rectangle as painted (row9 y10 outside a ten-row +viewport); original Region.overlaps can return true for that zero-height shape. +The control now consumes the existing Compositor._paint_regions owner plus the +original explicit target. Only that changed control was re-run: PASS, 0.35s +pytest / 1.49s controller. Native production is byte-equal across both receipts; +the other 32 accepted controls were not repeated. No Region behavior was patched +or timeout/geometry oracle relaxed. SANITY.json binds these exact limits. + +Actual source-App coverage: 240-row cached body forward/reverse acquisition, +offscreen explicit reader target, complete capture/full-map lookup, considered +membership, authored display invalidation, cache-budget/retirement; fixed child +and screen-overlay fallback/order against the original uncached scene; existing +native paint/layer/point-hit/damage and SpatialMap controls. + +After: native249/tests460/Toad288 parsed without omissions. Before/after complete +root candidate output is in owner-consumers.json; dynamic aliases and external +subclass resolution remain explicit limits. Pattern IMPL-12: original shared +capture/publication behavior replaces the duplicated source-enumeration path. + +No new env, build, keeper, installed prefix, Toad App, provider, physical recording +or CPU/smoothness qualification. Draft source checkpoint; affected installed +qualification belongs to a future meaningful Heis integration purpose. Frozen +Text63/64 08e3/wheel487 and Heis453 operands remain untouched. diff --git a/evidence/immutable-subtree-window-acquisition-20261005/owner-consumers.json b/evidence/immutable-subtree-window-acquisition-20261005/owner-consumers.json new file mode 100644 index 0000000000..775118a996 --- /dev/null +++ b/evidence/immutable-subtree-window-acquisition-20261005/owner-consumers.json @@ -0,0 +1,1792 @@ +{ + "before": { + "source": "08e3dd6b3c7839baafcac6ff9ea61eb510ff9573", + "roots": { + "src/textual": { + "parsed": 249, + "omissions": [] + }, + "tests": { + "parsed": 460, + "omissions": [] + } + }, + "sites": [ + { + "path": "src/textual/_compositor.py", + "line": 166, + "symbol": "SubtreeMapGeometry", + "kind": "ClassDef" + }, + { + "path": "src/textual/_compositor.py", + "line": 197, + "symbol": "SubtreeGeometry", + "kind": "ClassDef" + }, + { + "path": "src/textual/_compositor.py", + "line": 259, + "symbol": "PlacedSubtreeGeometry", + "kind": "ClassDef" + }, + { + "path": "src/textual/_compositor.py", + "line": 271, + "symbol": "IntrinsicSubtreeGeometry", + "kind": "ClassDef" + }, + { + "path": "src/textual/_compositor.py", + "line": 217, + "symbol": "restore_into", + "kind": "FunctionDef" + }, + { + "path": "src/textual/_compositor.py", + "line": 227, + "symbol": "project_into", + "kind": "FunctionDef" + }, + { + "path": "src/textual/_compositor.py", + "line": 254, + "symbol": "references_retired", + "kind": "FunctionDef" + }, + { + "path": "src/textual/_compositor.py", + "line": 169, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 193, + "symbol": "SubtreeMapGeometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 201, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 203, + "symbol": "invisible_widgets", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 259, + "symbol": "SubtreeGeometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 271, + "symbol": "SubtreeGeometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 271, + "symbol": "SubtreeMapGeometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 828, + "symbol": "invisible_widgets", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1140, + "symbol": "invisible_widgets", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1328, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1333, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 175, + "symbol": "geometry", + "kind": "Attribute" + }, + { + "path": "src/textual/_compositor.py", + "line": 211, + "symbol": "invisible_widgets", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 222, + "symbol": "project_into", + "kind": "Attribute" + }, + { + "path": "src/textual/_compositor.py", + "line": 222, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 225, + "symbol": "invisible_widgets", + "kind": "Attribute" + }, + { + "path": "src/textual/_compositor.py", + "line": 291, + "symbol": "invisible_widgets", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 296, + "symbol": "SubtreeMapGeometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 735, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 824, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1329, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1330, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1331, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1359, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1363, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1377, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1591, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1607, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 182, + "symbol": "geometry", + "kind": "Attribute" + }, + { + "path": "src/textual/_compositor.py", + "line": 183, + "symbol": "geometry", + "kind": "Attribute" + }, + { + "path": "src/textual/_compositor.py", + "line": 186, + "symbol": "geometry", + "kind": "Attribute" + }, + { + "path": "src/textual/_compositor.py", + "line": 211, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 225, + "symbol": "invisible_widgets", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 237, + "symbol": "geometry", + "kind": "Attribute" + }, + { + "path": "src/textual/_compositor.py", + "line": 282, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 282, + "symbol": "invisible_widgets", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 285, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 290, + "symbol": "SubtreeMapGeometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 527, + "symbol": "SubtreeGeometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 570, + "symbol": "references_retired", + "kind": "Attribute" + }, + { + "path": "src/textual/_compositor.py", + "line": 737, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 747, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1114, + "symbol": "restore_into", + "kind": "Attribute" + }, + { + "path": "src/textual/_compositor.py", + "line": 1114, + "symbol": "invisible_widgets", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1343, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1360, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1361, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1364, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1365, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1592, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1610, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1612, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 184, + "symbol": "geometry", + "kind": "Attribute" + }, + { + "path": "src/textual/_compositor.py", + "line": 185, + "symbol": "geometry", + "kind": "Attribute" + }, + { + "path": "src/textual/_compositor.py", + "line": 185, + "symbol": "geometry", + "kind": "Attribute" + }, + { + "path": "src/textual/_compositor.py", + "line": 187, + "symbol": "geometry", + "kind": "Attribute" + }, + { + "path": "src/textual/_compositor.py", + "line": 246, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 281, + "symbol": "PlacedSubtreeGeometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 289, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 289, + "symbol": "invisible_widgets", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 790, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1099, + "symbol": "invisible_widgets", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1100, + "symbol": "invisible_widgets", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1104, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1112, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1347, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 255, + "symbol": "geometry", + "kind": "Attribute" + }, + { + "path": "src/textual/_compositor.py", + "line": 255, + "symbol": "invisible_widgets", + "kind": "Attribute" + }, + { + "path": "src/textual/_compositor.py", + "line": 280, + "symbol": "invisible_widgets", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 288, + "symbol": "PlacedSubtreeGeometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 577, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 744, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 896, + "symbol": "invisible_widgets", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1107, + "symbol": "invisible_widgets", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1593, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 188, + "symbol": "geometry", + "kind": "Attribute" + }, + { + "path": "src/textual/_compositor.py", + "line": 579, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 740, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 748, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1105, + "symbol": "invisible_widgets", + "kind": "Name" + }, + { + "path": "src/textual/_spatial_map.py", + "line": 15, + "symbol": "SpatialMap", + "kind": "ClassDef" + }, + { + "path": "src/textual/_spatial_map.py", + "line": 84, + "symbol": "get_values_in_region", + "kind": "FunctionDef" + }, + { + "path": "src/textual/css/styles.py", + "line": 978, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/css/styles.py", + "line": 990, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/dom.py", + "line": 153, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/dom.py", + "line": 155, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/dom.py", + "line": 179, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/dom.py", + "line": 169, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/dom.py", + "line": 170, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/dom.py", + "line": 179, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/dom.py", + "line": 184, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/dom.py", + "line": 169, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/dom.py", + "line": 184, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/layout.py", + "line": 55, + "symbol": "SpatialMap", + "kind": "Name" + }, + { + "path": "src/textual/layout.py", + "line": 34, + "symbol": "SpatialMap", + "kind": "Name" + }, + { + "path": "src/textual/layout.py", + "line": 97, + "symbol": "get_values_in_region", + "kind": "Attribute" + }, + { + "path": "src/textual/layout.py", + "line": 58, + "symbol": "SpatialMap", + "kind": "Name" + }, + { + "path": "src/textual/screen.py", + "line": 1502, + "symbol": "references_retired", + "kind": "Attribute" + }, + { + "path": "src/textual/selection.py", + "line": 184, + "symbol": "references_retired", + "kind": "FunctionDef" + }, + { + "path": "src/textual/widget.py", + "line": 336, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/widget.py", + "line": 340, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/widget.py", + "line": 342, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/widget.py", + "line": 763, + "symbol": "PlacedSubtreeGeometry", + "kind": "Name" + }, + { + "path": "src/textual/widget.py", + "line": 763, + "symbol": "IntrinsicSubtreeGeometry", + "kind": "Name" + }, + { + "path": "src/textual/widget.py", + "line": 337, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "tests/css/test_programmatic_style_changes.py", + "line": 18, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "tests/css/test_programmatic_style_changes.py", + "line": 29, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "tests/css/test_programmatic_style_changes.py", + "line": 21, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "tests/css/test_programmatic_style_changes.py", + "line": 32, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "tests/test_compositor.py", + "line": 204, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "tests/test_compositor.py", + "line": 208, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "tests/test_compositor.py", + "line": 254, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "tests/test_compositor.py", + "line": 218, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "tests/test_compositor.py", + "line": 205, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "tests/test_compositor.py", + "line": 205, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "tests/test_compositor.py", + "line": 209, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "tests/test_compositor.py", + "line": 209, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "tests/test_compositor.py", + "line": 255, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "tests/test_compositor.py", + "line": 255, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "tests/test_compositor.py", + "line": 221, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "tests/test_compositor.py", + "line": 223, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "tests/test_compositor.py", + "line": 224, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "tests/test_spatial_map.py", + "line": 37, + "symbol": "SpatialMap", + "kind": "Name" + }, + { + "path": "tests/test_spatial_map.py", + "line": 43, + "symbol": "SpatialMap", + "kind": "Name" + }, + { + "path": "tests/test_spatial_map.py", + "line": 43, + "symbol": "SpatialMap", + "kind": "Name" + }, + { + "path": "tests/test_spatial_map.py", + "line": 53, + "symbol": "get_values_in_region", + "kind": "Attribute" + }, + { + "path": "tests/test_spatial_map.py", + "line": 57, + "symbol": "get_values_in_region", + "kind": "Attribute" + }, + { + "path": "tests/test_spatial_map.py", + "line": 58, + "symbol": "get_values_in_region", + "kind": "Attribute" + }, + { + "path": "tests/test_spatial_map.py", + "line": 59, + "symbol": "get_values_in_region", + "kind": "Attribute" + }, + { + "path": "tests/test_spatial_map.py", + "line": 60, + "symbol": "get_values_in_region", + "kind": "Attribute" + }, + { + "path": "tests/test_viewport_geometry_targets.py", + "line": 74, + "symbol": "geometry", + "kind": "Attribute" + }, + { + "path": "tests/test_viewport_geometry_targets.py", + "line": 50, + "symbol": "geometry", + "kind": "Attribute" + }, + { + "path": "tests/test_viewport_geometry_targets.py", + "line": 59, + "symbol": "geometry", + "kind": "Attribute" + } + ], + "limits": "Syntactic declaration/attribute matches; geometry attribute uses include unrelated shapes. Dynamic aliases/external subclasses are unresolved, not counted absent." + }, + "after": { + "source": "0ab59a0466cff6fd2bd8c3747b1be7e2fd660586", + "roots": { + "src/textual": { + "parsed": 249, + "omissions": [] + }, + "tests": { + "parsed": 460, + "omissions": [] + } + }, + "sites": [ + { + "path": "src/textual/_compositor.py", + "line": 167, + "symbol": "SubtreeMapGeometry", + "kind": "ClassDef" + }, + { + "path": "src/textual/_compositor.py", + "line": 198, + "symbol": "SubtreeGeometry", + "kind": "ClassDef" + }, + { + "path": "src/textual/_compositor.py", + "line": 285, + "symbol": "PlacedSubtreeGeometry", + "kind": "ClassDef" + }, + { + "path": "src/textual/_compositor.py", + "line": 297, + "symbol": "IntrinsicSubtreeGeometry", + "kind": "ClassDef" + }, + { + "path": "src/textual/_compositor.py", + "line": 232, + "symbol": "restore_into", + "kind": "FunctionDef" + }, + { + "path": "src/textual/_compositor.py", + "line": 242, + "symbol": "project_into", + "kind": "FunctionDef" + }, + { + "path": "src/textual/_compositor.py", + "line": 278, + "symbol": "references_retired", + "kind": "FunctionDef" + }, + { + "path": "src/textual/_compositor.py", + "line": 170, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 194, + "symbol": "SubtreeMapGeometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 202, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 205, + "symbol": "invisible_widgets", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 285, + "symbol": "SubtreeGeometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 297, + "symbol": "SubtreeGeometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 297, + "symbol": "SubtreeMapGeometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 219, + "symbol": "SpatialMap", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 855, + "symbol": "invisible_widgets", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1167, + "symbol": "invisible_widgets", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1355, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1360, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 176, + "symbol": "geometry", + "kind": "Attribute" + }, + { + "path": "src/textual/_compositor.py", + "line": 216, + "symbol": "invisible_widgets", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 221, + "symbol": "SpatialMap", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 221, + "symbol": "SpatialMap", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 237, + "symbol": "project_into", + "kind": "Attribute" + }, + { + "path": "src/textual/_compositor.py", + "line": 237, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 240, + "symbol": "invisible_widgets", + "kind": "Attribute" + }, + { + "path": "src/textual/_compositor.py", + "line": 317, + "symbol": "invisible_widgets", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 323, + "symbol": "SubtreeMapGeometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 762, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 851, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1356, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1357, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1358, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1386, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1390, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1404, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1618, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1634, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 183, + "symbol": "geometry", + "kind": "Attribute" + }, + { + "path": "src/textual/_compositor.py", + "line": 184, + "symbol": "geometry", + "kind": "Attribute" + }, + { + "path": "src/textual/_compositor.py", + "line": 187, + "symbol": "geometry", + "kind": "Attribute" + }, + { + "path": "src/textual/_compositor.py", + "line": 240, + "symbol": "invisible_widgets", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 308, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 308, + "symbol": "invisible_widgets", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 311, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 316, + "symbol": "SubtreeMapGeometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 554, + "symbol": "SubtreeGeometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 597, + "symbol": "references_retired", + "kind": "Attribute" + }, + { + "path": "src/textual/_compositor.py", + "line": 764, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 774, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1141, + "symbol": "restore_into", + "kind": "Attribute" + }, + { + "path": "src/textual/_compositor.py", + "line": 1141, + "symbol": "invisible_widgets", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1370, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1387, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1388, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1391, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1392, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1619, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1637, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1639, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 185, + "symbol": "geometry", + "kind": "Attribute" + }, + { + "path": "src/textual/_compositor.py", + "line": 186, + "symbol": "geometry", + "kind": "Attribute" + }, + { + "path": "src/textual/_compositor.py", + "line": 186, + "symbol": "geometry", + "kind": "Attribute" + }, + { + "path": "src/textual/_compositor.py", + "line": 188, + "symbol": "geometry", + "kind": "Attribute" + }, + { + "path": "src/textual/_compositor.py", + "line": 253, + "symbol": "get_values_in_region", + "kind": "Attribute" + }, + { + "path": "src/textual/_compositor.py", + "line": 270, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 307, + "symbol": "PlacedSubtreeGeometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 315, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 315, + "symbol": "invisible_widgets", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 817, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1126, + "symbol": "invisible_widgets", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1127, + "symbol": "invisible_widgets", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1131, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1139, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1374, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 281, + "symbol": "invisible_widgets", + "kind": "Attribute" + }, + { + "path": "src/textual/_compositor.py", + "line": 306, + "symbol": "invisible_widgets", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 314, + "symbol": "PlacedSubtreeGeometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 604, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 771, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 923, + "symbol": "invisible_widgets", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1134, + "symbol": "invisible_widgets", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1620, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 189, + "symbol": "geometry", + "kind": "Attribute" + }, + { + "path": "src/textual/_compositor.py", + "line": 224, + "symbol": "geometry", + "kind": "Attribute" + }, + { + "path": "src/textual/_compositor.py", + "line": 257, + "symbol": "geometry", + "kind": "Attribute" + }, + { + "path": "src/textual/_compositor.py", + "line": 260, + "symbol": "geometry", + "kind": "Attribute" + }, + { + "path": "src/textual/_compositor.py", + "line": 606, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 767, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 775, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 1132, + "symbol": "invisible_widgets", + "kind": "Name" + }, + { + "path": "src/textual/_compositor.py", + "line": 255, + "symbol": "geometry", + "kind": "Attribute" + }, + { + "path": "src/textual/_compositor.py", + "line": 279, + "symbol": "geometry", + "kind": "Attribute" + }, + { + "path": "src/textual/_compositor.py", + "line": 215, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/_spatial_map.py", + "line": 15, + "symbol": "SpatialMap", + "kind": "ClassDef" + }, + { + "path": "src/textual/_spatial_map.py", + "line": 84, + "symbol": "get_values_in_region", + "kind": "FunctionDef" + }, + { + "path": "src/textual/css/styles.py", + "line": 978, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/css/styles.py", + "line": 990, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/dom.py", + "line": 153, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/dom.py", + "line": 155, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/dom.py", + "line": 179, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/dom.py", + "line": 169, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/dom.py", + "line": 170, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/dom.py", + "line": 179, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/dom.py", + "line": 184, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/dom.py", + "line": 169, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/dom.py", + "line": 184, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/layout.py", + "line": 55, + "symbol": "SpatialMap", + "kind": "Name" + }, + { + "path": "src/textual/layout.py", + "line": 34, + "symbol": "SpatialMap", + "kind": "Name" + }, + { + "path": "src/textual/layout.py", + "line": 97, + "symbol": "get_values_in_region", + "kind": "Attribute" + }, + { + "path": "src/textual/layout.py", + "line": 58, + "symbol": "SpatialMap", + "kind": "Name" + }, + { + "path": "src/textual/screen.py", + "line": 1502, + "symbol": "references_retired", + "kind": "Attribute" + }, + { + "path": "src/textual/selection.py", + "line": 184, + "symbol": "references_retired", + "kind": "FunctionDef" + }, + { + "path": "src/textual/widget.py", + "line": 336, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/widget.py", + "line": 340, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/widget.py", + "line": 342, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "src/textual/widget.py", + "line": 763, + "symbol": "PlacedSubtreeGeometry", + "kind": "Name" + }, + { + "path": "src/textual/widget.py", + "line": 763, + "symbol": "IntrinsicSubtreeGeometry", + "kind": "Name" + }, + { + "path": "src/textual/widget.py", + "line": 337, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "tests/css/test_programmatic_style_changes.py", + "line": 18, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "tests/css/test_programmatic_style_changes.py", + "line": 29, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "tests/css/test_programmatic_style_changes.py", + "line": 21, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "tests/css/test_programmatic_style_changes.py", + "line": 32, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "tests/test_compositor.py", + "line": 204, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "tests/test_compositor.py", + "line": 208, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "tests/test_compositor.py", + "line": 254, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "tests/test_compositor.py", + "line": 218, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "tests/test_compositor.py", + "line": 205, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "tests/test_compositor.py", + "line": 205, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "tests/test_compositor.py", + "line": 209, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "tests/test_compositor.py", + "line": 209, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "tests/test_compositor.py", + "line": 255, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "tests/test_compositor.py", + "line": 255, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "tests/test_compositor.py", + "line": 221, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "tests/test_compositor.py", + "line": 223, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "tests/test_compositor.py", + "line": 224, + "symbol": "geometry", + "kind": "Name" + }, + { + "path": "tests/test_spatial_map.py", + "line": 37, + "symbol": "SpatialMap", + "kind": "Name" + }, + { + "path": "tests/test_spatial_map.py", + "line": 43, + "symbol": "SpatialMap", + "kind": "Name" + }, + { + "path": "tests/test_spatial_map.py", + "line": 43, + "symbol": "SpatialMap", + "kind": "Name" + }, + { + "path": "tests/test_spatial_map.py", + "line": 53, + "symbol": "get_values_in_region", + "kind": "Attribute" + }, + { + "path": "tests/test_spatial_map.py", + "line": 57, + "symbol": "get_values_in_region", + "kind": "Attribute" + }, + { + "path": "tests/test_spatial_map.py", + "line": 58, + "symbol": "get_values_in_region", + "kind": "Attribute" + }, + { + "path": "tests/test_spatial_map.py", + "line": 59, + "symbol": "get_values_in_region", + "kind": "Attribute" + }, + { + "path": "tests/test_spatial_map.py", + "line": 60, + "symbol": "get_values_in_region", + "kind": "Attribute" + }, + { + "path": "tests/test_viewport_geometry_targets.py", + "line": 135, + "symbol": "geometry", + "kind": "Attribute" + }, + { + "path": "tests/test_viewport_geometry_targets.py", + "line": 64, + "symbol": "PlacedSubtreeGeometry", + "kind": "Name" + }, + { + "path": "tests/test_viewport_geometry_targets.py", + "line": 116, + "symbol": "get_values_in_region", + "kind": "Attribute" + }, + { + "path": "tests/test_viewport_geometry_targets.py", + "line": 103, + "symbol": "geometry", + "kind": "Attribute" + }, + { + "path": "tests/test_viewport_geometry_targets.py", + "line": 121, + "symbol": "geometry", + "kind": "Attribute" + }, + { + "path": "tests/test_viewport_geometry_targets.py", + "line": 112, + "symbol": "geometry", + "kind": "Attribute" + } + ], + "limits": "Syntactic declaration/attribute matches; geometry attribute uses include unrelated shapes. Dynamic aliases/external subclasses are unresolved, not counted absent." + }, + "dependent_toad": { + "source": "09147f7d99cd30a75022c56d1b70f679fea7319b", + "parsed": 288, + "omissions": [], + "sites": [ + { + "path": "src/toad/widgets/viewport_body.py", + "line": 653, + "symbol": "render_subtree_strips" + }, + { + "path": "src/toad/widgets/viewport_body.py", + "line": 648, + "symbol": "published_geometry" + } + ] + }, + "ownership": "SubtreeGeometry.capture owns source ordinals and immutable entries once. Existing SpatialMap is private derived spatial resource, indexes ordinal/widget references only. No geometry copy/ordinal registry/independent mutation path. Both leafs use shared capture/projection; original membership fact and O(n) union retained." +} diff --git a/src/textual/_compositor.py b/src/textual/_compositor.py index 039ffd722d..f7073d7b1a 100644 --- a/src/textual/_compositor.py +++ b/src/textual/_compositor.py @@ -40,7 +40,8 @@ from textual import errors from textual._context import visible_screen_stack from textual._loop import loop_last -from textual.geometry import NULL_SPACING, Offset, Region, Size, Spacing +from textual._spatial_map import SpatialMap +from textual.geometry import NULL_OFFSET, NULL_SPACING, Offset, Region, Size, Spacing from textual.map_geometry import MapGeometry from textual.strip import Strip, StripRenderable from textual.widget import Widget @@ -198,7 +199,8 @@ class SubtreeGeometry(ABC, Generic[GeometryEntry]): """One immutable native arrangement resource in the compositor's cache.""" key: SubtreeGeometryKey - geometry: Mapping[Widget, GeometryEntry] + geometry: Mapping[Widget, tuple[int, GeometryEntry]] + """Original source ordinals and placements, indexed by their native widget.""" widgets: frozenset[Widget] invisible_widgets: frozenset[Widget] @@ -208,7 +210,20 @@ def complete_arrangement(cls, enclosing_complete: bool) -> bool: @classmethod def capture(cls, key, geometry, widgets, invisible_widgets, clips, clip, screen_coordinates): - return cls(key, MappingProxyType(geometry), widgets, invisible_widgets) + return cls(key, MappingProxyType({ + node: (ordinal, entry) + for ordinal, (node, entry) in enumerate(geometry.items()) + }), widgets, invisible_widgets) + + @cached_property + def _spatial_map(self) -> SpatialMap[tuple[int, Widget]]: + """Derive spatial admission from this immutable arrangement, once.""" + spatial_map: SpatialMap[tuple[int, Widget]] = SpatialMap() + spatial_map.insert( + (entry.region, NULL_OFFSET, False, False, (ordinal, node)) + for node, (ordinal, entry) in self.geometry.items() + ) + return spatial_map @abstractmethod def matches(self, key: SubtreeGeometryKey) -> bool: @@ -234,7 +249,16 @@ def project_into(self, geometry: CompositorMap, key: SubtreeGeometryKey, 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(): + if visible_only: + candidates = dict(self._spatial_map.get_values_in_region(source_bounds)) + for node in (root, *retained): + if (indexed := self.geometry.get(node)) is not None: + candidates[indexed[0]] = node + entries = ((node, self.geometry[node][1]) + for _, node in sorted(candidates.items())) + else: + entries = ((node, entry) for node, (_, entry) in self.geometry.items()) + for node, entry in entries: 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 @@ -252,7 +276,9 @@ def _project_entry(self, entry: GeometryEntry, key: SubtreeGeometryKey, ... def references_retired(self, owner: Widget, retired: set[Widget]) -> bool: - return not retired.isdisjoint((owner, *self.geometry, *self.widgets, *self.invisible_widgets)) + return (owner in retired or not self.geometry.keys().isdisjoint(retired) + or not self.widgets.isdisjoint(retired) + or not self.invisible_widgets.isdisjoint(retired)) @dataclass(frozen=True) @@ -288,7 +314,8 @@ def capture(cls, key, geometry, widgets, invisible_widgets, clips, clip, screen_ return PlacedSubtreeGeometry.capture( key, geometry, widgets, invisible_widgets, clips, clip, screen_coordinates) intrinsic[node] = SubtreeMapGeometry(entry, bounds) - return cls(key, MappingProxyType(intrinsic), widgets, invisible_widgets) + return super().capture(key, intrinsic, widgets, invisible_widgets, + clips, clip, screen_coordinates) def matches(self, key: SubtreeGeometryKey) -> bool: return self.key.intrinsic() == key.intrinsic() diff --git a/tests/test_viewport_geometry_targets.py b/tests/test_viewport_geometry_targets.py index e4fcf33364..5780ebc89d 100644 --- a/tests/test_viewport_geometry_targets.py +++ b/tests/test_viewport_geometry_targets.py @@ -25,12 +25,65 @@ def compose(self): yield Static("second line " * 5) +async def test_cached_overlay_and_fixed_children_match_the_original_scene(): + from textual._compositor import Compositor, PlacedSubtreeGeometry + + class CachedBody(VerticalGroup): + CACHE_SUBTREE_GEOMETRY = True + + body = CachedBody( + Static("Fixed heading", id="fixed-heading"), + Static("Screen overlay", id="screen-overlay"), + *(Static(f"Source row {index}", id=f"source-row-{index}") for index in range(80)), + id="cached-body", + ) + + class OverlayScreen(TargetedScreen): + CSS = """ + #fixed-heading { dock: top; height: 1; } + #screen-overlay { overlay: screen; width: 15; height: 1; offset: 5 3; } + """ + + def compose(self): + with VerticalScroll(id="history"): + yield body + + app = App() + async with app.run_test(size=(40, 10)) as pilot: + screen = OverlayScreen() + screen.targets = (body,) + await app.push_screen(screen) + await pilot.pause() + history = screen.query_one("#history", VerticalScroll) + compositor = screen._compositor + for position in (0, 30, 0): + history.scroll_to(y=position, immediate=True, animate=False) + screen._refresh_layout(app.size, scroll=True) + # The overlay's clip reaches the outer screen, so this resource + # keeps placed geometry rather than manufacturing an intrinsic clip. + assert isinstance(compositor._subtree_geometry[body], PlacedSubtreeGeometry) + expected, _ = Compositor(max_subtree_geometry_entries=0)._arrange_root( + screen, app.size, visible_only=False, + ) + paint = compositor._paint_regions(compositor._ordered_geometry(expected), app.size.region) + admitted = {node: expected[node] for node in paint} + admitted[body] = expected[body] # The original explicit geometry target. + for node, entry in admitted.items(): + assert node in compositor._visible_map, (position, node.id, entry) + assert compositor._visible_map[node] == entry + actual_layers = compositor._ordered_geometry(compositor._visible_map) + expected_layers = compositor._ordered_geometry(expected) + assert [(node, entry) for node, entry in actual_layers if node in admitted] == [ + (node, entry) for node, entry in expected_layers if node in admitted + ] + + async def test_complete_cached_body_keeps_capture_and_explicit_reader_geometry(): class CachedBody(VerticalGroup): CACHE_SUBTREE_GEOMETRY = True rows = [Static(f"Original row {index}", id=f"cached-row-{index}") - for index in range(40)] + for index in range(240)] body = CachedBody(*rows, id="cached-body") class CachedScreen(TargetedScreen): @@ -41,7 +94,7 @@ def compose(self): app = App() async with app.run_test(size=(40, 10)) as pilot: screen = CachedScreen() - screen.targets = (body, rows[35]) + screen.targets = (body, rows[235]) await app.push_screen(screen) await pilot.pause() history = screen.query_one("#history", VerticalScroll) @@ -49,18 +102,26 @@ def compose(self): resource = compositor._subtree_geometry[body] assert all(row in resource.geometry for row in rows) - for position in (0, 20, 0): + for position in (0, 200, 0): history.scroll_to(y=position, immediate=True, animate=False) screen._refresh_layout(app.size, scroll=True) viewport = compositor._visible_map - assert body in viewport and rows[35] in viewport - omitted = rows[20] if position == 0 else rows[0] + assert body in viewport and rows[235] in viewport + omitted = rows[200] if position == 0 else rows[0] assert omitted not in viewport assert all(row in resource.geometry for row in rows) - # Capture remains complete without replacing the published viewport. + assert all(row in compositor.widgets for row in rows) _, placement = next(compositor.published_geometry((body,))) + source_bounds = app.size.region - (placement.region.offset - resource.key.region.offset) + candidates = resource._spatial_map.get_values_in_region(source_bounds) + # The immutable source stays complete; spatial admission and the + # explicit offscreen reader path remain different original facts. + assert omitted not in {node for _, node in candidates} + assert rows[235] not in {node for _, node in candidates} + assert len(candidates) < len(resource.geometry) + # Capture remains complete without replacing the published viewport. size, strips = compositor.render_subtree_strips(body, placement) - assert size.height == 40 + assert size.height == 240 assert all(f"Original row {index}" in strip.text for index, strip in enumerate(strips)) assert compositor._visible_map is viewport @@ -72,7 +133,7 @@ def compose(self): await pilot.pause() screen._refresh_layout(app.size, scroll=True) assert rows[5] not in compositor._subtree_geometry[body].geometry - assert rows[35] in compositor._visible_map + assert rows[235] in compositor._visible_map async def test_offscreen_targets_match_full_geometry_without_full_tree_traversal():