From de72bf37177725852382e6654c60bf3dbe527fd7 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 6 Oct 2026 21:06:13 -0400 Subject: [PATCH 1/3] Acquire eligible focus ordering geometry as one compositor demand --- .../before.json | 313 ++++++++++++++++++ src/textual/_compositor.py | 37 ++- src/textual/screen.py | 53 +-- 3 files changed, 375 insertions(+), 28 deletions(-) create mode 100644 evidence/focus-geometry-demand-20261006/before.json diff --git a/evidence/focus-geometry-demand-20261006/before.json b/evidence/focus-geometry-demand-20261006/before.json new file mode 100644 index 0000000000..a1e10ac87d --- /dev/null +++ b/evidence/focus-geometry-demand-20261006/before.json @@ -0,0 +1,313 @@ +{ + "native": { + "modules": 249, + "omissions": [], + "sites": [ + [ + "src/textual/_compositor.py", + 1418, + "find_widget", + "FunctionDef" + ], + [ + "src/textual/_compositor.py", + 186, + "virtual_region", + "Attribute" + ], + [ + "src/textual/_compositor.py", + 186, + "virtual_region", + "Attribute" + ], + [ + "src/textual/_compositor.py", + 188, + "virtual_region", + "Attribute" + ], + [ + "src/textual/_compositor.py", + 1203, + "virtual_region", + "Attribute" + ], + [ + "src/textual/_compositor.py", + 1246, + "find_widget", + "Attribute" + ], + [ + "src/textual/_compositor.py", + 98, + "virtual_region", + "Attribute" + ], + [ + "src/textual/_compositor.py", + 1260, + "find_widget", + "Attribute" + ], + [ + "src/textual/await_remove.py", + 61, + "_reset_focus", + "Attribute" + ], + [ + "src/textual/css/query.py", + 461, + "allow_focus", + "Attribute" + ], + [ + "src/textual/css/query.py", + 476, + "_reset_focus", + "Attribute" + ], + [ + "src/textual/screen.py", + 756, + "find_widget", + "FunctionDef" + ], + [ + "src/textual/screen.py", + 788, + "focus_chain", + "FunctionDef" + ], + [ + "src/textual/screen.py", + 1037, + "_reset_focus", + "FunctionDef" + ], + [ + "src/textual/screen.py", + 867, + "focus_chain", + "Attribute" + ], + [ + "src/textual/screen.py", + 1055, + "focus_chain", + "Attribute" + ], + [ + "src/textual/screen.py", + 768, + "find_widget", + "Attribute" + ], + [ + "src/textual/screen.py", + 824, + "_check_disabled", + "Attribute" + ], + [ + "src/textual/screen.py", + 832, + "allow_focus_children", + "Attribute" + ], + [ + "src/textual/screen.py", + 839, + "allow_focus", + "Attribute" + ], + [ + "src/textual/screen.py", + 1829, + "find_widget", + "Attribute" + ], + [ + "src/textual/screen.py", + 2136, + "find_widget", + "Attribute" + ], + [ + "src/textual/selection.py", + 413, + "find_widget", + "Attribute" + ], + [ + "src/textual/widget.py", + 903, + "_check_disabled", + "FunctionDef" + ], + [ + "src/textual/widget.py", + 943, + "allow_focus", + "FunctionDef" + ], + [ + "src/textual/widget.py", + 954, + "allow_focus_children", + "FunctionDef" + ], + [ + "src/textual/widget.py", + 2532, + "virtual_region", + "FunctionDef" + ], + [ + "src/textual/widget.py", + 2589, + "_focus_sort_key", + "FunctionDef" + ], + [ + "src/textual/widget.py", + 2591, + "virtual_region", + "Attribute" + ], + [ + "src/textual/widget.py", + 645, + "_check_disabled", + "Attribute" + ], + [ + "src/textual/widget.py", + 655, + "_check_disabled", + "Attribute" + ], + [ + "src/textual/widget.py", + 2541, + "virtual_region", + "Attribute" + ], + [ + "src/textual/widget.py", + 459, + "allow_focus", + "Attribute" + ], + [ + "src/textual/widget.py", + 2565, + "virtual_region", + "Attribute" + ], + [ + "src/textual/widget.py", + 2583, + "allow_focus", + "Attribute" + ], + [ + "src/textual/widget.py", + 2664, + "find_widget", + "Attribute" + ], + [ + "src/textual/widget.py", + 4880, + "_reset_focus", + "Attribute" + ], + [ + "src/textual/widget.py", + 2490, + "find_widget", + "Attribute" + ], + [ + "src/textual/widget.py", + 2503, + "find_widget", + "Attribute" + ], + [ + "src/textual/widget.py", + 2515, + "find_widget", + "Attribute" + ], + [ + "src/textual/widget.py", + 2541, + "find_widget", + "Attribute" + ], + [ + "src/textual/widgets/_tabs.py", + 660, + "virtual_region", + "Attribute" + ] + ] + }, + "toad": { + "modules": 288, + "omissions": [], + "sites": [ + [ + "src/toad/screens/agent_modal.py", + 22, + "allow_focus", + "FunctionDef" + ], + [ + "src/toad/screens/store.py", + 323, + "allow_focus", + "FunctionDef" + ], + [ + "src/toad/screens/store.py", + 325, + "allow_focus", + "Attribute" + ], + [ + "src/toad/widgets/history_anchor.py", + 365, + "virtual_region", + "Attribute" + ], + [ + "src/toad/widgets/history_anchor.py", + 365, + "virtual_region", + "Attribute" + ], + [ + "src/toad/widgets/session_tabs.py", + 214, + "virtual_region", + "Attribute" + ], + [ + "src/toad/widgets/terminal.py", + 181, + "allow_focus", + "FunctionDef" + ] + ] + }, + "core": { + "modules": 324, + "omissions": [], + "sites": [] + } +} diff --git a/src/textual/_compositor.py b/src/textual/_compositor.py index c06fa461ae..2927259874 100644 --- a/src/textual/_compositor.py +++ b/src/textual/_compositor.py @@ -1429,14 +1429,7 @@ def find_widget(self, widget: Widget) -> MapGeometry: """ geometry = self._get_geometry(widget) if geometry is None: - if self.root is not None and self._full_map_invalidated and not self._arranging: - # A position query requires this original ancestry path, not - # every offscreen descendant. Preserve the scene's existing - # reader placements in the same original reflow publication. - self.reflow_visible( - self.root, self.size, - retain_geometry=(*self._published_map, widget), - ) + if self.acquire_geometry((widget,)): geometry = self._get_geometry(widget) else: # Measurement inside arrangement must not recursively arrange; @@ -1446,6 +1439,34 @@ def find_widget(self, widget: Widget) -> MapGeometry: raise errors.NoWidget("Widget is not in layout") return geometry + def acquire_geometry(self, widgets: Iterable[Widget]) -> bool: + """Acquire missing reader paths in one original scene publication. + + A focus query requires offscreen ordering paths as well as visible + placements. Keep the committed scene's readers and add the complete + demand together, instead of reflowing for each missing position. + Captured descendants retain their strict original arrangement boundary. + Returns whether a viewport arrangement was performed. + """ + if self.root is None or not self._full_map_invalidated or self._arranging: + return False + missing = [] + for widget in widgets: + try: + geometry = self._get_geometry(widget) + except errors.NoWidget: + # The ordinary reader still refuses an omitted descendant of + # the active capture; this acquisition cannot widen its scope. + continue + if geometry is None: + missing.append(widget) + if not missing: + return False + self.reflow_visible( + self.root, self.size, retain_geometry=(*self._published_map, *missing), + ) + return True + def _get_geometry(self, widget: Widget) -> MapGeometry | None: """Select geometry from its original current publication. diff --git a/src/textual/screen.py b/src/textual/screen.py index fdfbe86b4f..9bbf5c6a14 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -787,17 +787,6 @@ def _select_all_in_widget(self, widget: Widget) -> None: @property def focus_chain(self) -> list[Widget]: """A list of widgets that may receive focus, in focus order.""" - # TODO: Calculating a focus chain is moderately expensive. - # Suspect we can move focus without calculating the entire thing again. - - widgets: list[Widget] = [] - add_widget = widgets.append - focus_sorter = attrgetter("_focus_sort_key") - # We traverse the DOM and keep track of where we are at with a node stack. - # Additionally, we manually keep track of the visibility of the DOM - # instead of relying on the property `.visible` to save on DOM traversals. - # node_stack: list[tuple[iterator over node children, node visibility]] - root_node = self.screen if (focused := self.focused) is not None: @@ -806,11 +795,13 @@ def focus_chain(self) -> list[Widget]: root_node = node break + # Acquire membership and inherited visibility once. Only branches with + # an eligible focus target need an ordering position; unfocusable leaf + # geometry cannot change the resulting chain. + branches = {root_node: tuple(root_node.displayed_children)} + focusable: set[Widget] = set() node_stack: list[tuple[Iterator[Widget], bool]] = [ - ( - iter(sorted(root_node.displayed_children, key=focus_sorter)), - self.visible, - ) + (iter(branches[root_node]), self.visible) ] pop = node_stack.pop push = node_stack.append @@ -830,14 +821,36 @@ def focus_chain(self) -> list[Widget]: else parent_visibility # Inherit visibility if the style is unset. ) if node.is_container and node.allow_focus_children(): - sorted_displayed_children = sorted( - node.displayed_children, key=focus_sorter - ) - push((iter(sorted_displayed_children), node_is_visible)) + branches[node] = tuple(node.displayed_children) + push((iter(branches[node]), node_is_visible)) # Same check as `if node.focusable`, but we cached inherited visibility # and we also skipped disabled nodes altogether. if node_is_visible and node.allow_focus(): - add_widget(node) + focusable.add(node) + + # Child branches were acquired after their parents. Prune in reverse + # so ancestors of offscreen or visibility-overriding targets survive. + for parent in reversed(branches): + branches[parent] = tuple( + child for child in branches[parent] + if child in focusable or branches.get(child) + ) + + self._compositor.acquire_geometry( + child for children in branches.values() for child in children + ) + focus_sorter = attrgetter("_focus_sort_key") + widgets: list[Widget] = [] + ordered = [iter(sorted(branches[root_node], key=focus_sorter))] + while ordered: + node = next(ordered[-1], None) + if node is None: + ordered.pop() + continue + if node in focusable: + widgets.append(node) + if children := branches.get(node): + ordered.append(iter(sorted(children, key=focus_sorter))) return widgets From 3b138ec41ed7ef8a3fea7f0ad781118353d8f4b4 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 6 Oct 2026 21:09:40 -0400 Subject: [PATCH 2/3] Confirm offscreen focus order and the loaded compose disable boundary --- tests/focused_editor_disable_pilot.py | 76 +++++++++++++++++++++++ tests/test_geometry_reader_acquisition.py | 58 ++++++++++++++++- 2 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 tests/focused_editor_disable_pilot.py diff --git a/tests/focused_editor_disable_pilot.py b/tests/focused_editor_disable_pilot.py new file mode 100644 index 0000000000..d9638d588c --- /dev/null +++ b/tests/focused_editor_disable_pilot.py @@ -0,0 +1,76 @@ +"""Measure the real focused compose-disable boundary without submitting input. + +Uses the existing loaded sidebar App fixture and original PromptTextArea. This +is source App confirmation, not installed channel delivery or terminal latency. +""" +import asyncio +import cProfile +import json +import os +from pathlib import Path +import pstats +import tempfile +from time import perf_counter + +from agent_comms.comms import wire +from agent_comms.threads import Thread +from sidebar_collapse_latency_pilot import FrameApp +from toad.widgets.agent_response import AgentResponse +from toad.widgets.prompt import Prompt + + +async def main(output: Path): + output.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory(prefix="focus-editor-disable-", dir=output) as folder: + root = Path(folder) + os.environ.update(XDG_CONFIG_HOME=str(root / "config"), + XDG_STATE_HOME=str(root / "state"), + XDG_DATA_HOME=str(root / "data"), AGENT_COMMS_ROOT=str(root / "wire")) + comms = wire(root / "wire") + comms.registry.declare(Thread(root.name, frozenset(), str(root))) + for index in range(24): + comms.registry.declare(Thread(f"worker-{index:02}", frozenset(), str(root))) + app = FrameApp(project_dir=str(root)) + async with app.run_test(size=(120, 42)) as pilot: + await pilot.pause() + for _ in range(9): + await app.session_navigation.new(app.session_navigation.default_source) + conversation = app.selected_session.conversation + await conversation.contents.mount(*[ + AgentResponse(f"## Loaded response {index}\n\n" + "paragraph and line\n\n" * 10) + for index in range(12) + ]) + await pilot.pause() + editor = app.selected_session.query_one(Prompt).prompt_text_area + editor.text = "original unsent draft" + editor.focus(scroll_visible=False) + await pilot.pause() + assert app.screen.focused is editor + profile = cProfile.Profile() + started = perf_counter() + profile.enable() + editor.disabled = True + profile.disable() + elapsed = perf_counter() - started + profile.dump_stats(str(output / "disable.pstats")) + assert editor.disabled and app.screen.focused is not editor + assert editor.text == "original unsent draft" + calls = { + name: sum(values[1] for (_, _, function), values in pstats.Stats(profile).stats.items() + if function == name) + for name in ("_reset_focus", "_focus_sort_key", "acquire_geometry", + "reflow_visible", "_arrange_root") + } + await pilot.pause() + assert app._exception is None + result = dict(boundary="source synchronous focused PromptTextArea.disabled watcher", + disabled_seconds=elapsed, calls=calls, tabs=10, + active_widgets=len(app.screen.query("*")), + inputs_submitted=0, provider_calls=0, draft_preserved=True) + print(json.dumps(result, indent=2), flush=True) + (output / "result.json").write_text(json.dumps(result, indent=2) + "\n") + + +if __name__ == "__main__": + import sys + asyncio.run(main(Path(sys.argv[1]))) diff --git a/tests/test_geometry_reader_acquisition.py b/tests/test_geometry_reader_acquisition.py index bca7ba3e0e..b0410496cd 100644 --- a/tests/test_geometry_reader_acquisition.py +++ b/tests/test_geometry_reader_acquisition.py @@ -5,7 +5,7 @@ from textual.app import App, ComposeResult from textual.containers import VerticalScroll from textual.screen import Screen -from textual.widgets import Static, TextArea +from textual.widgets import Button, Static, TextArea class ObservedCompositor(Compositor): @@ -46,6 +46,62 @@ def get_default_screen(self): return ReaderScreen() +async def test_disabled_editor_batches_offscreen_focus_order(): + class FocusScreen(ReaderScreen): + CSS = "VerticalScroll { height: 1fr; } Button { height: 3; } TextArea { height: 3; }" + + def compose(self): + with VerticalScroll(id="history"): + for index in range(40): + yield Button(f"Focus {index}", id=f"button-{index}") + yield Static(f"Unfocusable source {index}", id=f"source-{index}") + yield TextArea("original unsent draft", id="editor") + + class FocusApp(ReaderApp): + def get_default_screen(self): + return FocusScreen() + + app = FocusApp() + + async def drive(pilot): + await pilot.pause() + screen = app.screen + compositor = screen._compositor + editor = screen.query_one("#editor", TextArea) + screen.set_focus(editor, scroll_visible=False) + screen._refresh_layout(app.size, scroll=True) + committed = dict(compositor._published_map) + full_map = compositor._full_map + assert screen.query_one("#button-39") not in committed + compositor.arrangements.clear() + + # This is the same original synchronous watcher/blur/reset path as a + # send disabling its focused compose editor. No input is submitted. + editor.disabled = True + assert len(compositor.arrangements) == 1 + _, visible, retained, _ = compositor.arrangements[0] + assert visible and compositor._full_map is full_map + assert all(screen.query_one(f"#button-{index}") in retained for index in range(40)) + assert screen.query_one("#source-39") not in retained + assert editor.text == "original unsent draft" and screen.focused is not editor + assert all(compositor._published_map[node] == geometry for node, geometry in committed.items()) + + actual = [node.id for node in screen.focus_chain] + assert [name for name in actual if name and name.startswith("button-")] == [ + f"button-{index}" for index in range(40) + ] + assert "editor" not in actual + assert len(compositor.arrangements) == 1 + # Complete geometry gives the same ordering, rather than treating the + # viewport as the full focus scope or manufacturing missing positions. + compositor.reflow(screen, app.size) + assert [node.id for node in screen.focus_chain] == actual + app.exit() + + await asyncio.wait_for(app.run_async(headless=True, size=(40, 8), auto_pilot=drive), 10) + assert app._exception is None and app._task is None + + async def test_real_undo_acquires_only_missing_editor_and_preserves_reader_paths(): app = ReaderApp() From 76bc0ef3c4f0d1d819a50ccfaa14d0836e767a62 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 6 Oct 2026 21:10:27 -0400 Subject: [PATCH 3/3] Publish focused editor source App and offscreen geometry qualification --- .../focus-geometry-demand-20261006/README.md | 55 +++++++ .../RESULT.json | 36 +++++ .../focus-geometry-demand-20261006/after.json | 150 ++++++++++++++++++ .../controls.log | 2 + 4 files changed, 243 insertions(+) create mode 100644 evidence/focus-geometry-demand-20261006/README.md create mode 100644 evidence/focus-geometry-demand-20261006/RESULT.json create mode 100644 evidence/focus-geometry-demand-20261006/after.json create mode 100644 evidence/focus-geometry-demand-20261006/controls.log diff --git a/evidence/focus-geometry-demand-20261006/README.md b/evidence/focus-geometry-demand-20261006/README.md new file mode 100644 index 0000000000..863de35a75 --- /dev/null +++ b/evidence/focus-geometry-demand-20261006/README.md @@ -0,0 +1,55 @@ +# Acquire focus ordering geometry together + +The installed submit trace reaches native focus reset when the focused compose +editor becomes disabled. Screen previously sorted every displayed sibling, +including branches without a focus target. Each sort key reads virtual geometry. +For an omitted offscreen node, Compositor published another visible arrangement +retaining the accumulated reader paths plus that one node. Completeness stays +invalidated, so the next missing node repeated the operation. + +Screen now acquires the focus subtree's membership, disabled/loading decisions, +inherited visibility, and allow-focus hooks once in this synchronous query. +Branches without any eligible target are pruned before requesting positions. +Ancestors of offscreen targets and visible descendants overriding a hidden +parent survive. Compositor receives the complete ordering demand together and +uses its original visible arrangement with existing committed reader paths. + +The resulting branches are ordered through the original `_focus_sort_key`, +including subclass overrides and stable sibling ties. Containers precede their +children as before. Focus traps select the same original root. This does not +equate viewport paint membership with full focus eligibility. + +`find_widget` delegates its original single-path acquisition to the same +Compositor method. No full-map invalidation rule, retained geometry, mutation +hold, virtual-region fallback or captured-descendant refusal was relaxed. +The local branch tuples live only for this synchronous query; there is no new +persistent geometry/focus cache, queue or reader authority. No Toad edit. + +## Confirmation + +22 focus/geometry checks passed in 1.66s, including existing focus trap, hook, +disabled, inherited visibility and traversal controls and strict original +capture scope. The new real `run_async` check disables a focused TextArea with +40 offscreen buttons: one visible arrangement acquires all ordering paths; +unfocusable source rows are omitted; previous committed placements survive; +the unsent draft survives; ordering matches a complete original arrangement. + +One loaded source Toad App used its actual PromptTextArea disable watcher with +565 widgets / 10 tabs. Under cProfile the synchronous setter took 8.106ms: +one reset, one ordering key, one geometry acquisition, zero arrangements. +Exit zero, empty stderr, original unsent draft preserved, zero submitted inputs +or provider calls. This is not a measurement of the installed channel-send +workflow, terminal repaint, or an overall latency improvement. + +Raw result/profile: `/home/ts/.cache/agent-scratch/focus-geometry-demand-20261006/`. +The supplied P7 trace remains unchanged. Its changed stacks do not establish +call counts or exact elapsed time in this method. + +The original Package parser covered native 249, Toad 288 and Core 324 production +modules with zero omissions. Before/after sites are retained here. No tracked +production override of the sort-key declaration was found. Arbitrary extensions +mutating the DOM or focus predicates during a sort/layout query are not proved +by this static source pass or these checks. + +Not installed. Parent owns the Toad sender and live frontend delivery; no +package, artifact, original session, recorder or public-owner operation occurred. diff --git a/evidence/focus-geometry-demand-20261006/RESULT.json b/evidence/focus-geometry-demand-20261006/RESULT.json new file mode 100644 index 0000000000..2b987c2d57 --- /dev/null +++ b/evidence/focus-geometry-demand-20261006/RESULT.json @@ -0,0 +1,36 @@ +{ + "source_head": "3b138ec41ed7ef8a3fea7f0ad781118353d8f4b4", + "controls": "22 passed in 1.66s", + "app": { + "boundary": "source synchronous focused PromptTextArea.disabled watcher", + "disabled_seconds": 0.008106090012006462, + "calls": { + "_reset_focus": 1, + "_focus_sort_key": 1, + "acquire_geometry": 1, + "reflow_visible": 0, + "_arrange_root": 0 + }, + "tabs": 10, + "active_widgets": 565, + "inputs_submitted": 0, + "provider_calls": 0, + "draft_preserved": true + }, + "app_exit": 0, + "stderr_bytes": 0, + "keepers": { + "src/textual/screen.py": "2579e6443bf554c39358dc086a6b5d16b2565f273f3c0a0d4d259ed5f71a8dde", + "src/textual/_compositor.py": "468e0303ea53acc17818bb809f8c6b07817f9f640b9b17109074a87d54033360", + "tests/test_geometry_reader_acquisition.py": "8548a4e4595012ae84195afa7eed112b47e711628a2b4c81844e5a99ef12a935", + "tests/focused_editor_disable_pilot.py": "05c99523c87dc7af056bbecfd514b7a9d65f3394debcc5eb339cf5a776ab5485", + "evidence/focus-geometry-demand-20261006/before.json": "0d3e26564bd7b6bf43343496549e90fed73398817bde175464f5a176ff0e58ab", + "evidence/focus-geometry-demand-20261006/after.json": "b2154e9902478d1861795382f6e3d494ea9dd249df031923b6ed2810460e78ca", + "evidence/focus-geometry-demand-20261006/controls.log": "d2a25651aafa6cce137d896aa47ff6d39c40666c916a57a74a3c28a713b57bc6", + "/home/ts/.cache/agent-scratch/focus-geometry-demand-20261006/app.stdout.log": "feab232e08191c811fedf2be2b8c36445154bd8ee9cbf18d46f8e3b358cc05da", + "/home/ts/.cache/agent-scratch/focus-geometry-demand-20261006/app.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "/home/ts/.cache/agent-scratch/focus-geometry-demand-20261006/app01/result.json": "feab232e08191c811fedf2be2b8c36445154bd8ee9cbf18d46f8e3b358cc05da", + "/home/ts/.cache/agent-scratch/focus-geometry-demand-20261006/app01/disable.pstats": "f6e21dd6c5b7826d34e3c13c761a26e7306580346b87a8be05f7a14cb16c3b62" + }, + "installed": false +} diff --git a/evidence/focus-geometry-demand-20261006/after.json b/evidence/focus-geometry-demand-20261006/after.json new file mode 100644 index 0000000000..b267db5128 --- /dev/null +++ b/evidence/focus-geometry-demand-20261006/after.json @@ -0,0 +1,150 @@ +{ + "modules": 249, + "omissions": [], + "sites": [ + [ + "src/textual/_compositor.py", + 1418, + "find_widget", + "FunctionDef" + ], + [ + "src/textual/_compositor.py", + 1442, + "acquire_geometry", + "FunctionDef" + ], + [ + "src/textual/_compositor.py", + 1246, + "find_widget", + "Attribute" + ], + [ + "src/textual/_compositor.py", + 1432, + "acquire_geometry", + "Attribute" + ], + [ + "src/textual/_compositor.py", + 1260, + "find_widget", + "Attribute" + ], + [ + "src/textual/await_remove.py", + 61, + "_reset_focus", + "Attribute" + ], + [ + "src/textual/css/query.py", + 476, + "_reset_focus", + "Attribute" + ], + [ + "src/textual/screen.py", + 756, + "find_widget", + "FunctionDef" + ], + [ + "src/textual/screen.py", + 788, + "focus_chain", + "FunctionDef" + ], + [ + "src/textual/screen.py", + 1050, + "_reset_focus", + "FunctionDef" + ], + [ + "src/textual/screen.py", + 880, + "focus_chain", + "Attribute" + ], + [ + "src/textual/screen.py", + 1068, + "focus_chain", + "Attribute" + ], + [ + "src/textual/screen.py", + 768, + "find_widget", + "Attribute" + ], + [ + "src/textual/screen.py", + 839, + "acquire_geometry", + "Attribute" + ], + [ + "src/textual/screen.py", + 1842, + "find_widget", + "Attribute" + ], + [ + "src/textual/screen.py", + 2149, + "find_widget", + "Attribute" + ], + [ + "src/textual/selection.py", + 413, + "find_widget", + "Attribute" + ], + [ + "src/textual/widget.py", + 2589, + "_focus_sort_key", + "FunctionDef" + ], + [ + "src/textual/widget.py", + 2664, + "find_widget", + "Attribute" + ], + [ + "src/textual/widget.py", + 4880, + "_reset_focus", + "Attribute" + ], + [ + "src/textual/widget.py", + 2490, + "find_widget", + "Attribute" + ], + [ + "src/textual/widget.py", + 2503, + "find_widget", + "Attribute" + ], + [ + "src/textual/widget.py", + 2515, + "find_widget", + "Attribute" + ], + [ + "src/textual/widget.py", + 2541, + "find_widget", + "Attribute" + ] + ] +} diff --git a/evidence/focus-geometry-demand-20261006/controls.log b/evidence/focus-geometry-demand-20261006/controls.log new file mode 100644 index 0000000000..a0272d13d9 --- /dev/null +++ b/evidence/focus-geometry-demand-20261006/controls.log @@ -0,0 +1,2 @@ +...................... [100%] +22 passed in 1.66s