diff --git a/src/textual/dom.py b/src/textual/dom.py index 44e1396e36..473c50a3b8 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -492,6 +492,10 @@ def trap_focus(self, trap_focus: bool = True) -> None: trap_focus: `True` to trap focus. `False` to restore default behavior. """ self._trap_focus = trap_focus + try: + self.screen._focus_chain_cache = None + except Exception: + pass def run_worker( self, @@ -1328,6 +1332,10 @@ def _add_children(self, *nodes: Widget) -> None: node._attach(self) _append(node) node._add_children(*node._pending_children) + try: + self.screen._focus_chain_cache = None + except Exception: + pass WalkType = TypeVar("WalkType", bound="DOMNode") diff --git a/src/textual/screen.py b/src/textual/screen.py index af9070538e..c6cf667e71 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -326,6 +326,9 @@ def __init__( self._css_update_count = -1 """Track updates to CSS.""" + self._focus_chain_cache: list[Widget] | None = None + """Cached focus chain, invalidated on layout or focus change.""" + self._layout_widgets: dict[DOMNode, set[Widget]] = {} """Widgets whose layout may have changed.""" @@ -771,8 +774,8 @@ 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. + if self._focus_chain_cache is not None: + return self._focus_chain_cache widgets: list[Widget] = [] add_widget = widgets.append @@ -823,6 +826,7 @@ def focus_chain(self) -> list[Widget]: if node_is_visible and node.allow_focus(): add_widget(node) + self._focus_chain_cache = widgets return widgets def _move_focus( @@ -1113,6 +1117,7 @@ def set_focus( # Widget is already focused return + self._focus_chain_cache = None focused: Widget | None = None blurred: Widget | None = None @@ -1423,6 +1428,7 @@ async def _on_layout(self, message: messages.Layout) -> None: break widget = ancestor + self._focus_chain_cache = None if layout_required and not self._layout_required: self._layout_required = True self.check_idle()