lazy reveal

This commit is contained in:
Will McGugan
2024-11-11 15:30:14 +00:00
parent a3fee688bb
commit 22ff9337de
10 changed files with 179 additions and 9 deletions

View File

@@ -11,6 +11,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Fixed duplicated key displays in the help panel https://github.com/Textualize/textual/issues/5037
### Added
- Added `can_focus` and `can_focus_children` parameters to scrollable container types.
- Added `textual.lazy.Reveal`
## [0.85.2] - 2024-11-02
- Fixed broken focus-within https://github.com/Textualize/textual/pull/5190

View File

@@ -73,6 +73,38 @@ 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 = True,
can_focus_children: bool = True,
) -> 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,
)
self.can_focus = can_focus
self.can_focus_children = can_focus_children
class Vertical(Widget, inherit_bindings=False):
"""An expanding container with vertical layout and no scrollbars."""

View File

@@ -1,3 +1,5 @@
from __future__ import annotations
from textual.app import App
from textual.binding import Binding
from textual.demo.home import HomeScreen

View File

@@ -1,3 +1,5 @@
from __future__ import annotations
import asyncio
from importlib.metadata import version

View File

@@ -1,3 +1,5 @@
from __future__ import annotations
import inspect
from rich.syntax import Syntax

View File

@@ -1,3 +1,5 @@
from __future__ import annotations
from dataclasses import dataclass
from textual import events, on

View File

@@ -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,22 @@ 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)]
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()

View File

@@ -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)

10
tests/test_demo.py Normal file
View 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)

View File

@@ -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,29 @@ 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()
assert app.query_one("#foo").display
assert app.query_one("#bar").display
assert app.query_one("#baz").display