mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Merge branch 'main' into in-band-resize
This commit is contained in:
@@ -19,6 +19,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
- `Driver.process_event` is now `Driver.process_message` https://github.com/Textualize/textual/pull/5217
|
||||
- `Driver.send_event` is now `Driver.send_message` https://github.com/Textualize/textual/pull/5217
|
||||
- Added `can_focus` and `can_focus_children` parameters to scrollable container types. https://github.com/Textualize/textual/pull/5226
|
||||
- Added `textual.lazy.Reveal` https://github.com/Textualize/textual/pull/5226
|
||||
- Added `Screen.action_blur` https://github.com/Textualize/textual/pull/5226
|
||||
|
||||
## [0.85.2] - 2024-11-02
|
||||
|
||||
|
||||
@@ -73,6 +73,40 @@ class ScrollableContainer(Widget, can_focus=True, inherit_bindings=False):
|
||||
| ctrl+pagedown | Scroll right one page, if horizontal scrolling is available. |
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*children: Widget,
|
||||
name: str | None = None,
|
||||
id: str | None = None,
|
||||
classes: str | None = None,
|
||||
disabled: bool = False,
|
||||
can_focus: bool | None = None,
|
||||
can_focus_children: bool | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
|
||||
Args:
|
||||
*children: Child widgets.
|
||||
name: The name of the widget.
|
||||
id: The ID of the widget in the DOM.
|
||||
classes: The CSS classes for the widget.
|
||||
disabled: Whether the widget is disabled or not.
|
||||
can_focus: Can this container be focused?
|
||||
can_focus_children: Can this container's children be focused?
|
||||
"""
|
||||
|
||||
super().__init__(
|
||||
*children,
|
||||
name=name,
|
||||
id=id,
|
||||
classes=classes,
|
||||
disabled=disabled,
|
||||
)
|
||||
if can_focus is not None:
|
||||
self.can_focus = can_focus
|
||||
if can_focus_children is not None:
|
||||
self.can_focus_children = can_focus_children
|
||||
|
||||
|
||||
class Vertical(Widget, inherit_bindings=False):
|
||||
"""An expanding container with vertical layout and no scrollbars."""
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from textual.app import App
|
||||
from textual.binding import Binding
|
||||
from textual.demo.home import HomeScreen
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from importlib.metadata import version
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
|
||||
from rich.syntax import Syntax
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from textual import events, on
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import io
|
||||
from math import sin
|
||||
@@ -6,7 +8,7 @@ from rich.syntax import Syntax
|
||||
from rich.table import Table
|
||||
from rich.traceback import Traceback
|
||||
|
||||
from textual import containers
|
||||
from textual import containers, lazy
|
||||
from textual.app import ComposeResult
|
||||
from textual.binding import Binding
|
||||
from textual.demo.data import COUNTRIES
|
||||
@@ -439,19 +441,21 @@ class WidgetsScreen(PageScreen):
|
||||
CSS = """
|
||||
WidgetsScreen {
|
||||
align-horizontal: center;
|
||||
& > VerticalScroll > * {
|
||||
&:last-of-type { margin-bottom: 2; }
|
||||
&:even { background: $boost; }
|
||||
padding-bottom: 1;
|
||||
}
|
||||
& > VerticalScroll {
|
||||
scrollbar-gutter: stable;
|
||||
&> * {
|
||||
&:last-of-type { margin-bottom: 2; }
|
||||
&:even { background: $boost; }
|
||||
padding-bottom: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
BINDINGS = [Binding("escape", "unfocus", "Unfocus any focused widget", show=False)]
|
||||
BINDINGS = [Binding("escape", "blur", "Unfocus any focused widget", show=False)]
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with containers.VerticalScroll() as container:
|
||||
container.can_focus = False
|
||||
with lazy.Reveal(containers.VerticalScroll(can_focus=False)):
|
||||
yield Markdown(WIDGETS_MD, classes="column")
|
||||
yield Buttons()
|
||||
yield Checkboxes()
|
||||
@@ -461,6 +465,3 @@ class WidgetsScreen(PageScreen):
|
||||
yield Logs()
|
||||
yield Sparklines()
|
||||
yield Footer()
|
||||
|
||||
def action_unfocus(self) -> None:
|
||||
self.set_focus(None)
|
||||
|
||||
@@ -4,6 +4,8 @@ Tools for lazy loading widgets.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import partial
|
||||
|
||||
from textual.widget import Widget
|
||||
|
||||
|
||||
@@ -61,3 +63,85 @@ class Lazy(Widget):
|
||||
await self.remove()
|
||||
|
||||
self.call_after_refresh(mount)
|
||||
|
||||
|
||||
class Reveal(Widget):
|
||||
DEFAULT_CSS = """
|
||||
Reveal {
|
||||
display: none;
|
||||
}
|
||||
"""
|
||||
|
||||
def __init__(self, widget: Widget, delay: float = 1 / 60) -> None:
|
||||
"""Similar to [Lazy][textual.lazy.Lazy], but also displays *children* sequentially.
|
||||
|
||||
The first frame will display the first child with all other children hidden.
|
||||
The remaining children will be displayed 1-by-1, over as may frames are required.
|
||||
|
||||
This is useful when you have so many child widgets that there is a noticeable delay before
|
||||
you see anything. By mounting the children over several frames, the user will feel that
|
||||
something is happening.
|
||||
|
||||
Example:
|
||||
```python
|
||||
def compose(self) -> ComposeResult:
|
||||
with lazy.Reveal(containers.VerticalScroll(can_focus=False)):
|
||||
yield Markdown(WIDGETS_MD, classes="column")
|
||||
yield Buttons()
|
||||
yield Checkboxes()
|
||||
yield Datatables()
|
||||
yield Inputs()
|
||||
yield ListViews()
|
||||
yield Logs()
|
||||
yield Sparklines()
|
||||
yield Footer()
|
||||
```
|
||||
|
||||
Args:
|
||||
widget: A widget that should be mounted after a refresh.
|
||||
delay: A (short) delay between mounting widgets.
|
||||
"""
|
||||
self._replace_widget = widget
|
||||
self._delay = delay
|
||||
super().__init__()
|
||||
|
||||
@classmethod
|
||||
def _reveal(cls, parent: Widget, delay: float = 1 / 60) -> None:
|
||||
"""Reveal children lazily.
|
||||
|
||||
Args:
|
||||
parent: The parent widget.
|
||||
delay: A delay between reveals.
|
||||
"""
|
||||
|
||||
def check_children() -> None:
|
||||
"""Check for un-displayed children."""
|
||||
iter_children = iter(parent.children)
|
||||
for child in iter_children:
|
||||
if not child.display:
|
||||
child.display = True
|
||||
break
|
||||
for child in iter_children:
|
||||
if not child.display:
|
||||
parent.set_timer(
|
||||
delay, partial(parent.call_after_refresh, check_children)
|
||||
)
|
||||
break
|
||||
|
||||
check_children()
|
||||
|
||||
def compose_add_child(self, widget: Widget) -> None:
|
||||
widget.display = False
|
||||
self._replace_widget.compose_add_child(widget)
|
||||
|
||||
async def mount_composed_widgets(self, widgets: list[Widget]) -> None:
|
||||
parent = self.parent
|
||||
if parent is None:
|
||||
return
|
||||
assert isinstance(parent, Widget)
|
||||
|
||||
if self._replace_widget.children:
|
||||
self._replace_widget.children[0].display = True
|
||||
await parent.mount(self._replace_widget, after=self)
|
||||
await self.remove()
|
||||
self._reveal(self._replace_widget, self._delay)
|
||||
|
||||
@@ -780,6 +780,10 @@ class Screen(Generic[ScreenResultType], Widget):
|
||||
"""Action to minimize the currently maximized widget."""
|
||||
self.minimize()
|
||||
|
||||
def action_blur(self) -> None:
|
||||
"""Action to remove focus (if set)."""
|
||||
self.set_focus(None)
|
||||
|
||||
def _reset_focus(
|
||||
self, widget: Widget, avoiding: list[Widget] | None = None
|
||||
) -> None:
|
||||
|
||||
10
tests/test_demo.py
Normal file
10
tests/test_demo.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from textual.demo.demo_app import DemoApp
|
||||
|
||||
|
||||
async def test_demo():
|
||||
"""Test the demo runs."""
|
||||
# Test he demo can at least run.
|
||||
# This exists mainly to catch screw-ups that might effect only certain Python versions.
|
||||
app = DemoApp()
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause(0.1)
|
||||
@@ -1,6 +1,6 @@
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.containers import Horizontal, Vertical
|
||||
from textual.lazy import Lazy
|
||||
from textual.lazy import Lazy, Reveal
|
||||
from textual.widgets import Label
|
||||
|
||||
|
||||
@@ -24,3 +24,32 @@ async def test_lazy():
|
||||
# #bar mounted after refresh
|
||||
assert len(app.query("#foo")) == 1
|
||||
assert len(app.query("#bar")) == 1
|
||||
|
||||
|
||||
class RevealApp(App):
|
||||
def compose(self) -> ComposeResult:
|
||||
with Reveal(Vertical()):
|
||||
yield Label(id="foo")
|
||||
yield Label(id="bar")
|
||||
yield Label(id="baz")
|
||||
|
||||
|
||||
async def test_lazy_reveal():
|
||||
app = RevealApp()
|
||||
async with app.run_test() as pilot:
|
||||
# No #foo on initial mount
|
||||
|
||||
# Only first child should be visible initially
|
||||
assert app.query_one("#foo").display
|
||||
assert not app.query_one("#bar").display
|
||||
assert not app.query_one("#baz").display
|
||||
|
||||
# All children should be visible after a pause
|
||||
await pilot.pause()
|
||||
for n in range(3):
|
||||
await pilot.pause(1 / 60)
|
||||
await pilot.pause()
|
||||
|
||||
assert app.query_one("#foo").display
|
||||
assert app.query_one("#bar").display
|
||||
assert app.query_one("#baz").display
|
||||
|
||||
Reference in New Issue
Block a user