From 3ffc5826c2dc77e5c987fce7ca09a74f59f7565c Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 10 Jan 2022 11:31:06 +0000 Subject: [PATCH 1/4] psuedo classes --- examples/basic.css | 4 ++++ src/textual/app.py | 8 ++++++++ src/textual/css/model.py | 14 +++++++------- src/textual/dom.py | 19 ++++++++++++------- src/textual/events.py | 2 +- src/textual/messages.py | 9 +++++++++ src/textual/widget.py | 20 +++++++++++++++++++- 7 files changed, 60 insertions(+), 16 deletions(-) diff --git a/examples/basic.css b/examples/basic.css index d3dae65cb..0e648157a 100644 --- a/examples/basic.css +++ b/examples/basic.css @@ -4,6 +4,10 @@ App > View { docks: side=left/1; } +Widget:hover { + outline: solid green; +} + #sidebar { text: #09312e on #3caea3; dock: side; diff --git a/src/textual/app.py b/src/textual/app.py index 23d224aa7..8eefce9b8 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -239,6 +239,9 @@ class App(DOMNode): self.stylesheet.update(self) self.view.refresh(layout=True) + def update_styles(self) -> None: + self.post_message_no_wait(messages.RefreshStyles(self)) + def mount(self, *anon_widgets: Widget, **widgets: Widget) -> None: self.register(self.view, *anon_widgets, **widgets) self.view.refresh() @@ -622,6 +625,11 @@ class App(DOMNode): self.view.query(selector).toggle_class(class_name) self.view.refresh(layout=True) + async def handle_refresh_styles(self, message: messages.RefreshStyles) -> None: + self.reset_styles() + self.stylesheet.update(self) + self.view.refresh(layout=True) + if __name__ == "__main__": import asyncio diff --git a/src/textual/css/model.py b/src/textual/css/model.py index ddb0629ae..dcdcb63e2 100644 --- a/src/textual/css/model.py +++ b/src/textual/css/model.py @@ -45,15 +45,15 @@ class Selector: @property def css(self) -> str: - psuedo_suffix = "".join(f":{name}" for name in self.pseudo_classes) + pseudo_suffix = "".join(f":{name}" for name in self.pseudo_classes) if self.type == SelectorType.UNIVERSAL: return "*" elif self.type == SelectorType.TYPE: - return f"{self.name}{psuedo_suffix}" + return f"{self.name}{pseudo_suffix}" elif self.type == SelectorType.CLASS: - return f".{self.name}{psuedo_suffix}" + return f".{self.name}{pseudo_suffix}" else: - return f"#{self.name}{psuedo_suffix}" + return f"#{self.name}{pseudo_suffix}" def __post_init__(self) -> None: self._name_lower = self.name.lower() @@ -73,21 +73,21 @@ class Selector: 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): + if self.pseudo_classes and not node.has_pseudo_class(*self.pseudo_classes): return False return True 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): + if self.pseudo_classes and not node.has_pseudo_class(*self.pseudo_classes): return False return True 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): + if self.pseudo_classes and not node.has_pseudo_class(*self.pseudo_classes): return False return True diff --git a/src/textual/dom.py b/src/textual/dom.py index d1b52674b..79d4fbb35 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -99,9 +99,11 @@ class DOMNode(MessagePump): return frozenset(self._classes) @property - def psuedo_classes(self) -> set[str]: - """Get a set of all psuedo classes""" - return set() + 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 def css_type(self) -> str: @@ -186,6 +188,9 @@ class DOMNode(MessagePump): add_children(tree, self) return tree + def get_pseudo_classes(self) -> Iterable[str]: + return () + def reset_styles(self) -> None: from .widget import Widget @@ -255,7 +260,7 @@ class DOMNode(MessagePump): self._classes.symmetric_difference_update(class_names) self.app.stylesheet.update(self.app) - def has_psuedo_class(self, *class_names: str) -> bool: - """Check for psuedo class (such as hover, focus etc)""" - has_psuedo_classes = self.psuedo_classes.issuperset(class_names) - return has_psuedo_classes + def has_pseudo_class(self, *class_names: str) -> bool: + """Check for pseudo class (such as hover, focus etc)""" + has_pseudo_classes = self.pseudo_classes.issuperset(class_names) + return has_pseudo_classes diff --git a/src/textual/events.py b/src/textual/events.py index 02dafafcf..a4222531c 100644 --- a/src/textual/events.py +++ b/src/textual/events.py @@ -66,7 +66,7 @@ class Load(Event, bubble=False): class Idle(Event, bubble=False): """Sent when there are no more items in the message queue. - This is a psuedo-event in that it is created by the Textual system and doesn't go + This is a pseudo-event in that it is created by the Textual system and doesn't go through the usual message queue. """ diff --git a/src/textual/messages.py b/src/textual/messages.py index 87331e981..56a85df10 100644 --- a/src/textual/messages.py +++ b/src/textual/messages.py @@ -41,3 +41,12 @@ class CursorMove(Message): def __init__(self, sender: MessagePump, line: int) -> None: self.line = line super().__init__(sender) + + +@rich.repr.auto +class RefreshStyles(Message): + def __init__(self, sender: MessagePump) -> None: + super().__init__(sender) + + def can_replace(self, message: Message) -> bool: + return isinstance(message, RefreshStyles) diff --git a/src/textual/widget.py b/src/textual/widget.py index 90072034e..ff35af5ef 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -1,6 +1,6 @@ from __future__ import annotations -from logging import getLogger +from logging import PercentStyle, getLogger from typing import ( Any, Awaitable, @@ -77,6 +77,7 @@ class Widget(DOMNode): self._layout_required = False self._animate: BoundAnimator | None = None self._reactive_watches: dict[str, Callable] = {} + self._mouse_over: bool = False self.render_cache: RenderCache | None = None self.highlight_style: Style | None = None @@ -92,11 +93,19 @@ class Widget(DOMNode): yield "name", self.name if self.classes: yield "classes", self.classes + pseudo_classes = self.pseudo_classes + if pseudo_classes: + yield "pseudo_classes", pseudo_classes + yield "outline", self.styles.outline def __rich__(self) -> RenderableType: renderable = self.render_styled() return renderable + def get_pseudo_classes(self) -> Iterable[str]: + if self._mouse_over: + yield "hover" + def get_child_by_id(self, id: str) -> Widget: """Get a child with a given id. @@ -212,6 +221,7 @@ class Widget(DOMNode): return gutter def on_style_change(self) -> None: + self.log("style_Change", self) self.clear_render_cache() def _update_size(self, size: Size) -> None: @@ -359,3 +369,11 @@ class Widget(DOMNode): async def on_click(self, event: events.Click) -> None: await self.broker_event("click", event) + + async def on_enter(self, event: events.Enter) -> None: + self._mouse_over = True + self.app.update_styles() + + async def on_leave(self, event: events.Leave) -> None: + self._mouse_over = False + self.app.update_styles() From 4abfb7751760fb09246f00800678ce48003ef76e Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 10 Jan 2022 15:18:58 +0000 Subject: [PATCH 2/4] 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: From c611fd84ff3a4f67a1f2a5a38d42fad37215cb9a Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 11 Jan 2022 09:48:06 +0000 Subject: [PATCH 3/4] docstrings --- src/textual/app.py | 5 +++++ src/textual/css/model.py | 5 +++++ src/textual/dom.py | 5 +++++ src/textual/widget.py | 2 ++ 4 files changed, 17 insertions(+) diff --git a/src/textual/app.py b/src/textual/app.py index 8eefce9b8..cb136ae5f 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -240,6 +240,11 @@ class App(DOMNode): self.view.refresh(layout=True) def update_styles(self) -> None: + """Request update of styles. + + Should be called whenever CSS classes / pseudo classes change. + + """ self.post_message_no_wait(messages.RefreshStyles(self)) def mount(self, *anon_widgets: Widget, **widgets: Widget) -> None: diff --git a/src/textual/css/model.py b/src/textual/css/model.py index b534bf122..2dd7a5cc0 100644 --- a/src/textual/css/model.py +++ b/src/textual/css/model.py @@ -65,6 +65,11 @@ class Selector: } def _add_pseudo_class(self, pseudo_class: str) -> None: + """Adds a pseudo class and updates specificity. + + Args: + pseudo_class (str): Name of pseudo class. + """ self.pseudo_classes.append(pseudo_class) specificity1, specificity2, specificity3 = self.specificity self.specificity = (specificity1, specificity2 + 1, specificity3) diff --git a/src/textual/dom.py b/src/textual/dom.py index 3cca7e0c1..308d577db 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -188,6 +188,11 @@ class DOMNode(MessagePump): return tree def get_pseudo_classes(self) -> Iterable[str]: + """Get any pseudo classes applicable to this Node, e.g. hover, focus. + + Returns: + Iterable[str]: Iterable of strings, such as a generator. + """ return () def reset_styles(self) -> None: diff --git a/src/textual/widget.py b/src/textual/widget.py index b71e2ec09..a9ecfb25b 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -103,8 +103,10 @@ class Widget(DOMNode): return renderable def get_pseudo_classes(self) -> Iterable[str]: + """Pseudo classes for a widget""" if self._mouse_over: yield "hover" + # TODO: focus def get_child_by_id(self, id: str) -> Widget: """Get a child with a given id. From d76b0ee11c1a9d42a7d476d878b16c52a5cbb6c7 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 11 Jan 2022 13:49:19 +0000 Subject: [PATCH 4/4] change to naming convention of messages --- src/textual/app.py | 4 ++-- src/textual/messages.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/textual/app.py b/src/textual/app.py index cb136ae5f..0e6dee254 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -245,7 +245,7 @@ class App(DOMNode): Should be called whenever CSS classes / pseudo classes change. """ - self.post_message_no_wait(messages.RefreshStyles(self)) + self.post_message_no_wait(messages.StylesUpdated(self)) def mount(self, *anon_widgets: Widget, **widgets: Widget) -> None: self.register(self.view, *anon_widgets, **widgets) @@ -630,7 +630,7 @@ class App(DOMNode): self.view.query(selector).toggle_class(class_name) self.view.refresh(layout=True) - async def handle_refresh_styles(self, message: messages.RefreshStyles) -> None: + async def handle_styles_updated(self, message: messages.StylesUpdated) -> None: self.reset_styles() self.stylesheet.update(self) self.view.refresh(layout=True) diff --git a/src/textual/messages.py b/src/textual/messages.py index 56a85df10..d43dc6a6c 100644 --- a/src/textual/messages.py +++ b/src/textual/messages.py @@ -44,9 +44,9 @@ class CursorMove(Message): @rich.repr.auto -class RefreshStyles(Message): +class StylesUpdated(Message): def __init__(self, sender: MessagePump) -> None: super().__init__(sender) def can_replace(self, message: Message) -> bool: - return isinstance(message, RefreshStyles) + return isinstance(message, StylesUpdated)