Skip to content

Commit 8778814

Browse files
authored
Merge pull request #5999 from Textualize/empty-class
empty class
2 parents f754852 + 7a83455 commit 8778814

File tree

18 files changed

+305
-13
lines changed

18 files changed

+305
-13
lines changed

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,21 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](http://keepachangelog.com/)
66
and this project adheres to [Semantic Versioning](http://semver.org/).
77

8+
## Unrleeased
9+
10+
### Added
11+
12+
- Added `empty` pseudo class, which applies when a widget has no displayed children https://github.com/Textualize/textual/pull/5999
13+
- Added `Screen.action_focus` https://github.com/Textualize/textual/pull/5999
14+
15+
### Changed
16+
17+
- `last-child`, `last-of-type`, `first-child`, and `first-of-type` apply to displayed children only https://github.com/Textualize/textual/pull/5999
18+
- `textual.compose` is now public https://github.com/Textualize/textual/pull/5999
19+
820
## [5.0.1] - 2025-07-25
921

22+
1023
### Fixed
1124

1225
- Fixed appending to Markdown widgets that were constructed with an existing document https://github.com/Textualize/textual/pull/5990

docs/api/compose.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
title: "textual.compose"
3+
---
4+
5+
::: textual.compose

mkdocs-nav.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ nav:
194194
- "api/command.md"
195195
- "api/constants.md"
196196
- "api/containers.md"
197+
- "api/compose.md"
197198
- "api/content.md"
198199
- "api/coordinate.md"
199200
- "api/dom_node.md"

src/textual/_node_list.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import sys
44
import weakref
55
from operator import attrgetter
6-
from typing import TYPE_CHECKING, Any, Callable, Iterator, Sequence, overload
6+
from typing import TYPE_CHECKING, Any, Callable, Iterable, Iterator, Sequence, overload
77

88
import rich.repr
99

@@ -186,6 +186,20 @@ def __iter__(self) -> Iterator[Widget]:
186186
def __reversed__(self) -> Iterator[Widget]:
187187
return reversed(self._nodes)
188188

189+
@property
190+
def displayed(self) -> Iterable[Widget]:
191+
"""Just the nodes where `display==True`."""
192+
for node in self._nodes:
193+
if node.display:
194+
yield node
195+
196+
@property
197+
def displayed_reverse(self) -> Iterable[Widget]:
198+
"""Just the nodes where `display==True`, in reverse order."""
199+
for node in reversed(self._nodes):
200+
if node.display:
201+
yield node
202+
189203
if TYPE_CHECKING:
190204

191205
@overload

src/textual/app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,6 @@
7575
from textual._ansi_theme import ALABASTER, MONOKAI
7676
from textual._callback import invoke
7777
from textual._compat import cached_property
78-
from textual._compose import compose
7978
from textual._compositor import CompositorUpdate
8079
from textual._context import active_app, active_message_pump
8180
from textual._context import message_hook as message_hook_context_var
@@ -94,6 +93,7 @@
9493
from textual.await_remove import AwaitRemove
9594
from textual.binding import Binding, BindingsMap, BindingType, Keymap
9695
from textual.command import CommandListItem, CommandPalette, Provider, SimpleProvider
96+
from textual.compose import compose
9797
from textual.css.errors import StylesheetError
9898
from textual.css.query import NoMatches
9999
from textual.css.stylesheet import RulesMap, Stylesheet

src/textual/_compose.py renamed to src/textual/compose.py

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,44 @@
1+
"""
2+
3+
The compose method allows you to mount widgets using the same syntax as the [compose][textual.widget.Widget.compose] method.
4+
5+
```python
6+
7+
def on_key(self, event:events.Key) -> None:
8+
9+
def add_key(key:str) -> ComposeResult:
10+
with containers.HorizontalGroup():
11+
yield Label("You pressed:")
12+
yield Label(key)
13+
14+
self.mount_all(
15+
compose(self, add_key(event.key)),
16+
)
17+
18+
```
19+
20+
21+
"""
22+
123
from __future__ import annotations
224

325
from typing import TYPE_CHECKING
426

527
if TYPE_CHECKING:
6-
from textual.app import App
28+
from textual.app import App, ComposeResult
729
from textual.widget import Widget
830

31+
__all__ = ["compose"]
32+
933

10-
def compose(node: App | Widget) -> list[Widget]:
34+
def compose(
35+
node: App | Widget, compose_result: ComposeResult | None = None
36+
) -> list[Widget]:
1137
"""Compose child widgets.
1238
1339
Args:
1440
node: The parent node.
41+
compose_result: A compose result, or `None` to call `node.compose()`.
1542
1643
Returns:
1744
A list of widgets.
@@ -25,7 +52,9 @@ def compose(node: App | Widget) -> list[Widget]:
2552
composed: list[Widget] = []
2653
app._compose_stacks.append(compose_stack)
2754
app._composed.append(composed)
28-
iter_compose = iter(node.compose())
55+
iter_compose = iter(
56+
compose_result if compose_result is not None else node.compose()
57+
)
2958
is_generator = hasattr(iter_compose, "throw")
3059
try:
3160
while True:

src/textual/css/_style_properties.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -783,6 +783,7 @@ class StringEnumProperty(Generic[EnumType]):
783783
default: The default value (or a factory thereof) of the property.
784784
layout: Whether to refresh the node layout on value change.
785785
refresh_children: Whether to refresh the node children on value change.
786+
display: Does this property change display?
786787
"""
787788

788789
def __init__(
@@ -792,12 +793,14 @@ def __init__(
792793
layout: bool = False,
793794
refresh_children: bool = False,
794795
refresh_parent: bool = False,
796+
display: bool = False,
795797
) -> None:
796798
self._valid_values = valid_values
797799
self._default = default
798800
self._layout = layout
799801
self._refresh_children = refresh_children
800802
self._refresh_parent = refresh_parent
803+
self._display = display
801804

802805
def __set_name__(self, owner: StylesBase, name: str) -> None:
803806
self.name = name
@@ -849,6 +852,12 @@ def __set__(self, obj: StylesBase, value: EnumType | None = None):
849852
),
850853
)
851854
if obj.set_rule(self.name, value):
855+
if self._display and obj.node is not None:
856+
node = obj.node
857+
if node.parent:
858+
node._nodes.updated()
859+
node.parent._refresh_styles()
860+
852861
self._before_refresh(obj, value)
853862
obj.refresh(
854863
layout=self._layout,

src/textual/css/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
"last-child",
8181
"odd",
8282
"even",
83+
"empty",
8384
}
8485
VALID_OVERLAY: Final = {"none", "screen"}
8586
VALID_CONSTRAIN: Final = {"inflect", "inside", "none"}

src/textual/css/styles.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ class StylesBase:
253253

254254
node: DOMNode | None = None
255255

256-
display = StringEnumProperty(VALID_DISPLAY, "block", layout=True)
256+
display = StringEnumProperty(VALID_DISPLAY, "block", layout=True, display=True)
257257
"""Set the display of the widget, defining how it's rendered.
258258
259259
Valid values are "block" or "none".

src/textual/css/stylesheet.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,7 @@ def _check_rule(
464464
"odd",
465465
"even",
466466
"focus-within",
467+
"empty",
467468
}
468469

469470
def apply(
@@ -505,7 +506,7 @@ def apply(
505506
node._has_hover_style = "hover" in all_pseudo_classes
506507
node._has_focus_within = "focus-within" in all_pseudo_classes
507508
node._has_order_style = not all_pseudo_classes.isdisjoint(
508-
{"first-of-type", "last-of-type", "first-child", "last-child"}
509+
{"first-of-type", "last-of-type", "first-child", "last-child", "empty"}
509510
)
510511
node._has_odd_or_even = (
511512
"odd" in all_pseudo_classes or "even" in all_pseudo_classes

0 commit comments

Comments
 (0)