mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
optimize focus (#2460)
* optimize focus * immediate call * update previews * snapshot
This commit is contained in:
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
@@ -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):
|
||||||
|
|||||||
Reference in New Issue
Block a user