Skip to content

Commit 1267403

Browse files
author
Sergi Pons Freixes
committed
Add support for first-child and last-child pseudo-classes
They work almost identically than the existing `first-of-type` and `last-of-type`, with the difference that they don't distinguish between widget type.
1 parent 7f5ed0e commit 1267403

File tree

4 files changed

+41
-3
lines changed

4 files changed

+41
-3
lines changed

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)