Skip to content

Commit f2fa5ce

Browse files
authored
Merge branch 'main' into in-band-resize
2 parents 31d9ad9 + da1178f commit f2fa5ce

File tree

11 files changed

+186
-13
lines changed

11 files changed

+186
-13
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1919

2020
- `Driver.process_event` is now `Driver.process_message` https://github.com/Textualize/textual/pull/5217
2121
- `Driver.send_event` is now `Driver.send_message` https://github.com/Textualize/textual/pull/5217
22+
- Added `can_focus` and `can_focus_children` parameters to scrollable container types. https://github.com/Textualize/textual/pull/5226
23+
- Added `textual.lazy.Reveal` https://github.com/Textualize/textual/pull/5226
24+
- Added `Screen.action_blur` https://github.com/Textualize/textual/pull/5226
2225

2326
## [0.85.2] - 2024-11-02
2427

src/textual/containers.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,40 @@ class ScrollableContainer(Widget, can_focus=True, inherit_bindings=False):
7373
| ctrl+pagedown | Scroll right one page, if horizontal scrolling is available. |
7474
"""
7575

76+
def __init__(
77+
self,
78+
*children: Widget,
79+
name: str | None = None,
80+
id: str | None = None,
81+
classes: str | None = None,
82+
disabled: bool = False,
83+
can_focus: bool | None = None,
84+
can_focus_children: bool | None = None,
85+
) -> None:
86+
"""
87+
88+
Args:
89+
*children: Child widgets.
90+
name: The name of the widget.
91+
id: The ID of the widget in the DOM.
92+
classes: The CSS classes for the widget.
93+
disabled: Whether the widget is disabled or not.
94+
can_focus: Can this container be focused?
95+
can_focus_children: Can this container's children be focused?
96+
"""
97+
98+
super().__init__(
99+
*children,
100+
name=name,
101+
id=id,
102+
classes=classes,
103+
disabled=disabled,
104+
)
105+
if can_focus is not None:
106+
self.can_focus = can_focus
107+
if can_focus_children is not None:
108+
self.can_focus_children = can_focus_children
109+
76110

77111
class Vertical(Widget, inherit_bindings=False):
78112
"""An expanding container with vertical layout and no scrollbars."""

src/textual/demo/demo_app.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
from textual.app import App
24
from textual.binding import Binding
35
from textual.demo.home import HomeScreen

src/textual/demo/home.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
import asyncio
24
from importlib.metadata import version
35

src/textual/demo/page.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
import inspect
24

35
from rich.syntax import Syntax

src/textual/demo/projects.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
from dataclasses import dataclass
24

35
from textual import events, on

src/textual/demo/widgets.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
import csv
24
import io
35
from math import sin
@@ -6,7 +8,7 @@
68
from rich.table import Table
79
from rich.traceback import Traceback
810

9-
from textual import containers
11+
from textual import containers, lazy
1012
from textual.app import ComposeResult
1113
from textual.binding import Binding
1214
from textual.demo.data import COUNTRIES
@@ -439,19 +441,21 @@ class WidgetsScreen(PageScreen):
439441
CSS = """
440442
WidgetsScreen {
441443
align-horizontal: center;
442-
& > VerticalScroll > * {
443-
&:last-of-type { margin-bottom: 2; }
444-
&:even { background: $boost; }
445-
padding-bottom: 1;
446-
}
444+
& > VerticalScroll {
445+
scrollbar-gutter: stable;
446+
&> * {
447+
&:last-of-type { margin-bottom: 2; }
448+
&:even { background: $boost; }
449+
padding-bottom: 1;
450+
}
451+
}
447452
}
448453
"""
449454

450-
BINDINGS = [Binding("escape", "unfocus", "Unfocus any focused widget", show=False)]
455+
BINDINGS = [Binding("escape", "blur", "Unfocus any focused widget", show=False)]
451456

452457
def compose(self) -> ComposeResult:
453-
with containers.VerticalScroll() as container:
454-
container.can_focus = False
458+
with lazy.Reveal(containers.VerticalScroll(can_focus=False)):
455459
yield Markdown(WIDGETS_MD, classes="column")
456460
yield Buttons()
457461
yield Checkboxes()
@@ -461,6 +465,3 @@ def compose(self) -> ComposeResult:
461465
yield Logs()
462466
yield Sparklines()
463467
yield Footer()
464-
465-
def action_unfocus(self) -> None:
466-
self.set_focus(None)

src/textual/lazy.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
from __future__ import annotations
66

7+
from functools import partial
8+
79
from textual.widget import Widget
810

911

@@ -61,3 +63,85 @@ async def mount() -> None:
6163
await self.remove()
6264

6365
self.call_after_refresh(mount)
66+
67+
68+
class Reveal(Widget):
69+
DEFAULT_CSS = """
70+
Reveal {
71+
display: none;
72+
}
73+
"""
74+
75+
def __init__(self, widget: Widget, delay: float = 1 / 60) -> None:
76+
"""Similar to [Lazy][textual.lazy.Lazy], but also displays *children* sequentially.
77+
78+
The first frame will display the first child with all other children hidden.
79+
The remaining children will be displayed 1-by-1, over as may frames are required.
80+
81+
This is useful when you have so many child widgets that there is a noticeable delay before
82+
you see anything. By mounting the children over several frames, the user will feel that
83+
something is happening.
84+
85+
Example:
86+
```python
87+
def compose(self) -> ComposeResult:
88+
with lazy.Reveal(containers.VerticalScroll(can_focus=False)):
89+
yield Markdown(WIDGETS_MD, classes="column")
90+
yield Buttons()
91+
yield Checkboxes()
92+
yield Datatables()
93+
yield Inputs()
94+
yield ListViews()
95+
yield Logs()
96+
yield Sparklines()
97+
yield Footer()
98+
```
99+
100+
Args:
101+
widget: A widget that should be mounted after a refresh.
102+
delay: A (short) delay between mounting widgets.
103+
"""
104+
self._replace_widget = widget
105+
self._delay = delay
106+
super().__init__()
107+
108+
@classmethod
109+
def _reveal(cls, parent: Widget, delay: float = 1 / 60) -> None:
110+
"""Reveal children lazily.
111+
112+
Args:
113+
parent: The parent widget.
114+
delay: A delay between reveals.
115+
"""
116+
117+
def check_children() -> None:
118+
"""Check for un-displayed children."""
119+
iter_children = iter(parent.children)
120+
for child in iter_children:
121+
if not child.display:
122+
child.display = True
123+
break
124+
for child in iter_children:
125+
if not child.display:
126+
parent.set_timer(
127+
delay, partial(parent.call_after_refresh, check_children)
128+
)
129+
break
130+
131+
check_children()
132+
133+
def compose_add_child(self, widget: Widget) -> None:
134+
widget.display = False
135+
self._replace_widget.compose_add_child(widget)
136+
137+
async def mount_composed_widgets(self, widgets: list[Widget]) -> None:
138+
parent = self.parent
139+
if parent is None:
140+
return
141+
assert isinstance(parent, Widget)
142+
143+
if self._replace_widget.children:
144+
self._replace_widget.children[0].display = True
145+
await parent.mount(self._replace_widget, after=self)
146+
await self.remove()
147+
self._reveal(self._replace_widget, self._delay)

src/textual/screen.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -780,6 +780,10 @@ def action_minimize(self) -> None:
780780
"""Action to minimize the currently maximized widget."""
781781
self.minimize()
782782

783+
def action_blur(self) -> None:
784+
"""Action to remove focus (if set)."""
785+
self.set_focus(None)
786+
783787
def _reset_focus(
784788
self, widget: Widget, avoiding: list[Widget] | None = None
785789
) -> None:

tests/test_demo.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from textual.demo.demo_app import DemoApp
2+
3+
4+
async def test_demo():
5+
"""Test the demo runs."""
6+
# Test he demo can at least run.
7+
# This exists mainly to catch screw-ups that might effect only certain Python versions.
8+
app = DemoApp()
9+
async with app.run_test() as pilot:
10+
await pilot.pause(0.1)

0 commit comments

Comments
 (0)