From f000c9ff54018dfc3c907a25c2290a5e79bd7073 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 5 Oct 2026 09:41:42 -0400 Subject: [PATCH 1/6] docs: scope original native damage span admission --- .../SCOPE.md | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 evidence/native-damage-span-admission-20261005/SCOPE.md diff --git a/evidence/native-damage-span-admission-20261005/SCOPE.md b/evidence/native-damage-span-admission-20261005/SCOPE.md new file mode 100644 index 0000000000..683db16a79 --- /dev/null +++ b/evidence/native-damage-span-admission-20261005/SCOPE.md @@ -0,0 +1,24 @@ +# Keep original damage spans through native paint admission + +Source base: merged Text67 d7337ee084f6dcda42cbd8a41c7417e87d60ae78. + +`Compositor.render_partial_update` already owns exact nonoverlapping damaged +horizontal spans. It passes only row membership into `_render_chops`, which +allocates every cut on each dirty row inside the broad damage union. Separated +horizontal damage therefore asks intervening foreground content to render; +`ChopsUpdate` only removes that work later when publishing exact spans. + +Use the existing `ChopsUpdate` cut-boundary owner for both compositor admission +and terminal / Rich publication. Carry original spans through every full, +partial, inline/export and subtree-capture caller. Preserve layer order, +wide-cell splitting, source metadata, clipping and exposure. Delete the +row-only reduction and separate cut-selection calculation. No new map, cache, +flag, timer, class, scene backend or Toad implementation. + +NRA/refactor-audit IMPL-12: one damage/cut relation calculated differently at +paint admission and publication. AST before/after will cover all native and +Toad production, test and tool callers, with parsing and dynamic limits explicit. + +Source reasoning and complete implementation precede proportionate affected +controls. Installed saved-history motion/CPU improvement is unqualified. No +build, environment, installed package, provider or recording purpose exists. From 68a9755b411819090283f180a362bf7659757b01 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 5 Oct 2026 09:44:07 -0400 Subject: [PATCH 2/6] perf: admit original damaged cut cells before native painting --- .../before.json | 1810 +++++++++++++++++ src/textual/_compositor.py | 52 +- 2 files changed, 1839 insertions(+), 23 deletions(-) create mode 100644 evidence/native-damage-span-admission-20261005/before.json diff --git a/evidence/native-damage-span-admission-20261005/before.json b/evidence/native-damage-span-admission-20261005/before.json new file mode 100644 index 0000000000..225f9609bd --- /dev/null +++ b/evidence/native-damage-span-admission-20261005/before.json @@ -0,0 +1,1810 @@ +{ + "roots": [ + { + "repository": "/home/ts/wt/textual-native-subtree-strips-20261002", + "head": "f000c9ff54018dfc3c907a25c2290a5e79bd7073", + "root": "src/textual", + "parsed": 249, + "omitted": [] + }, + { + "repository": "/home/ts/wt/textual-native-subtree-strips-20261002", + "head": "f000c9ff54018dfc3c907a25c2290a5e79bd7073", + "root": "tests", + "parsed": 460, + "omitted": [] + }, + { + "repository": "/home/ts/wt/toad-viewport-raster-cpu-continuation-20261001", + "head": "83247c4b81834f42f308dbd01349a0c5795d49f8", + "root": "src/toad", + "parsed": 288, + "omitted": [] + }, + { + "repository": "/home/ts/wt/toad-viewport-raster-cpu-continuation-20261001", + "head": "83247c4b81834f42f308dbd01349a0c5795d49f8", + "root": "tests", + "parsed": 397, + "omitted": [] + }, + { + "repository": "/home/ts/wt/toad-viewport-raster-cpu-continuation-20261001", + "head": "83247c4b81834f42f308dbd01349a0c5795d49f8", + "root": "tools", + "parsed": 41, + "omitted": [] + } + ], + "sites": [ + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 435, + "node": "ClassDef", + "name": "ChopsUpdate", + "source": "class ChopsUpdate(CompositorUpdate):" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 455, + "node": "FunctionDef", + "name": "_get_line_chops", + "source": " def _get_line_chops(self, y: int, x1: int, x2: int) -> Iterator[tuple[int, Strip]]:" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 615, + "node": "FunctionDef", + "name": "_regions_to_spans", + "source": " def _regions_to_spans(" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1440, + "node": "FunctionDef", + "name": "_get_renders", + "source": " def _get_renders(" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1535, + "node": "FunctionDef", + "name": "render_full_update", + "source": " def render_full_update(self, simplify: bool = False) -> LayoutUpdate:" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1562, + "node": "FunctionDef", + "name": "render_partial_update", + "source": " def render_partial_update(self) -> ChopsUpdate | None:" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1584, + "node": "FunctionDef", + "name": "render_strips", + "source": " def render_strips(self, size: Size | None = None) -> list[Strip]:" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1617, + "node": "FunctionDef", + "name": "render_subtree_strips", + "source": " def render_subtree_strips(" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1639, + "node": "FunctionDef", + "name": "_render_chops", + "source": " def _render_chops(" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1550, + "node": "Name", + "name": "render_strips", + "source": "render_strips" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1562, + "node": "Name", + "name": "ChopsUpdate", + "source": "ChopsUpdate" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1598, + "node": "Name", + "name": "render_strips", + "source": "render_strips" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1599, + "node": "Name", + "name": "render_strips", + "source": "render_strips" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1532, + "node": "Attribute", + "name": "render_strips", + "source": "self.render_strips" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1547, + "node": "Attribute", + "name": "_render_chops", + "source": "self._render_chops" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1554, + "node": "Name", + "name": "render_strips", + "source": "render_strips" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1558, + "node": "Name", + "name": "render_strips", + "source": "render_strips" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1560, + "node": "Name", + "name": "render_strips", + "source": "render_strips" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1579, + "node": "Attribute", + "name": "_render_chops", + "source": "self._render_chops" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1582, + "node": "Name", + "name": "ChopsUpdate", + "source": "ChopsUpdate" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1595, + "node": "Attribute", + "name": "_render_chops", + "source": "self._render_chops" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1704, + "node": "Attribute", + "name": "_get_renders", + "source": "self._get_renders" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 480, + "node": "Attribute", + "name": "_get_line_chops", + "source": "self._get_line_chops" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 503, + "node": "Attribute", + "name": "_get_line_chops", + "source": "self._get_line_chops" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1511, + "node": "Attribute", + "name": "render_full_update", + "source": "self.render_full_update" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1513, + "node": "Attribute", + "name": "render_partial_update", + "source": "self.render_partial_update" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1576, + "node": "Attribute", + "name": "_regions_to_spans", + "source": "self._regions_to_spans" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1635, + "node": "Attribute", + "name": "_render_chops", + "source": "self._render_chops" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1728, + "node": "Attribute", + "name": "render_strips", + "source": "self.render_strips" + }, + { + "root": "src/textual", + "path": "src/textual/content.py", + "line": 728, + "node": "FunctionDef", + "name": "render_strips", + "source": " def render_strips(" + }, + { + "root": "src/textual", + "path": "src/textual/renderables/blank.py", + "line": 27, + "node": "FunctionDef", + "name": "render_strips", + "source": " def render_strips(" + }, + { + "root": "src/textual", + "path": "src/textual/visual.py", + "line": 131, + "node": "FunctionDef", + "name": "render_strips", + "source": " def render_strips(" + }, + { + "root": "src/textual", + "path": "src/textual/visual.py", + "line": 324, + "node": "FunctionDef", + "name": "render_strips", + "source": " def render_strips(" + }, + { + "root": "src/textual", + "path": "src/textual/visual.py", + "line": 391, + "node": "FunctionDef", + "name": "render_strips", + "source": " def render_strips(" + }, + { + "root": "src/textual", + "path": "src/textual/visual.py", + "line": 227, + "node": "Attribute", + "name": "render_strips", + "source": "visual.render_strips" + }, + { + "root": "src/textual", + "path": "src/textual/visual.py", + "line": 411, + "node": "Attribute", + "name": "render_strips", + "source": "self._visual.render_strips" + }, + { + "root": "tests", + "path": "tests/frame_damage_installed_pilot.py", + "line": 16, + "node": "Attribute", + "name": "render_partial_update", + "source": "compositor.render_partial_update" + }, + { + "root": "tests", + "path": "tests/test_compositor.py", + "line": 390, + "node": "Attribute", + "name": "_get_renders", + "source": "compositor._get_renders" + }, + { + "root": "tests", + "path": "tests/test_compositor.py", + "line": 310, + "node": "Attribute", + "name": "_get_renders", + "source": "compositor._get_renders" + }, + { + "root": "tests", + "path": "tests/test_compositor.py", + "line": 354, + "node": "Attribute", + "name": "_get_renders", + "source": "compositor._get_renders" + }, + { + "root": "tests", + "path": "tests/test_compositor.py", + "line": 388, + "node": "Attribute", + "name": "render_strips", + "source": "compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/test_compositor.py", + "line": 308, + "node": "Attribute", + "name": "render_strips", + "source": "compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/test_compositor.py", + "line": 349, + "node": "Attribute", + "name": "render_partial_update", + "source": "compositor.render_partial_update" + }, + { + "root": "tests", + "path": "tests/test_compositor.py", + "line": 392, + "node": "Attribute", + "name": "render_strips", + "source": "compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/test_compositor.py", + "line": 313, + "node": "Attribute", + "name": "render_strips", + "source": "compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/test_compositor.py", + "line": 357, + "node": "Attribute", + "name": "render_partial_update", + "source": "compositor.render_partial_update" + }, + { + "root": "tests", + "path": "tests/test_compositor_frame_damage.py", + "line": 76, + "node": "Name", + "name": "ChopsUpdate", + "source": "ChopsUpdate" + }, + { + "root": "tests", + "path": "tests/test_compositor_frame_damage.py", + "line": 17, + "node": "Attribute", + "name": "render_partial_update", + "source": "compositor.render_partial_update" + }, + { + "root": "tests", + "path": "tests/test_compositor_frame_damage.py", + "line": 58, + "node": "Attribute", + "name": "render_partial_update", + "source": "compositor.render_partial_update" + }, + { + "root": "tests", + "path": "tests/test_compositor_frame_damage.py", + "line": 103, + "node": "Attribute", + "name": "render_partial_update", + "source": "compositor.render_partial_update" + }, + { + "root": "tests", + "path": "tests/test_compositor_frame_damage.py", + "line": 124, + "node": "Attribute", + "name": "render_subtree_strips", + "source": "compositor.render_subtree_strips" + }, + { + "root": "tests", + "path": "tests/test_compositor_frame_damage.py", + "line": 144, + "node": "Attribute", + "name": "render_subtree_strips", + "source": "compositor.render_subtree_strips" + }, + { + "root": "tests", + "path": "tests/test_compositor_horizontal_damage.py", + "line": 50, + "node": "Attribute", + "name": "_get_renders", + "source": "compositor._get_renders" + }, + { + "root": "tests", + "path": "tests/test_compositor_horizontal_damage.py", + "line": 46, + "node": "Attribute", + "name": "render_strips", + "source": "compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/test_compositor_horizontal_damage.py", + "line": 52, + "node": "Attribute", + "name": "render_strips", + "source": "compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/test_compositor_regions_to_spans.py", + "line": 6, + "node": "Attribute", + "name": "_regions_to_spans", + "source": "Compositor._regions_to_spans" + }, + { + "root": "tests", + "path": "tests/test_compositor_regions_to_spans.py", + "line": 11, + "node": "Attribute", + "name": "_regions_to_spans", + "source": "Compositor._regions_to_spans" + }, + { + "root": "tests", + "path": "tests/test_compositor_regions_to_spans.py", + "line": 19, + "node": "Attribute", + "name": "_regions_to_spans", + "source": "Compositor._regions_to_spans" + }, + { + "root": "tests", + "path": "tests/test_compositor_regions_to_spans.py", + "line": 28, + "node": "Attribute", + "name": "_regions_to_spans", + "source": "Compositor._regions_to_spans" + }, + { + "root": "tests", + "path": "tests/test_compositor_regions_to_spans.py", + "line": 37, + "node": "Attribute", + "name": "_regions_to_spans", + "source": "Compositor._regions_to_spans" + }, + { + "root": "tests", + "path": "tests/test_compositor_regions_to_spans.py", + "line": 42, + "node": "Attribute", + "name": "_regions_to_spans", + "source": "Compositor._regions_to_spans" + }, + { + "root": "tests", + "path": "tests/test_compositor_regions_to_spans.py", + "line": 51, + "node": "Attribute", + "name": "_regions_to_spans", + "source": "Compositor._regions_to_spans" + }, + { + "root": "tests", + "path": "tests/test_content.py", + "line": 303, + "node": "Attribute", + "name": "render_strips", + "source": "content.render_strips" + }, + { + "root": "tests", + "path": "tests/test_frame_transaction.py", + "line": 19, + "node": "Attribute", + "name": "render_strips", + "source": "screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/test_removed_scene_ownership.py", + "line": 146, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/test_removed_scene_ownership.py", + "line": 168, + "node": "Attribute", + "name": "render_strips", + "source": "owner._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/test_removed_scene_ownership.py", + "line": 197, + "node": "Attribute", + "name": "render_strips", + "source": "owner._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/test_removed_scene_ownership.py", + "line": 228, + "node": "Attribute", + "name": "render_strips", + "source": "owner._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/test_removed_scene_ownership.py", + "line": 178, + "node": "Attribute", + "name": "render_strips", + "source": "owner._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/test_removed_scene_ownership.py", + "line": 209, + "node": "Attribute", + "name": "render_strips", + "source": "owner._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/test_viewport_geometry_targets.py", + "line": 123, + "node": "Attribute", + "name": "render_subtree_strips", + "source": "compositor.render_subtree_strips" + }, + { + "root": "tests", + "path": "tests/test_viewport_geometry_targets.py", + "line": 240, + "node": "Attribute", + "name": "render_subtree_strips", + "source": "compositor.render_subtree_strips" + }, + { + "root": "tests", + "path": "tests/test_viewport_geometry_targets.py", + "line": 271, + "node": "Attribute", + "name": "render_subtree_strips", + "source": "compositor.render_subtree_strips" + }, + { + "root": "tests", + "path": "tests/test_viewport_geometry_targets.py", + "line": 281, + "node": "Attribute", + "name": "render_subtree_strips", + "source": "compositor.render_subtree_strips" + }, + { + "root": "tests", + "path": "tests/test_viewport_geometry_targets.py", + "line": 166, + "node": "Attribute", + "name": "render_strips", + "source": "compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/test_viewport_geometry_targets.py", + "line": 171, + "node": "Attribute", + "name": "render_strips", + "source": "compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/test_viewport_layout.py", + "line": 22, + "node": "Attribute", + "name": "render_strips", + "source": "screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/toad_frame_damage_installed_pilot.py", + "line": 33, + "node": "Attribute", + "name": "render_strips", + "source": "self.screen._compositor.render_strips" + }, + { + "root": "src/toad", + "path": "src/toad/visuals/columns.py", + "line": 44, + "node": "FunctionDef", + "name": "render_strips", + "source": " def render_strips(" + }, + { + "root": "src/toad", + "path": "src/toad/widgets/conversation.py", + "line": 2129, + "node": "Attribute", + "name": "render_full_update", + "source": "compositor.render_full_update" + }, + { + "root": "src/toad", + "path": "src/toad/widgets/patch_diff.py", + "line": 245, + "node": "FunctionDef", + "name": "render_strips", + "source": " def render_strips(self, width, height, style, options):" + }, + { + "root": "src/toad", + "path": "src/toad/widgets/patch_diff.py", + "line": 339, + "node": "Attribute", + "name": "render_strips", + "source": "row.render_strips" + }, + { + "root": "src/toad", + "path": "src/toad/widgets/patch_diff.py", + "line": 367, + "node": "Attribute", + "name": "render_strips", + "source": "_DiffRow(self.source, y).render_strips" + }, + { + "root": "src/toad", + "path": "src/toad/widgets/path_search.py", + "line": 46, + "node": "FunctionDef", + "name": "render_strips", + "source": " def render_strips(" + }, + { + "root": "src/toad", + "path": "src/toad/widgets/prepared_markdown.py", + "line": 144, + "node": "FunctionDef", + "name": "render_strips", + "source": " def render_strips(" + }, + { + "root": "src/toad", + "path": "src/toad/widgets/prepared_markdown.py", + "line": 155, + "node": "Attribute", + "name": "render_strips", + "source": "self.content.render_strips" + }, + { + "root": "src/toad", + "path": "src/toad/widgets/throbber.py", + "line": 63, + "node": "FunctionDef", + "name": "render_strips", + "source": " def render_strips(" + }, + { + "root": "src/toad", + "path": "src/toad/widgets/viewport_body.py", + "line": 649, + "node": "Attribute", + "name": "render_subtree_strips", + "source": "compositor.render_subtree_strips" + }, + { + "root": "tests", + "path": "tests/acp_specification_installed_pilot.py", + "line": 24, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/agent_configuration_native_installed_pilot.py", + "line": 45, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/agent_selection_configured_installed_pilot.py", + "line": 194, + "node": "Attribute", + "name": "render_strips", + "source": "app.workspace_screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/agent_startup_failure_installed_pilot.py", + "line": 49, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/agent_startup_failure_installed_pilot.py", + "line": 54, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/async_thread_open_pilot.py", + "line": 108, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/async_thread_open_pilot.py", + "line": 168, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/blank_presentation_pilot.py", + "line": 381, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/blank_presentation_pilot.py", + "line": 572, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/blank_presentation_pilot.py", + "line": 293, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/block_interaction_pilot.py", + "line": 33, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/canonical_wire_conversation_installed_pilot.py", + "line": 264, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/catalog_actions_native_installed_pilot.py", + "line": 88, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/channel_disclosure_publication_pilot.py", + "line": 235, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/channel_partial_paint_pilot.py", + "line": 75, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/checkpoint_source_pivot_pilot.py", + "line": 92, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/client_session_native_installed_pilot.py", + "line": 50, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/client_session_native_installed_pilot.py", + "line": 49, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/client_session_native_installed_pilot.py", + "line": 61, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/client_session_native_installed_pilot.py", + "line": 74, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/client_session_native_installed_pilot.py", + "line": 102, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/command_family_pilot.py", + "line": 62, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/command_family_pilot.py", + "line": 130, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/compaction_pilot.py", + "line": 77, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/compaction_summary_stream_native_installed_pilot.py", + "line": 30, + "node": "Attribute", + "name": "render_strips", + "source": "self.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/compaction_summary_stream_native_installed_pilot.py", + "line": 137, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/compaction_summary_stream_native_installed_pilot.py", + "line": 164, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/compaction_summary_stream_native_installed_pilot.py", + "line": 172, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/compaction_summary_stream_native_installed_pilot.py", + "line": 184, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/compaction_summary_stream_native_installed_pilot.py", + "line": 369, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/context_compaction_native_installed_pilot.py", + "line": 20, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/context_native_installed_pilot.py", + "line": 27, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/context_native_installed_pilot.py", + "line": 37, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/context_usage_pilot.py", + "line": 44, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/context_usage_pilot.py", + "line": 65, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/diff_row_damage_pilot.py", + "line": 37, + "node": "Attribute", + "name": "render_strips", + "source": "visual.render_strips" + }, + { + "root": "tests", + "path": "tests/diff_row_damage_pilot.py", + "line": 39, + "node": "Attribute", + "name": "render_strips", + "source": "_DiffRow(visual, y).render_strips" + }, + { + "root": "tests", + "path": "tests/existing_thread_tab_title_pilot.py", + "line": 130, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/file_path_preview_pilot.py", + "line": 128, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/file_path_preview_pilot.py", + "line": 162, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/file_path_preview_pilot.py", + "line": 172, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/file_path_preview_pilot.py", + "line": 183, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/file_preview_latency_pilot.py", + "line": 77, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/first_fork_native_installed_pilot.py", + "line": 300, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/fresh_fork_pilot.py", + "line": 104, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/fresh_fork_pilot.py", + "line": 132, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/goal_details_pilot.py", + "line": 41, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/goal_details_pilot.py", + "line": 45, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/goal_objective_edit_pilot.py", + "line": 127, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/goal_objective_edit_pilot.py", + "line": 112, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/goal_server_poll_pilot.py", + "line": 123, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/goal_server_poll_pilot.py", + "line": 136, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/goal_server_poll_pilot.py", + "line": 144, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/goal_standby_history_pilot.py", + "line": 196, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/goal_standby_history_pilot.py", + "line": 170, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/goal_standby_history_pilot.py", + "line": 183, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/guards/test_goal_interaction.py", + "line": 49, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/guards/test_live_output.py", + "line": 56, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/guards/test_tool_output.py", + "line": 87, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/guards/test_transcript_publication.py", + "line": 150, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/history_availability_installed_pilot.py", + "line": 67, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/history_availability_installed_pilot.py", + "line": 79, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/history_availability_installed_pilot.py", + "line": 72, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/history_frame_publication_pilot.py", + "line": 48, + "node": "Attribute", + "name": "render_strips", + "source": "screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/in_out_underfill_pilot.py", + "line": 67, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/in_out_underfill_pilot.py", + "line": 95, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/input_history_native_installed_pilot.py", + "line": 23, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/input_history_native_installed_pilot.py", + "line": 27, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/l0a_native_installed_pilot.py", + "line": 45, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/l0a_native_installed_pilot.py", + "line": 542, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/large_stream_pilot.py", + "line": 128, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/large_stream_pilot.py", + "line": 70, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/long_message_pilot.py", + "line": 49, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/long_message_pilot.py", + "line": 52, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/long_message_pilot.py", + "line": 56, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/mcp_decision_pty_pilot.py", + "line": 34, + "node": "Attribute", + "name": "render_strips", + "source": "widget.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/message_dividers_pilot.py", + "line": 336, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/native_loaded_return_cache_pilot.py", + "line": 64, + "node": "Attribute", + "name": "render_strips", + "source": "screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/native_loaded_return_cache_pilot.py", + "line": 459, + "node": "Attribute", + "name": "render_strips", + "source": "frame._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/native_loaded_return_cache_pilot.py", + "line": 244, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/native_permission_ui.py", + "line": 70, + "node": "Attribute", + "name": "render_strips", + "source": "self.app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/native_permission_ui.py", + "line": 80, + "node": "Attribute", + "name": "render_strips", + "source": "self.app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/native_session_retention_pilot.py", + "line": 91, + "node": "Attribute", + "name": "render_strips", + "source": "screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/native_session_retention_pilot.py", + "line": 53, + "node": "Attribute", + "name": "render_strips", + "source": "screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/native_tool_output_pilot.py", + "line": 46, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/native_widget_actions_installed_pilot.py", + "line": 30, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/original_turn_resource_real_installed_pilot.py", + "line": 249, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/pending_thread_open_pilot.py", + "line": 52, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/permission_pending_mount_pilot.py", + "line": 112, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/permission_pending_mount_pilot.py", + "line": 75, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/persistent_workspace_source_navigation_pilot.py", + "line": 58, + "node": "Attribute", + "name": "render_strips", + "source": "frame._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/prepared_diff_rows_pilot.py", + "line": 55, + "node": "Attribute", + "name": "render_strips", + "source": "_DiffRow(visual, 0).render_strips" + }, + { + "root": "tests", + "path": "tests/quit_save_installed_pilot.py", + "line": 20, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/reader_geometry_pilot.py", + "line": 30, + "node": "Attribute", + "name": "render_strips", + "source": "screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/readonly_saved_reader_pilot.py", + "line": 53, + "node": "Attribute", + "name": "render_strips", + "source": "screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/receiver_inbound_installed_pilot.py", + "line": 16, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/reconnect_source_native_installed_pilot.py", + "line": 50, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/resize_render_pilot.py", + "line": 39, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/resize_render_pilot.py", + "line": 51, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/retained_fragment_geometry_pilot.py", + "line": 212, + "node": "Attribute", + "name": "render_subtree_strips", + "source": "scene.render_subtree_strips" + }, + { + "root": "tests", + "path": "tests/retained_fragment_geometry_pilot.py", + "line": 138, + "node": "Attribute", + "name": "render_strips", + "source": "scene.render_strips" + }, + { + "root": "tests", + "path": "tests/retained_input_origin_observer.py", + "line": 131, + "node": "Attribute", + "name": "render_strips", + "source": "self.app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/saved_state_user_journey_pilot.py", + "line": 161, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/session_details_pilot.py", + "line": 51, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/session_details_pilot.py", + "line": 102, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/session_route_installed_pilot.py", + "line": 51, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/sidebar_navigation_pilot.py", + "line": 38, + "node": "Attribute", + "name": "render_strips", + "source": "screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/sidebar_retirement_pilot.py", + "line": 61, + "node": "Attribute", + "name": "render_strips", + "source": "widget.screen.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/tab_history_controls_pilot.py", + "line": 26, + "node": "Attribute", + "name": "render_strips", + "source": "screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/tab_history_controls_pilot.py", + "line": 87, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/tab_scrollbar_top_pilot.py", + "line": 63, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/tab_scrollbar_top_pilot.py", + "line": 121, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/terminal_attention_installed_journey.py", + "line": 142, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/terminal_attention_installed_journey.py", + "line": 163, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/terminal_attention_installed_journey.py", + "line": 212, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/thread_activation_pilot.py", + "line": 38, + "node": "Attribute", + "name": "render_strips", + "source": "screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/thread_navigation_installed_journey.py", + "line": 122, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/thread_navigation_installed_journey.py", + "line": 150, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/thread_navigation_installed_journey.py", + "line": 163, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/throbber_render_cache_pilot.py", + "line": 40, + "node": "Attribute", + "name": "render_strips", + "source": "visual.render_strips" + }, + { + "root": "tests", + "path": "tests/throbber_render_cache_pilot.py", + "line": 44, + "node": "Attribute", + "name": "render_strips", + "source": "ThrobberVisual(get_time=lambda value=clock: value).render_strips" + }, + { + "root": "tests", + "path": "tests/tool_diff_lifecycle_pilot.py", + "line": 32, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/tool_diff_pilot.py", + "line": 42, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/transcript_filter_installed_journey.py", + "line": 83, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/transcript_history_pilot.py", + "line": 136, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/transcript_history_pilot.py", + "line": 73, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/viewport_body_order_pilot.py", + "line": 447, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/viewport_layout_reentry_pilot.py", + "line": 69, + "node": "Attribute", + "name": "render_strips", + "source": "screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/viewport_rapid_scroll_pilot.py", + "line": 82, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/worker_native_size_pilot.py", + "line": 160, + "node": "Attribute", + "name": "render_strips", + "source": "scene.render_strips" + }, + { + "root": "tests", + "path": "tests/worker_preview_pilot.py", + "line": 99, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/worker_preview_pilot.py", + "line": 111, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/worker_preview_pilot.py", + "line": 133, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + } + ], + "limits": "Syntactic declarations, name and attribute consumers; arbitrary dynamic rebinding and external third-party private API calls are not resolved." +} diff --git a/src/textual/_compositor.py b/src/textual/_compositor.py index f01879ef40..d0a851c829 100644 --- a/src/textual/_compositor.py +++ b/src/textual/_compositor.py @@ -452,17 +452,29 @@ def __init__( self.spans = spans self.cuts = cuts + @staticmethod + def _span_cuts( + spans: Iterable[tuple[int, int, int]], cuts: list[list[int]], y_origin: int, + ) -> Iterator[tuple[int, int, int]]: + """Select original cut cells touched by damage, including wide edges. + + Paint admission and both publication formats use the same cells. A + span inside a cell still requires that whole strip before final crop. + """ + for y, x1, x2 in spans: + line_cuts = cuts[y - y_origin] + first = max(0, bisect_right(line_cuts, x1) - 1) + last = bisect_left(line_cuts, x2) + for index in range(first, last): + yield y, line_cuts[index], line_cuts[index + 1] + def _get_line_chops(self, y: int, x1: int, x2: int) -> Iterator[tuple[int, Strip]]: """Clip the original painted chops once for either publication format. Strip owns cell splitting, including wide characters and metadata. Borrow the original cuts instead of copying ends for every screen row. """ - cuts = self.cuts[y] - first = max(0, bisect_right(cuts, x1) - 1) - last = bisect_left(cuts, x2) - for index in range(first, last): - x, end = cuts[index], cuts[index + 1] + for _, x, end in self._span_cuts(((y, x1, x2),), self.cuts, 0): strip = self.chops[y].get(x) if strip is None: continue @@ -1544,7 +1556,7 @@ def render_full_update(self, simplify: bool = False) -> LayoutUpdate: screen_region = self.size.region self._dirty_regions.clear() crop = screen_region - chops = self._render_chops(crop, lambda y: True, + chops = self._render_chops(crop, self._regions_to_spans((screen_region,)), widgets=self.visible_widgets, cuts=self.cuts, bounds=screen_region) render_strips: list[Iterable[Strip]] @@ -1574,9 +1586,8 @@ def render_partial_update(self) -> ChopsUpdate | None: return None crop = Region.from_union(update_regions) spans = list(self._regions_to_spans(update_regions)) - is_rendered_line = {y for y, _, _ in spans}.__contains__ cuts = self.cuts - chops = self._render_chops(crop, is_rendered_line, + chops = self._render_chops(crop, spans, widgets=self.visible_widgets, cuts=cuts, bounds=screen_region) return ChopsUpdate(chops, spans, cuts) @@ -1592,7 +1603,8 @@ def render_strips(self, size: Size | None = None) -> list[Strip]: """ if size is None: size = self.size - chops = self._render_chops(size.region, lambda y: True, + rows = Region(0, 0, self.size.width, min(size.height, self.size.height)) + chops = self._render_chops(size.region, self._regions_to_spans((rows,)), widgets=self.visible_widgets, cuts=self.cuts, bounds=self.size.region) render_strips = [Strip.join(chop.values()) for chop in chops[: size.height]] @@ -1632,14 +1644,14 @@ def render_subtree_strips( widgets = self._paint_regions(self._ordered_geometry(geometry), bounds) cuts = self._cuts_for_regions(bounds, widgets) with self._using_geometry(root, geometry): - chops = self._render_chops(bounds, lambda y: True, + chops = self._render_chops(bounds, self._regions_to_spans((bounds,)), widgets=widgets, cuts=cuts, bounds=bounds) return bounds.size, [Strip.join(chop.values()) for chop in chops] def _render_chops( self, crop: Region, - is_rendered_line: Callable[[int], bool], + spans: Iterable[tuple[int, int, int]], *, widgets: Mapping[Widget, tuple[Region, Region]], cuts: list[list[int]], bounds: Region, ) -> Sequence[Mapping[int, Strip]]: @@ -1647,19 +1659,15 @@ def _render_chops( Args: crop: Region to crop to. - is_rendered_line: Callable to check if line should be rendered. + spans: Original damaged horizontal spans to admit before painting. bounds: Original screen or body bounds represented by the chops. Returns: Chops structure. """ - fromkeys = cast("Callable[[list[int]], dict[int, Strip | None]]", dict.fromkeys) - chops: list[dict[int, Strip | None]] - chops = [ - fromkeys(cut_set[:-1]) - if crop.y <= y < crop.bottom and is_rendered_line(y) else {} - for y, cut_set in enumerate(cuts, bounds.y) - ] + chops: list[dict[int, Strip | None]] = [{} for _ in cuts] + for y, x, _end in ChopsUpdate._span_cuts(spans, cuts, bounds.y): + chops[y - bounds.y][x] = None remaining = [len(line) for line in chops] def render_regions(region: Region) -> Iterable[Region]: @@ -1679,8 +1687,7 @@ def render_regions(region: Region) -> Iterable[Region]: row_cuts = cuts[row] for index in range(bisect_left(row_cuts, first), bisect_left(row_cuts, last)): x = row_cuts[index] - value = chops[row][x] - if value is None: + if x in chops[row] and chops[row][x] is None: if start_x is None: start_x = x elif start_x is not None: @@ -1717,9 +1724,8 @@ def render_regions(region: Region) -> Iterable[Region]: cut_strips = strip.divide([cut - render_x for cut in final_cuts[1:]]) # Since we are painting front to back, the first segments for a cut "wins" - get_chops_line = chops_line.get for cut, strip in zip(final_cuts, cut_strips): - if get_chops_line(cut) is None: + if cut in chops_line and chops_line[cut] is None: chops_line[cut] = strip remaining[row] -= 1 return cast("Sequence[Mapping[int, Strip]]", chops) From 4b5076efbd4c6f02fa53990e082fee96ed530a07 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 5 Oct 2026 09:46:26 -0400 Subject: [PATCH 3/6] test: cover native damage islands without intervening paint --- tests/test_compositor_frame_damage.py | 54 +++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/tests/test_compositor_frame_damage.py b/tests/test_compositor_frame_damage.py index 37411ef4fc..bee5b505d5 100644 --- a/tests/test_compositor_frame_damage.py +++ b/tests/test_compositor_frame_damage.py @@ -107,6 +107,60 @@ def compose(self): assert "".join(segment.text for segment in segments if not segment.control) == "CDE\nEF" +@pytest.mark.parametrize('regions', [ + ((2, 0, 3, 1), (22, 0, 3, 1)), + ((1, 0, 2, 1), (2, 0, 3, 1), (21, 2, 2, 1)), +]) +async def test_horizontal_damage_keeps_intervening_native_content_unpainted(regions): + """Damage islands must not materialize the unchanged pane between them.""" + from rich.control import Control + from textual.geometry import Region + + class Counted(Static): + def __init__(self, **kwargs): + super().__init__("[link=https://example.com]A界BCDEFGH[/link]\nSECOND\nTHIRD", **kwargs) + self.crops = [] + + def render_lines(self, crop): + self.crops.append(crop) + return super().render_lines(crop) + + class IslandsApp(App): + CSS = "Screen { layout: horizontal; } Counted { width: 10; height: 3; }" + + def compose(self): + for name in ('left', 'middle', 'right'): + yield Counted(id=name) + + app = IslandsApp() + async with app.run_test(size=(30, 5)) as pilot: + await pilot.pause() + compositor = app.screen._compositor + reference = compositor.render_strips() + panes = [app.query_one(f'#{name}', Counted) for name in ('left', 'middle', 'right')] + for pane in panes: + pane.crops.clear() + compositor._dirty_regions = {Region(*region) for region in regions} + update = compositor.render_partial_update() + assert update is not None + assert panes[0].crops and panes[2].crops + assert panes[1].crops == [] + assert all(not region.translate(pane.region.offset).overlaps(panes[1].region) + for pane in (panes[0], panes[2]) for region in pane.crops) + + expected = [] + for y, x1, x2 in update.spans: + painted = list(update._get_line_chops(y, x1, x2)) + assert len(painted) == 1 + x, strip = painted[0] + assert x == x1 + assert strip == reference[y].crop(x1, x2) + expected.append(Control.move_to(x1, y).segment.text + strip.render(app.console)) + if y != update.spans[-1][0]: + expected.append('\n') + assert update.render_segments(app.console) == ''.join(expected) + + async def test_body_capture_keeps_original_nonzero_row_coordinates(): class BodyApp(App): CSS = "#body { width: 12; height: 3; offset: 5 4; }" From 8c4cb3563ae7dc14e84e87edccdd627f583e05d5 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 5 Oct 2026 09:48:00 -0400 Subject: [PATCH 4/6] docs: bind complete native damage consumer closure --- .../SCOPE.md | 42 + .../after.json | 726 ++++++++++++++++++ 2 files changed, 768 insertions(+) create mode 100644 evidence/native-damage-span-admission-20261005/after.json diff --git a/evidence/native-damage-span-admission-20261005/SCOPE.md b/evidence/native-damage-span-admission-20261005/SCOPE.md index 683db16a79..6fd5cf8e0c 100644 --- a/evidence/native-damage-span-admission-20261005/SCOPE.md +++ b/evidence/native-damage-span-admission-20261005/SCOPE.md @@ -22,3 +22,45 @@ Toad production, test and tool callers, with parsing and dynamic limits explicit Source reasoning and complete implementation precede proportionate affected controls. Installed saved-history motion/CPU improvement is unqualified. No build, environment, installed package, provider or recording purpose exists. + +## Working checkpoint + +Production: 68a9755b411819090283f180a362bf7659757b01. +Authored affected App controls: 4b5076efbd4c6f02fa53990e082fee96ed530a07. +One production file: 29 added / 23 deleted lines against merged Text67. + +- `ChopsUpdate._span_cuts` is the sole damaged-span/cut-cell selector. + The original Unicode-aware Strip crop remains at final publication. +- `Compositor._render_chops` admits those original cells before exposure and + `Widget.render_lines`; both exposure and strip assignment retain membership + in that same admitted chop resource. +- All four callers migrate: full update, partial update, render_strips (inline / + export), and nonzero/negative-origin subtree capture. Full capture and ordinary + full update still admit their complete bounds. render_strips retains the + original scene row limit when a requested size exceeds the published scene. +- Deleted the damaged-row predicate and the separate terminal cut-selection + algorithm. The existing geometry/cut maps and front-to-back order are intact. + +`before.json` covers native production249/tests460 and Toad +production288/tests397/tools41, all parse omissions0. `after.json` covers the +changed native production249/tests460, omissions0; unmodified dependency +consumer evidence is retained at its original pinned source. Four render-chop +callers, one span-cut declaration, zero old dirty-row predicates. Syntactic AST +queries do not prove arbitrary runtime rebinding or unregistered external +private-method consumers. + +## Remaining qualification + +Source-only / Draft. The new actual-native App controls are authored but UNRUN. +They check that horizontal damage islands do not render the intervening pane, +with output parity against the same scene's full strips, including mid-wide-cell +edges, links and disjoint / overlapping damaged rows. Existing frame resize, +partial vertical redraw, layered exposure and original subtree-coordinate +controls cover the other changed callers. One final bounded affected family +batch is sufficient; accepted Text67 controls are not part of it. + +No installed prefix, wheel, UI movie, provider or public mutation was attempted. +No causal CPU gain, measured cadence, smoothness or whole-workflow readiness is +claimed. All scene members are still walked to select layered candidates, and +full-frame cut construction cost is unmeasured; this removes unnecessary content +painting, not those legitimate traversal/geometry operations. diff --git a/evidence/native-damage-span-admission-20261005/after.json b/evidence/native-damage-span-admission-20261005/after.json new file mode 100644 index 0000000000..d1d991626c --- /dev/null +++ b/evidence/native-damage-span-admission-20261005/after.json @@ -0,0 +1,726 @@ +{ + "roots": [ + { + "repository": "/home/ts/wt/textual-native-subtree-strips-20261002", + "head": "4b5076efbd4c6f02fa53990e082fee96ed530a07", + "root": "src/textual", + "parsed": 249, + "omitted": [] + }, + { + "repository": "/home/ts/wt/textual-native-subtree-strips-20261002", + "head": "4b5076efbd4c6f02fa53990e082fee96ed530a07", + "root": "tests", + "parsed": 460, + "omitted": [] + } + ], + "sites": [ + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 435, + "node": "ClassDef", + "name": "ChopsUpdate", + "source": "class ChopsUpdate(CompositorUpdate):" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 456, + "node": "FunctionDef", + "name": "_span_cuts", + "source": " def _span_cuts(" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 471, + "node": "FunctionDef", + "name": "_get_line_chops", + "source": " def _get_line_chops(self, y: int, x1: int, x2: int) -> Iterator[tuple[int, Strip]]:" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 627, + "node": "FunctionDef", + "name": "_regions_to_spans", + "source": " def _regions_to_spans(" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1452, + "node": "FunctionDef", + "name": "_get_renders", + "source": " def _get_renders(" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1547, + "node": "FunctionDef", + "name": "render_full_update", + "source": " def render_full_update(self, simplify: bool = False) -> LayoutUpdate:" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1574, + "node": "FunctionDef", + "name": "render_partial_update", + "source": " def render_partial_update(self) -> ChopsUpdate | None:" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1595, + "node": "FunctionDef", + "name": "render_strips", + "source": " def render_strips(self, size: Size | None = None) -> list[Strip]:" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1629, + "node": "FunctionDef", + "name": "render_subtree_strips", + "source": " def render_subtree_strips(" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1651, + "node": "FunctionDef", + "name": "_render_chops", + "source": " def _render_chops(" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1562, + "node": "Name", + "name": "render_strips", + "source": "render_strips" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1574, + "node": "Name", + "name": "ChopsUpdate", + "source": "ChopsUpdate" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1610, + "node": "Name", + "name": "render_strips", + "source": "render_strips" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1611, + "node": "Name", + "name": "render_strips", + "source": "render_strips" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 477, + "node": "Attribute", + "name": "_span_cuts", + "source": "self._span_cuts" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1544, + "node": "Attribute", + "name": "render_strips", + "source": "self.render_strips" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1559, + "node": "Attribute", + "name": "_render_chops", + "source": "self._render_chops" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1566, + "node": "Name", + "name": "render_strips", + "source": "render_strips" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1570, + "node": "Name", + "name": "render_strips", + "source": "render_strips" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1572, + "node": "Name", + "name": "render_strips", + "source": "render_strips" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1590, + "node": "Attribute", + "name": "_render_chops", + "source": "self._render_chops" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1593, + "node": "Name", + "name": "ChopsUpdate", + "source": "ChopsUpdate" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1607, + "node": "Attribute", + "name": "_render_chops", + "source": "self._render_chops" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1669, + "node": "Attribute", + "name": "_span_cuts", + "source": "ChopsUpdate._span_cuts" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1711, + "node": "Attribute", + "name": "_get_renders", + "source": "self._get_renders" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 492, + "node": "Attribute", + "name": "_get_line_chops", + "source": "self._get_line_chops" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 515, + "node": "Attribute", + "name": "_get_line_chops", + "source": "self._get_line_chops" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1523, + "node": "Attribute", + "name": "render_full_update", + "source": "self.render_full_update" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1525, + "node": "Attribute", + "name": "render_partial_update", + "source": "self.render_partial_update" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1559, + "node": "Attribute", + "name": "_regions_to_spans", + "source": "self._regions_to_spans" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1588, + "node": "Attribute", + "name": "_regions_to_spans", + "source": "self._regions_to_spans" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1607, + "node": "Attribute", + "name": "_regions_to_spans", + "source": "self._regions_to_spans" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1647, + "node": "Attribute", + "name": "_render_chops", + "source": "self._render_chops" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1669, + "node": "Name", + "name": "ChopsUpdate", + "source": "ChopsUpdate" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1734, + "node": "Attribute", + "name": "render_strips", + "source": "self.render_strips" + }, + { + "root": "src/textual", + "path": "src/textual/_compositor.py", + "line": 1647, + "node": "Attribute", + "name": "_regions_to_spans", + "source": "self._regions_to_spans" + }, + { + "root": "src/textual", + "path": "src/textual/content.py", + "line": 728, + "node": "FunctionDef", + "name": "render_strips", + "source": " def render_strips(" + }, + { + "root": "src/textual", + "path": "src/textual/renderables/blank.py", + "line": 27, + "node": "FunctionDef", + "name": "render_strips", + "source": " def render_strips(" + }, + { + "root": "src/textual", + "path": "src/textual/visual.py", + "line": 131, + "node": "FunctionDef", + "name": "render_strips", + "source": " def render_strips(" + }, + { + "root": "src/textual", + "path": "src/textual/visual.py", + "line": 324, + "node": "FunctionDef", + "name": "render_strips", + "source": " def render_strips(" + }, + { + "root": "src/textual", + "path": "src/textual/visual.py", + "line": 391, + "node": "FunctionDef", + "name": "render_strips", + "source": " def render_strips(" + }, + { + "root": "src/textual", + "path": "src/textual/visual.py", + "line": 227, + "node": "Attribute", + "name": "render_strips", + "source": "visual.render_strips" + }, + { + "root": "src/textual", + "path": "src/textual/visual.py", + "line": 411, + "node": "Attribute", + "name": "render_strips", + "source": "self._visual.render_strips" + }, + { + "root": "tests", + "path": "tests/frame_damage_installed_pilot.py", + "line": 16, + "node": "Attribute", + "name": "render_partial_update", + "source": "compositor.render_partial_update" + }, + { + "root": "tests", + "path": "tests/test_compositor.py", + "line": 390, + "node": "Attribute", + "name": "_get_renders", + "source": "compositor._get_renders" + }, + { + "root": "tests", + "path": "tests/test_compositor.py", + "line": 310, + "node": "Attribute", + "name": "_get_renders", + "source": "compositor._get_renders" + }, + { + "root": "tests", + "path": "tests/test_compositor.py", + "line": 354, + "node": "Attribute", + "name": "_get_renders", + "source": "compositor._get_renders" + }, + { + "root": "tests", + "path": "tests/test_compositor.py", + "line": 388, + "node": "Attribute", + "name": "render_strips", + "source": "compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/test_compositor.py", + "line": 308, + "node": "Attribute", + "name": "render_strips", + "source": "compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/test_compositor.py", + "line": 349, + "node": "Attribute", + "name": "render_partial_update", + "source": "compositor.render_partial_update" + }, + { + "root": "tests", + "path": "tests/test_compositor.py", + "line": 392, + "node": "Attribute", + "name": "render_strips", + "source": "compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/test_compositor.py", + "line": 313, + "node": "Attribute", + "name": "render_strips", + "source": "compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/test_compositor.py", + "line": 357, + "node": "Attribute", + "name": "render_partial_update", + "source": "compositor.render_partial_update" + }, + { + "root": "tests", + "path": "tests/test_compositor_frame_damage.py", + "line": 76, + "node": "Name", + "name": "ChopsUpdate", + "source": "ChopsUpdate" + }, + { + "root": "tests", + "path": "tests/test_compositor_frame_damage.py", + "line": 17, + "node": "Attribute", + "name": "render_partial_update", + "source": "compositor.render_partial_update" + }, + { + "root": "tests", + "path": "tests/test_compositor_frame_damage.py", + "line": 58, + "node": "Attribute", + "name": "render_partial_update", + "source": "compositor.render_partial_update" + }, + { + "root": "tests", + "path": "tests/test_compositor_frame_damage.py", + "line": 103, + "node": "Attribute", + "name": "render_partial_update", + "source": "compositor.render_partial_update" + }, + { + "root": "tests", + "path": "tests/test_compositor_frame_damage.py", + "line": 139, + "node": "Attribute", + "name": "render_strips", + "source": "compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/test_compositor_frame_damage.py", + "line": 144, + "node": "Attribute", + "name": "render_partial_update", + "source": "compositor.render_partial_update" + }, + { + "root": "tests", + "path": "tests/test_compositor_frame_damage.py", + "line": 178, + "node": "Attribute", + "name": "render_subtree_strips", + "source": "compositor.render_subtree_strips" + }, + { + "root": "tests", + "path": "tests/test_compositor_frame_damage.py", + "line": 198, + "node": "Attribute", + "name": "render_subtree_strips", + "source": "compositor.render_subtree_strips" + }, + { + "root": "tests", + "path": "tests/test_compositor_frame_damage.py", + "line": 153, + "node": "Attribute", + "name": "_get_line_chops", + "source": "update._get_line_chops" + }, + { + "root": "tests", + "path": "tests/test_compositor_horizontal_damage.py", + "line": 50, + "node": "Attribute", + "name": "_get_renders", + "source": "compositor._get_renders" + }, + { + "root": "tests", + "path": "tests/test_compositor_horizontal_damage.py", + "line": 46, + "node": "Attribute", + "name": "render_strips", + "source": "compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/test_compositor_horizontal_damage.py", + "line": 52, + "node": "Attribute", + "name": "render_strips", + "source": "compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/test_compositor_regions_to_spans.py", + "line": 6, + "node": "Attribute", + "name": "_regions_to_spans", + "source": "Compositor._regions_to_spans" + }, + { + "root": "tests", + "path": "tests/test_compositor_regions_to_spans.py", + "line": 11, + "node": "Attribute", + "name": "_regions_to_spans", + "source": "Compositor._regions_to_spans" + }, + { + "root": "tests", + "path": "tests/test_compositor_regions_to_spans.py", + "line": 19, + "node": "Attribute", + "name": "_regions_to_spans", + "source": "Compositor._regions_to_spans" + }, + { + "root": "tests", + "path": "tests/test_compositor_regions_to_spans.py", + "line": 28, + "node": "Attribute", + "name": "_regions_to_spans", + "source": "Compositor._regions_to_spans" + }, + { + "root": "tests", + "path": "tests/test_compositor_regions_to_spans.py", + "line": 37, + "node": "Attribute", + "name": "_regions_to_spans", + "source": "Compositor._regions_to_spans" + }, + { + "root": "tests", + "path": "tests/test_compositor_regions_to_spans.py", + "line": 42, + "node": "Attribute", + "name": "_regions_to_spans", + "source": "Compositor._regions_to_spans" + }, + { + "root": "tests", + "path": "tests/test_compositor_regions_to_spans.py", + "line": 51, + "node": "Attribute", + "name": "_regions_to_spans", + "source": "Compositor._regions_to_spans" + }, + { + "root": "tests", + "path": "tests/test_content.py", + "line": 303, + "node": "Attribute", + "name": "render_strips", + "source": "content.render_strips" + }, + { + "root": "tests", + "path": "tests/test_frame_transaction.py", + "line": 19, + "node": "Attribute", + "name": "render_strips", + "source": "screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/test_removed_scene_ownership.py", + "line": 146, + "node": "Attribute", + "name": "render_strips", + "source": "app.screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/test_removed_scene_ownership.py", + "line": 168, + "node": "Attribute", + "name": "render_strips", + "source": "owner._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/test_removed_scene_ownership.py", + "line": 197, + "node": "Attribute", + "name": "render_strips", + "source": "owner._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/test_removed_scene_ownership.py", + "line": 228, + "node": "Attribute", + "name": "render_strips", + "source": "owner._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/test_removed_scene_ownership.py", + "line": 178, + "node": "Attribute", + "name": "render_strips", + "source": "owner._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/test_removed_scene_ownership.py", + "line": 209, + "node": "Attribute", + "name": "render_strips", + "source": "owner._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/test_viewport_geometry_targets.py", + "line": 123, + "node": "Attribute", + "name": "render_subtree_strips", + "source": "compositor.render_subtree_strips" + }, + { + "root": "tests", + "path": "tests/test_viewport_geometry_targets.py", + "line": 240, + "node": "Attribute", + "name": "render_subtree_strips", + "source": "compositor.render_subtree_strips" + }, + { + "root": "tests", + "path": "tests/test_viewport_geometry_targets.py", + "line": 271, + "node": "Attribute", + "name": "render_subtree_strips", + "source": "compositor.render_subtree_strips" + }, + { + "root": "tests", + "path": "tests/test_viewport_geometry_targets.py", + "line": 281, + "node": "Attribute", + "name": "render_subtree_strips", + "source": "compositor.render_subtree_strips" + }, + { + "root": "tests", + "path": "tests/test_viewport_geometry_targets.py", + "line": 166, + "node": "Attribute", + "name": "render_strips", + "source": "compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/test_viewport_geometry_targets.py", + "line": 171, + "node": "Attribute", + "name": "render_strips", + "source": "compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/test_viewport_layout.py", + "line": 22, + "node": "Attribute", + "name": "render_strips", + "source": "screen._compositor.render_strips" + }, + { + "root": "tests", + "path": "tests/toad_frame_damage_installed_pilot.py", + "line": 33, + "node": "Attribute", + "name": "render_strips", + "source": "self.screen._compositor.render_strips" + } + ], + "limits": "Syntactic declarations, name and attribute consumers; arbitrary dynamic rebinding and external third-party private API calls are not resolved.", + "retained_dependency_evidence": "before.json records pinned Toad production/tests/tools. No direct Toad _render_chops consumer; no changed Toad files in this branch." +} From 51f1971c2ea29b443ef15764036fa54b3de51c25 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 5 Oct 2026 09:53:39 -0400 Subject: [PATCH 5/6] test: use declared native quoted link markup --- tests/test_compositor_frame_damage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_compositor_frame_damage.py b/tests/test_compositor_frame_damage.py index bee5b505d5..e85532fc4a 100644 --- a/tests/test_compositor_frame_damage.py +++ b/tests/test_compositor_frame_damage.py @@ -118,7 +118,7 @@ async def test_horizontal_damage_keeps_intervening_native_content_unpainted(regi class Counted(Static): def __init__(self, **kwargs): - super().__init__("[link=https://example.com]A界BCDEFGH[/link]\nSECOND\nTHIRD", **kwargs) + super().__init__("[link='https://example.com']A界BCDEFGH[/link]\nSECOND\nTHIRD", **kwargs) self.crops = [] def render_lines(self, crop): From d9c8480586495133d68cb763951100a2940c438e Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 5 Oct 2026 09:55:12 -0400 Subject: [PATCH 6/6] docs: qualify original damage admission and preserve markup negative --- .../SCOPE.md | 37 +- .../source-batch01.json | 22 + .../source-batch01.log | 1000 +++++++++++++++++ .../source-batch02.json | 18 + .../source-batch02.log | 2 + 5 files changed, 1075 insertions(+), 4 deletions(-) create mode 100644 evidence/native-damage-span-admission-20261005/source-batch01.json create mode 100644 evidence/native-damage-span-admission-20261005/source-batch01.log create mode 100644 evidence/native-damage-span-admission-20261005/source-batch02.json create mode 100644 evidence/native-damage-span-admission-20261005/source-batch02.log diff --git a/evidence/native-damage-span-admission-20261005/SCOPE.md b/evidence/native-damage-span-admission-20261005/SCOPE.md index 6fd5cf8e0c..672be5a610 100644 --- a/evidence/native-damage-span-admission-20261005/SCOPE.md +++ b/evidence/native-damage-span-admission-20261005/SCOPE.md @@ -51,16 +51,45 @@ private-method consumers. ## Remaining qualification -Source-only / Draft. The new actual-native App controls are authored but UNRUN. -They check that horizontal damage islands do not render the intervening pane, +Native source App scope qualified / Draft; installed behavior unqualified. +The actual-native App controls check that horizontal damage islands do not render the intervening pane, with output parity against the same scene's full strips, including mid-wide-cell edges, links and disjoint / overlapping damaged rows. Existing frame resize, partial vertical redraw, layered exposure and original subtree-coordinate -controls cover the other changed callers. One final bounded affected family -batch is sufficient; accepted Text67 controls are not part of it. +controls cover the other changed callers. The authorized final bounded batch ran once; accepted Text67 controls were not +part of it. Only two invalid-markup fixture cases were corrected and rerun. No installed prefix, wheel, UI movie, provider or public mutation was attempted. No causal CPU gain, measured cadence, smoothness or whole-workflow readiness is claimed. All scene members are still walked to select layered candidates, and full-frame cut construction cost is unmeasured; this removes unnecessary content painting, not those legitimate traversal/geometry operations. + +## Actual source App qualification + +- Batch01 source8c4cb3563ae7dc14e84e87edccdd627f583e05d5: + 14 PASS / 2 FAIL in4.19s, controller5.798375586979091s, exit1. + Both failures occurred at App startup because the authored link markup used + an unquoted URL, contrary to native Content markup. Original full failure log + and source are retained; no damage assertion ran in those two cases. +- Correction51f1971c2ea29b443ef15764036fa54b3de51c25 changes ONLY that + fixture literal to the existing quoted native link declaration. No product + bytes or assertions changed. Only those two cases ran: 2 PASS in0.40s, + controller1.6918718189699575s, exit0. +- Thus16 affected cases are qualified across the original batch plus the two + corrected cases; this is not a claim of one clean16-case run. Exact production + remains68a9755b411819090283f180a362bf7659757b01. +- Damage islands left the middle native pane unrendered; resulting emitted + strips match the same full scene, including link metadata, mid-wide-cell + damage, overlap and differing selected rows. Existing partial vertical, + layered exposure, resize/old-caption damage, native writer crop, and subtree + original-coordinate/capture custody controls also passed. + +`source-batch01/02.json` bind their original logs by SHA. Resource readback +reported10.7GiB available RAM and17.0GiB swap; this was one short sequential +source batch and its two corrected cases, without a new environment or native +process/package/SDK/media purpose. No accepted Text67/66 controls were repeated. + +Remaining boundary: normal standalone artifact and joined installed saved +workflow acceptance. No source App control measures CPU, first paint cadence, +physical input/frame latency, smoothness,144Hz or a global performance gain. diff --git a/evidence/native-damage-span-admission-20261005/source-batch01.json b/evidence/native-damage-span-admission-20261005/source-batch01.json new file mode 100644 index 0000000000..c031620dd3 --- /dev/null +++ b/evidence/native-damage-span-admission-20261005/source-batch01.json @@ -0,0 +1,22 @@ +{ + "source": "8c4cb3563ae7dc14e84e87edccdd627f583e05d5", + "production_source": "68a9755b411819090283f180a362bf7659757b01", + "argv": [ + "/usr/bin/python", + "-m", + "pytest", + "-q", + "tests/test_compositor_frame_damage.py", + "tests/test_compositor.py::test_partial_redraw_requests_only_damaged_vertical_rows", + "tests/test_compositor.py::test_covered_widgets_skip_rendering_without_changing_output", + "tests/test_compositor.py::test_partly_covered_widgets_render_only_exposed_rows" + ], + "interpreter": "/usr/bin/python", + "seconds": 5.798375586979091, + "exit_code": 1, + "log": "/home/ts/wt/textual-native-subtree-strips-20261002/.artifacts/native-damage-span-admission-20261005/source-batch01.log", + "log_sha256": "978a85fdc13b0b68dfa6b3592e678703787453560f4122eb99dfdaec1481992a", + "scope": "ONE authorized source App batch for exact damage spans, Unicode/link metadata, frame resize/capture bounds, partial vertical rows and layered exposure. No installed prefix/build/SDK/provider/media or native67 controls.", + "installed_qualified": false, + "retained_log": "evidence/native-damage-span-admission-20261005/source-batch01.log" +} diff --git a/evidence/native-damage-span-admission-20261005/source-batch01.log b/evidence/native-damage-span-admission-20261005/source-batch01.log new file mode 100644 index 0000000000..97fe9dd755 --- /dev/null +++ b/evidence/native-damage-span-admission-20261005/source-batch01.log @@ -0,0 +1,1000 @@ +......FF........ [100%] +=================================== FAILURES =================================== +_ test_horizontal_damage_keeps_intervening_native_content_unpainted[regions0] __ + +regions = ((2, 0, 3, 1), (22, 0, 3, 1)) + + @pytest.mark.parametrize('regions', [ + ((2, 0, 3, 1), (22, 0, 3, 1)), + ((1, 0, 2, 1), (2, 0, 3, 1), (21, 2, 2, 1)), + ]) + async def test_horizontal_damage_keeps_intervening_native_content_unpainted(regions): + """Damage islands must not materialize the unchanged pane between them.""" + from rich.control import Control + from textual.geometry import Region + + class Counted(Static): + def __init__(self, **kwargs): + super().__init__("[link=https://example.com]A界BCDEFGH[/link]\nSECOND\nTHIRD", **kwargs) + self.crops = [] + + def render_lines(self, crop): + self.crops.append(crop) + return super().render_lines(crop) + + class IslandsApp(App): + CSS = "Screen { layout: horizontal; } Counted { width: 10; height: 3; }" + + def compose(self): + for name in ('left', 'middle', 'right'): + yield Counted(id=name) + + app = IslandsApp() +> async with app.run_test(size=(30, 5)) as pilot: + ^^^^^^^^^^^^^^^^^^^^^^^^^^ + +tests/test_compositor_frame_damage.py:136: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +/usr/lib/python3.14/contextlib.py:235: in __aexit__ + await self.gen.athrow(value) +src/textual/app.py:2228: in run_test + raise self._exception +src/textual/message_pump.py:707: in _process_messages_loop + await self._dispatch_message(message) +src/textual/message_pump.py:778: in _dispatch_message + await self.on_event(message) +src/textual/message_pump.py:859: in on_event + await self._on_message(event) +src/textual/message_pump.py:880: in _on_message + await invoke(method, message) +src/textual/_callback.py:97: in invoke + return await _invoke(callback, *params) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +src/textual/_callback.py:57: in _invoke + result = callback(*params[:parameter_count]) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +src/textual/screen.py:1563: in _on_screen_resume + self._refresh_layout(size) +src/textual/screen.py:1437: in _refresh_layout + self._compositor_refresh() +src/textual/screen.py:1243: in _compositor_refresh + update = self._compositor.render_update( +src/textual/_compositor.py:1523: in render_update + return self.render_full_update(simplify=simplify) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +src/textual/_compositor.py:1559: in render_full_update + chops = self._render_chops(crop, self._regions_to_spans((screen_region,)), +src/textual/_compositor.py:1713: in _render_chops + for render_region, strips in renders: + ^^^^^^^ +src/textual/_compositor.py:1493: in _get_renders + widget.render_lines( +tests/test_compositor_frame_damage.py:126: in render_lines + return super().render_lines(crop) + ^^^^^^^^^^^^^^^^^^^^^^^^^^ +src/textual/widget.py:4511: in render_lines + strips = self._styles_cache.render_widget(self, crop) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +src/textual/_styles_cache.py:117: in render_widget + strips = self.render( +src/textual/_styles_cache.py:222: in render + strip = render_line( +src/textual/_styles_cache.py:457: in render_line + line = render_content_line(y - gutter.top) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +src/textual/widget.py:4489: in render_line + self._render_content() +src/textual/widget.py:4471: in _render_content + visual = self._render() + ^^^^^^^^^^^^^^ +src/textual/widget.py:4698: in _render + visual = visualize(self, self.render(), markup=self._render_markup) + ^^^^^^^^^^^^^ +src/textual/widgets/_static.py:85: in render + return self.visual + ^^^^^^^^^^^ +src/textual/widgets/_static.py:64: in visual + self.__visual = visualize(self, self.__content, markup=self._render_markup) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +src/textual/visual.py:103: in visualize + return Content.from_markup(obj) if markup else Content(obj) + ^^^^^^^^^^^^^^^^^^^^^^^^ +src/textual/content.py:299: in from_markup + content = to_content(markup, template_variables=variables or None) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +markup = '[link=https://example.com]A界BCDEFGH[/link]\nSECOND\nTHIRD', style = '' +template_variables = None + + def to_content( + markup: str, + style: str | Style = "", + template_variables: Mapping[str, object] | None = None, + ) -> Content: + """Convert markup to Content. + + Args: + markup: String containing markup. + style: Optional base style. + template_variables: Mapping of string.Template variables + + Raises: + MarkupError: If the markup is invalid. + + Returns: + Content that renders the markup. + """ + _rich_traceback_omit = True + try: + return _to_content(markup, style, template_variables) + except UnexpectedEnd: + raise MarkupError( + "Unexpected end of markup; are you missing a closing square bracket?" + ) from None + except Exception as error: + # Ensure all errors are wrapped in a MarkupError +> raise MarkupError(str(error)) from None +E textual.markup.MarkupError: Expected markup value (found '://example.com]A界BCDEFGH[/link]\n'). + +src/textual/markup.py:320: MarkupError +----------------------------- Captured stderr call ----------------------------- +╭───────────────────── Traceback (most recent call last) ──────────────────────╮ +│ /home/ts/wt/textual-native-subtree-strips-20261002/tests/test_compositor_fra │ +│ me_damage.py:126 in render_lines │ +│ │ +│ 123 │ │ │ +│ 124 │ │ def render_lines(self, crop): │ +│ 125 │ │ │ self.crops.append(crop) │ +│ ❱ 126 │ │ │ return super().render_lines(crop) │ +│ 127 │ │ +│ 128 │ class IslandsApp(App): │ +│ 129 │ │ CSS = "Screen { layout: horizontal; } Counted { width: 10; hei │ +│ │ +│ ╭────────────────── locals ───────────────────╮ │ +│ │ crop = Region(x=0, y=0, width=10, height=3) │ │ +│ │ self = Counted(id='right') │ │ +│ ╰─────────────────────────────────────────────╯ │ +│ │ +│ /home/ts/wt/textual-native-subtree-strips-20261002/src/textual/widget.py:451 │ +│ 1 in render_lines │ +│ │ +│ 4508 │ │ │ │ Strip.blank(crop.width, self.visual_style.rich_style) │ +│ 4509 │ │ │ ] * crop.height │ +│ 4510 │ │ else: │ +│ ❱ 4511 │ │ │ strips = self._styles_cache.render_widget(self, crop) │ +│ 4512 │ │ return strips │ +│ 4513 │ │ +│ 4514 │ def get_style_at(self, x: int, y: int) -> Style: │ +│ │ +│ ╭────────────────── locals ───────────────────╮ │ +│ │ crop = Region(x=0, y=0, width=10, height=3) │ │ +│ │ self = Counted(id='right') │ │ +│ ╰─────────────────────────────────────────────╯ │ +│ │ +│ /home/ts/wt/textual-native-subtree-strips-20261002/src/textual/_styles_cache │ +│ .py:117 in render_widget │ +│ │ +│ 114 │ │ base_background, background = widget.background_colors │ +│ 115 │ │ styles = widget.styles │ +│ 116 │ │ app = widget.app │ +│ ❱ 117 │ │ strips = self.render( │ +│ 118 │ │ │ styles, │ +│ 119 │ │ │ widget.region.size, │ +│ 120 │ │ │ base_background, │ +│ │ +│ ╭───────────────────────────────── locals ─────────────────────────────────╮ │ +│ │ app = IslandsApp( │ │ +│ │ │ title='IslandsApp', │ │ +│ │ │ classes={'-dark-mode'}, │ │ +│ │ │ pseudo_classes={'focus', 'nocolor', 'dark'} │ │ +│ │ ) │ │ +│ │ background = Color(18, 18, 18) │ │ +│ │ base_background = Color(18, 18, 18) │ │ +│ │ border_subtitle = None │ │ +│ │ border_title = None │ │ +│ │ crop = Region(x=0, y=0, width=10, height=3) │ │ +│ │ self = │ │ +│ │ styles = RenderStyles( │ │ +│ │ │ Counted(id='right'), │ │ +│ │ │ background=Color(0, 0, 0, a=0), │ │ +│ │ │ width=Scalar( │ │ +│ │ │ │ value=10.0, │ │ +│ │ │ │ unit=, │ │ +│ │ │ │ percent_unit= │ │ +│ │ │ ), │ │ +│ │ │ height=Scalar( │ │ +│ │ │ │ value=3.0, │ │ +│ │ │ │ unit=, │ │ +│ │ │ │ percent_unit= │ │ +│ │ │ ), │ │ +│ │ │ scrollbar_color=Color(0, 48, 84), │ │ +│ │ │ scrollbar_color_hover=Color(0, 60, 106), │ │ +│ │ │ scrollbar_color_active=Color(1, 120, 212), │ │ +│ │ │ scrollbar_corner_color=Color(0, 0, 0), │ │ +│ │ │ scrollbar_background=Color(0, 0, 0), │ │ +│ │ │ scrollbar_background_hover=Color(0, 0, 0), │ │ +│ │ │ scrollbar_background_active=Color(0, 0, 0), │ │ +│ │ │ scrollbar_size_vertical=2, │ │ +│ │ │ scrollbar_size_horizontal=1, │ │ +│ │ │ link_color=Color(255, 255, 255, a=0.87), │ │ +│ │ │ auto_link_color=True, │ │ +│ │ │ link_background=Color(0, 0, 0, a=0), │ │ +│ │ │ link_style=Style(underline=True), │ │ +│ │ │ link_color_hover=Color(255, 255, 255, a=0.87), │ │ +│ │ │ auto_link_color_hover=True, │ │ +│ │ │ link_background_hover=Color(1, 120, 212), │ │ +│ │ │ link_style_hover=Style( │ │ +│ │ │ │ bold=True, │ │ +│ │ │ │ underline=False │ │ +│ │ │ ) │ │ +│ │ ) │ │ +│ │ widget = Counted(id='right') │ │ +│ ╰──────────────────────────────────────────────────────────────────────────╯ │ +│ │ +│ /home/ts/wt/textual-native-subtree-strips-20261002/src/textual/_styles_cache │ +│ .py:222 in render │ +│ │ +│ 219 │ │ │ +│ 220 │ │ for y in crop.line_range: │ +│ 221 │ │ │ if is_dirty(y) or y not in self._cache: │ +│ ❱ 222 │ │ │ │ strip = render_line( │ +│ 223 │ │ │ │ │ styles, │ +│ 224 │ │ │ │ │ y, │ +│ 225 │ │ │ │ │ size, │ +│ │ +│ ╭───────────────────────────────── locals ─────────────────────────────────╮ │ +│ │ _height = 3 │ │ +│ │ add_strip = │ │ +│ │ ansi_theme = │ │ +│ │ background = Color(18, 18, 18) │ │ +│ │ base_background = Color(18, 18, 18) │ │ +│ │ border_subtitle = None │ │ +│ │ border_title = None │ │ +│ │ content_size = Size(width=10, height=3) │ │ +│ │ crop = Region(x=0, y=0, width=10, height=3) │ │ +│ │ filters = [ │ │ +│ │ │ , │ │ +│ │ │ │ │ +│ │ ] │ │ +│ │ is_dirty = │ │ +│ │ native_ansi = False │ │ +│ │ opacity = 1.0 │ │ +│ │ padding = Spacing(top=0, right=0, bottom=0, left=0) │ │ +│ │ render_content_line = │ │ +│ │ render_line = > │ │ +│ │ self = │ │ +│ │ size = Size(width=10, height=3) │ │ +│ │ strips = [] │ │ +│ │ styles = RenderStyles( │ │ +│ │ │ Counted(id='right'), │ │ +│ │ │ background=Color(0, 0, 0, a=0), │ │ +│ │ │ width=Scalar( │ │ +│ │ │ │ value=10.0, │ │ +│ │ │ │ unit=, │ │ +│ │ │ │ percent_unit= │ │ +│ │ │ ), │ │ +│ │ │ height=Scalar( │ │ +│ │ │ │ value=3.0, │ │ +│ │ │ │ unit=, │ │ +│ │ │ │ percent_unit= │ │ +│ │ │ ), │ │ +│ │ │ scrollbar_color=Color(0, 48, 84), │ │ +│ │ │ scrollbar_color_hover=Color(0, 60, 106), │ │ +│ │ │ scrollbar_color_active=Color(1, 120, 212), │ │ +│ │ │ scrollbar_corner_color=Color(0, 0, 0), │ │ +│ │ │ scrollbar_background=Color(0, 0, 0), │ │ +│ │ │ scrollbar_background_hover=Color(0, 0, 0), │ │ +│ │ │ scrollbar_background_active=Color(0, 0, 0), │ │ +│ │ │ scrollbar_size_vertical=2, │ │ +│ │ │ scrollbar_size_horizontal=1, │ │ +│ │ │ link_color=Color(255, 255, 255, a=0.87), │ │ +│ │ │ auto_link_color=True, │ │ +│ │ │ link_background=Color(0, 0, 0, a=0), │ │ +│ │ │ link_style=Style(underline=True), │ │ +│ │ │ link_color_hover=Color( │ │ +│ │ │ │ 255, │ │ +│ │ │ │ 255, │ │ +│ │ │ │ 255, │ │ +│ │ │ │ a=0.87 │ │ +│ │ │ ), │ │ +│ │ │ auto_link_color_hover=True, │ │ +│ │ │ link_background_hover=Color(1, 120, 212), │ │ +│ │ │ link_style_hover=Style( │ │ +│ │ │ │ bold=True, │ │ +│ │ │ │ underline=False │ │ +│ │ │ ) │ │ +│ │ ) │ │ +│ │ width = 10 │ │ +│ │ y = 0 │ │ +│ ╰──────────────────────────────────────────────────────────────────────────╯ │ +│ │ +│ /home/ts/wt/textual-native-subtree-strips-20261002/src/textual/_styles_cache │ +│ .py:457 in render_line │ +│ │ +│ 454 │ │ │ # Content with border and padding (C) │ +│ 455 │ │ │ content_y = y - gutter.top │ +│ 456 │ │ │ if content_y < content_height: │ +│ ❱ 457 │ │ │ │ line = render_content_line(y - gutter.top) │ +│ 458 │ │ │ │ line = line.adjust_cell_length(content_width, inner.ri │ +│ 459 │ │ │ else: │ +│ 460 │ │ │ │ line = Strip.blank(content_width, inner.rich_style) │ +│ │ +│ ╭───────────────────────────────── locals ─────────────────────────────────╮ │ +│ │ ansi_theme = │ │ +│ │ background = Color(18, 18, 18) │ │ +│ │ base_background = Color(18, 18, 18) │ │ +│ │ border_bottom = '' │ │ +│ │ border_bottom_color = Color(0, 255, 0) │ │ +│ │ border_left = '' │ │ +│ │ border_left_color = Color(0, 255, 0) │ │ +│ │ border_right = '' │ │ +│ │ border_right_color = Color(0, 255, 0) │ │ +│ │ border_subtitle = None │ │ +│ │ border_title = None │ │ +│ │ border_top = '' │ │ +│ │ border_top_color = Color(0, 255, 0) │ │ +│ │ cache_simple_strip = False │ │ +│ │ content_height = 3 │ │ +│ │ content_size = Size(width=10, height=3) │ │ +│ │ content_width = 10 │ │ +│ │ content_y = 0 │ │ +│ │ from_color = > │ │ +│ │ gutter = Spacing(top=0, right=0, bottom=0, left=0) │ │ +│ │ height = 3 │ │ +│ │ inner = Style(background=Color(18, 18, 18)) │ │ +│ │ native_ansi = False │ │ +│ │ opacity = 1.0 │ │ +│ │ outer = Style(background=Color(18, 18, 18)) │ │ +│ │ outline_bottom = '' │ │ +│ │ outline_bottom_color = Color(0, 255, 0) │ │ +│ │ outline_left = '' │ │ +│ │ outline_left_color = Color(0, 255, 0) │ │ +│ │ outline_right = '' │ │ +│ │ outline_right_color = Color(0, 255, 0) │ │ +│ │ outline_top = '' │ │ +│ │ outline_top_color = Color(0, 255, 0) │ │ +│ │ pad_bottom = 0 │ │ +│ │ pad_left = 0 │ │ +│ │ pad_right = 0 │ │ +│ │ pad_top = 0 │ │ +│ │ padding = Spacing(top=0, right=0, bottom=0, left=0) │ │ +│ │ render_content_line = │ │ +│ │ self = │ │ +│ │ size = Size(width=10, height=3) │ │ +│ │ styles = RenderStyles( │ │ +│ │ │ Counted(id='right'), │ │ +│ │ │ background=Color(0, 0, 0, a=0), │ │ +│ │ │ width=Scalar( │ │ +│ │ │ │ value=10.0, │ │ +│ │ │ │ unit=, │ │ +│ │ │ │ percent_unit= │ │ +│ │ │ ), │ │ +│ │ │ height=Scalar( │ │ +│ │ │ │ value=3.0, │ │ +│ │ │ │ unit=, │ │ +│ │ │ │ percent_unit= │ │ +│ │ │ ), │ │ +│ │ │ scrollbar_color=Color(0, 48, 84), │ │ +│ │ │ scrollbar_color_hover=Color(0, 60, 106), │ │ +│ │ │ scrollbar_color_active=Color(1, 120, 212), │ │ +│ │ │ scrollbar_corner_color=Color(0, 0, 0), │ │ +│ │ │ scrollbar_background=Color(0, 0, 0), │ │ +│ │ │ scrollbar_background_hover=Color(0, 0, 0), │ │ +│ │ │ scrollbar_background_active=Color(0, 0, 0), │ │ +│ │ │ scrollbar_size_vertical=2, │ │ +│ │ │ scrollbar_size_horizontal=1, │ │ +│ │ │ link_color=Color(255, 255, 255, a=0.87), │ │ +│ │ │ auto_link_color=True, │ │ +│ │ │ link_background=Color(0, 0, 0, a=0), │ │ +│ │ │ link_style=Style(underline=True), │ │ +│ │ │ link_color_hover=Color( │ │ +│ │ │ │ 255, │ │ +│ │ │ │ 255, │ │ +│ │ │ │ 255, │ │ +│ │ │ │ a=0.87 │ │ +│ │ │ ), │ │ +│ │ │ auto_link_color_hover=True, │ │ +│ │ │ link_background_hover=Color(1, 120, 212), │ │ +│ │ │ link_style_hover=Style( │ │ +│ │ │ │ bold=True, │ │ +│ │ │ │ underline=False │ │ +│ │ │ ) │ │ +│ │ ) │ │ +│ │ width = 10 │ │ +│ │ y = 0 │ │ +│ ╰──────────────────────────────────────────────────────────────────────────╯ │ +│ │ +│ /home/ts/wt/textual-native-subtree-strips-20261002/src/textual/widget.py:448 │ +│ 9 in render_line │ +│ │ +│ 4486 │ │ │ return Strip.blank(self.size.width, self.visual_style.ric │ +│ 4487 │ │ │ +│ 4488 │ │ if self._dirty_regions or self._render_cache.size != self.siz │ +│ ❱ 4489 │ │ │ self._render_content() │ +│ 4490 │ │ try: │ +│ 4491 │ │ │ line = self._render_cache.lines[y] │ +│ 4492 │ │ except IndexError: │ +│ │ +│ ╭────────── locals ──────────╮ │ +│ │ self = Counted(id='right') │ │ +│ │ y = 0 │ │ +│ ╰────────────────────────────╯ │ +│ │ +│ /home/ts/wt/textual-native-subtree-strips-20261002/src/textual/widget.py:447 │ +│ 1 in _render_content │ +│ │ +│ 4468 │ │ """Render all lines.""" │ +│ 4469 │ │ size = self.size │ +│ 4470 │ │ width, height = size │ +│ ❱ 4471 │ │ visual = self._render() │ +│ 4472 │ │ strips = Visual.to_strips(self, visual, width, height, self.v │ +│ 4473 │ │ self._render_cache = _RenderCache(size, strips) │ +│ 4474 │ │ self._dirty_regions.clear() │ +│ │ +│ ╭───────────── locals ──────────────╮ │ +│ │ height = 3 │ │ +│ │ self = Counted(id='right') │ │ +│ │ size = Size(width=10, height=3) │ │ +│ │ width = 10 │ │ +│ ╰───────────────────────────────────╯ │ +│ │ +│ /home/ts/wt/textual-native-subtree-strips-20261002/src/textual/widget.py:469 │ +│ 8 in _render │ +│ │ +│ 4695 │ │ if cached_visual is not None: │ +│ 4696 │ │ │ assert isinstance(cached_visual, Visual) │ +│ 4697 │ │ │ return cached_visual │ +│ ❱ 4698 │ │ visual = visualize(self, self.render(), markup=self._render_m │ +│ 4699 │ │ self._layout_cache[cache_key] = visual │ +│ 4700 │ │ return visual │ +│ 4701 │ +│ │ +│ ╭────────────── locals ───────────────╮ │ +│ │ cache_key = '_render.visual' │ │ +│ │ cached_visual = None │ │ +│ │ self = Counted(id='right') │ │ +│ ╰─────────────────────────────────────╯ │ +│ │ +│ /home/ts/wt/textual-native-subtree-strips-20261002/src/textual/widgets/_stat │ +│ ic.py:85 in render │ +│ │ +│ 82 │ │ Returns: │ +│ 83 │ │ │ A rich renderable. │ +│ 84 │ │ """ │ +│ ❱ 85 │ │ return self.visual │ +│ 86 │ │ +│ 87 │ def _render_styles_sensitive(self) -> bool: │ +│ 88 │ │ """Only original Content has the native rule-measurement contr │ +│ │ +│ ╭────────── locals ──────────╮ │ +│ │ self = Counted(id='right') │ │ +│ ╰────────────────────────────╯ │ +│ │ +│ /home/ts/wt/textual-native-subtree-strips-20261002/src/textual/widgets/_stat │ +│ ic.py:64 in visual │ +│ │ +│ 61 │ │ │ +│ 62 │ │ """ │ +│ 63 │ │ if self.__visual is None: │ +│ ❱ 64 │ │ │ self.__visual = visualize(self, self.__content, markup=sel │ +│ 65 │ │ return self.__visual │ +│ 66 │ │ +│ 67 │ @property │ +│ │ +│ ╭────────── locals ──────────╮ │ +│ │ self = Counted(id='right') │ │ +│ ╰────────────────────────────╯ │ +╰──────────────────────────────────────────────────────────────────────────────╯ +MarkupError: Expected markup value (found '://example.com]A界BCDEFGH[/link]\n'). +_ test_horizontal_damage_keeps_intervening_native_content_unpainted[regions1] __ + +regions = ((1, 0, 2, 1), (2, 0, 3, 1), (21, 2, 2, 1)) + + @pytest.mark.parametrize('regions', [ + ((2, 0, 3, 1), (22, 0, 3, 1)), + ((1, 0, 2, 1), (2, 0, 3, 1), (21, 2, 2, 1)), + ]) + async def test_horizontal_damage_keeps_intervening_native_content_unpainted(regions): + """Damage islands must not materialize the unchanged pane between them.""" + from rich.control import Control + from textual.geometry import Region + + class Counted(Static): + def __init__(self, **kwargs): + super().__init__("[link=https://example.com]A界BCDEFGH[/link]\nSECOND\nTHIRD", **kwargs) + self.crops = [] + + def render_lines(self, crop): + self.crops.append(crop) + return super().render_lines(crop) + + class IslandsApp(App): + CSS = "Screen { layout: horizontal; } Counted { width: 10; height: 3; }" + + def compose(self): + for name in ('left', 'middle', 'right'): + yield Counted(id=name) + + app = IslandsApp() +> async with app.run_test(size=(30, 5)) as pilot: + ^^^^^^^^^^^^^^^^^^^^^^^^^^ + +tests/test_compositor_frame_damage.py:136: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +/usr/lib/python3.14/contextlib.py:235: in __aexit__ + await self.gen.athrow(value) +src/textual/app.py:2228: in run_test + raise self._exception +src/textual/message_pump.py:707: in _process_messages_loop + await self._dispatch_message(message) +src/textual/message_pump.py:778: in _dispatch_message + await self.on_event(message) +src/textual/message_pump.py:859: in on_event + await self._on_message(event) +src/textual/message_pump.py:880: in _on_message + await invoke(method, message) +src/textual/_callback.py:97: in invoke + return await _invoke(callback, *params) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +src/textual/_callback.py:57: in _invoke + result = callback(*params[:parameter_count]) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +src/textual/screen.py:1563: in _on_screen_resume + self._refresh_layout(size) +src/textual/screen.py:1437: in _refresh_layout + self._compositor_refresh() +src/textual/screen.py:1243: in _compositor_refresh + update = self._compositor.render_update( +src/textual/_compositor.py:1523: in render_update + return self.render_full_update(simplify=simplify) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +src/textual/_compositor.py:1559: in render_full_update + chops = self._render_chops(crop, self._regions_to_spans((screen_region,)), +src/textual/_compositor.py:1713: in _render_chops + for render_region, strips in renders: + ^^^^^^^ +src/textual/_compositor.py:1493: in _get_renders + widget.render_lines( +tests/test_compositor_frame_damage.py:126: in render_lines + return super().render_lines(crop) + ^^^^^^^^^^^^^^^^^^^^^^^^^^ +src/textual/widget.py:4511: in render_lines + strips = self._styles_cache.render_widget(self, crop) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +src/textual/_styles_cache.py:117: in render_widget + strips = self.render( +src/textual/_styles_cache.py:222: in render + strip = render_line( +src/textual/_styles_cache.py:457: in render_line + line = render_content_line(y - gutter.top) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +src/textual/widget.py:4489: in render_line + self._render_content() +src/textual/widget.py:4471: in _render_content + visual = self._render() + ^^^^^^^^^^^^^^ +src/textual/widget.py:4698: in _render + visual = visualize(self, self.render(), markup=self._render_markup) + ^^^^^^^^^^^^^ +src/textual/widgets/_static.py:85: in render + return self.visual + ^^^^^^^^^^^ +src/textual/widgets/_static.py:64: in visual + self.__visual = visualize(self, self.__content, markup=self._render_markup) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +src/textual/visual.py:103: in visualize + return Content.from_markup(obj) if markup else Content(obj) + ^^^^^^^^^^^^^^^^^^^^^^^^ +src/textual/content.py:299: in from_markup + content = to_content(markup, template_variables=variables or None) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +markup = '[link=https://example.com]A界BCDEFGH[/link]\nSECOND\nTHIRD', style = '' +template_variables = None + + def to_content( + markup: str, + style: str | Style = "", + template_variables: Mapping[str, object] | None = None, + ) -> Content: + """Convert markup to Content. + + Args: + markup: String containing markup. + style: Optional base style. + template_variables: Mapping of string.Template variables + + Raises: + MarkupError: If the markup is invalid. + + Returns: + Content that renders the markup. + """ + _rich_traceback_omit = True + try: + return _to_content(markup, style, template_variables) + except UnexpectedEnd: + raise MarkupError( + "Unexpected end of markup; are you missing a closing square bracket?" + ) from None + except Exception as error: + # Ensure all errors are wrapped in a MarkupError +> raise MarkupError(str(error)) from None +E textual.markup.MarkupError: Expected markup value (found '://example.com]A界BCDEFGH[/link]\n'). + +src/textual/markup.py:320: MarkupError +----------------------------- Captured stderr call ----------------------------- +╭───────────────────── Traceback (most recent call last) ──────────────────────╮ +│ /home/ts/wt/textual-native-subtree-strips-20261002/tests/test_compositor_fra │ +│ me_damage.py:126 in render_lines │ +│ │ +│ 123 │ │ │ +│ 124 │ │ def render_lines(self, crop): │ +│ 125 │ │ │ self.crops.append(crop) │ +│ ❱ 126 │ │ │ return super().render_lines(crop) │ +│ 127 │ │ +│ 128 │ class IslandsApp(App): │ +│ 129 │ │ CSS = "Screen { layout: horizontal; } Counted { width: 10; hei │ +│ │ +│ ╭────────────────── locals ───────────────────╮ │ +│ │ crop = Region(x=0, y=0, width=10, height=3) │ │ +│ │ self = Counted(id='right') │ │ +│ ╰─────────────────────────────────────────────╯ │ +│ │ +│ /home/ts/wt/textual-native-subtree-strips-20261002/src/textual/widget.py:451 │ +│ 1 in render_lines │ +│ │ +│ 4508 │ │ │ │ Strip.blank(crop.width, self.visual_style.rich_style) │ +│ 4509 │ │ │ ] * crop.height │ +│ 4510 │ │ else: │ +│ ❱ 4511 │ │ │ strips = self._styles_cache.render_widget(self, crop) │ +│ 4512 │ │ return strips │ +│ 4513 │ │ +│ 4514 │ def get_style_at(self, x: int, y: int) -> Style: │ +│ │ +│ ╭────────────────── locals ───────────────────╮ │ +│ │ crop = Region(x=0, y=0, width=10, height=3) │ │ +│ │ self = Counted(id='right') │ │ +│ ╰─────────────────────────────────────────────╯ │ +│ │ +│ /home/ts/wt/textual-native-subtree-strips-20261002/src/textual/_styles_cache │ +│ .py:117 in render_widget │ +│ │ +│ 114 │ │ base_background, background = widget.background_colors │ +│ 115 │ │ styles = widget.styles │ +│ 116 │ │ app = widget.app │ +│ ❱ 117 │ │ strips = self.render( │ +│ 118 │ │ │ styles, │ +│ 119 │ │ │ widget.region.size, │ +│ 120 │ │ │ base_background, │ +│ │ +│ ╭───────────────────────────────── locals ─────────────────────────────────╮ │ +│ │ app = IslandsApp( │ │ +│ │ │ title='IslandsApp', │ │ +│ │ │ classes={'-dark-mode'}, │ │ +│ │ │ pseudo_classes={'focus', 'nocolor', 'dark'} │ │ +│ │ ) │ │ +│ │ background = Color(18, 18, 18) │ │ +│ │ base_background = Color(18, 18, 18) │ │ +│ │ border_subtitle = None │ │ +│ │ border_title = None │ │ +│ │ crop = Region(x=0, y=0, width=10, height=3) │ │ +│ │ self = │ │ +│ │ styles = RenderStyles( │ │ +│ │ │ Counted(id='right'), │ │ +│ │ │ background=Color(0, 0, 0, a=0), │ │ +│ │ │ width=Scalar( │ │ +│ │ │ │ value=10.0, │ │ +│ │ │ │ unit=, │ │ +│ │ │ │ percent_unit= │ │ +│ │ │ ), │ │ +│ │ │ height=Scalar( │ │ +│ │ │ │ value=3.0, │ │ +│ │ │ │ unit=, │ │ +│ │ │ │ percent_unit= │ │ +│ │ │ ), │ │ +│ │ │ scrollbar_color=Color(0, 48, 84), │ │ +│ │ │ scrollbar_color_hover=Color(0, 60, 106), │ │ +│ │ │ scrollbar_color_active=Color(1, 120, 212), │ │ +│ │ │ scrollbar_corner_color=Color(0, 0, 0), │ │ +│ │ │ scrollbar_background=Color(0, 0, 0), │ │ +│ │ │ scrollbar_background_hover=Color(0, 0, 0), │ │ +│ │ │ scrollbar_background_active=Color(0, 0, 0), │ │ +│ │ │ scrollbar_size_vertical=2, │ │ +│ │ │ scrollbar_size_horizontal=1, │ │ +│ │ │ link_color=Color(255, 255, 255, a=0.87), │ │ +│ │ │ auto_link_color=True, │ │ +│ │ │ link_background=Color(0, 0, 0, a=0), │ │ +│ │ │ link_style=Style(underline=True), │ │ +│ │ │ link_color_hover=Color(255, 255, 255, a=0.87), │ │ +│ │ │ auto_link_color_hover=True, │ │ +│ │ │ link_background_hover=Color(1, 120, 212), │ │ +│ │ │ link_style_hover=Style( │ │ +│ │ │ │ bold=True, │ │ +│ │ │ │ underline=False │ │ +│ │ │ ) │ │ +│ │ ) │ │ +│ │ widget = Counted(id='right') │ │ +│ ╰──────────────────────────────────────────────────────────────────────────╯ │ +│ │ +│ /home/ts/wt/textual-native-subtree-strips-20261002/src/textual/_styles_cache │ +│ .py:222 in render │ +│ │ +│ 219 │ │ │ +│ 220 │ │ for y in crop.line_range: │ +│ 221 │ │ │ if is_dirty(y) or y not in self._cache: │ +│ ❱ 222 │ │ │ │ strip = render_line( │ +│ 223 │ │ │ │ │ styles, │ +│ 224 │ │ │ │ │ y, │ +│ 225 │ │ │ │ │ size, │ +│ │ +│ ╭───────────────────────────────── locals ─────────────────────────────────╮ │ +│ │ _height = 3 │ │ +│ │ add_strip = │ │ +│ │ ansi_theme = │ │ +│ │ background = Color(18, 18, 18) │ │ +│ │ base_background = Color(18, 18, 18) │ │ +│ │ border_subtitle = None │ │ +│ │ border_title = None │ │ +│ │ content_size = Size(width=10, height=3) │ │ +│ │ crop = Region(x=0, y=0, width=10, height=3) │ │ +│ │ filters = [ │ │ +│ │ │ , │ │ +│ │ │ │ │ +│ │ ] │ │ +│ │ is_dirty = │ │ +│ │ native_ansi = False │ │ +│ │ opacity = 1.0 │ │ +│ │ padding = Spacing(top=0, right=0, bottom=0, left=0) │ │ +│ │ render_content_line = │ │ +│ │ render_line = > │ │ +│ │ self = │ │ +│ │ size = Size(width=10, height=3) │ │ +│ │ strips = [] │ │ +│ │ styles = RenderStyles( │ │ +│ │ │ Counted(id='right'), │ │ +│ │ │ background=Color(0, 0, 0, a=0), │ │ +│ │ │ width=Scalar( │ │ +│ │ │ │ value=10.0, │ │ +│ │ │ │ unit=, │ │ +│ │ │ │ percent_unit= │ │ +│ │ │ ), │ │ +│ │ │ height=Scalar( │ │ +│ │ │ │ value=3.0, │ │ +│ │ │ │ unit=, │ │ +│ │ │ │ percent_unit= │ │ +│ │ │ ), │ │ +│ │ │ scrollbar_color=Color(0, 48, 84), │ │ +│ │ │ scrollbar_color_hover=Color(0, 60, 106), │ │ +│ │ │ scrollbar_color_active=Color(1, 120, 212), │ │ +│ │ │ scrollbar_corner_color=Color(0, 0, 0), │ │ +│ │ │ scrollbar_background=Color(0, 0, 0), │ │ +│ │ │ scrollbar_background_hover=Color(0, 0, 0), │ │ +│ │ │ scrollbar_background_active=Color(0, 0, 0), │ │ +│ │ │ scrollbar_size_vertical=2, │ │ +│ │ │ scrollbar_size_horizontal=1, │ │ +│ │ │ link_color=Color(255, 255, 255, a=0.87), │ │ +│ │ │ auto_link_color=True, │ │ +│ │ │ link_background=Color(0, 0, 0, a=0), │ │ +│ │ │ link_style=Style(underline=True), │ │ +│ │ │ link_color_hover=Color( │ │ +│ │ │ │ 255, │ │ +│ │ │ │ 255, │ │ +│ │ │ │ 255, │ │ +│ │ │ │ a=0.87 │ │ +│ │ │ ), │ │ +│ │ │ auto_link_color_hover=True, │ │ +│ │ │ link_background_hover=Color(1, 120, 212), │ │ +│ │ │ link_style_hover=Style( │ │ +│ │ │ │ bold=True, │ │ +│ │ │ │ underline=False │ │ +│ │ │ ) │ │ +│ │ ) │ │ +│ │ width = 10 │ │ +│ │ y = 0 │ │ +│ ╰──────────────────────────────────────────────────────────────────────────╯ │ +│ │ +│ /home/ts/wt/textual-native-subtree-strips-20261002/src/textual/_styles_cache │ +│ .py:457 in render_line │ +│ │ +│ 454 │ │ │ # Content with border and padding (C) │ +│ 455 │ │ │ content_y = y - gutter.top │ +│ 456 │ │ │ if content_y < content_height: │ +│ ❱ 457 │ │ │ │ line = render_content_line(y - gutter.top) │ +│ 458 │ │ │ │ line = line.adjust_cell_length(content_width, inner.ri │ +│ 459 │ │ │ else: │ +│ 460 │ │ │ │ line = Strip.blank(content_width, inner.rich_style) │ +│ │ +│ ╭───────────────────────────────── locals ─────────────────────────────────╮ │ +│ │ ansi_theme = │ │ +│ │ background = Color(18, 18, 18) │ │ +│ │ base_background = Color(18, 18, 18) │ │ +│ │ border_bottom = '' │ │ +│ │ border_bottom_color = Color(0, 255, 0) │ │ +│ │ border_left = '' │ │ +│ │ border_left_color = Color(0, 255, 0) │ │ +│ │ border_right = '' │ │ +│ │ border_right_color = Color(0, 255, 0) │ │ +│ │ border_subtitle = None │ │ +│ │ border_title = None │ │ +│ │ border_top = '' │ │ +│ │ border_top_color = Color(0, 255, 0) │ │ +│ │ cache_simple_strip = False │ │ +│ │ content_height = 3 │ │ +│ │ content_size = Size(width=10, height=3) │ │ +│ │ content_width = 10 │ │ +│ │ content_y = 0 │ │ +│ │ from_color = > │ │ +│ │ gutter = Spacing(top=0, right=0, bottom=0, left=0) │ │ +│ │ height = 3 │ │ +│ │ inner = Style(background=Color(18, 18, 18)) │ │ +│ │ native_ansi = False │ │ +│ │ opacity = 1.0 │ │ +│ │ outer = Style(background=Color(18, 18, 18)) │ │ +│ │ outline_bottom = '' │ │ +│ │ outline_bottom_color = Color(0, 255, 0) │ │ +│ │ outline_left = '' │ │ +│ │ outline_left_color = Color(0, 255, 0) │ │ +│ │ outline_right = '' │ │ +│ │ outline_right_color = Color(0, 255, 0) │ │ +│ │ outline_top = '' │ │ +│ │ outline_top_color = Color(0, 255, 0) │ │ +│ │ pad_bottom = 0 │ │ +│ │ pad_left = 0 │ │ +│ │ pad_right = 0 │ │ +│ │ pad_top = 0 │ │ +│ │ padding = Spacing(top=0, right=0, bottom=0, left=0) │ │ +│ │ render_content_line = │ │ +│ │ self = │ │ +│ │ size = Size(width=10, height=3) │ │ +│ │ styles = RenderStyles( │ │ +│ │ │ Counted(id='right'), │ │ +│ │ │ background=Color(0, 0, 0, a=0), │ │ +│ │ │ width=Scalar( │ │ +│ │ │ │ value=10.0, │ │ +│ │ │ │ unit=, │ │ +│ │ │ │ percent_unit= │ │ +│ │ │ ), │ │ +│ │ │ height=Scalar( │ │ +│ │ │ │ value=3.0, │ │ +│ │ │ │ unit=, │ │ +│ │ │ │ percent_unit= │ │ +│ │ │ ), │ │ +│ │ │ scrollbar_color=Color(0, 48, 84), │ │ +│ │ │ scrollbar_color_hover=Color(0, 60, 106), │ │ +│ │ │ scrollbar_color_active=Color(1, 120, 212), │ │ +│ │ │ scrollbar_corner_color=Color(0, 0, 0), │ │ +│ │ │ scrollbar_background=Color(0, 0, 0), │ │ +│ │ │ scrollbar_background_hover=Color(0, 0, 0), │ │ +│ │ │ scrollbar_background_active=Color(0, 0, 0), │ │ +│ │ │ scrollbar_size_vertical=2, │ │ +│ │ │ scrollbar_size_horizontal=1, │ │ +│ │ │ link_color=Color(255, 255, 255, a=0.87), │ │ +│ │ │ auto_link_color=True, │ │ +│ │ │ link_background=Color(0, 0, 0, a=0), │ │ +│ │ │ link_style=Style(underline=True), │ │ +│ │ │ link_color_hover=Color( │ │ +│ │ │ │ 255, │ │ +│ │ │ │ 255, │ │ +│ │ │ │ 255, │ │ +│ │ │ │ a=0.87 │ │ +│ │ │ ), │ │ +│ │ │ auto_link_color_hover=True, │ │ +│ │ │ link_background_hover=Color(1, 120, 212), │ │ +│ │ │ link_style_hover=Style( │ │ +│ │ │ │ bold=True, │ │ +│ │ │ │ underline=False │ │ +│ │ │ ) │ │ +│ │ ) │ │ +│ │ width = 10 │ │ +│ │ y = 0 │ │ +│ ╰──────────────────────────────────────────────────────────────────────────╯ │ +│ │ +│ /home/ts/wt/textual-native-subtree-strips-20261002/src/textual/widget.py:448 │ +│ 9 in render_line │ +│ │ +│ 4486 │ │ │ return Strip.blank(self.size.width, self.visual_style.ric │ +│ 4487 │ │ │ +│ 4488 │ │ if self._dirty_regions or self._render_cache.size != self.siz │ +│ ❱ 4489 │ │ │ self._render_content() │ +│ 4490 │ │ try: │ +│ 4491 │ │ │ line = self._render_cache.lines[y] │ +│ 4492 │ │ except IndexError: │ +│ │ +│ ╭────────── locals ──────────╮ │ +│ │ self = Counted(id='right') │ │ +│ │ y = 0 │ │ +│ ╰────────────────────────────╯ │ +│ │ +│ /home/ts/wt/textual-native-subtree-strips-20261002/src/textual/widget.py:447 │ +│ 1 in _render_content │ +│ │ +│ 4468 │ │ """Render all lines.""" │ +│ 4469 │ │ size = self.size │ +│ 4470 │ │ width, height = size │ +│ ❱ 4471 │ │ visual = self._render() │ +│ 4472 │ │ strips = Visual.to_strips(self, visual, width, height, self.v │ +│ 4473 │ │ self._render_cache = _RenderCache(size, strips) │ +│ 4474 │ │ self._dirty_regions.clear() │ +│ │ +│ ╭───────────── locals ──────────────╮ │ +│ │ height = 3 │ │ +│ │ self = Counted(id='right') │ │ +│ │ size = Size(width=10, height=3) │ │ +│ │ width = 10 │ │ +│ ╰───────────────────────────────────╯ │ +│ │ +│ /home/ts/wt/textual-native-subtree-strips-20261002/src/textual/widget.py:469 │ +│ 8 in _render │ +│ │ +│ 4695 │ │ if cached_visual is not None: │ +│ 4696 │ │ │ assert isinstance(cached_visual, Visual) │ +│ 4697 │ │ │ return cached_visual │ +│ ❱ 4698 │ │ visual = visualize(self, self.render(), markup=self._render_m │ +│ 4699 │ │ self._layout_cache[cache_key] = visual │ +│ 4700 │ │ return visual │ +│ 4701 │ +│ │ +│ ╭────────────── locals ───────────────╮ │ +│ │ cache_key = '_render.visual' │ │ +│ │ cached_visual = None │ │ +│ │ self = Counted(id='right') │ │ +│ ╰─────────────────────────────────────╯ │ +│ │ +│ /home/ts/wt/textual-native-subtree-strips-20261002/src/textual/widgets/_stat │ +│ ic.py:85 in render │ +│ │ +│ 82 │ │ Returns: │ +│ 83 │ │ │ A rich renderable. │ +│ 84 │ │ """ │ +│ ❱ 85 │ │ return self.visual │ +│ 86 │ │ +│ 87 │ def _render_styles_sensitive(self) -> bool: │ +│ 88 │ │ """Only original Content has the native rule-measurement contr │ +│ │ +│ ╭────────── locals ──────────╮ │ +│ │ self = Counted(id='right') │ │ +│ ╰────────────────────────────╯ │ +│ │ +│ /home/ts/wt/textual-native-subtree-strips-20261002/src/textual/widgets/_stat │ +│ ic.py:64 in visual │ +│ │ +│ 61 │ │ │ +│ 62 │ │ """ │ +│ 63 │ │ if self.__visual is None: │ +│ ❱ 64 │ │ │ self.__visual = visualize(self, self.__content, markup=sel │ +│ 65 │ │ return self.__visual │ +│ 66 │ │ +│ 67 │ @property │ +│ │ +│ ╭────────── locals ──────────╮ │ +│ │ self = Counted(id='right') │ │ +│ ╰────────────────────────────╯ │ +╰──────────────────────────────────────────────────────────────────────────────╯ +MarkupError: Expected markup value (found '://example.com]A界BCDEFGH[/link]\n'). +=========================== short test summary info ============================ +FAILED tests/test_compositor_frame_damage.py::test_horizontal_damage_keeps_intervening_native_content_unpainted[regions0] +FAILED tests/test_compositor_frame_damage.py::test_horizontal_damage_keeps_intervening_native_content_unpainted[regions1] +2 failed, 14 passed in 4.19s diff --git a/evidence/native-damage-span-admission-20261005/source-batch02.json b/evidence/native-damage-span-admission-20261005/source-batch02.json new file mode 100644 index 0000000000..9c09d7ee92 --- /dev/null +++ b/evidence/native-damage-span-admission-20261005/source-batch02.json @@ -0,0 +1,18 @@ +{ + "source": "51f1971c2ea29b443ef15764036fa54b3de51c25", + "production_source": "68a9755b411819090283f180a362bf7659757b01", + "argv": [ + "/usr/bin/python", + "-m", + "pytest", + "-q", + "tests/test_compositor_frame_damage.py::test_horizontal_damage_keeps_intervening_native_content_unpainted" + ], + "seconds": 1.6918718189699575, + "exit_code": 0, + "log": "/home/ts/wt/textual-native-subtree-strips-20261002/.artifacts/native-damage-span-admission-20261005/source-batch02.log", + "log_sha256": "96930e0475760ea3818d98efd9c208416bb41c820336d7ab9d9949a8ae04a4ad", + "scope": "Only two corrected authored damage-island cases; first batch14PASS/2invalid-markup FAIL remains. Production unchanged; native quoted-link syntax corrected at original fixture, no assertions relaxed.", + "installed_qualified": false, + "retained_log": "evidence/native-damage-span-admission-20261005/source-batch02.log" +} diff --git a/evidence/native-damage-span-admission-20261005/source-batch02.log b/evidence/native-damage-span-admission-20261005/source-batch02.log new file mode 100644 index 0000000000..773e17c6b9 --- /dev/null +++ b/evidence/native-damage-span-admission-20261005/source-batch02.log @@ -0,0 +1,2 @@ +.. [100%] +2 passed in 0.40s