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()