mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
refactor of styles
This commit is contained in:
@@ -173,6 +173,10 @@ class Edges(NamedTuple):
|
||||
bottom: tuple[BoxType, Color]
|
||||
left: tuple[BoxType, Color]
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
(top, _), (right, _), (bottom, _), (left, _) = self
|
||||
return bool(top or right or bottom or left)
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
top, right, bottom, left = self
|
||||
if top[0]:
|
||||
@@ -749,9 +753,6 @@ class StyleFlagsProperty:
|
||||
class TransitionsProperty:
|
||||
"""Descriptor for getting transitions properties"""
|
||||
|
||||
def __set_name__(self, owner: Styles, name: str) -> None:
|
||||
self.name = name
|
||||
|
||||
def __get__(
|
||||
self, obj: Styles, objtype: type[Styles] | None = None
|
||||
) -> dict[str, Transition]:
|
||||
@@ -766,4 +767,10 @@ class TransitionsProperty:
|
||||
e.g. ``{"offset": Transition(...), ...}``. If no transitions have been set, an empty ``dict``
|
||||
is returned.
|
||||
"""
|
||||
return obj.get_rule(self.name, {})
|
||||
return obj.get_rule("transitions", {})
|
||||
|
||||
def __set__(self, obj: Styles, transitions: dict[str, Transition] | None) -> None:
|
||||
if transitions is None:
|
||||
obj.clear_rule("transitions")
|
||||
else:
|
||||
obj.set_rule("transitions", transitions.copy())
|
||||
|
||||
@@ -119,6 +119,43 @@ class StylesBase(ABC):
|
||||
"min_height",
|
||||
}
|
||||
|
||||
display = StringEnumProperty(VALID_DISPLAY, "block")
|
||||
visibility = StringEnumProperty(VALID_VISIBILITY, "visible")
|
||||
layout = LayoutProperty()
|
||||
|
||||
text = StyleProperty()
|
||||
text_color = ColorProperty()
|
||||
text_background = ColorProperty()
|
||||
text_style = StyleFlagsProperty()
|
||||
|
||||
padding = SpacingProperty()
|
||||
margin = SpacingProperty()
|
||||
offset = OffsetProperty()
|
||||
|
||||
border = BorderProperty()
|
||||
border_top = BoxProperty()
|
||||
border_right = BoxProperty()
|
||||
border_bottom = BoxProperty()
|
||||
border_left = BoxProperty()
|
||||
|
||||
outline = BorderProperty()
|
||||
outline_top = BoxProperty()
|
||||
outline_right = BoxProperty()
|
||||
outline_bottom = BoxProperty()
|
||||
outline_left = BoxProperty()
|
||||
|
||||
width = ScalarProperty(percent_unit=Unit.WIDTH)
|
||||
height = ScalarProperty(percent_unit=Unit.HEIGHT)
|
||||
min_width = ScalarProperty(percent_unit=Unit.WIDTH)
|
||||
min_height = ScalarProperty(percent_unit=Unit.HEIGHT)
|
||||
|
||||
dock = DockProperty()
|
||||
docks = DocksProperty()
|
||||
|
||||
layer = NameProperty()
|
||||
layers = NameListProperty()
|
||||
transitions = TransitionsProperty()
|
||||
|
||||
@abstractmethod
|
||||
def has_rule(self, rule: str) -> bool:
|
||||
...
|
||||
@@ -164,10 +201,6 @@ class StylesBase(ABC):
|
||||
tuple[bool, bool]: (repaint required, layout_required)
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def get_transition(self, key: str) -> Transition | None:
|
||||
"""Get a transition for a given key (or ``None`` if it doesn't exist)"""
|
||||
|
||||
@abstractmethod
|
||||
def reset(self) -> None:
|
||||
"""Reset the rules to initial state."""
|
||||
@@ -180,9 +213,25 @@ class StylesBase(ABC):
|
||||
other (Styles): A Styles object.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def merge_rules(self, rules: RulesMap) -> None:
|
||||
"""Merge rules in to Styles.
|
||||
|
||||
Args:
|
||||
rules (RulesMap): A mapping of rules.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def is_animatable(cls, rule_name: str) -> bool:
|
||||
return rule_name in cls.ANIMATABLE
|
||||
def is_animatable(cls, rule: str) -> bool:
|
||||
"""Check if a given rule may be animated.
|
||||
|
||||
Args:
|
||||
rule (str): Name of the rule.
|
||||
|
||||
Returns:
|
||||
bool: ``True`` if the rule may be animated, otherwise ``False``.
|
||||
"""
|
||||
return rule in cls.ANIMATABLE
|
||||
|
||||
@classmethod
|
||||
@lru_cache(maxsize=1024)
|
||||
@@ -203,42 +252,11 @@ class StylesBase(ABC):
|
||||
styles.node = node
|
||||
return styles
|
||||
|
||||
display = StringEnumProperty(VALID_DISPLAY, "block")
|
||||
visibility = StringEnumProperty(VALID_VISIBILITY, "visible")
|
||||
layout = LayoutProperty()
|
||||
|
||||
text = StyleProperty()
|
||||
text_color = ColorProperty()
|
||||
text_background = ColorProperty()
|
||||
text_style = StyleFlagsProperty()
|
||||
|
||||
padding = SpacingProperty()
|
||||
margin = SpacingProperty()
|
||||
offset = OffsetProperty()
|
||||
|
||||
border = BorderProperty()
|
||||
border_top = BoxProperty()
|
||||
border_right = BoxProperty()
|
||||
border_bottom = BoxProperty()
|
||||
border_left = BoxProperty()
|
||||
|
||||
outline = BorderProperty()
|
||||
outline_top = BoxProperty()
|
||||
outline_right = BoxProperty()
|
||||
outline_bottom = BoxProperty()
|
||||
outline_left = BoxProperty()
|
||||
|
||||
width = ScalarProperty(percent_unit=Unit.WIDTH)
|
||||
height = ScalarProperty(percent_unit=Unit.HEIGHT)
|
||||
min_width = ScalarProperty(percent_unit=Unit.WIDTH)
|
||||
min_height = ScalarProperty(percent_unit=Unit.HEIGHT)
|
||||
|
||||
dock = DockProperty()
|
||||
docks = DocksProperty()
|
||||
|
||||
layer = NameProperty()
|
||||
layers = NameListProperty()
|
||||
transitions = TransitionsProperty()
|
||||
def get_transition(self, key: str) -> Transition | None:
|
||||
if key in self.ANIMATABLE:
|
||||
return self.transitions.get(key, None)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
@rich.repr.auto
|
||||
@@ -286,12 +304,6 @@ class Styles(StylesBase):
|
||||
self._repaint_required = self._layout_required = False
|
||||
return result
|
||||
|
||||
def get_transition(self, key: str) -> Transition | None:
|
||||
if key in self.ANIMATABLE:
|
||||
return self.transitions.get(key, None)
|
||||
else:
|
||||
return None
|
||||
|
||||
def reset(self) -> None:
|
||||
"""
|
||||
Reset internal style rules to ``None``, reverting to default styles.
|
||||
@@ -307,6 +319,9 @@ class Styles(StylesBase):
|
||||
|
||||
self._rules.update(other._rules)
|
||||
|
||||
def merge_rules(self, rules: RulesMap) -> None:
|
||||
self._rules.update(rules)
|
||||
|
||||
def extract_rules(
|
||||
self, specificity: Specificity3
|
||||
) -> list[tuple[str, Specificity4, Any]]:
|
||||
@@ -319,46 +334,39 @@ class Styles(StylesBase):
|
||||
|
||||
return rules
|
||||
|
||||
def apply_rules(self, rules: RulesMap, animate: bool = False):
|
||||
"""Apply rules to this Styles object, animating as required.
|
||||
|
||||
Args:
|
||||
rules (RulesMap): A map containing rules to apply.
|
||||
animate (bool, optional): ``True`` if the rules should animate, or ``False``
|
||||
to set rules without any animation. Defaults to ``False``.
|
||||
"""
|
||||
|
||||
if animate and self.node is not None:
|
||||
styles = self
|
||||
is_animatable = styles.ANIMATABLE.__contains__
|
||||
_rules = self._rules
|
||||
for key, value in rules.items():
|
||||
current = _rules.get(key)
|
||||
if current == value:
|
||||
continue
|
||||
if is_animatable(key):
|
||||
transition = styles.get_transition(key)
|
||||
if transition is None:
|
||||
_rules[key] = value
|
||||
else:
|
||||
duration, easing, delay = transition
|
||||
self.node.app.animator.animate(
|
||||
styles, key, value, duration=duration, easing=easing
|
||||
)
|
||||
else:
|
||||
rules[key] = value
|
||||
else:
|
||||
self._rules.update(rules)
|
||||
|
||||
if self.node is not None:
|
||||
self.node.on_style_change()
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
for name, value in self._rules.items():
|
||||
yield name, value
|
||||
has_rule = self.has_rule
|
||||
for name in RULE_NAMES:
|
||||
if has_rule(name):
|
||||
yield name, getattr(self, name)
|
||||
if self.important:
|
||||
yield "important", self.important
|
||||
|
||||
def __textual_animation__(
|
||||
self,
|
||||
attribute: str,
|
||||
value: Any,
|
||||
start_time: float,
|
||||
duration: float | None,
|
||||
speed: float | None,
|
||||
easing: EasingFunction,
|
||||
) -> Animation | None:
|
||||
from ..widget import Widget
|
||||
|
||||
assert isinstance(self.node, Widget)
|
||||
if isinstance(value, ScalarOffset):
|
||||
return ScalarAnimation(
|
||||
self.node,
|
||||
self,
|
||||
start_time,
|
||||
attribute,
|
||||
value,
|
||||
duration=duration,
|
||||
speed=speed,
|
||||
easing=easing,
|
||||
)
|
||||
return None
|
||||
|
||||
def _get_border_css_lines(
|
||||
self, rules: RulesMap, name: str
|
||||
) -> Iterable[tuple[str, str]]:
|
||||
@@ -393,26 +401,26 @@ class Styles(StylesBase):
|
||||
left = get_rule(f"{name}_left")
|
||||
|
||||
if top == right and right == bottom and bottom == left:
|
||||
border_type, border_style = rules[f"{name}_top"]
|
||||
yield name, f"{border_type} {border_style}"
|
||||
border_type, border_color = rules[f"{name}_top"]
|
||||
yield name, f"{border_type} {border_color.name}"
|
||||
return
|
||||
|
||||
# Check for edges
|
||||
if has_top:
|
||||
border_type, border_style = rules[f"{name}_top"]
|
||||
yield f"{name}-top", f"{border_type} {border_style}"
|
||||
border_type, border_color = rules[f"{name}_top"]
|
||||
yield f"{name}-top", f"{border_type} {border_color.name}"
|
||||
|
||||
if has_right:
|
||||
border_type, border_style = rules[f"{name}_right"]
|
||||
yield f"{name}-right", f"{border_type} {border_style}"
|
||||
border_type, border_color = rules[f"{name}_right"]
|
||||
yield f"{name}-right", f"{border_type} {border_color.name}"
|
||||
|
||||
if has_bottom:
|
||||
border_type, border_style = rules[f"{name}_bottom"]
|
||||
yield f"{name}-bottom", f"{border_type} {border_style}"
|
||||
border_type, border_color = rules[f"{name}_bottom"]
|
||||
yield f"{name}-bottom", f"{border_type} {border_color.name}"
|
||||
|
||||
if has_left:
|
||||
border_type, border_style = rules[f"{name}_left"]
|
||||
yield f"{name}-left", f"{border_type} {border_style}"
|
||||
border_type, border_color = rules[f"{name}_left"]
|
||||
yield f"{name}-left", f"{border_type} {border_color.name}"
|
||||
|
||||
@property
|
||||
def css_lines(self) -> list[str]:
|
||||
@@ -473,9 +481,9 @@ class Styles(StylesBase):
|
||||
append_declaration("text", str(self.text))
|
||||
else:
|
||||
if has_rule("text_color"):
|
||||
append_declaration("text-color", get_rule("text_color").name)
|
||||
if has_rule("text_bgcolor"):
|
||||
append_declaration("text-bgcolor", str(get_rule("text_bgcolor").name))
|
||||
append_declaration("text-color", self.text_color.name)
|
||||
if has_rule("text_background"):
|
||||
append_declaration("text-background", self.text_background.name)
|
||||
if has_rule("text_style"):
|
||||
append_declaration("text-style", str(get_rule("text_style")))
|
||||
|
||||
@@ -504,29 +512,6 @@ class Styles(StylesBase):
|
||||
return "\n".join(self.css_lines)
|
||||
|
||||
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
GetType = TypeVar("GetType")
|
||||
SetType = TypeVar("SetType")
|
||||
|
||||
|
||||
class StyleViewProperty(Generic[GetType, SetType]):
|
||||
"""Presents a view of a base Styles object, plus inline styles."""
|
||||
|
||||
def __set_name__(self, owner: RenderStyles, name: str) -> None:
|
||||
self.name = name
|
||||
|
||||
def __set__(self, obj: RenderStyles, value: SetType) -> None:
|
||||
setattr(obj._inline_styles, self.name, value)
|
||||
|
||||
def __get__(
|
||||
self, obj: RenderStyles, objtype: type[RenderStyles] | None = None
|
||||
) -> GetType:
|
||||
if obj._inline_styles.has_rule(self.name):
|
||||
return getattr(obj._inline_styles, self.name)
|
||||
return getattr(obj._base_styles, self.name)
|
||||
|
||||
|
||||
@rich.repr.auto
|
||||
class RenderStyles(StylesBase):
|
||||
"""Presents a combined view of two Styles object: a base Styles and inline Styles."""
|
||||
@@ -538,10 +523,12 @@ class RenderStyles(StylesBase):
|
||||
|
||||
@property
|
||||
def base(self) -> Styles:
|
||||
"""Quick access to base (css) style."""
|
||||
return self._base_styles
|
||||
|
||||
@property
|
||||
def inline(self) -> Styles:
|
||||
"""Quick access to the inline styles."""
|
||||
return self._inline_styles
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
@@ -564,6 +551,9 @@ class RenderStyles(StylesBase):
|
||||
"""
|
||||
self._inline_styles.merge(other)
|
||||
|
||||
def merge_rules(self, rules: RulesMap) -> None:
|
||||
self._inline_styles.merge_rules(rules)
|
||||
|
||||
def check_refresh(self) -> tuple[bool, bool]:
|
||||
"""Check if the Styles must be refreshed.
|
||||
|
||||
@@ -596,37 +586,6 @@ class RenderStyles(StylesBase):
|
||||
rules = {**self._base_styles._rules, **self._inline_styles._rules}
|
||||
return cast(RulesMap, rules)
|
||||
|
||||
def __textual_animation__(
|
||||
self,
|
||||
attribute: str,
|
||||
value: Any,
|
||||
start_time: float,
|
||||
duration: float | None,
|
||||
speed: float | None,
|
||||
easing: EasingFunction,
|
||||
) -> Animation | None:
|
||||
from ..widget import Widget
|
||||
|
||||
assert isinstance(self.node, Widget)
|
||||
if isinstance(value, ScalarOffset):
|
||||
return ScalarAnimation(
|
||||
self.node,
|
||||
self,
|
||||
start_time,
|
||||
attribute,
|
||||
value,
|
||||
duration=duration,
|
||||
speed=speed,
|
||||
easing=easing,
|
||||
)
|
||||
return None
|
||||
|
||||
def get_transition(self, key: str) -> Transition | None:
|
||||
if key in self.ANIMATABLE:
|
||||
return self.transitions.get(key, None)
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def css(self) -> str:
|
||||
"""Get the CSS for the combined styles."""
|
||||
|
||||
@@ -23,6 +23,7 @@ from .parse import parse
|
||||
from .styles import RulesMap
|
||||
from .types import Specificity3, Specificity4
|
||||
from ..dom import DOMNode
|
||||
from .. import log
|
||||
|
||||
|
||||
class StylesheetParseError(Exception):
|
||||
@@ -135,7 +136,7 @@ class Stylesheet:
|
||||
yield selector_set.specificity
|
||||
|
||||
def apply(self, node: DOMNode, animate: bool = False) -> None:
|
||||
"""pply the stylesheet to a DOM node.
|
||||
"""Apply the stylesheet to a DOM node.
|
||||
|
||||
Args:
|
||||
node (DOMNode): The ``DOMNode`` to apply the stylesheet to.
|
||||
@@ -143,9 +144,6 @@ class Stylesheet:
|
||||
If the same rule is defined multiple times for the node (e.g. multiple
|
||||
classes modifying the same CSS property), then only the most specific
|
||||
rule will be applied.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
|
||||
# Dictionary of rule attribute names e.g. "text_background" to list of tuples.
|
||||
@@ -159,7 +157,7 @@ class Stylesheet:
|
||||
_check_rule = self._check_rule
|
||||
|
||||
# TODO: The line below breaks inline styles and animations
|
||||
node._css_styles.reset()
|
||||
# node._css_styles.reset()
|
||||
|
||||
# Collect default node CSS rules
|
||||
for key, default_specificity, value in node._default_rules:
|
||||
@@ -182,37 +180,48 @@ class Stylesheet:
|
||||
for name, specificity_rules in rule_attributes.items()
|
||||
},
|
||||
)
|
||||
node._css_styles.apply_rules(node_rules, animate=animate)
|
||||
|
||||
self.apply_rules(node, node_rules, animate=animate)
|
||||
|
||||
@classmethod
|
||||
def apply_rules(
|
||||
cls, node: DOMNode | None, rules: RulesMap, animate: bool = False
|
||||
) -> None:
|
||||
def apply_rules(cls, node: DOMNode, rules: RulesMap, animate: bool = False) -> None:
|
||||
"""Apply style rules to a node, animating as required.
|
||||
|
||||
if animate and node is not None:
|
||||
Args:
|
||||
node (DOMNode): A DOM node.
|
||||
rules (RulesMap): Mapping of rules.
|
||||
animate (bool, optional): Enable animation. Defaults to False.
|
||||
"""
|
||||
styles = node.styles
|
||||
if animate:
|
||||
|
||||
is_animatable = styles.is_animatable
|
||||
current_rules = styles.get_rules()
|
||||
set_rule = styles.base.set_rule
|
||||
|
||||
is_animatable = node._css_styles.ANIMATABLE.__contains__
|
||||
_rules = node.styles.get_rules()
|
||||
for key, value in rules.items():
|
||||
current = _rules.get(key)
|
||||
current = current_rules.get(key)
|
||||
if current == value:
|
||||
continue
|
||||
if is_animatable(key):
|
||||
transition = styles.get_transition(key)
|
||||
if transition is None:
|
||||
_rules[key] = value
|
||||
styles.base.set_rule(key, value)
|
||||
else:
|
||||
duration, easing, delay = transition
|
||||
self.node.app.animator.animate(
|
||||
styles, key, value, duration=duration, easing=easing
|
||||
node.app.animator.animate(
|
||||
node.styles.base,
|
||||
key,
|
||||
value,
|
||||
duration=duration,
|
||||
easing=easing,
|
||||
)
|
||||
else:
|
||||
rules[key] = value
|
||||
set_rule(key, value)
|
||||
else:
|
||||
self._rules.update(rules)
|
||||
styles.base.merge_rules(rules)
|
||||
|
||||
if self.node is not None:
|
||||
self.node.on_style_change()
|
||||
node.on_style_change()
|
||||
|
||||
def update(self, root: DOMNode, animate: bool = False) -> None:
|
||||
"""Update a node and its children."""
|
||||
|
||||
@@ -213,7 +213,6 @@ 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:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from rich.color import Color
|
||||
from rich.style import Style
|
||||
|
||||
from textual.css.styles import Styles, StylesView
|
||||
from textual.css.styles import Styles, RenderStyles
|
||||
|
||||
|
||||
def test_styles_reset():
|
||||
@@ -16,7 +16,7 @@ def test_styles_view_text():
|
||||
"""Test inline styles override base styles"""
|
||||
base = Styles()
|
||||
inline = Styles()
|
||||
styles_view = StylesView(None, base, inline)
|
||||
styles_view = RenderStyles(None, base, inline)
|
||||
|
||||
# Both styles are empty
|
||||
assert styles_view.text == Style()
|
||||
@@ -43,19 +43,19 @@ def test_styles_view_border():
|
||||
|
||||
base = Styles()
|
||||
inline = Styles()
|
||||
styles_view = StylesView(None, base, inline)
|
||||
styles_view = RenderStyles(None, base, inline)
|
||||
|
||||
base.border_top = ("heavy", "red")
|
||||
# Base has border-top: heavy red
|
||||
assert styles_view.border_top == ("heavy", Style.parse("red"))
|
||||
assert styles_view.border_top == ("heavy", Color.parse("red"))
|
||||
|
||||
inline.border_left = ("rounded", "green")
|
||||
# Base has border-top heavy red, inline has border-left: rounded green
|
||||
assert styles_view.border_top == ("heavy", Style.parse("red"))
|
||||
assert styles_view.border_left == ("rounded", Style.parse("green"))
|
||||
assert styles_view.border_top == ("heavy", Color.parse("red"))
|
||||
assert styles_view.border_left == ("rounded", Color.parse("green"))
|
||||
assert styles_view.border == (
|
||||
("heavy", Color.parse("red")),
|
||||
("", Color()),
|
||||
("", Color()),
|
||||
("", Color.default()),
|
||||
("", Color.default()),
|
||||
("rounded", Color.parse("green")),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user