diff --git a/examples/basic.py b/examples/basic.py index 1c9060c51..6cfa20e5b 100644 --- a/examples/basic.py +++ b/examples/basic.py @@ -10,11 +10,13 @@ class BasicApp(App): App > DockView { layout: dock; docks: sidebar=left widgets=top; + layers: base panels; } #sidebar { dock-group: sidebar; width: 40; + layer: panels; } #widget1 { diff --git a/src/textual/css/_style_properties.py b/src/textual/css/_style_properties.py index 2f956b41f..77b1fcf94 100644 --- a/src/textual/css/_style_properties.py +++ b/src/textual/css/_style_properties.py @@ -312,7 +312,7 @@ class NameProperty: self._internal_name = f"_rule_{name}" def __get__(self, obj: Styles, objtype: type[Styles] | None) -> str: - return getattr(obj, self._internal_name, None) or "" + return getattr(obj, self._internal_name) or "" def __set__(self, obj: Styles, name: str | None) -> str | None: if not isinstance(name, str): diff --git a/src/textual/css/_styles_builder.py b/src/textual/css/_styles_builder.py index 212aee699..f7e112f40 100644 --- a/src/textual/css/_styles_builder.py +++ b/src/textual/css/_styles_builder.py @@ -320,3 +320,16 @@ class StylesBuilder: self.styles.dock_edge = tokens[0].value if tokens else "" except StyleValueError as error: self.error(name, tokens[0], str(error)) + + def process_layer(self, name: str, tokens: list[Token]) -> None: + if len(tokens) > 1: + self.error(name, tokens[1], f"unexpected tokens in dock-edge declaration") + self.styles._rule_layer = tokens[0].value + + def process_layers(self, name: str, tokens: list[Token]) -> None: + layers: list[str] = [] + for token in tokens: + if token.name != "token": + self.error(name, token, "{token.name} not expected here") + layers.append(token.value) + self.styles._rule_layers = tuple(layers) diff --git a/src/textual/css/match.py b/src/textual/css/match.py index f36a7fe04..94b3d989c 100644 --- a/src/textual/css/match.py +++ b/src/textual/css/match.py @@ -8,11 +8,10 @@ if TYPE_CHECKING: from ..dom import DOMNode -def match(selector_sets: Iterable[SelectorSet], node: DOMNode): - for selector_set in selector_sets: - if _check_selectors(selector_set.selectors, node): - return True - return False +def match(selector_sets: Iterable[SelectorSet], node: DOMNode) -> bool: + return any( + _check_selectors(selector_set.selectors, node) for selector_set in selector_sets + ) def _check_selectors(selectors: list[Selector], node: DOMNode) -> bool: diff --git a/src/textual/css/model.py b/src/textual/css/model.py index c77b334be..ea400730e 100644 --- a/src/textual/css/model.py +++ b/src/textual/css/model.py @@ -32,10 +32,6 @@ class Location: column: tuple[int, int] -def _default_check(node: DOMNode) -> bool | None: - return True - - @dataclass class Selector: name: str @@ -67,27 +63,27 @@ class Selector: SelectorType.ID: self._check_id, } - def check(self, node: DOMNode) -> bool | None: + def check(self, node: DOMNode) -> bool: return self._checks[self.type](node) - def _check_universal(self, node: DOMNode) -> bool | None: + def _check_universal(self, node: DOMNode) -> bool: return True - def _check_type(self, node: DOMNode) -> bool | None: + def _check_type(self, node: DOMNode) -> bool: if node.css_type != self._name_lower: return False if self.pseudo_classes and not node.has_psuedo_class(*self.pseudo_classes): return False return True - def _check_class(self, node: DOMNode) -> bool | None: + def _check_class(self, node: DOMNode) -> bool: if not node.has_class(self._name_lower): return False if self.pseudo_classes and not node.has_psuedo_class(*self.pseudo_classes): return False return True - def _check_id(self, node: DOMNode) -> bool | None: + def _check_id(self, node: DOMNode) -> bool: if not node.id == self._name_lower: return False if self.pseudo_classes and not node.has_psuedo_class(*self.pseudo_classes): @@ -110,9 +106,8 @@ class SelectorSet: def __post_init__(self) -> None: SAME = CombinatorType.SAME - # self.selectors[-1].advance = 1 for selector, next_selector in zip(self.selectors, self.selectors[1:]): - selector.advance = 0 if next_selector.combinator == SAME else 1 + selector.advance = int(next_selector.combinator != SAME) def __rich_repr__(self) -> rich.repr.Result: selectors = RuleSet._selector_to_css(self.selectors) @@ -157,6 +152,11 @@ class RuleSet: @property def css(self) -> str: + """Generate the CSS this RuleSet + + Returns: + str: A string containing CSS code. + """ declarations = "\n".join(f" {line}" for line in self.styles.css_lines) css = f"{self.selectors} {{\n{declarations}\n}}" return css diff --git a/src/textual/css/styles.py b/src/textual/css/styles.py index 692640fe1..54f4bdea2 100644 --- a/src/textual/css/styles.py +++ b/src/textual/css/styles.py @@ -74,8 +74,8 @@ class Styles: _rule_dock_edge: str | None = None _rule_docks: tuple[tuple[str, str], ...] | None = None - _rule_layers: str | None = None - _rule_layer: tuple[str, ...] | None = None + _rule_layers: tuple[str, ...] | None = None + _rule_layer: str | None = None important: set[str] = field(default_factory=set) diff --git a/src/textual/dom.py b/src/textual/dom.py index adaf2459e..b0597d81d 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -17,6 +17,10 @@ if TYPE_CHECKING: from .widget import Widget +class NoParent(Exception): + pass + + @rich.repr.auto class DOMNode(MessagePump): """A node in a hierarchy of things forming the UI. @@ -39,6 +43,13 @@ class DOMNode(MessagePump): if self._classes: yield "classes", self._classes + @property + def parent(self) -> DOMNode: + if self._parent is None: + raise NoParent(f"{self._parent} has no parent") + assert isinstance(self._parent, DOMNode) + return self._parent + @property def id(self) -> str | None: return self._id @@ -84,6 +95,27 @@ class DOMNode(MessagePump): def visible(self) -> bool: return self.styles.display != "none" + @property + def z(self) -> tuple[int, ...]: + """Get the z index tuple for this node. + + Returns: + tuple[int, ...]: A tuple of ints to sort layers by. + """ + indexes: list[int] = [] + append = indexes.append + node = self + while node._parent: + styles = node.styles + parent_styles = node.parent.styles + append( + parent_styles.layers.index(styles.layer) + if styles.layer in parent_styles.layers + else 0 + ) + node = node.parent + return tuple(reversed(indexes)) + @property def tree(self) -> Tree: highlighter = ReprHighlighter() diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 0da7c6953..f9e80db55 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -48,10 +48,8 @@ class MessagePump: return self._task @property - def parent(self) -> MessagePump: - if self._parent is None: - raise NoParent(f"{self._parent} has no parent") - return self._parent + def has_parent(self) -> bool: + return self._parent is not None @property def app(self) -> "App":