Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions src/textual/css/stylesheet.py
Original file line number Diff line number Diff line change
Expand Up @@ -637,7 +637,6 @@ def apply(

cache_key: tuple | None = None
path_key: tuple | None = None
css_path_nodes: list[DOMNode] | None = None

if cache is not None and all_pseudo_classes.isdisjoint(
self._EXCLUDE_PSEUDO_CLASSES_FROM_CACHE
Expand All @@ -660,12 +659,20 @@ def apply(
self._process_component_classes(node)
return

# Matching already declares whether a group addresses its target
# alone. Read the live declaration only after the first cache miss;
# selector lists remain editable and hits need no path planning.
requires_path = any(
not group.is_compound for rule in rules for group in rule.selector_set
)
css_path_nodes = node.css_path_nodes if requires_path else [node]

if cache_key is not None:
# Widgets in repeated rows have distinct parent *instances* but
# frequently the same selector/pseudo-class ancestry. A CSS rule
# cannot distinguish these paths when positional/focus-within
# selectors were excluded above. Share resolved rules within this
# update batch rather than matching every one from scratch.
css_path_nodes = node.css_path_nodes
path_key = (
"css_path",
rule_key,
Expand All @@ -681,9 +688,6 @@ def apply(
self._process_component_classes(node)
return

if css_path_nodes is None:
css_path_nodes = node.css_path_nodes

# Rules that may be set to the special value `initial`
initial: set[str] = set()
# Rules in DEFAULT_CSS set to the special value `initial`
Expand Down
26 changes: 26 additions & 0 deletions tests/css/test_stylesheet.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,32 @@
from textual.widgets import Static


def test_target_rule_publication_acquires_only_declared_css_path():
class PathReads(DOMNode):
path_reads = 0

@property
def css_path_nodes(self):
self.path_reads += 1
return super().css_path_nodes

parent = DOMNode(classes="outer")
node = PathReads(classes="leaf")
node._attach(parent)
stylesheet = Stylesheet()
stylesheet.add_source(".leaf { color: red; }")
stylesheet.apply(node)
assert node.styles.color == Color.parse("red")
assert node.path_reads == 0

# Reparse a real child declaration: full ancestry and specificity must
# become authoritative again, not the previous target-only key.
stylesheet.add_source(".outer > .leaf { color: blue; }")
stylesheet.apply(node)
assert node.styles.color == Color.parse("blue")
assert node.path_reads == 1


async def test_equal_current_rules_retarget_a_pending_transition():
app = App()
async with app.run_test() as pilot:
Expand Down
Loading