mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
focus level bindings
This commit is contained in:
@@ -1,14 +1,14 @@
|
|||||||
#tree-view {
|
#tree-view {
|
||||||
display: none;
|
display: none;
|
||||||
scrollbar-gutter: stable;
|
scrollbar-gutter: stable;
|
||||||
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
CodeBrowser.-show-tree #tree-view {
|
CodeBrowser.-show-tree #tree-view {
|
||||||
display: block;
|
display: block;
|
||||||
dock: left;
|
dock: left;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: auto;
|
max-width: 50%;
|
||||||
max-width:50%;
|
|
||||||
background: $surface;
|
background: $surface;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import sys
|
|||||||
|
|
||||||
from rich.syntax import Syntax
|
from rich.syntax import Syntax
|
||||||
from rich.traceback import Traceback
|
from rich.traceback import Traceback
|
||||||
|
|
||||||
from textual.app import App, ComposeResult
|
from textual.app import App, ComposeResult
|
||||||
from textual.layout import Container, Vertical
|
from textual.layout import Container, Vertical
|
||||||
from textual.reactive import Reactive
|
from textual.reactive import Reactive
|
||||||
@@ -9,17 +10,21 @@ from textual.widgets import DirectoryTree, Footer, Header, Static
|
|||||||
|
|
||||||
|
|
||||||
class CodeBrowser(App):
|
class CodeBrowser(App):
|
||||||
|
"""Textual code browser app."""
|
||||||
|
|
||||||
|
BINDINGS = [
|
||||||
|
("t", "toggle_tree", "Toggle Tree"),
|
||||||
|
("q", "quit", "Quit"),
|
||||||
|
]
|
||||||
|
|
||||||
show_tree = Reactive.init(True)
|
show_tree = Reactive.init(True)
|
||||||
|
|
||||||
def watch_show_tree(self, show_tree: bool) -> None:
|
def watch_show_tree(self, show_tree: bool) -> None:
|
||||||
|
"""Called when show_tree is modified."""
|
||||||
self.set_class(show_tree, "-show-tree")
|
self.set_class(show_tree, "-show-tree")
|
||||||
|
|
||||||
def on_load(self) -> None:
|
|
||||||
self.bind("t", "toggle_tree", description="Toggle Tree")
|
|
||||||
self.bind("q", "quit", description="Quit")
|
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
|
"""Compose our UI."""
|
||||||
path = "./" if len(sys.argv) < 2 else sys.argv[1]
|
path = "./" if len(sys.argv) < 2 else sys.argv[1]
|
||||||
yield Header()
|
yield Header()
|
||||||
yield Container(
|
yield Container(
|
||||||
@@ -29,6 +34,7 @@ class CodeBrowser(App):
|
|||||||
yield Footer()
|
yield Footer()
|
||||||
|
|
||||||
def on_directory_tree_file_click(self, event: DirectoryTree.FileClick) -> None:
|
def on_directory_tree_file_click(self, event: DirectoryTree.FileClick) -> None:
|
||||||
|
"""Called when the user click a file in the directory tree."""
|
||||||
code_view = self.query_one("#code", Static)
|
code_view = self.query_one("#code", Static)
|
||||||
try:
|
try:
|
||||||
syntax = Syntax.from_path(
|
syntax = Syntax.from_path(
|
||||||
|
|||||||
@@ -95,15 +95,15 @@ class StylesCache:
|
|||||||
Lines: Rendered lines.
|
Lines: Rendered lines.
|
||||||
"""
|
"""
|
||||||
base_background, background = widget.background_colors
|
base_background, background = widget.background_colors
|
||||||
padding = widget.styles.padding + widget.scrollbar_gutter
|
styles = widget.styles
|
||||||
lines = self.render(
|
lines = self.render(
|
||||||
widget.styles,
|
styles,
|
||||||
widget.region.size,
|
widget.region.size,
|
||||||
base_background,
|
base_background,
|
||||||
background,
|
background,
|
||||||
widget.render_line,
|
widget.render_line,
|
||||||
content_size=widget.content_region.size,
|
content_size=widget.content_region.size,
|
||||||
padding=padding,
|
padding=styles.padding,
|
||||||
crop=crop,
|
crop=crop,
|
||||||
)
|
)
|
||||||
return lines
|
return lines
|
||||||
|
|||||||
@@ -191,7 +191,6 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
self._animator = Animator(self)
|
self._animator = Animator(self)
|
||||||
self.animate = self._animator.bind(self)
|
self.animate = self._animator.bind(self)
|
||||||
self.mouse_position = Offset(0, 0)
|
self.mouse_position = Offset(0, 0)
|
||||||
self.bindings = Bindings()
|
|
||||||
if title is None:
|
if title is None:
|
||||||
self._title = f"{self.__class__.__name__}"
|
self._title = f"{self.__class__.__name__}"
|
||||||
else:
|
else:
|
||||||
@@ -199,7 +198,7 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
|
|
||||||
self._logger = Logger()
|
self._logger = Logger()
|
||||||
|
|
||||||
self.bindings.bind("ctrl+c", "quit", show=False, allow_forward=False)
|
self._bindings.bind("ctrl+c", "quit", show=False, allow_forward=False)
|
||||||
self._refresh_required = False
|
self._refresh_required = False
|
||||||
|
|
||||||
self.design = DEFAULT_COLORS
|
self.design = DEFAULT_COLORS
|
||||||
@@ -319,6 +318,13 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
|
|
||||||
return widgets
|
return widgets
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bindings(self) -> Bindings:
|
||||||
|
if self.focused is None:
|
||||||
|
return self._bindings
|
||||||
|
else:
|
||||||
|
return Bindings.merge(node._bindings for node in self.focused.ancestors)
|
||||||
|
|
||||||
def _set_active(self) -> None:
|
def _set_active(self) -> None:
|
||||||
"""Set this app to be the currently active app."""
|
"""Set this app to be the currently active app."""
|
||||||
active_app.set(self)
|
active_app.set(self)
|
||||||
@@ -595,7 +601,7 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
show (bool, optional): Show key in UI. Defaults to True.
|
show (bool, optional): Show key in UI. Defaults to True.
|
||||||
key_display (str, optional): Replacement text for key, or None to use default. Defaults to None.
|
key_display (str, optional): Replacement text for key, or None to use default. Defaults to None.
|
||||||
"""
|
"""
|
||||||
self.bindings.bind(
|
self._bindings.bind(
|
||||||
keys, action, description, show=show, key_display=key_display
|
keys, action, description, show=show, key_display=key_display
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1318,7 +1324,7 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
bool: True if the key was handled by a binding, otherwise False
|
bool: True if the key was handled by a binding, otherwise False
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
binding = self.bindings.get_key(key)
|
binding = self._bindings.get_key(key)
|
||||||
except NoBinding:
|
except NoBinding:
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
@@ -1355,15 +1361,14 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
await super().on_event(event)
|
await super().on_event(event)
|
||||||
|
|
||||||
async def action(
|
async def action(
|
||||||
self,
|
self, action: str, default_namespace: object | None = None
|
||||||
action: str,
|
|
||||||
default_namespace: object | None = None,
|
|
||||||
modifiers: set[str] | None = None,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Perform an action.
|
"""Perform an action.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
action (str): Action encoded in a string.
|
action (str): Action encoded in a string.
|
||||||
|
default_namespace (object | None): Namespace to use if not provided in the action,
|
||||||
|
or None to use app. Defaults to None.
|
||||||
"""
|
"""
|
||||||
target, params = actions.parse(action)
|
target, params = actions.parse(action)
|
||||||
if "." in target:
|
if "." in target:
|
||||||
@@ -1412,15 +1417,13 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
return False
|
return False
|
||||||
try:
|
try:
|
||||||
modifiers, action = extract_handler_actions(event_name, style.meta)
|
_modifiers, action = extract_handler_actions(event_name, style.meta)
|
||||||
except NoHandler:
|
except NoHandler:
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
event.stop()
|
event.stop()
|
||||||
if isinstance(action, str):
|
if isinstance(action, str):
|
||||||
await self.action(
|
await self.action(action, default_namespace=default_namespace)
|
||||||
action, default_namespace=default_namespace, modifiers=modifiers
|
|
||||||
)
|
|
||||||
elif callable(action):
|
elif callable(action):
|
||||||
await action()
|
await action()
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -1,5 +1,18 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import rich.repr
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from typing import Iterable, MutableMapping
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 10):
|
||||||
|
from typing import TypeAlias
|
||||||
|
else: # pragma: no cover
|
||||||
|
from typing_extensions import TypeAlias
|
||||||
|
|
||||||
|
|
||||||
|
BindingType: TypeAlias = "Binding | tuple[str, str] | tuple[str, str, str]"
|
||||||
|
|
||||||
|
|
||||||
class NoBinding(Exception):
|
class NoBinding(Exception):
|
||||||
@@ -11,16 +24,46 @@ class Binding:
|
|||||||
key: str
|
key: str
|
||||||
action: str
|
action: str
|
||||||
description: str
|
description: str
|
||||||
show: bool = False
|
show: bool = True
|
||||||
key_display: str | None = None
|
key_display: str | None = None
|
||||||
allow_forward: bool = True
|
allow_forward: bool = True
|
||||||
|
|
||||||
|
|
||||||
|
@rich.repr.auto
|
||||||
class Bindings:
|
class Bindings:
|
||||||
"""Manage a set of bindings."""
|
"""Manage a set of bindings."""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self, bindings: Iterable[BindingType] | None = None) -> None:
|
||||||
self.keys: dict[str, Binding] = {}
|
def make_bindings(bindings: Iterable[BindingType]) -> Iterable[Binding]:
|
||||||
|
for binding in bindings:
|
||||||
|
if isinstance(binding, Binding):
|
||||||
|
yield binding
|
||||||
|
else:
|
||||||
|
yield Binding(*binding)
|
||||||
|
|
||||||
|
self.keys: MutableMapping[str, Binding] = (
|
||||||
|
{binding.key: binding for binding in make_bindings(bindings)}
|
||||||
|
if bindings
|
||||||
|
else {}
|
||||||
|
)
|
||||||
|
|
||||||
|
def __rich_repr__(self) -> rich.repr.Result:
|
||||||
|
yield self.keys
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def merge(cls, bindings: Iterable[Bindings]) -> Bindings:
|
||||||
|
"""Merge a bindings. Subsequence bound keys override initial keys.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bindings (Iterable[Bindings]): A number of bindings.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Bindings: New bindings.
|
||||||
|
"""
|
||||||
|
keys: dict[str, Binding] = {}
|
||||||
|
for _bindings in bindings:
|
||||||
|
keys |= _bindings.keys
|
||||||
|
return Bindings(keys.values())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def shown_keys(self) -> list[Binding]:
|
def shown_keys(self) -> list[Binding]:
|
||||||
@@ -58,23 +101,3 @@ class Bindings:
|
|||||||
if binding is None:
|
if binding is None:
|
||||||
return True
|
return True
|
||||||
return binding.allow_forward
|
return binding.allow_forward
|
||||||
|
|
||||||
|
|
||||||
class BindingStack:
|
|
||||||
"""Manage a stack of bindings."""
|
|
||||||
|
|
||||||
def __init__(self, *bindings: Bindings) -> None:
|
|
||||||
self._stack: list[Bindings] = list(bindings)
|
|
||||||
|
|
||||||
def push(self, bindings: Bindings) -> None:
|
|
||||||
self._stack.append(bindings)
|
|
||||||
|
|
||||||
def pop(self) -> Bindings:
|
|
||||||
return self._stack.pop()
|
|
||||||
|
|
||||||
def get_key(self, key: str) -> Binding:
|
|
||||||
for bindings in reversed(self._stack):
|
|
||||||
binding = bindings.keys.get(key, None)
|
|
||||||
if binding is not None:
|
|
||||||
return binding
|
|
||||||
raise NoBinding(f"No binding for {key}") from None
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ Bar {
|
|||||||
|
|
||||||
#other {
|
#other {
|
||||||
width: 1fr;
|
width: 1fr;
|
||||||
background: #555555;
|
background: $panel;
|
||||||
padding: 1;
|
padding: 1;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border-left: vkey $background;
|
border-left: vkey $background;
|
||||||
@@ -35,6 +35,7 @@ Bar {
|
|||||||
|
|
||||||
#opacity-widget {
|
#opacity-widget {
|
||||||
padding: 1;
|
padding: 1;
|
||||||
background: $panel;
|
background: $warning;
|
||||||
border: wide #969696;
|
color: $text-warning;
|
||||||
|
border: wide $background;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,12 +29,30 @@ class Bar(Widget):
|
|||||||
position = Reactive.init(START_POSITION)
|
position = Reactive.init(START_POSITION)
|
||||||
animation_running = Reactive(False)
|
animation_running = Reactive(False)
|
||||||
|
|
||||||
|
DEFAULT_CSS = """
|
||||||
|
|
||||||
|
Bar {
|
||||||
|
background: $surface;
|
||||||
|
color: $error;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bar.-active {
|
||||||
|
background: $surface;
|
||||||
|
color: $success;
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def watch_animation_running(self, running: bool) -> None:
|
||||||
|
self.set_class(running, "-active")
|
||||||
|
|
||||||
def render(self) -> RenderableType:
|
def render(self) -> RenderableType:
|
||||||
|
|
||||||
return ScrollBarRender(
|
return ScrollBarRender(
|
||||||
virtual_size=VIRTUAL_SIZE,
|
virtual_size=VIRTUAL_SIZE,
|
||||||
window_size=WINDOW_SIZE,
|
window_size=WINDOW_SIZE,
|
||||||
position=self.position,
|
position=self.position,
|
||||||
style="green" if self.animation_running else "red",
|
style=self.rich_style,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ from rich.tree import Tree
|
|||||||
|
|
||||||
from ._context import NoActiveAppError
|
from ._context import NoActiveAppError
|
||||||
from ._node_list import NodeList
|
from ._node_list import NodeList
|
||||||
|
from .binding import Bindings, BindingType
|
||||||
from .color import Color, WHITE, BLACK
|
from .color import Color, WHITE, BLACK
|
||||||
from .css._error_tools import friendly_list
|
from .css._error_tools import friendly_list
|
||||||
from .css.constants import VALID_DISPLAY, VALID_VISIBILITY
|
from .css.constants import VALID_DISPLAY, VALID_VISIBILITY
|
||||||
@@ -91,6 +92,9 @@ class DOMNode(MessagePump):
|
|||||||
# Virtual DOM nodes
|
# Virtual DOM nodes
|
||||||
COMPONENT_CLASSES: ClassVar[set[str]] = set()
|
COMPONENT_CLASSES: ClassVar[set[str]] = set()
|
||||||
|
|
||||||
|
# Mapping of key bindings
|
||||||
|
BINDINGS: ClassVar[list[BindingType]] = []
|
||||||
|
|
||||||
# True if this node inherits the CSS from the base class.
|
# True if this node inherits the CSS from the base class.
|
||||||
_inherit_css: ClassVar[bool] = True
|
_inherit_css: ClassVar[bool] = True
|
||||||
# List of names of base class (lower cased) that inherit CSS
|
# List of names of base class (lower cased) that inherit CSS
|
||||||
@@ -123,6 +127,7 @@ class DOMNode(MessagePump):
|
|||||||
self._auto_refresh: float | None = None
|
self._auto_refresh: float | None = None
|
||||||
self._auto_refresh_timer: Timer | None = None
|
self._auto_refresh_timer: Timer | None = None
|
||||||
self._css_types = {cls.__name__ for cls in self._css_bases(self.__class__)}
|
self._css_types = {cls.__name__ for cls in self._css_bases(self.__class__)}
|
||||||
|
self._bindings = Bindings(self.BINDINGS)
|
||||||
|
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
@@ -475,21 +480,25 @@ class DOMNode(MessagePump):
|
|||||||
Returns:
|
Returns:
|
||||||
Style: Rich Style object.
|
Style: Rich Style object.
|
||||||
"""
|
"""
|
||||||
|
return Style.combine(
|
||||||
# TODO: Feels like there may be opportunity for caching here.
|
node.styles.text_style for node in reversed(self.ancestors)
|
||||||
|
)
|
||||||
style = Style()
|
|
||||||
for node in reversed(self.ancestors):
|
|
||||||
style += node.styles.text_style
|
|
||||||
return style
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def rich_style(self) -> Style:
|
def rich_style(self) -> Style:
|
||||||
"""Get a Rich Style object for this DOMNode."""
|
"""Get a Rich Style object for this DOMNode."""
|
||||||
_, _, background, color = self.colors
|
background = WHITE
|
||||||
style = (
|
color = BLACK
|
||||||
Style.from_color((background + color).rich_color, background.rich_color)
|
style = Style()
|
||||||
+ self.text_style
|
for node in reversed(self.ancestors):
|
||||||
|
styles = node.styles
|
||||||
|
if styles.has_rule("background"):
|
||||||
|
background += styles.background
|
||||||
|
if styles.has_rule("color"):
|
||||||
|
color = styles.color
|
||||||
|
style += styles.text_style
|
||||||
|
style += Style.from_color(
|
||||||
|
(background + color).rich_color, background.rich_color
|
||||||
)
|
)
|
||||||
return style
|
return style
|
||||||
|
|
||||||
@@ -501,9 +510,7 @@ class DOMNode(MessagePump):
|
|||||||
tuple[Color, Color]: Tuple of (base background, background)
|
tuple[Color, Color]: Tuple of (base background, background)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
base_background = background = BLACK
|
base_background = background = BLACK
|
||||||
|
|
||||||
for node in reversed(self.ancestors):
|
for node in reversed(self.ancestors):
|
||||||
styles = node.styles
|
styles = node.styles
|
||||||
if styles.has_rule("background"):
|
if styles.has_rule("background"):
|
||||||
@@ -533,15 +540,13 @@ class DOMNode(MessagePump):
|
|||||||
@property
|
@property
|
||||||
def ancestors(self) -> list[DOMNode]:
|
def ancestors(self) -> list[DOMNode]:
|
||||||
"""Get a list of Nodes by tracing ancestors all the way back to App."""
|
"""Get a list of Nodes by tracing ancestors all the way back to App."""
|
||||||
nodes: list[DOMNode] = [self]
|
nodes: list[MessagePump | None] = []
|
||||||
add_node = nodes.append
|
add_node = nodes.append
|
||||||
node: DOMNode = self
|
node: MessagePump | None = self
|
||||||
while True:
|
while node is not None:
|
||||||
node = node._parent
|
|
||||||
if node is None:
|
|
||||||
break
|
|
||||||
add_node(node)
|
add_node(node)
|
||||||
return nodes
|
node = node._parent
|
||||||
|
return cast(list[DOMNode], nodes)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def displayed_children(self) -> list[Widget]:
|
def displayed_children(self) -> list[Widget]:
|
||||||
|
|||||||
@@ -7,13 +7,8 @@ from operator import attrgetter
|
|||||||
from typing import TYPE_CHECKING, ClassVar, Collection, Iterable, NamedTuple
|
from typing import TYPE_CHECKING, ClassVar, Collection, Iterable, NamedTuple
|
||||||
|
|
||||||
import rich.repr
|
import rich.repr
|
||||||
from rich.console import (
|
from rich.console import Console, ConsoleRenderable, JustifyMethod, RenderableType
|
||||||
Console,
|
from rich.measure import Measurement
|
||||||
ConsoleRenderable,
|
|
||||||
Measurement,
|
|
||||||
JustifyMethod,
|
|
||||||
RenderableType,
|
|
||||||
)
|
|
||||||
from rich.segment import Segment
|
from rich.segment import Segment
|
||||||
from rich.style import Style
|
from rich.style import Style
|
||||||
from rich.styled import Styled
|
from rich.styled import Styled
|
||||||
@@ -27,6 +22,7 @@ from ._layout import Layout
|
|||||||
from ._segment_tools import align_lines
|
from ._segment_tools import align_lines
|
||||||
from ._styles_cache import StylesCache
|
from ._styles_cache import StylesCache
|
||||||
from ._types import Lines
|
from ._types import Lines
|
||||||
|
from .binding import NoBinding
|
||||||
from .box_model import BoxModel, get_box_model
|
from .box_model import BoxModel, get_box_model
|
||||||
from .css.constants import VALID_TEXT_ALIGN
|
from .css.constants import VALID_TEXT_ALIGN
|
||||||
from .dom import DOMNode, NoScreen
|
from .dom import DOMNode, NoScreen
|
||||||
@@ -522,7 +518,7 @@ class Widget(DOMNode):
|
|||||||
"""Get the height used by the *horizontal* scrollbar."""
|
"""Get the height used by the *horizontal* scrollbar."""
|
||||||
styles = self.styles
|
styles = self.styles
|
||||||
if styles.scrollbar_gutter == "stable" and styles.overflow_x == "auto":
|
if styles.scrollbar_gutter == "stable" and styles.overflow_x == "auto":
|
||||||
return self.styles.scrollbar_size_horizontal
|
return styles.scrollbar_size_horizontal
|
||||||
return styles.scrollbar_size_horizontal if self.show_horizontal_scrollbar else 0
|
return styles.scrollbar_size_horizontal if self.show_horizontal_scrollbar else 0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -1474,7 +1470,12 @@ class Widget(DOMNode):
|
|||||||
await self.broker_event("click", event)
|
await self.broker_event("click", event)
|
||||||
|
|
||||||
async def _on_key(self, event: events.Key) -> None:
|
async def _on_key(self, event: events.Key) -> None:
|
||||||
await self.dispatch_key(event)
|
try:
|
||||||
|
binding = self._bindings.get_key(event.key)
|
||||||
|
except NoBinding:
|
||||||
|
await self.dispatch_key(event)
|
||||||
|
else:
|
||||||
|
await self.action(binding.action)
|
||||||
|
|
||||||
def _on_mount(self, event: events.Mount) -> None:
|
def _on_mount(self, event: events.Mount) -> None:
|
||||||
widgets = self.compose()
|
widgets = self.compose()
|
||||||
|
|||||||
Reference in New Issue
Block a user