From 41dbc66b234d8e8aac2b26a8510e11b550860b26 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 3 May 2023 11:48:56 +0100 Subject: [PATCH] optimize focus (#2460) * optimize focus * immediate call * update previews * snapshot --- src/textual/app.py | 1 + src/textual/cli/previews/borders.py | 4 +- src/textual/cli/previews/colors.py | 6 +- src/textual/cli/previews/easing.py | 6 +- src/textual/screen.py | 37 +++++ src/textual/widget.py | 10 -- .../__snapshots__/test_snapshots.ambr | 143 +++++++++--------- tests/snapshot_tests/test_snapshots.py | 2 +- 8 files changed, 118 insertions(+), 91 deletions(-) diff --git a/src/textual/app.py b/src/textual/app.py index d81bb817e..e2199a47f 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -1253,6 +1253,7 @@ class App(Generic[ReturnType], DOMNode): will be added, and this method is called to apply the corresponding :hover styles. """ + descendants = node.walk_children(with_self=True) self.stylesheet.update_nodes(descendants, animate=True) diff --git a/src/textual/cli/previews/borders.py b/src/textual/cli/previews/borders.py index 282a42826..923ad9cc5 100644 --- a/src/textual/cli/previews/borders.py +++ b/src/textual/cli/previews/borders.py @@ -1,5 +1,5 @@ from textual.app import App, ComposeResult -from textual.containers import VerticalScroll +from textual.containers import Vertical from textual.css.constants import VALID_BORDER from textual.widgets import Button, Label @@ -12,7 +12,7 @@ And when it has gone past, I will turn the inner eye to see its path. Where the fear has gone there will be nothing. Only I will remain.""" -class BorderButtons(VerticalScroll): +class BorderButtons(Vertical): DEFAULT_CSS = """ BorderButtons { dock: left; diff --git a/src/textual/cli/previews/colors.py b/src/textual/cli/previews/colors.py index d8028f7c5..4d578f6dc 100644 --- a/src/textual/cli/previews/colors.py +++ b/src/textual/cli/previews/colors.py @@ -1,5 +1,5 @@ from textual.app import App, ComposeResult -from textual.containers import Horizontal, VerticalScroll +from textual.containers import Horizontal, Vertical, VerticalScroll from textual.design import ColorSystem from textual.widget import Widget from textual.widgets import Button, Footer, Label, Static @@ -20,11 +20,11 @@ class ColorItem(Horizontal): pass -class ColorGroup(VerticalScroll): +class ColorGroup(Vertical): pass -class Content(VerticalScroll): +class Content(Vertical): pass diff --git a/src/textual/cli/previews/easing.py b/src/textual/cli/previews/easing.py index b62faa7ca..49baab974 100644 --- a/src/textual/cli/previews/easing.py +++ b/src/textual/cli/previews/easing.py @@ -5,7 +5,7 @@ from rich.console import RenderableType from textual._easing import EASING from textual.app import App, ComposeResult from textual.cli.previews.borders import TEXT -from textual.containers import Container, Horizontal, VerticalScroll +from textual.containers import Horizontal, Vertical from textual.reactive import reactive, var from textual.scrollbar import ScrollBarRender from textual.widget import Widget @@ -72,13 +72,13 @@ class EasingApp(App): ) yield EasingButtons() - with VerticalScroll(): + with Vertical(): with Horizontal(id="inputs"): yield Label("Animation Duration:", id="label") yield duration_input with Horizontal(): yield self.animated_bar - yield VerticalScroll(self.opacity_widget, id="other") + yield Vertical(self.opacity_widget, id="other") yield Footer() def on_button_pressed(self, event: Button.Pressed) -> None: diff --git a/src/textual/screen.py b/src/textual/screen.py index dac819314..34db473ef 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -31,6 +31,7 @@ from .binding import Binding from .css.match import match from .css.parse import parse_selectors from .css.query import QueryType +from .dom import DOMNode from .geometry import Offset, Region, Size from .reactive import Reactive from .renderables.background_screen import BackgroundScreen @@ -404,6 +405,32 @@ class Screen(Generic[ScreenResultType], Widget): # Go with the what was found. self.set_focus(chosen) + def _update_focus_styles( + self, focused: Widget | None = None, blurred: Widget | None = None + ) -> None: + """Update CSS for focus changes. + + Args: + focused: The widget that was focused. + blurred: The widget that was blurred. + """ + widgets: set[DOMNode] = set() + + if focused is not None: + for widget in reversed(focused.ancestors_with_self): + if widget._has_focus_within: + widgets.update(widget.walk_children(with_self=True)) + break + if blurred is not None: + for widget in reversed(blurred.ancestors_with_self): + if widget._has_focus_within: + widgets.update(widget.walk_children(with_self=True)) + break + if widgets: + self.app.stylesheet.update_nodes( + [widget for widget in widgets if widget._has_focus_within], animate=True + ) + def set_focus(self, widget: Widget | None, scroll_visible: bool = True) -> None: """Focus (or un-focus) a widget. A focused widget will receive key events first. @@ -415,25 +442,35 @@ class Screen(Generic[ScreenResultType], Widget): # Widget is already focused return + focused: Widget | None = None + blurred: Widget | None = None + if widget is None: # No focus, so blur currently focused widget if it exists if self.focused is not None: self.focused.post_message(events.Blur()) self.focused = None + blurred = self.focused self.log.debug("focus was removed") elif widget.focusable: if self.focused != widget: if self.focused is not None: # Blur currently focused widget self.focused.post_message(events.Blur()) + blurred = self.focused # Change focus self.focused = widget # Send focus event if scroll_visible: self.screen.scroll_to_widget(widget) widget.post_message(events.Focus()) + focused = widget + + self._update_focus_styles(self.focused, widget) self.log.debug(widget, "was focused") + self._update_focus_styles(focused, blurred) + async def _on_idle(self, event: events.Idle) -> None: # Check for any widgets marked as 'dirty' (needs a repaint) event.prevent_default() diff --git a/src/textual/widget.py b/src/textual/widget.py index 841593298..b4e9e506e 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -3077,21 +3077,11 @@ class Widget(DOMNode): def _on_focus(self, event: events.Focus) -> None: self.has_focus = True self.refresh() - for widget in reversed(self.ancestors_with_self): - if widget._has_focus_within: - widget._update_styles() - break - self.post_message(events.DescendantFocus()) def _on_blur(self, event: events.Blur) -> None: self.has_focus = False self.refresh() - for widget in reversed(self.ancestors_with_self): - if widget._has_focus_within: - widget._update_styles() - break - self.post_message(events.DescendantBlur()) def _on_mouse_scroll_down(self, event: events.MouseScrollDown) -> None: diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index 7d57deece..b69b5dfe4 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -14265,148 +14265,147 @@ font-weight: 700; } - .terminal-391329861-matrix { + .terminal-2863933047-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-391329861-title { + .terminal-2863933047-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-391329861-r1 { fill: #454a50 } - .terminal-391329861-r2 { fill: #e1e1e1 } - .terminal-391329861-r3 { fill: #c5c8c6 } - .terminal-391329861-r4 { fill: #e2e3e3;font-weight: bold } - .terminal-391329861-r5 { fill: #262626 } - .terminal-391329861-r6 { fill: #000000 } - .terminal-391329861-r7 { fill: #e2e2e2 } - .terminal-391329861-r8 { fill: #e3e3e3 } - .terminal-391329861-r9 { fill: #14191f } - .terminal-391329861-r10 { fill: #b93c5b } - .terminal-391329861-r11 { fill: #121212 } - .terminal-391329861-r12 { fill: #1e1e1e } - .terminal-391329861-r13 { fill: #e2e3e3 } - .terminal-391329861-r14 { fill: #fea62b } - .terminal-391329861-r15 { fill: #211505;font-weight: bold } - .terminal-391329861-r16 { fill: #211505 } - .terminal-391329861-r17 { fill: #dde8f3;font-weight: bold } - .terminal-391329861-r18 { fill: #ddedf9 } + .terminal-2863933047-r1 { fill: #454a50 } + .terminal-2863933047-r2 { fill: #e1e1e1 } + .terminal-2863933047-r3 { fill: #c5c8c6 } + .terminal-2863933047-r4 { fill: #e2e3e3;font-weight: bold } + .terminal-2863933047-r5 { fill: #262626 } + .terminal-2863933047-r6 { fill: #000000 } + .terminal-2863933047-r7 { fill: #e2e2e2 } + .terminal-2863933047-r8 { fill: #e3e3e3 } + .terminal-2863933047-r9 { fill: #14191f } + .terminal-2863933047-r10 { fill: #b93c5b } + .terminal-2863933047-r11 { fill: #121212 } + .terminal-2863933047-r12 { fill: #1e1e1e } + .terminal-2863933047-r13 { fill: #fea62b } + .terminal-2863933047-r14 { fill: #211505;font-weight: bold } + .terminal-2863933047-r15 { fill: #211505 } + .terminal-2863933047-r16 { fill: #dde8f3;font-weight: bold } + .terminal-2863933047-r17 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - EasingApp + EasingApp - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - round▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁Animation Duration:1.0 - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - out_sine - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - out_quint - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁Welcome to Textual! - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - out_quartI must not fear. - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁Fear is the  - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔mind-killer. - out_quadFear is the  - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁little-death that  - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔brings total  - out_expoobliteration.▆▆ - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁I will face my fear. - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔I will permit it to  - out_elasticpass over me and  - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁through me. - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔And when it has gone  - out_cubic - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ CTRL+P  Focus: Duration Input  CTRL+B  Toggle Dark  + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + round▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁Animation Duration:1.0 + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + out_sine + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + out_quint + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁Welcome to Textual! + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + out_quartI must not fear. + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁Fear is the  + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔mind-killer. + out_quadFear is the  + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁little-death that  + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔brings total  + out_expoobliteration. + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁I will face my fear. + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔I will permit it to  + out_elasticpass over me and  + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁through me. + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔And when it has gone  + out_cubic + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ CTRL+P  Focus: Duration Input  CTRL+B  Toggle Dark  diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index b55405306..c9aee9636 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -294,7 +294,7 @@ def test_programmatic_scrollbar_gutter_change(snap_compare): def test_borders_preview(snap_compare): - assert snap_compare(CLI_PREVIEWS_DIR / "borders.py", press=["tab", "tab", "enter"]) + assert snap_compare(CLI_PREVIEWS_DIR / "borders.py", press=["tab", "enter"]) def test_colors_preview(snap_compare):