mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Merge pull request #201 from Textualize/hover-pseudo-class
Hover pseudo class
This commit is contained in:
@@ -4,6 +4,11 @@ App > View {
|
||||
docks: side=left/1;
|
||||
}
|
||||
|
||||
Widget:hover {
|
||||
outline: heavy;
|
||||
text: bold !important;
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
text: #09312e on #3caea3;
|
||||
dock: side;
|
||||
|
||||
@@ -239,6 +239,14 @@ class App(DOMNode):
|
||||
self.stylesheet.update(self)
|
||||
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.StylesUpdated(self))
|
||||
|
||||
def mount(self, *anon_widgets: Widget, **widgets: Widget) -> None:
|
||||
self.register(self.view, *anon_widgets, **widgets)
|
||||
self.view.refresh()
|
||||
@@ -622,6 +630,11 @@ class App(DOMNode):
|
||||
self.view.query(selector).toggle_class(class_name)
|
||||
self.view.refresh(layout=True)
|
||||
|
||||
async def handle_styles_updated(self, message: messages.StylesUpdated) -> None:
|
||||
self.reset_styles()
|
||||
self.stylesheet.update(self)
|
||||
self.view.refresh(layout=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import asyncio
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 = ""
|
||||
|
||||
@@ -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()
|
||||
@@ -64,6 +64,16 @@ class Selector:
|
||||
SelectorType.ID: self._check_id,
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
def check(self, node: DOMNode) -> bool:
|
||||
return self._checks[self.type](node)
|
||||
|
||||
@@ -73,21 +83,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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = [
|
||||
(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -99,9 +99,10 @@ 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()})
|
||||
return pseudo_classes
|
||||
|
||||
@property
|
||||
def css_type(self) -> str:
|
||||
@@ -186,6 +187,14 @@ class DOMNode(MessagePump):
|
||||
add_children(tree, self)
|
||||
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:
|
||||
from .widget import Widget
|
||||
|
||||
@@ -255,7 +264,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
|
||||
|
||||
@@ -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.
|
||||
|
||||
"""
|
||||
|
||||
@@ -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 StylesUpdated(Message):
|
||||
def __init__(self, sender: MessagePump) -> None:
|
||||
super().__init__(sender)
|
||||
|
||||
def can_replace(self, message: Message) -> bool:
|
||||
return isinstance(message, StylesUpdated)
|
||||
|
||||
@@ -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,21 @@ 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]:
|
||||
"""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.
|
||||
|
||||
@@ -167,7 +178,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
|
||||
@@ -212,6 +223,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 +371,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()
|
||||
|
||||
Reference in New Issue
Block a user