From 4abfb7751760fb09246f00800678ce48003ef76e Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 10 Jan 2022 15:18:58 +0000 Subject: [PATCH] hover pseudo class --- examples/basic.css | 3 +- src/textual/css/_style_properties.py | 11 ++- src/textual/css/_styles_builder.py | 109 ++++++++++++++++++--------- src/textual/css/model.py | 5 ++ src/textual/css/parse.py | 4 +- src/textual/css/styles.py | 9 +-- src/textual/css/stylesheet.py | 7 ++ src/textual/dom.py | 1 - src/textual/widget.py | 4 +- 9 files changed, 99 insertions(+), 54 deletions(-) diff --git a/examples/basic.css b/examples/basic.css index 0e648157a..911fb9a88 100644 --- a/examples/basic.css +++ b/examples/basic.css @@ -5,7 +5,8 @@ App > View { } Widget:hover { - outline: solid green; + outline: heavy; + text: bold !important; } #sidebar { diff --git a/src/textual/css/_style_properties.py b/src/textual/css/_style_properties.py index 975562482..a1c42d2dd 100644 --- a/src/textual/css/_style_properties.py +++ b/src/textual/css/_style_properties.py @@ -228,8 +228,8 @@ class StyleProperty: bgcolor = getattr(obj, self._bgcolor_name) style = Style.from_color(color, bgcolor) style_flags = getattr(obj, self._style_name) - if style_flags is not None: - style += Style.parse(style_flags) + if style_flags: + style += style_flags return style def __set__(self, obj: Styles, style: Style | str | None) -> Style | str | None: @@ -241,13 +241,12 @@ class StyleProperty: elif isinstance(style, Style): setattr(obj, self._color_name, style.color) setattr(obj, self._bgcolor_name, style.bgcolor) - setattr(obj, self._style_name, str(style.without_color)) + setattr(obj, self._style_name, style.without_color) elif isinstance(style, str): new_style = Style.parse(style) setattr(obj, self._color_name, new_style.color) setattr(obj, self._bgcolor_name, new_style.bgcolor) - style_str = str(new_style.without_color) - setattr(obj, self._style_name, style_str if style_str != "none" else "") + setattr(obj, self._style_name, new_style.without_color) return style @@ -457,7 +456,7 @@ class StyleFlagsProperty: for word in words: if not valid_word(word): raise StyleValueError(f"unknown word {word!r} in style flags") - style = Style.parse(" ".join(words)) + style = Style.parse(style_flags) setattr(obj, self._internal_name, style) return style_flags diff --git a/src/textual/css/_styles_builder.py b/src/textual/css/_styles_builder.py index e70bf06da..560adf323 100644 --- a/src/textual/css/_styles_builder.py +++ b/src/textual/css/_styles_builder.py @@ -43,9 +43,8 @@ class StylesBuilder: def add_declaration(self, declaration: Declaration) -> None: if not declaration.tokens: return - process_method = getattr( - self, f"process_{declaration.name.replace('-', '_')}", None - ) + rule_name = declaration.name.replace("-", "_") + process_method = getattr(self, f"process_{rule_name}", None) if process_method is None: self.error( @@ -55,17 +54,19 @@ class StylesBuilder: ) else: tokens = declaration.tokens - if tokens[-1].name == "important": + + important = tokens[-1].name == "important" + if important: tokens = tokens[:-1] - self.styles.important.add(declaration.name) + self.styles.important.add(rule_name) try: - process_method(declaration.name, tokens) + process_method(declaration.name, tokens, important) except DeclarationError as error: raise except Exception as error: self.error(declaration.name, declaration.token, str(error)) - def process_display(self, name: str, tokens: list[Token]) -> None: + def process_display(self, name: str, tokens: list[Token], important: bool) -> None: for token in tokens: name, value, _, _, location = token @@ -90,19 +91,25 @@ class StylesBuilder: else: self.error(name, tokens[0], "a single scalar is expected") - def process_width(self, name: str, tokens: list[Token]) -> None: + def process_width(self, name: str, tokens: list[Token], important: bool) -> None: self._process_scalar(name, tokens) - def process_height(self, name: str, tokens: list[Token]) -> None: + def process_height(self, name: str, tokens: list[Token], important: bool) -> None: self._process_scalar(name, tokens) - def process_min_width(self, name: str, tokens: list[Token]) -> None: + def process_min_width( + self, name: str, tokens: list[Token], important: bool + ) -> None: self._process_scalar(name, tokens) - def process_min_height(self, name: str, tokens: list[Token]) -> None: + def process_min_height( + self, name: str, tokens: list[Token], important: bool + ) -> None: self._process_scalar(name, tokens) - def process_visibility(self, name: str, tokens: list[Token]) -> None: + def process_visibility( + self, name: str, tokens: list[Token], important: bool + ) -> None: for token in tokens: name, value, _, _, location = token if name == "token": @@ -140,10 +147,10 @@ class StylesBuilder: Spacing.unpack(cast(SpacingDimensions, tuple(space))), ) - def process_padding(self, name: str, tokens: list[Token]) -> None: + def process_padding(self, name: str, tokens: list[Token], important: bool) -> None: self._process_space(name, tokens) - def process_margin(self, name: str, tokens: list[Token]) -> None: + def process_margin(self, name: str, tokens: list[Token], important: bool) -> None: self._process_space(name, tokens) def _parse_border(self, name: str, tokens: list[Token]) -> tuple[str, Style]: @@ -173,47 +180,63 @@ class StylesBuilder: border = self._parse_border("border", tokens) setattr(self.styles, f"_rule_border_{edge}", border) - def process_border(self, name: str, tokens: list[Token]) -> None: + def process_border(self, name: str, tokens: list[Token], important: bool) -> None: border = self._parse_border("border", tokens) styles = self.styles styles._rule_border_top = styles._rule_border_right = border styles._rule_border_bottom = styles._rule_border_left = border - def process_border_top(self, name: str, tokens: list[Token]) -> None: + def process_border_top( + self, name: str, tokens: list[Token], important: bool + ) -> None: self._process_border("top", name, tokens) - def process_border_right(self, name: str, tokens: list[Token]) -> None: + def process_border_right( + self, name: str, tokens: list[Token], important: bool + ) -> None: self._process_border("right", name, tokens) - def process_border_bottom(self, name: str, tokens: list[Token]) -> None: + def process_border_bottom( + self, name: str, tokens: list[Token], important: bool + ) -> None: self._process_border("bottom", name, tokens) - def process_border_left(self, name: str, tokens: list[Token]) -> None: + def process_border_left( + self, name: str, tokens: list[Token], important: bool + ) -> None: self._process_border("left", name, tokens) def _process_outline(self, edge: str, name: str, tokens: list[Token]) -> None: border = self._parse_border("outline", tokens) setattr(self.styles, f"_rule_outline_{edge}", border) - def process_outline(self, name: str, tokens: list[Token]) -> None: + def process_outline(self, name: str, tokens: list[Token], important: bool) -> None: border = self._parse_border("outline", tokens) styles = self.styles styles._rule_outline_top = styles._rule_outline_right = border styles._rule_outline_bottom = styles._rule_outline_left = border - def process_outline_top(self, name: str, tokens: list[Token]) -> None: + def process_outline_top( + self, name: str, tokens: list[Token], important: bool + ) -> None: self._process_outline("top", name, tokens) - def process_parse_border_right(self, name: str, tokens: list[Token]) -> None: + def process_parse_border_right( + self, name: str, tokens: list[Token], important: bool + ) -> None: self._process_outline("right", name, tokens) - def process_outline_bottom(self, name: str, tokens: list[Token]) -> None: + def process_outline_bottom( + self, name: str, tokens: list[Token], important: bool + ) -> None: self._process_outline("bottom", name, tokens) - def process_outline_left(self, name: str, tokens: list[Token]) -> None: + def process_outline_left( + self, name: str, tokens: list[Token], important: bool + ) -> None: self._process_outline("left", name, tokens) - def process_offset(self, name: str, tokens: list[Token]) -> None: + def process_offset(self, name: str, tokens: list[Token], important: bool) -> None: if not tokens: return if len(tokens) != 2: @@ -229,7 +252,7 @@ class StylesBuilder: scalar_y = Scalar.parse(token2.value, Unit.HEIGHT) self.styles._rule_offset = ScalarOffset(scalar_x, scalar_y) - def process_offset_x(self, name: str, tokens: list[Token]) -> None: + def process_offset_x(self, name: str, tokens: list[Token], important: bool) -> None: if not tokens: return if len(tokens) != 1: @@ -242,7 +265,7 @@ class StylesBuilder: y = self.styles.offset.y self.styles._rule_offset = ScalarOffset(x, y) - def process_offset_y(self, name: str, tokens: list[Token]) -> None: + def process_offset_y(self, name: str, tokens: list[Token], important: bool) -> None: if not tokens: return if len(tokens) != 1: @@ -255,22 +278,28 @@ class StylesBuilder: x = self.styles.offset.x self.styles._rule_offset = ScalarOffset(x, y) - def process_layout(self, name: str, tokens: list[Token]) -> None: + def process_layout(self, name: str, tokens: list[Token], important: bool) -> None: if tokens: if len(tokens) != 1: self.error(name, tokens[0], "unexpected tokens in declaration") else: self.styles._rule_layout = tokens[0].value - def process_text(self, name: str, tokens: list[Token]) -> None: + def process_text(self, name: str, tokens: list[Token], important: bool) -> None: style_definition = " ".join(token.value for token in tokens) try: style = Style.parse(style_definition) except Exception as error: self.error(name, tokens[0], f"failed to parse style; {error}") + if important: + self.styles.important.update( + {"text_style", "text_background", "text_color"} + ) self.styles.text = style - def process_text_color(self, name: str, tokens: list[Token]) -> None: + def process_text_color( + self, name: str, tokens: list[Token], important: bool + ) -> None: for token in tokens: if token.name in ("color", "token"): try: @@ -284,7 +313,9 @@ class StylesBuilder: name, token, f"unexpected token {token.value!r} in declaration" ) - def process_text_background(self, name: str, tokens: list[Token]) -> None: + def process_text_background( + self, name: str, tokens: list[Token], important: bool + ) -> None: for token in tokens: if token.name in ("color", "token"): try: @@ -298,11 +329,13 @@ class StylesBuilder: name, token, f"unexpected token {token.value!r} in declaration" ) - def process_text_style(self, name: str, tokens: list[Token]) -> None: + def process_text_style( + self, name: str, tokens: list[Token], important: bool + ) -> None: style_definition = " ".join(token.value for token in tokens) self.styles.text_style = style_definition - def process_dock(self, name: str, tokens: list[Token]) -> None: + def process_dock(self, name: str, tokens: list[Token], important: bool) -> None: if len(tokens) > 1: self.error( @@ -312,7 +345,7 @@ class StylesBuilder: ) self.styles._rule_dock = tokens[0].value if tokens else "" - def process_docks(self, name: str, tokens: list[Token]) -> None: + def process_docks(self, name: str, tokens: list[Token], important: bool) -> None: docks: list[DockGroup] = [] for token in tokens: if token.name == "key_value": @@ -343,12 +376,12 @@ class StylesBuilder: ) self.styles._rule_docks = tuple(docks + [DockGroup("_default", "top", 0)]) - def process_layer(self, name: str, tokens: list[Token]) -> None: + def process_layer(self, name: str, tokens: list[Token], important: bool) -> 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: + def process_layers(self, name: str, tokens: list[Token], important: bool) -> None: layers: list[str] = [] for token in tokens: if token.name != "token": @@ -356,7 +389,9 @@ class StylesBuilder: layers.append(token.value) self.styles._rule_layers = tuple(layers) - def process_transition(self, name: str, tokens: list[Token]) -> None: + def process_transition( + self, name: str, tokens: list[Token], important: bool + ) -> None: transitions: dict[str, Transition] = {} css_property = "" diff --git a/src/textual/css/model.py b/src/textual/css/model.py index dcdcb63e2..b534bf122 100644 --- a/src/textual/css/model.py +++ b/src/textual/css/model.py @@ -64,6 +64,11 @@ class Selector: SelectorType.ID: self._check_id, } + def _add_pseudo_class(self, pseudo_class: str) -> None: + self.pseudo_classes.append(pseudo_class) + specificity1, specificity2, specificity3 = self.specificity + self.specificity = (specificity1, specificity2 + 1, specificity3) + def check(self, node: DOMNode) -> bool: return self._checks[self.type](node) diff --git a/src/textual/css/parse.py b/src/textual/css/parse.py index a2adcab01..ba0bc6783 100644 --- a/src/textual/css/parse.py +++ b/src/textual/css/parse.py @@ -48,7 +48,7 @@ def parse_selectors(css_selectors: str) -> tuple[SelectorSet, ...]: except EOFError: break if token.name == "pseudo_class": - selectors[-1].pseudo_classes.append(token.value.lstrip(":")) + selectors[-1]._add_pseudo_class(token.value.lstrip(":")) elif token.name == "whitespace": if combinator is None or combinator == CombinatorType.SAME: combinator = CombinatorType.DESCENDENT @@ -92,7 +92,7 @@ def parse_rule_set(tokens: Iterator[Token], token: Token) -> Iterable[RuleSet]: while True: if token.name == "pseudo_class": - selectors[-1].pseudo_classes.append(token.value.lstrip(":")) + selectors[-1]._add_pseudo_class(token.value.lstrip(":")) elif token.name == "whitespace": if combinator is None or combinator == CombinatorType.SAME: combinator = CombinatorType.DESCENDENT diff --git a/src/textual/css/styles.py b/src/textual/css/styles.py index 3554c12bf..4e48fa95b 100644 --- a/src/textual/css/styles.py +++ b/src/textual/css/styles.py @@ -16,6 +16,7 @@ from .._types import MessageTarget from .errors import StyleValueError from .. import events from ._error_tools import friendly_list +from .types import Specificity3, Specificity4 from .constants import ( VALID_DISPLAY, VALID_VISIBILITY, @@ -159,7 +160,7 @@ class Styles: """Get the gutter (additional space reserved for margin / padding / border). Returns: - Spacing: [description] + Spacing: Space around edges. """ gutter = self.margin + self.padding + self.border.spacing return gutter @@ -200,8 +201,6 @@ class Styles: def refresh(self, layout: bool = False) -> None: self._repaint_required = True self._layout_required = layout - # if self.node is not None: - # self.node.post_message_no_wait(events.Null(self.node)) def check_refresh(self) -> tuple[bool, bool]: result = (self._repaint_required, self._layout_required) @@ -237,8 +236,8 @@ class Styles: return None def extract_rules( - self, specificity: tuple[int, int, int] - ) -> list[tuple[str, tuple[int, int, int, int], Any]]: + self, specificity: Specificity3 + ) -> list[tuple[str, Specificity4, Any]]: is_important = self.important.__contains__ rules = [ ( diff --git a/src/textual/css/stylesheet.py b/src/textual/css/stylesheet.py index fb7dda99f..77e4ab28a 100644 --- a/src/textual/css/stylesheet.py +++ b/src/textual/css/stylesheet.py @@ -124,10 +124,17 @@ class Stylesheet: get_first_item = itemgetter(0) + log("") + log(node) + for key, attributes in rule_attributes.items(): + log(key, key in node.styles.important) + log("\t", attributes) + node_rules = [ (name, max(specificity_rules, key=get_first_item)[1]) for name, specificity_rules in rule_attributes.items() ] + node.styles.apply_rules(node_rules) def update(self, root: DOMNode) -> None: diff --git a/src/textual/dom.py b/src/textual/dom.py index 79d4fbb35..3cca7e0c1 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -102,7 +102,6 @@ class DOMNode(MessagePump): def pseudo_classes(self) -> frozenset[str]: """Get a set of all pseudo classes""" pseudo_classes = frozenset({*self.get_pseudo_classes()}) - self.log(pseudo_classes) return pseudo_classes @property diff --git a/src/textual/widget.py b/src/textual/widget.py index ff35af5ef..b71e2ec09 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -176,7 +176,7 @@ class Widget(DOMNode): if styles.has_outline: renderable = Border( - renderable, styles.outline, outline=True, style=parent_text_style + renderable, styles.outline, outline=True, style=renderable_text_style ) return renderable @@ -221,7 +221,7 @@ class Widget(DOMNode): return gutter def on_style_change(self) -> None: - self.log("style_Change", self) + self.log("style_change", self) self.clear_render_cache() def _update_size(self, size: Size) -> None: