optimize focus (#2460)

* optimize focus

* immediate call

* update previews

* snapshot
This commit is contained in:
Will McGugan
2023-05-03 11:48:56 +01:00
committed by GitHub
parent 90d9693168
commit 41dbc66b23
8 changed files with 118 additions and 91 deletions

View File

@@ -1253,6 +1253,7 @@ class App(Generic[ReturnType], DOMNode):
will be added, and this method is called to apply the corresponding will be added, and this method is called to apply the corresponding
:hover styles. :hover styles.
""" """
descendants = node.walk_children(with_self=True) descendants = node.walk_children(with_self=True)
self.stylesheet.update_nodes(descendants, animate=True) self.stylesheet.update_nodes(descendants, animate=True)

View File

@@ -1,5 +1,5 @@
from textual.app import App, ComposeResult 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.css.constants import VALID_BORDER
from textual.widgets import Button, Label 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.""" Where the fear has gone there will be nothing. Only I will remain."""
class BorderButtons(VerticalScroll): class BorderButtons(Vertical):
DEFAULT_CSS = """ DEFAULT_CSS = """
BorderButtons { BorderButtons {
dock: left; dock: left;

View File

@@ -1,5 +1,5 @@
from textual.app import App, ComposeResult 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.design import ColorSystem
from textual.widget import Widget from textual.widget import Widget
from textual.widgets import Button, Footer, Label, Static from textual.widgets import Button, Footer, Label, Static
@@ -20,11 +20,11 @@ class ColorItem(Horizontal):
pass pass
class ColorGroup(VerticalScroll): class ColorGroup(Vertical):
pass pass
class Content(VerticalScroll): class Content(Vertical):
pass pass

View File

@@ -5,7 +5,7 @@ from rich.console import RenderableType
from textual._easing import EASING from textual._easing import EASING
from textual.app import App, ComposeResult from textual.app import App, ComposeResult
from textual.cli.previews.borders import TEXT 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.reactive import reactive, var
from textual.scrollbar import ScrollBarRender from textual.scrollbar import ScrollBarRender
from textual.widget import Widget from textual.widget import Widget
@@ -72,13 +72,13 @@ class EasingApp(App):
) )
yield EasingButtons() yield EasingButtons()
with VerticalScroll(): with Vertical():
with Horizontal(id="inputs"): with Horizontal(id="inputs"):
yield Label("Animation Duration:", id="label") yield Label("Animation Duration:", id="label")
yield duration_input yield duration_input
with Horizontal(): with Horizontal():
yield self.animated_bar yield self.animated_bar
yield VerticalScroll(self.opacity_widget, id="other") yield Vertical(self.opacity_widget, id="other")
yield Footer() yield Footer()
def on_button_pressed(self, event: Button.Pressed) -> None: def on_button_pressed(self, event: Button.Pressed) -> None:

View File

@@ -31,6 +31,7 @@ from .binding import Binding
from .css.match import match from .css.match import match
from .css.parse import parse_selectors from .css.parse import parse_selectors
from .css.query import QueryType from .css.query import QueryType
from .dom import DOMNode
from .geometry import Offset, Region, Size from .geometry import Offset, Region, Size
from .reactive import Reactive from .reactive import Reactive
from .renderables.background_screen import BackgroundScreen from .renderables.background_screen import BackgroundScreen
@@ -404,6 +405,32 @@ class Screen(Generic[ScreenResultType], Widget):
# Go with the what was found. # Go with the what was found.
self.set_focus(chosen) 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: 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. """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 # Widget is already focused
return return
focused: Widget | None = None
blurred: Widget | None = None
if widget is None: if widget is None:
# No focus, so blur currently focused widget if it exists # No focus, so blur currently focused widget if it exists
if self.focused is not None: if self.focused is not None:
self.focused.post_message(events.Blur()) self.focused.post_message(events.Blur())
self.focused = None self.focused = None
blurred = self.focused
self.log.debug("focus was removed") self.log.debug("focus was removed")
elif widget.focusable: elif widget.focusable:
if self.focused != widget: if self.focused != widget:
if self.focused is not None: if self.focused is not None:
# Blur currently focused widget # Blur currently focused widget
self.focused.post_message(events.Blur()) self.focused.post_message(events.Blur())
blurred = self.focused
# Change focus # Change focus
self.focused = widget self.focused = widget
# Send focus event # Send focus event
if scroll_visible: if scroll_visible:
self.screen.scroll_to_widget(widget) self.screen.scroll_to_widget(widget)
widget.post_message(events.Focus()) widget.post_message(events.Focus())
focused = widget
self._update_focus_styles(self.focused, widget)
self.log.debug(widget, "was focused") self.log.debug(widget, "was focused")
self._update_focus_styles(focused, blurred)
async def _on_idle(self, event: events.Idle) -> None: async def _on_idle(self, event: events.Idle) -> None:
# Check for any widgets marked as 'dirty' (needs a repaint) # Check for any widgets marked as 'dirty' (needs a repaint)
event.prevent_default() event.prevent_default()

View File

@@ -3077,21 +3077,11 @@ class Widget(DOMNode):
def _on_focus(self, event: events.Focus) -> None: def _on_focus(self, event: events.Focus) -> None:
self.has_focus = True self.has_focus = True
self.refresh() self.refresh()
for widget in reversed(self.ancestors_with_self):
if widget._has_focus_within:
widget._update_styles()
break
self.post_message(events.DescendantFocus()) self.post_message(events.DescendantFocus())
def _on_blur(self, event: events.Blur) -> None: def _on_blur(self, event: events.Blur) -> None:
self.has_focus = False self.has_focus = False
self.refresh() self.refresh()
for widget in reversed(self.ancestors_with_self):
if widget._has_focus_within:
widget._update_styles()
break
self.post_message(events.DescendantBlur()) self.post_message(events.DescendantBlur())
def _on_mouse_scroll_down(self, event: events.MouseScrollDown) -> None: def _on_mouse_scroll_down(self, event: events.MouseScrollDown) -> None:

File diff suppressed because one or more lines are too long

View File

@@ -294,7 +294,7 @@ def test_programmatic_scrollbar_gutter_change(snap_compare):
def test_borders_preview(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): def test_colors_preview(snap_compare):