mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Merge branch 'main' into checkbox-switch
This commit is contained in:
@@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
- Added Shift+scroll wheel and ctrl+scroll wheel to scroll horizontally
|
- Added Shift+scroll wheel and ctrl+scroll wheel to scroll horizontally
|
||||||
- Added `Tree.action_toggle_node` to toggle a node without selecting, and bound it to <kbd>Space</kbd> https://github.com/Textualize/textual/issues/1433
|
- Added `Tree.action_toggle_node` to toggle a node without selecting, and bound it to <kbd>Space</kbd> https://github.com/Textualize/textual/issues/1433
|
||||||
- Added `Tree.reset` to fully reset a `Tree` https://github.com/Textualize/textual/issues/1437
|
- Added `Tree.reset` to fully reset a `Tree` https://github.com/Textualize/textual/issues/1437
|
||||||
|
- Added DOMNode.watch and DOMNode.is_attached methods https://github.com/Textualize/textual/pull/1750
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
@@ -43,6 +44,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
- Methods `MessagePump.emit` and `MessagePump.emit_no_wait` https://github.com/Textualize/textual/pull/1738
|
- Methods `MessagePump.emit` and `MessagePump.emit_no_wait` https://github.com/Textualize/textual/pull/1738
|
||||||
|
- Removed `reactive.watch` in favor of DOMNode.watch.
|
||||||
|
|
||||||
## [0.10.1] - 2023-01-20
|
## [0.10.1] - 2023-01-20
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ 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, Vertical
|
from textual.containers import Container, Horizontal, Vertical
|
||||||
from textual.reactive import Reactive
|
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
|
||||||
from textual.widgets import Button, Footer, Label, Input
|
from textual.widgets import Button, Footer, Label, Input
|
||||||
@@ -23,8 +23,8 @@ class EasingButtons(Widget):
|
|||||||
|
|
||||||
|
|
||||||
class Bar(Widget):
|
class Bar(Widget):
|
||||||
position = Reactive.init(START_POSITION)
|
position = reactive(START_POSITION)
|
||||||
animation_running = Reactive(False)
|
animation_running = reactive(False)
|
||||||
|
|
||||||
DEFAULT_CSS = """
|
DEFAULT_CSS = """
|
||||||
|
|
||||||
@@ -53,8 +53,8 @@ class Bar(Widget):
|
|||||||
|
|
||||||
|
|
||||||
class EasingApp(App):
|
class EasingApp(App):
|
||||||
position = Reactive.init(START_POSITION)
|
position = reactive(START_POSITION)
|
||||||
duration = Reactive.var(1.0)
|
duration = var(1.0)
|
||||||
|
|
||||||
def on_load(self):
|
def on_load(self):
|
||||||
self.bind(
|
self.bind(
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from rich.text import Text
|
|||||||
from textual.app import App, ComposeResult
|
from textual.app import App, ComposeResult
|
||||||
from textual.binding import Binding
|
from textual.binding import Binding
|
||||||
from textual.containers import Container, Horizontal
|
from textual.containers import Container, Horizontal
|
||||||
from textual.reactive import reactive, watch
|
from textual.reactive import reactive
|
||||||
from textual.widgets import (
|
from textual.widgets import (
|
||||||
Button,
|
Button,
|
||||||
DataTable,
|
DataTable,
|
||||||
@@ -203,7 +203,7 @@ class DarkSwitch(Horizontal):
|
|||||||
yield Static("Dark mode toggle", classes="label")
|
yield Static("Dark mode toggle", classes="label")
|
||||||
|
|
||||||
def on_mount(self) -> None:
|
def on_mount(self) -> None:
|
||||||
watch(self.app, "dark", self.on_dark_change, init=False)
|
self.watch(self.app, "dark", self.on_dark_change, init=False)
|
||||||
|
|
||||||
def on_dark_change(self, dark: bool) -> None:
|
def on_dark_change(self, dark: bool) -> None:
|
||||||
self.query_one(Switch).value = self.app.dark
|
self.query_one(Switch).value = self.app.dark
|
||||||
|
|||||||
@@ -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 ._types import CallbackType
|
||||||
from .binding import Bindings, BindingType
|
from .binding import Bindings, BindingType
|
||||||
from .color import BLACK, WHITE, Color
|
from .color import BLACK, WHITE, Color
|
||||||
from .css._error_tools import friendly_list
|
from .css._error_tools import friendly_list
|
||||||
@@ -31,7 +32,7 @@ from .css.parse import parse_declarations
|
|||||||
from .css.styles import RenderStyles, Styles
|
from .css.styles import RenderStyles, Styles
|
||||||
from .css.tokenize import IDENTIFIER
|
from .css.tokenize import IDENTIFIER
|
||||||
from .message_pump import MessagePump
|
from .message_pump import MessagePump
|
||||||
from .reactive import Reactive
|
from .reactive import Reactive, _watch
|
||||||
from .timer import Timer
|
from .timer import Timer
|
||||||
from .walk import walk_breadth_first, walk_depth_first
|
from .walk import walk_breadth_first, walk_depth_first
|
||||||
|
|
||||||
@@ -210,6 +211,10 @@ class DOMNode(MessagePump):
|
|||||||
styles = self._component_styles[name]
|
styles = self._component_styles[name]
|
||||||
return styles
|
return styles
|
||||||
|
|
||||||
|
def _post_mount(self):
|
||||||
|
"""Called after the object has been mounted."""
|
||||||
|
Reactive._initialize_object(self)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _node_bases(self) -> Iterator[Type[DOMNode]]:
|
def _node_bases(self) -> Iterator[Type[DOMNode]]:
|
||||||
"""Iterator[Type[DOMNode]]: The DOMNode bases classes (including self.__class__)"""
|
"""Iterator[Type[DOMNode]]: The DOMNode bases classes (including self.__class__)"""
|
||||||
@@ -643,6 +648,23 @@ class DOMNode(MessagePump):
|
|||||||
"""
|
"""
|
||||||
return [child for child in self.children if child.display]
|
return [child for child in self.children if child.display]
|
||||||
|
|
||||||
|
def watch(
|
||||||
|
self,
|
||||||
|
obj: DOMNode,
|
||||||
|
attribute_name: str,
|
||||||
|
callback: CallbackType,
|
||||||
|
init: bool = True,
|
||||||
|
) -> None:
|
||||||
|
"""Watches for modifications to reactive attributes on another object.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
obj: Object containing attribute to watch.
|
||||||
|
attribute_name: Attribute to watch.
|
||||||
|
callback: A callback to run when attribute changes.
|
||||||
|
init: Check watchers on first call.
|
||||||
|
"""
|
||||||
|
_watch(self, obj, attribute_name, callback, init=init)
|
||||||
|
|
||||||
def get_pseudo_classes(self) -> Iterable[str]:
|
def get_pseudo_classes(self) -> Iterable[str]:
|
||||||
"""Get any pseudo classes applicable to this Node, e.g. hover, focus.
|
"""Get any pseudo classes applicable to this Node, e.g. hover, focus.
|
||||||
|
|
||||||
|
|||||||
@@ -123,6 +123,19 @@ class MessagePump(metaclass=MessagePumpMeta):
|
|||||||
"""
|
"""
|
||||||
return self.app._logger
|
return self.app._logger
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_attached(self) -> bool:
|
||||||
|
"""Is the node is attached to the app via the DOM."""
|
||||||
|
from .app import App
|
||||||
|
|
||||||
|
node = self
|
||||||
|
|
||||||
|
while not isinstance(node, App):
|
||||||
|
if node._parent is None:
|
||||||
|
return False
|
||||||
|
node = node._parent
|
||||||
|
return True
|
||||||
|
|
||||||
def _attach(self, parent: MessagePump) -> None:
|
def _attach(self, parent: MessagePump) -> None:
|
||||||
"""Set the parent, and therefore attach this node to the tree.
|
"""Set the parent, and therefore attach this node to the tree.
|
||||||
|
|
||||||
@@ -358,7 +371,10 @@ class MessagePump(metaclass=MessagePumpMeta):
|
|||||||
finally:
|
finally:
|
||||||
# This is critical, mount may be waiting
|
# This is critical, mount may be waiting
|
||||||
self._mounted_event.set()
|
self._mounted_event.set()
|
||||||
Reactive._initialize_object(self)
|
self._post_mount()
|
||||||
|
|
||||||
|
def _post_mount(self):
|
||||||
|
"""Called after the object has been mounted."""
|
||||||
|
|
||||||
async def _process_messages_loop(self) -> None:
|
async def _process_messages_loop(self) -> None:
|
||||||
"""Process messages until the queue is closed."""
|
"""Process messages until the queue is closed."""
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ class Pilot(Generic[ReturnType]):
|
|||||||
async def wait_for_scheduled_animations(self) -> None:
|
async def wait_for_scheduled_animations(self) -> None:
|
||||||
"""Wait for any current and scheduled animations to complete."""
|
"""Wait for any current and scheduled animations to complete."""
|
||||||
await self._app.animator.wait_until_complete()
|
await self._app.animator.wait_until_complete()
|
||||||
|
await wait_for_idle(0)
|
||||||
|
|
||||||
async def exit(self, result: ReturnType) -> None:
|
async def exit(self, result: ReturnType) -> None:
|
||||||
"""Exit the app with the given result.
|
"""Exit the app with the given result.
|
||||||
|
|||||||
@@ -7,36 +7,26 @@ from typing import (
|
|||||||
Any,
|
Any,
|
||||||
Awaitable,
|
Awaitable,
|
||||||
Callable,
|
Callable,
|
||||||
|
ClassVar,
|
||||||
Generic,
|
Generic,
|
||||||
Type,
|
Type,
|
||||||
TypeVar,
|
TypeVar,
|
||||||
Union,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
import rich.repr
|
import rich.repr
|
||||||
|
|
||||||
from . import events
|
from . import events
|
||||||
from ._callback import count_parameters, invoke
|
from ._callback import count_parameters
|
||||||
from ._types import MessageTarget
|
from ._types import MessageTarget, CallbackType
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .app import App
|
from .dom import DOMNode
|
||||||
from .widget import Widget
|
|
||||||
|
|
||||||
Reactable = Union[Widget, App]
|
Reactable = DOMNode
|
||||||
|
|
||||||
ReactiveType = TypeVar("ReactiveType")
|
ReactiveType = TypeVar("ReactiveType")
|
||||||
|
|
||||||
|
|
||||||
class _NotSet:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
_NOT_SET = _NotSet()
|
|
||||||
|
|
||||||
T = TypeVar("T")
|
|
||||||
|
|
||||||
|
|
||||||
@rich.repr.auto
|
@rich.repr.auto
|
||||||
class Reactive(Generic[ReactiveType]):
|
class Reactive(Generic[ReactiveType]):
|
||||||
"""Reactive descriptor.
|
"""Reactive descriptor.
|
||||||
@@ -50,7 +40,7 @@ class Reactive(Generic[ReactiveType]):
|
|||||||
compute: Run compute methods when attribute is changed. Defaults to True.
|
compute: Run compute methods when attribute is changed. Defaults to True.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_reactives: TypeVar[dict[str, object]] = {}
|
_reactives: ClassVar[dict[str, object]] = {}
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -77,37 +67,6 @@ class Reactive(Generic[ReactiveType]):
|
|||||||
yield "always_update", self._always_update
|
yield "always_update", self._always_update
|
||||||
yield "compute", self._run_compute
|
yield "compute", self._run_compute
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def init(
|
|
||||||
cls,
|
|
||||||
default: ReactiveType | Callable[[], ReactiveType],
|
|
||||||
*,
|
|
||||||
layout: bool = False,
|
|
||||||
repaint: bool = True,
|
|
||||||
always_update: bool = False,
|
|
||||||
compute: bool = True,
|
|
||||||
) -> Reactive:
|
|
||||||
"""A reactive variable that calls watchers and compute on initialize (post mount).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
default: A default value or callable that returns a default.
|
|
||||||
layout: Perform a layout on change. Defaults to False.
|
|
||||||
repaint: Perform a repaint on change. Defaults to True.
|
|
||||||
always_update: Call watchers even when the new value equals the old value. Defaults to False.
|
|
||||||
compute: Run compute methods when attribute is changed. Defaults to True.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A Reactive instance which calls watchers or initialize.
|
|
||||||
"""
|
|
||||||
return cls(
|
|
||||||
default,
|
|
||||||
layout=layout,
|
|
||||||
repaint=repaint,
|
|
||||||
init=True,
|
|
||||||
always_update=always_update,
|
|
||||||
compute=compute,
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def var(
|
def var(
|
||||||
cls,
|
cls,
|
||||||
@@ -254,7 +213,7 @@ class Reactive(Generic[ReactiveType]):
|
|||||||
|
|
||||||
def invoke_watcher(
|
def invoke_watcher(
|
||||||
watch_function: Callable, old_value: object, value: object
|
watch_function: Callable, old_value: object, value: object
|
||||||
) -> bool:
|
) -> None:
|
||||||
"""Invoke a watch function.
|
"""Invoke a watch function.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -262,8 +221,6 @@ class Reactive(Generic[ReactiveType]):
|
|||||||
old_value: The old value of the attribute.
|
old_value: The old value of the attribute.
|
||||||
value: The new value of the attribute.
|
value: The new value of the attribute.
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if the watcher was run, or False if it was posted.
|
|
||||||
"""
|
"""
|
||||||
_rich_traceback_omit = True
|
_rich_traceback_omit = True
|
||||||
param_count = count_parameters(watch_function)
|
param_count = count_parameters(watch_function)
|
||||||
@@ -280,17 +237,23 @@ class Reactive(Generic[ReactiveType]):
|
|||||||
sender=obj, callback=partial(await_watcher, watch_result)
|
sender=obj, callback=partial(await_watcher, watch_result)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return False
|
|
||||||
else:
|
|
||||||
return True
|
|
||||||
|
|
||||||
watch_function = getattr(obj, f"watch_{name}", None)
|
watch_function = getattr(obj, f"watch_{name}", None)
|
||||||
if callable(watch_function):
|
if callable(watch_function):
|
||||||
invoke_watcher(watch_function, old_value, value)
|
invoke_watcher(watch_function, old_value, value)
|
||||||
|
|
||||||
watchers: list[Callable] = getattr(obj, "__watchers", {}).get(name, [])
|
# Process "global" watchers
|
||||||
for watcher in watchers:
|
watchers: list[tuple[Reactable, Callable]]
|
||||||
invoke_watcher(watcher, old_value, value)
|
watchers = getattr(obj, "__watchers", {}).get(name, [])
|
||||||
|
# Remove any watchers for reactables that have since closed
|
||||||
|
if watchers:
|
||||||
|
watchers[:] = [
|
||||||
|
(reactable, callback)
|
||||||
|
for reactable, callback in watchers
|
||||||
|
if reactable.is_attached and not reactable._closing
|
||||||
|
]
|
||||||
|
for _, callback in watchers:
|
||||||
|
invoke_watcher(callback, old_value, value)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _compute(cls, obj: Reactable) -> None:
|
def _compute(cls, obj: Reactable) -> None:
|
||||||
@@ -362,10 +325,12 @@ class var(Reactive[ReactiveType]):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def watch(
|
def _watch(
|
||||||
|
node: DOMNode,
|
||||||
obj: Reactable,
|
obj: Reactable,
|
||||||
attribute_name: str,
|
attribute_name: str,
|
||||||
callback: Callable[[Any], object],
|
callback: CallbackType,
|
||||||
|
*,
|
||||||
init: bool = True,
|
init: bool = True,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Watch a reactive variable on an object.
|
"""Watch a reactive variable on an object.
|
||||||
@@ -379,11 +344,11 @@ def watch(
|
|||||||
|
|
||||||
if not hasattr(obj, "__watchers"):
|
if not hasattr(obj, "__watchers"):
|
||||||
setattr(obj, "__watchers", {})
|
setattr(obj, "__watchers", {})
|
||||||
watchers: dict[str, list[Callable]] = getattr(obj, "__watchers")
|
watchers: dict[str, list[tuple[Reactable, Callable]]] = getattr(obj, "__watchers")
|
||||||
watcher_list = watchers.setdefault(attribute_name, [])
|
watcher_list = watchers.setdefault(attribute_name, [])
|
||||||
if callback in watcher_list:
|
if callback in watcher_list:
|
||||||
return
|
return
|
||||||
watcher_list.append(callback)
|
watcher_list.append((node, callback))
|
||||||
if init:
|
if init:
|
||||||
current_value = getattr(obj, attribute_name, None)
|
current_value = getattr(obj, attribute_name, None)
|
||||||
Reactive._check_watchers(obj, attribute_name, current_value)
|
Reactive._check_watchers(obj, attribute_name, current_value)
|
||||||
|
|||||||
@@ -14,5 +14,4 @@ from ._static import Static as Static
|
|||||||
from ._switch import Switch as Switch
|
from ._switch import Switch as Switch
|
||||||
from ._text_log import TextLog as TextLog
|
from ._text_log import TextLog as TextLog
|
||||||
from ._tree import Tree as Tree
|
from ._tree import Tree as Tree
|
||||||
from ._tree_node import TreeNode as TreeNode
|
|
||||||
from ._welcome import Welcome as Welcome
|
from ._welcome import Welcome as Welcome
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from rich.text import Text, TextType
|
|||||||
from .. import events
|
from .. import events
|
||||||
from ..css._error_tools import friendly_list
|
from ..css._error_tools import friendly_list
|
||||||
from ..message import Message
|
from ..message import Message
|
||||||
from ..reactive import Reactive
|
from ..reactive import reactive
|
||||||
from ..widgets import Static
|
from ..widgets import Static
|
||||||
|
|
||||||
|
|
||||||
@@ -151,13 +151,13 @@ class Button(Static, can_focus=True):
|
|||||||
ACTIVE_EFFECT_DURATION = 0.3
|
ACTIVE_EFFECT_DURATION = 0.3
|
||||||
"""When buttons are clicked they get the `-active` class for this duration (in seconds)"""
|
"""When buttons are clicked they get the `-active` class for this duration (in seconds)"""
|
||||||
|
|
||||||
label: Reactive[RenderableType] = Reactive("")
|
label: reactive[RenderableType] = reactive("")
|
||||||
"""The text label that appears within the button."""
|
"""The text label that appears within the button."""
|
||||||
|
|
||||||
variant = Reactive.init("default")
|
variant = reactive("default")
|
||||||
"""The variant name for the button."""
|
"""The variant name for the button."""
|
||||||
|
|
||||||
disabled = Reactive(False)
|
disabled = reactive(False)
|
||||||
"""The disabled state of the button; `True` if disabled, `False` if not."""
|
"""The disabled state of the button; `True` if disabled, `False` if not."""
|
||||||
|
|
||||||
class Pressed(Message, bubble=True):
|
class Pressed(Message, bubble=True):
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from rich.console import RenderableType
|
|||||||
from rich.text import Text
|
from rich.text import Text
|
||||||
|
|
||||||
from .. import events
|
from .. import events
|
||||||
from ..reactive import Reactive, watch
|
from ..reactive import Reactive
|
||||||
from ..widget import Widget
|
from ..widget import Widget
|
||||||
|
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ class Footer(Widget):
|
|||||||
self.refresh()
|
self.refresh()
|
||||||
|
|
||||||
def on_mount(self) -> None:
|
def on_mount(self) -> None:
|
||||||
watch(self.screen, "focused", self._focus_changed)
|
self.watch(self.screen, "focused", self._focus_changed)
|
||||||
|
|
||||||
def _focus_changed(self, focused: Widget | None) -> None:
|
def _focus_changed(self, focused: Widget | None) -> None:
|
||||||
self._key_text = None
|
self._key_text = None
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from datetime import datetime
|
|||||||
from rich.text import Text
|
from rich.text import Text
|
||||||
|
|
||||||
from ..widget import Widget
|
from ..widget import Widget
|
||||||
from ..reactive import Reactive, watch
|
from ..reactive import Reactive
|
||||||
|
|
||||||
|
|
||||||
class HeaderIcon(Widget):
|
class HeaderIcon(Widget):
|
||||||
@@ -133,5 +133,5 @@ class Header(Widget):
|
|||||||
def set_sub_title(sub_title: str) -> None:
|
def set_sub_title(sub_title: str) -> None:
|
||||||
self.query_one(HeaderTitle).sub_text = sub_title
|
self.query_one(HeaderTitle).sub_text = sub_title
|
||||||
|
|
||||||
watch(self.app, "title", set_title)
|
self.watch(self.app, "title", set_title)
|
||||||
watch(self.app, "sub_title", set_sub_title)
|
self.watch(self.app, "sub_title", set_sub_title)
|
||||||
|
|||||||
@@ -13964,6 +13964,164 @@
|
|||||||
|
|
||||||
'''
|
'''
|
||||||
# ---
|
# ---
|
||||||
|
# name: test_screen_switch
|
||||||
|
'''
|
||||||
|
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<!-- Generated with Rich https://www.textualize.io -->
|
||||||
|
<style>
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Fira Code";
|
||||||
|
src: local("FiraCode-Regular"),
|
||||||
|
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
|
||||||
|
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: "Fira Code";
|
||||||
|
src: local("FiraCode-Bold"),
|
||||||
|
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
|
||||||
|
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
|
||||||
|
font-style: bold;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-1316892474-matrix {
|
||||||
|
font-family: Fira Code, monospace;
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 24.4px;
|
||||||
|
font-variant-east-asian: full-width;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-1316892474-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-family: arial;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-1316892474-r1 { fill: #c5c8c6 }
|
||||||
|
.terminal-1316892474-r2 { fill: #e3e3e3 }
|
||||||
|
.terminal-1316892474-r3 { fill: #e1e1e1 }
|
||||||
|
.terminal-1316892474-r4 { fill: #dde8f3;font-weight: bold }
|
||||||
|
.terminal-1316892474-r5 { fill: #ddedf9 }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<defs>
|
||||||
|
<clipPath id="terminal-1316892474-clip-terminal">
|
||||||
|
<rect x="0" y="0" width="975.0" height="584.5999999999999" />
|
||||||
|
</clipPath>
|
||||||
|
<clipPath id="terminal-1316892474-line-0">
|
||||||
|
<rect x="0" y="1.5" width="976" height="24.65"/>
|
||||||
|
</clipPath>
|
||||||
|
<clipPath id="terminal-1316892474-line-1">
|
||||||
|
<rect x="0" y="25.9" width="976" height="24.65"/>
|
||||||
|
</clipPath>
|
||||||
|
<clipPath id="terminal-1316892474-line-2">
|
||||||
|
<rect x="0" y="50.3" width="976" height="24.65"/>
|
||||||
|
</clipPath>
|
||||||
|
<clipPath id="terminal-1316892474-line-3">
|
||||||
|
<rect x="0" y="74.7" width="976" height="24.65"/>
|
||||||
|
</clipPath>
|
||||||
|
<clipPath id="terminal-1316892474-line-4">
|
||||||
|
<rect x="0" y="99.1" width="976" height="24.65"/>
|
||||||
|
</clipPath>
|
||||||
|
<clipPath id="terminal-1316892474-line-5">
|
||||||
|
<rect x="0" y="123.5" width="976" height="24.65"/>
|
||||||
|
</clipPath>
|
||||||
|
<clipPath id="terminal-1316892474-line-6">
|
||||||
|
<rect x="0" y="147.9" width="976" height="24.65"/>
|
||||||
|
</clipPath>
|
||||||
|
<clipPath id="terminal-1316892474-line-7">
|
||||||
|
<rect x="0" y="172.3" width="976" height="24.65"/>
|
||||||
|
</clipPath>
|
||||||
|
<clipPath id="terminal-1316892474-line-8">
|
||||||
|
<rect x="0" y="196.7" width="976" height="24.65"/>
|
||||||
|
</clipPath>
|
||||||
|
<clipPath id="terminal-1316892474-line-9">
|
||||||
|
<rect x="0" y="221.1" width="976" height="24.65"/>
|
||||||
|
</clipPath>
|
||||||
|
<clipPath id="terminal-1316892474-line-10">
|
||||||
|
<rect x="0" y="245.5" width="976" height="24.65"/>
|
||||||
|
</clipPath>
|
||||||
|
<clipPath id="terminal-1316892474-line-11">
|
||||||
|
<rect x="0" y="269.9" width="976" height="24.65"/>
|
||||||
|
</clipPath>
|
||||||
|
<clipPath id="terminal-1316892474-line-12">
|
||||||
|
<rect x="0" y="294.3" width="976" height="24.65"/>
|
||||||
|
</clipPath>
|
||||||
|
<clipPath id="terminal-1316892474-line-13">
|
||||||
|
<rect x="0" y="318.7" width="976" height="24.65"/>
|
||||||
|
</clipPath>
|
||||||
|
<clipPath id="terminal-1316892474-line-14">
|
||||||
|
<rect x="0" y="343.1" width="976" height="24.65"/>
|
||||||
|
</clipPath>
|
||||||
|
<clipPath id="terminal-1316892474-line-15">
|
||||||
|
<rect x="0" y="367.5" width="976" height="24.65"/>
|
||||||
|
</clipPath>
|
||||||
|
<clipPath id="terminal-1316892474-line-16">
|
||||||
|
<rect x="0" y="391.9" width="976" height="24.65"/>
|
||||||
|
</clipPath>
|
||||||
|
<clipPath id="terminal-1316892474-line-17">
|
||||||
|
<rect x="0" y="416.3" width="976" height="24.65"/>
|
||||||
|
</clipPath>
|
||||||
|
<clipPath id="terminal-1316892474-line-18">
|
||||||
|
<rect x="0" y="440.7" width="976" height="24.65"/>
|
||||||
|
</clipPath>
|
||||||
|
<clipPath id="terminal-1316892474-line-19">
|
||||||
|
<rect x="0" y="465.1" width="976" height="24.65"/>
|
||||||
|
</clipPath>
|
||||||
|
<clipPath id="terminal-1316892474-line-20">
|
||||||
|
<rect x="0" y="489.5" width="976" height="24.65"/>
|
||||||
|
</clipPath>
|
||||||
|
<clipPath id="terminal-1316892474-line-21">
|
||||||
|
<rect x="0" y="513.9" width="976" height="24.65"/>
|
||||||
|
</clipPath>
|
||||||
|
<clipPath id="terminal-1316892474-line-22">
|
||||||
|
<rect x="0" y="538.3" width="976" height="24.65"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="992" height="633.6" rx="8"/><text class="terminal-1316892474-title" fill="#c5c8c6" text-anchor="middle" x="496" y="27">ModalApp</text>
|
||||||
|
<g transform="translate(26,22)">
|
||||||
|
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
|
||||||
|
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
|
||||||
|
<circle cx="44" cy="0" r="7" fill="#28c840"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g transform="translate(9, 41)" clip-path="url(#terminal-1316892474-clip-terminal)">
|
||||||
|
<rect fill="#282828" x="0" y="1.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#282828" x="12.2" y="1.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#282828" x="24.4" y="1.5" width="61" height="24.65" shape-rendering="crispEdges"/><rect fill="#282828" x="85.4" y="1.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#282828" x="97.6" y="1.5" width="329.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#282828" x="427" y="1.5" width="97.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#282828" x="524.6" y="1.5" width="329.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#282828" x="854" y="1.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#282828" x="866.2" y="1.5" width="0" height="24.65" shape-rendering="crispEdges"/><rect fill="#282828" x="866.2" y="1.5" width="97.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#282828" x="963.8" y="1.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="25.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="12.2" y="25.9" width="963.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="50.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="74.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="99.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="123.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="147.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="172.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="196.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="221.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="245.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="269.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="294.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="318.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="343.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="367.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="391.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="416.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="440.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="465.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="489.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="513.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="538.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#0053aa" x="0" y="562.7" width="36.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#0178d4" x="36.6" y="562.7" width="183" height="24.65" shape-rendering="crispEdges"/><rect fill="#0178d4" x="219.6" y="562.7" width="756.4" height="24.65" shape-rendering="crispEdges"/>
|
||||||
|
<g class="terminal-1316892474-matrix">
|
||||||
|
<text class="terminal-1316892474-r2" x="12.2" y="20" textLength="12.2" clip-path="url(#terminal-1316892474-line-0)">⭘</text><text class="terminal-1316892474-r2" x="427" y="20" textLength="97.6" clip-path="url(#terminal-1316892474-line-0)">ModalApp</text><text class="terminal-1316892474-r1" x="976" y="20" textLength="12.2" clip-path="url(#terminal-1316892474-line-0)">
|
||||||
|
</text><text class="terminal-1316892474-r3" x="0" y="44.4" textLength="12.2" clip-path="url(#terminal-1316892474-line-1)">B</text><text class="terminal-1316892474-r1" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-1316892474-line-1)">
|
||||||
|
</text><text class="terminal-1316892474-r1" x="976" y="68.8" textLength="12.2" clip-path="url(#terminal-1316892474-line-2)">
|
||||||
|
</text><text class="terminal-1316892474-r1" x="976" y="93.2" textLength="12.2" clip-path="url(#terminal-1316892474-line-3)">
|
||||||
|
</text><text class="terminal-1316892474-r1" x="976" y="117.6" textLength="12.2" clip-path="url(#terminal-1316892474-line-4)">
|
||||||
|
</text><text class="terminal-1316892474-r1" x="976" y="142" textLength="12.2" clip-path="url(#terminal-1316892474-line-5)">
|
||||||
|
</text><text class="terminal-1316892474-r1" x="976" y="166.4" textLength="12.2" clip-path="url(#terminal-1316892474-line-6)">
|
||||||
|
</text><text class="terminal-1316892474-r1" x="976" y="190.8" textLength="12.2" clip-path="url(#terminal-1316892474-line-7)">
|
||||||
|
</text><text class="terminal-1316892474-r1" x="976" y="215.2" textLength="12.2" clip-path="url(#terminal-1316892474-line-8)">
|
||||||
|
</text><text class="terminal-1316892474-r1" x="976" y="239.6" textLength="12.2" clip-path="url(#terminal-1316892474-line-9)">
|
||||||
|
</text><text class="terminal-1316892474-r1" x="976" y="264" textLength="12.2" clip-path="url(#terminal-1316892474-line-10)">
|
||||||
|
</text><text class="terminal-1316892474-r1" x="976" y="288.4" textLength="12.2" clip-path="url(#terminal-1316892474-line-11)">
|
||||||
|
</text><text class="terminal-1316892474-r1" x="976" y="312.8" textLength="12.2" clip-path="url(#terminal-1316892474-line-12)">
|
||||||
|
</text><text class="terminal-1316892474-r1" x="976" y="337.2" textLength="12.2" clip-path="url(#terminal-1316892474-line-13)">
|
||||||
|
</text><text class="terminal-1316892474-r1" x="976" y="361.6" textLength="12.2" clip-path="url(#terminal-1316892474-line-14)">
|
||||||
|
</text><text class="terminal-1316892474-r1" x="976" y="386" textLength="12.2" clip-path="url(#terminal-1316892474-line-15)">
|
||||||
|
</text><text class="terminal-1316892474-r1" x="976" y="410.4" textLength="12.2" clip-path="url(#terminal-1316892474-line-16)">
|
||||||
|
</text><text class="terminal-1316892474-r1" x="976" y="434.8" textLength="12.2" clip-path="url(#terminal-1316892474-line-17)">
|
||||||
|
</text><text class="terminal-1316892474-r1" x="976" y="459.2" textLength="12.2" clip-path="url(#terminal-1316892474-line-18)">
|
||||||
|
</text><text class="terminal-1316892474-r1" x="976" y="483.6" textLength="12.2" clip-path="url(#terminal-1316892474-line-19)">
|
||||||
|
</text><text class="terminal-1316892474-r1" x="976" y="508" textLength="12.2" clip-path="url(#terminal-1316892474-line-20)">
|
||||||
|
</text><text class="terminal-1316892474-r1" x="976" y="532.4" textLength="12.2" clip-path="url(#terminal-1316892474-line-21)">
|
||||||
|
</text><text class="terminal-1316892474-r1" x="976" y="556.8" textLength="12.2" clip-path="url(#terminal-1316892474-line-22)">
|
||||||
|
</text><text class="terminal-1316892474-r4" x="0" y="581.2" textLength="36.6" clip-path="url(#terminal-1316892474-line-23)"> A </text><text class="terminal-1316892474-r5" x="36.6" y="581.2" textLength="183" clip-path="url(#terminal-1316892474-line-23)"> Push screen A </text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
'''
|
||||||
|
# ---
|
||||||
# name: test_switches
|
# name: test_switches
|
||||||
'''
|
'''
|
||||||
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
|
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
|||||||
38
tests/snapshot_tests/snapshot_apps/screen_switch.py
Normal file
38
tests/snapshot_tests/snapshot_apps/screen_switch.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.screen import Screen
|
||||||
|
from textual.widgets import Static, Header, Footer
|
||||||
|
|
||||||
|
|
||||||
|
class ScreenA(Screen):
|
||||||
|
BINDINGS = [("b", "switch_to_b", "Switch to screen B")]
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Header()
|
||||||
|
yield Static("A")
|
||||||
|
yield Footer()
|
||||||
|
|
||||||
|
def action_switch_to_b(self):
|
||||||
|
self.app.switch_screen(ScreenB())
|
||||||
|
|
||||||
|
|
||||||
|
class ScreenB(Screen):
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Header()
|
||||||
|
yield Static("B")
|
||||||
|
yield Footer()
|
||||||
|
|
||||||
|
|
||||||
|
class ModalApp(App):
|
||||||
|
BINDINGS = [("a", "push_a", "Push screen A")]
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Header()
|
||||||
|
yield Footer()
|
||||||
|
|
||||||
|
def action_push_a(self) -> None:
|
||||||
|
self.push_screen(ScreenA())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = ModalApp()
|
||||||
|
app.run()
|
||||||
@@ -214,3 +214,7 @@ def test_auto_width_input(snap_compare):
|
|||||||
assert snap_compare(
|
assert snap_compare(
|
||||||
SNAPSHOT_APPS_DIR / "auto_width_input.py", press=["tab", *"Hello"]
|
SNAPSHOT_APPS_DIR / "auto_width_input.py", press=["tab", *"Hello"]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_screen_switch(snap_compare):
|
||||||
|
assert snap_compare(SNAPSHOT_APPS_DIR / "screen_switch.py", press=["a", "b"])
|
||||||
|
|||||||
@@ -8,9 +8,13 @@ from textual.containers import Container
|
|||||||
async def test_remove_single_widget():
|
async def test_remove_single_widget():
|
||||||
"""It should be possible to the only widget on a screen."""
|
"""It should be possible to the only widget on a screen."""
|
||||||
async with App().run_test() as pilot:
|
async with App().run_test() as pilot:
|
||||||
await pilot.app.mount(Static())
|
widget = Static()
|
||||||
|
assert not widget.is_attached
|
||||||
|
await pilot.app.mount(widget)
|
||||||
|
assert widget.is_attached
|
||||||
assert len(pilot.app.screen.children) == 1
|
assert len(pilot.app.screen.children) == 1
|
||||||
await pilot.app.query_one(Static).remove()
|
await pilot.app.query_one(Static).remove()
|
||||||
|
assert not widget.is_attached
|
||||||
assert len(pilot.app.screen.children) == 0
|
assert len(pilot.app.screen.children) == 0
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user