Skip to content

Commit 1aba013

Browse files
authored
Merge pull request #5776 from sponsfreixes/feature/first_child
Add support for `first-child` and `last-child` pseudo-classes
2 parents 9095140 + d60be19 commit 1aba013

File tree

10 files changed

+244
-3
lines changed

10 files changed

+244
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1010
### Fixed
1111

1212
- Fixed `OptionList` causing excessive redrawing https://github.com/Textualize/textual/pull/5766
13+
- Added `:first-child` and `:last-child` pseudo classes https://github.com/Textualize/textual/pull/5776
1314
- Log messages could be written to stdout when there was no app, which could happen when using run_async or threads. Now they will be suppressed, unless the env var `TEXTUAL_DEBUG` is set https://github.com/Textualize/textual/pull/5782
1415

1516
### Added

docs/guide/CSS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,10 +343,12 @@ Here are some other pseudo classes:
343343
- `:disabled` Matches widgets which are in a disabled state.
344344
- `:enabled` Matches widgets which are in an enabled state.
345345
- `:even` Matches a widget at an evenly numbered position within its siblings.
346+
- `:first-child` Matches a widget that is the first amongst its siblings.
346347
- `:first-of-type` Matches a widget that is the first of its type amongst its siblings.
347348
- `:focus-within` Matches widgets with a focused child widget.
348349
- `:focus` Matches widgets which have input focus.
349350
- `:inline` Matches widgets when the app is running in inline mode.
351+
- `:last-child` Matches a widget that is the last amongst its siblings.
350352
- `:last-of-type` Matches a widget that is the last of its type amongst its siblings.
351353
- `:light` Matches widgets in light themes (where `App.theme.dark == False`).
352354
- `:odd` Matches a widget at an oddly numbered position within its siblings.

src/textual/css/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@
7676
"nocolor",
7777
"first-of-type",
7878
"last-of-type",
79+
"first-child",
80+
"last-child",
7981
"odd",
8082
"even",
8183
}

src/textual/css/stylesheet.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,8 @@ def _check_rule(
459459
_EXCLUDE_PSEUDO_CLASSES_FROM_CACHE: Final[set[str]] = {
460460
"first-of-type",
461461
"last-of-type",
462+
"first-child",
463+
"last-child",
462464
"odd",
463465
"even",
464466
"focus-within",
@@ -503,7 +505,7 @@ def apply(
503505
node._has_hover_style = "hover" in all_pseudo_classes
504506
node._has_focus_within = "focus-within" in all_pseudo_classes
505507
node._has_order_style = not all_pseudo_classes.isdisjoint(
506-
{"first-of-type", "last-of-type"}
508+
{"first-of-type", "last-of-type", "first-child", "last-child"}
507509
)
508510
node._has_odd_or_even = (
509511
"odd" in all_pseudo_classes or "even" in all_pseudo_classes

src/textual/dom.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ def __init__(
224224
self._has_hover_style: bool = False
225225
self._has_focus_within: bool = False
226226
self._has_order_style: bool = False
227-
"""The node has an ordered dependent pseudo-style (`:odd`, `:even`, `:first-of-type`, `:last-of-type`)"""
227+
"""The node has an ordered dependent pseudo-style (`:odd`, `:even`, `:first-of-type`, `:last-of-type`, `:first-child`, `:last-child`)"""
228228
self._has_odd_or_even: bool = False
229229
"""The node has the pseudo class `odd` or `even`."""
230230
self._reactive_connect: (

src/textual/widget.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,8 @@ class Widget(DOMNode):
389389
"nocolor": lambda widget: widget.app.no_color,
390390
"first-of-type": lambda widget: widget.first_of_type,
391391
"last-of-type": lambda widget: widget.last_of_type,
392+
"first-child": lambda widget: widget.first_child,
393+
"last-child": lambda widget: widget.last_child,
392394
"odd": lambda widget: widget.is_odd,
393395
"even": lambda widget: widget.is_even,
394396
} # type: ignore[assignment]
@@ -500,6 +502,10 @@ def __init__(
500502
"""Used to cache :first-of-type pseudoclass state."""
501503
self._last_of_type: tuple[int, bool] = (-1, False)
502504
"""Used to cache :last-of-type pseudoclass state."""
505+
self._first_child: tuple[int, bool] = (-1, False)
506+
"""Used to cache :first-child pseudoclass state."""
507+
self._last_child: tuple[int, bool] = (-1, False)
508+
"""Used to cache :last-child pseudoclass state."""
503509
self._odd: tuple[int, bool] = (-1, False)
504510
"""Used to cache :odd pseudoclass state."""
505511
self._last_scroll_time = monotonic()
@@ -852,6 +858,34 @@ def last_of_type(self) -> bool:
852858
return self._last_of_type[1]
853859
return False
854860

861+
@property
862+
def first_child(self) -> bool:
863+
"""Is this the first widget in its siblings?"""
864+
parent = self.parent
865+
if parent is None:
866+
return True
867+
# This pseudo class only changes when the parent's nodes._updates changes
868+
if parent._nodes._updates == self._first_child[0]:
869+
return self._first_child[1]
870+
for node in parent._nodes:
871+
self._first_child = (parent._nodes._updates, node is self)
872+
return self._first_child[1]
873+
return False
874+
875+
@property
876+
def last_child(self) -> bool:
877+
"""Is this the last widget in its siblings?"""
878+
parent = self.parent
879+
if parent is None:
880+
return True
881+
# This pseudo class only changes when the parent's nodes._updates changes
882+
if parent._nodes._updates == self._last_child[0]:
883+
return self._last_child[1]
884+
for node in reversed(parent._nodes):
885+
self._last_child = (parent._nodes._updates, node is self)
886+
return self._last_child[1]
887+
return False
888+
855889
@property
856890
def is_odd(self) -> bool:
857891
"""Is this widget at an oddly numbered position within its siblings?"""
@@ -1304,7 +1338,7 @@ def update_styles(children: list[DOMNode]) -> None:
13041338
"""Update order related CSS"""
13051339
if before is not None or after is not None:
13061340
# If the new children aren't at the end.
1307-
# we need to update both odd/even and first-of-type/last-of-type
1341+
# we need to update both odd/even, first-of-type/last-of-type and first-child/last-child
13081342
for child in children:
13091343
if child._has_order_style or child._has_odd_or_even:
13101344
child._update_styles()

0 commit comments

Comments
 (0)