convert styles to using sipler dict

This commit is contained in:
Will McGugan
2022-02-04 18:39:50 +00:00
parent b2974aad6e
commit 2a8fbd5505
11 changed files with 414 additions and 369 deletions

View File

@@ -17,6 +17,7 @@ class BasicApp(App):
footer=Widget(), footer=Widget(),
sidebar=Widget(), sidebar=Widget(),
) )
self.panic(self.stylesheet.css)
BasicApp.run(css_file="basic.css", watch_css=True, log="textual.log") BasicApp.run(css_file="basic.css", watch_css=True, log="textual.log")

View File

@@ -9,7 +9,8 @@ when setting and getting.
from __future__ import annotations from __future__ import annotations
from typing import Iterable, NamedTuple, Sequence, TYPE_CHECKING
from typing import Iterable, Iterator, NamedTuple, Sequence, TYPE_CHECKING
import rich.repr import rich.repr
from rich.color import Color from rich.color import Color
@@ -54,7 +55,6 @@ class ScalarProperty:
def __set_name__(self, owner: Styles, name: str) -> None: def __set_name__(self, owner: Styles, name: str) -> None:
self.name = name self.name = name
self.internal_name = f"_rule_{name}"
def __get__( def __get__(
self, obj: Styles, objtype: type[Styles] | None = None self, obj: Styles, objtype: type[Styles] | None = None
@@ -68,7 +68,7 @@ class ScalarProperty:
Returns: Returns:
The Scalar object or ``None`` if it's not set. The Scalar object or ``None`` if it's not set.
""" """
value = getattr(obj, self.internal_name) value = obj._rules.get(self.name)
return value return value
def __set__(self, obj: Styles, value: float | Scalar | str | None) -> None: def __set__(self, obj: Styles, value: float | Scalar | str | None) -> None:
@@ -88,8 +88,9 @@ class ScalarProperty:
cannot be parsed for any other reason. cannot be parsed for any other reason.
""" """
if value is None: if value is None:
new_value = None obj._rules.pop(self.name, None)
elif isinstance(value, float): return
if isinstance(value, float):
new_value = Scalar(float(value), Unit.CELLS, Unit.WIDTH) new_value = Scalar(float(value), Unit.CELLS, Unit.WIDTH)
elif isinstance(value, Scalar): elif isinstance(value, Scalar):
new_value = value new_value = value
@@ -106,7 +107,7 @@ class ScalarProperty:
) )
if new_value is not None and new_value.is_percent: if new_value is not None and new_value.is_percent:
new_value = Scalar(float(new_value.value), self.percent_unit, Unit.WIDTH) new_value = Scalar(float(new_value.value), self.percent_unit, Unit.WIDTH)
setattr(obj, self.internal_name, new_value) obj._rules[self.name] = new_value
obj.refresh() obj.refresh()
@@ -118,7 +119,7 @@ class BoxProperty:
DEFAULT = ("", Style()) DEFAULT = ("", Style())
def __set_name__(self, owner: Styles, name: str) -> None: def __set_name__(self, owner: Styles, name: str) -> None:
self.internal_name = f"_rule_{name}" self.name = name
_type, edge = name.split("_") _type, edge = name.split("_")
self._type = _type self._type = _type
self.edge = edge self.edge = edge
@@ -136,8 +137,8 @@ class BoxProperty:
A ``tuple[BoxType, Style]`` containing the string type of the box and A ``tuple[BoxType, Style]`` containing the string type of the box and
it's style. Example types are "rounded", "solid", and "dashed". it's style. Example types are "rounded", "solid", and "dashed".
""" """
value = getattr(obj, self.internal_name) value = obj._rules.get(self.name) or self.DEFAULT
return value or self.DEFAULT return value
def __set__(self, obj: Styles, border: tuple[BoxType, str | Color | Style] | None): def __set__(self, obj: Styles, border: tuple[BoxType, str | Color | Style] | None):
"""Set the box property """Set the box property
@@ -152,7 +153,7 @@ class BoxProperty:
StyleSyntaxError: If the string supplied for the color has invalid syntax. StyleSyntaxError: If the string supplied for the color has invalid syntax.
""" """
if border is None: if border is None:
new_value = None obj._rules.pop(self.name, None)
else: else:
_type, color = border _type, color = border
if isinstance(color, str): if isinstance(color, str):
@@ -161,7 +162,7 @@ class BoxProperty:
new_value = (_type, Style.from_color(color)) new_value = (_type, Style.from_color(color))
else: else:
new_value = (_type, Style.from_color(Color.parse(color))) new_value = (_type, Style.from_color(Color.parse(color)))
setattr(obj, self.internal_name, new_value) obj._rules[self.name] = new_value
obj.refresh() obj.refresh()
@@ -169,13 +170,14 @@ class BoxProperty:
class Edges(NamedTuple): class Edges(NamedTuple):
"""Stores edges for border / outline.""" """Stores edges for border / outline."""
css_name: str
top: tuple[BoxType, Style] top: tuple[BoxType, Style]
right: tuple[BoxType, Style] right: tuple[BoxType, Style]
bottom: tuple[BoxType, Style] bottom: tuple[BoxType, Style]
left: tuple[BoxType, Style] left: tuple[BoxType, Style]
def __rich_repr__(self) -> rich.repr.Result: def __rich_repr__(self) -> rich.repr.Result:
top, right, bottom, left = self _, top, right, bottom, left = self
if top[0]: if top[0]:
yield "top", top yield "top", top
if right[0]: if right[0]:
@@ -191,7 +193,7 @@ class Edges(NamedTuple):
Returns: Returns:
tuple[int, int, int, int]: Spacing for top, right, bottom, and left. tuple[int, int, int, int]: Spacing for top, right, bottom, and left.
""" """
top, right, bottom, left = self _, top, right, bottom, left = self
return ( return (
1 if top[0] else 0, 1 if top[0] else 0,
1 if right[0] else 0, 1 if right[0] else 0,
@@ -204,6 +206,7 @@ class BorderProperty:
"""Descriptor for getting and setting full borders and outlines.""" """Descriptor for getting and setting full borders and outlines."""
def __set_name__(self, owner: Styles, name: str) -> None: def __set_name__(self, owner: Styles, name: str) -> None:
self.name = name
self._properties = ( self._properties = (
f"{name}_top", f"{name}_top",
f"{name}_right", f"{name}_right",
@@ -222,7 +225,9 @@ class BorderProperty:
An ``Edges`` object describing the type and style of each edge. An ``Edges`` object describing the type and style of each edge.
""" """
top, right, bottom, left = self._properties top, right, bottom, left = self._properties
border = Edges( border = Edges(
self.name,
getattr(obj, top), getattr(obj, top),
getattr(obj, right), getattr(obj, right),
getattr(obj, bottom), getattr(obj, bottom),
@@ -292,11 +297,6 @@ class StyleProperty:
DEFAULT_STYLE = Style() DEFAULT_STYLE = Style()
def __set_name__(self, owner: Styles, name: str) -> None:
self._color_name = f"_rule_{name}_color"
self._bgcolor_name = f"_rule_{name}_background"
self._style_name = f"_rule_{name}_style"
def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> Style: def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> Style:
"""Get the Style """Get the Style
@@ -307,12 +307,11 @@ class StyleProperty:
Returns: Returns:
A ``Style`` object. A ``Style`` object.
""" """
color = getattr(obj, self._color_name) rules_get = obj._rules.get
bgcolor = getattr(obj, self._bgcolor_name) color = rules_get("text_color") or Color.default()
bgcolor = rules_get("text_background") or Color.default()
style = Style.from_color(color, bgcolor) style = Style.from_color(color, bgcolor)
style_flags = getattr(obj, self._style_name) style += rules_get("text_style") or Style()
if style_flags:
style += style_flags
return style return style
def __set__(self, obj: Styles, style: Style | str | None): def __set__(self, obj: Styles, style: Style | str | None):
@@ -327,26 +326,28 @@ class StyleProperty:
StyleSyntaxError: When the supplied style string has invalid syntax. StyleSyntaxError: When the supplied style string has invalid syntax.
""" """
obj.refresh() obj.refresh()
rules_set = obj._rules.__setitem__
if style is None: if style is None:
setattr(obj, self._color_name, None) rules = obj._rules
setattr(obj, self._bgcolor_name, None) rules.pop("text_color")
setattr(obj, self._style_name, None) rules.pop("text_background")
rules.pop("text_style")
elif isinstance(style, Style): elif isinstance(style, Style):
setattr(obj, self._color_name, style.color) rules_set("text_color", style.color)
setattr(obj, self._bgcolor_name, style.bgcolor) rules_set("text_background", style.bgcolor)
setattr(obj, self._style_name, style.without_color) rules_set("text_style", style.without_color)
elif isinstance(style, str): elif isinstance(style, str):
new_style = Style.parse(style) new_style = Style.parse(style)
setattr(obj, self._color_name, new_style.color) rules_set("text_color", new_style.color)
setattr(obj, self._bgcolor_name, new_style.bgcolor) rules_set("text_background", new_style.bgcolor)
setattr(obj, self._style_name, new_style.without_color) rules_set("text_style", new_style.without_color)
class SpacingProperty: class SpacingProperty:
"""Descriptor for getting and setting spacing properties (e.g. padding and margin).""" """Descriptor for getting and setting spacing properties (e.g. padding and margin)."""
def __set_name__(self, owner: Styles, name: str) -> None: def __set_name__(self, owner: Styles, name: str) -> None:
self._internal_name = f"_rule_{name}" self.name = name
def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> Spacing: def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> Spacing:
"""Get the Spacing """Get the Spacing
@@ -358,9 +359,9 @@ class SpacingProperty:
Returns: Returns:
Spacing: The Spacing. If unset, returns the null spacing ``(0, 0, 0, 0)``. Spacing: The Spacing. If unset, returns the null spacing ``(0, 0, 0, 0)``.
""" """
return getattr(obj, self._internal_name) or NULL_SPACING return obj._rules.get(self.name, NULL_SPACING)
def __set__(self, obj: Styles, spacing: SpacingDimensions): def __set__(self, obj: Styles, spacing: SpacingDimensions | None):
"""Set the Spacing """Set the Spacing
Args: Args:
@@ -373,8 +374,10 @@ class SpacingProperty:
not 1, 2, or 4. not 1, 2, or 4.
""" """
obj.refresh(layout=True) obj.refresh(layout=True)
spacing = Spacing.unpack(spacing) if spacing is None:
setattr(obj, self._internal_name, spacing) obj._rules.pop(self.name)
else:
obj._rules[self.name] = Spacing.unpack(spacing)
class DocksProperty: class DocksProperty:
@@ -394,7 +397,7 @@ class DocksProperty:
Returns: Returns:
tuple[DockGroup, ...]: A ``tuple`` containing the defined docks. tuple[DockGroup, ...]: A ``tuple`` containing the defined docks.
""" """
return obj._rule_docks or () return obj._rules.get("docks") or ()
def __set__(self, obj: Styles, docks: Iterable[DockGroup] | None): def __set__(self, obj: Styles, docks: Iterable[DockGroup] | None):
"""Set the Docks property """Set the Docks property
@@ -405,9 +408,9 @@ class DocksProperty:
""" """
obj.refresh(layout=True) obj.refresh(layout=True)
if docks is None: if docks is None:
obj._rule_docks = None obj._rules.pop("docks")
else: else:
obj._rule_docks = tuple(docks) obj._rules["docks"] = tuple(docks)
class DockProperty: class DockProperty:
@@ -427,7 +430,7 @@ class DockProperty:
Returns: Returns:
str: The dock name as a string, or "" if the rule is not set. str: The dock name as a string, or "" if the rule is not set.
""" """
return obj._rule_dock or "" return obj._rules.get("dock") or ""
def __set__(self, obj: Styles, spacing: str | None): def __set__(self, obj: Styles, spacing: str | None):
"""Set the Dock property """Set the Dock property
@@ -437,14 +440,17 @@ class DockProperty:
spacing (str | None): The spacing to use. spacing (str | None): The spacing to use.
""" """
obj.refresh(layout=True) obj.refresh(layout=True)
obj._rule_dock = spacing if spacing is None:
obj._rules.pop("dock")
else:
obj._rules["dock"] = spacing
class LayoutProperty: class LayoutProperty:
"""Descriptor for getting and setting layout.""" """Descriptor for getting and setting layout."""
def __set_name__(self, owner: Styles, name: str) -> None: def __set_name__(self, owner: Styles, name: str) -> None:
self._internal_name = f"_rule_{name}" self.name = name
def __get__( def __get__(
self, obj: Styles, objtype: type[Styles] | None = None self, obj: Styles, objtype: type[Styles] | None = None
@@ -456,9 +462,9 @@ class LayoutProperty:
Returns: Returns:
The ``Layout`` object. The ``Layout`` object.
""" """
return getattr(obj, self._internal_name) return obj._rules.get(self.name)
def __set__(self, obj: Styles, layout: str | Layout): def __set__(self, obj: Styles, layout: str | Layout | None):
""" """
Args: Args:
obj (Styles): The Styles object. obj (Styles): The Styles object.
@@ -469,11 +475,13 @@ class LayoutProperty:
from ..layouts.factory import get_layout, Layout # Prevents circular import from ..layouts.factory import get_layout, Layout # Prevents circular import
obj.refresh(layout=True) obj.refresh(layout=True)
if isinstance(layout, Layout):
new_layout = layout if layout is None:
obj._rules.pop("layout")
elif isinstance(layout, Layout):
obj._rules["layout"] = layout
else: else:
new_layout = get_layout(layout) obj._rules["layout"] = get_layout(layout)
setattr(obj, self._internal_name, new_layout)
class OffsetProperty: class OffsetProperty:
@@ -483,7 +491,7 @@ class OffsetProperty:
""" """
def __set_name__(self, owner: Styles, name: str) -> None: def __set_name__(self, owner: Styles, name: str) -> None:
self._internal_name = f"_rule_{name}" self.name = name
def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> ScalarOffset: def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> ScalarOffset:
"""Get the offset """Get the offset
@@ -496,11 +504,11 @@ class OffsetProperty:
ScalarOffset: The ``ScalarOffset`` indicating the adjustment that ScalarOffset: The ``ScalarOffset`` indicating the adjustment that
will be made to widget position prior to it being rendered. will be made to widget position prior to it being rendered.
""" """
return getattr(obj, self._internal_name) or ScalarOffset( return obj._rules.get(self.name, ScalarOffset.null())
Scalar.from_number(0), Scalar.from_number(0)
)
def __set__(self, obj: Styles, offset: tuple[int | str, int | str] | ScalarOffset): def __set__(
self, obj: Styles, offset: tuple[int | str, int | str] | ScalarOffset | None
):
"""Set the offset """Set the offset
Args: Args:
@@ -515,9 +523,11 @@ class OffsetProperty:
be parsed into a Scalar. For example, if you specify an non-existent unit. be parsed into a Scalar. For example, if you specify an non-existent unit.
""" """
obj.refresh(layout=True) obj.refresh(layout=True)
if isinstance(offset, ScalarOffset): if offset is None:
setattr(obj, self._internal_name, offset) obj._rules.pop(self.name, None)
return offset elif isinstance(offset, ScalarOffset):
obj._rules[self.name] = offset
else:
x, y = offset x, y = offset
scalar_x = ( scalar_x = (
Scalar.parse(x, Unit.WIDTH) Scalar.parse(x, Unit.WIDTH)
@@ -530,42 +540,7 @@ class OffsetProperty:
else Scalar(float(y), Unit.CELLS, Unit.HEIGHT) else Scalar(float(y), Unit.CELLS, Unit.HEIGHT)
) )
_offset = ScalarOffset(scalar_x, scalar_y) _offset = ScalarOffset(scalar_x, scalar_y)
setattr(obj, self._internal_name, _offset) obj._rules[self.name] = _offset
class IntegerProperty:
"""Descriptor for getting and setting integer properties"""
def __set_name__(self, owner: Styles, name: str) -> None:
self._name = name
self._internal_name = f"_{name}"
def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> int:
"""Get the integer property, or the default ``0`` if not set.
Args:
obj (Styles): The ``Styles`` object.
objtype (type[Styles]): The ``Styles`` class.
Returns:
int: The integer property value
"""
return getattr(obj, self._internal_name, 0)
def __set__(self, obj: Styles, value: int):
"""Set the integer property
Args:
obj: The ``Styles`` object
value: The value to set the integer to
Raises:
StyleTypeError: If the supplied value is not an integer.
"""
obj.refresh()
if not isinstance(value, int):
raise StyleTypeError(f"{self._name} must be an integer")
setattr(obj, self._internal_name, value)
class StringEnumProperty: class StringEnumProperty:
@@ -578,8 +553,7 @@ class StringEnumProperty:
self._default = default self._default = default
def __set_name__(self, owner: Styles, name: str) -> None: def __set_name__(self, owner: Styles, name: str) -> None:
self._name = name self.name = name
self._internal_name = f"_rule_{name}"
def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> str: def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> str:
"""Get the string property, or the default value if it's not set """Get the string property, or the default value if it's not set
@@ -591,7 +565,7 @@ class StringEnumProperty:
Returns: Returns:
str: The string property value str: The string property value
""" """
return getattr(obj, self._internal_name, None) or self._default return obj._rules.get(self.name, self._default)
def __set__(self, obj: Styles, value: str | None = None): def __set__(self, obj: Styles, value: str | None = None):
"""Set the string property and ensure it is in the set of allowed values. """Set the string property and ensure it is in the set of allowed values.
@@ -604,12 +578,14 @@ class StringEnumProperty:
StyleValueError: If the value is not in the set of valid values. StyleValueError: If the value is not in the set of valid values.
""" """
obj.refresh() obj.refresh()
if value is not None: if value is None:
obj._rules.pop(self.name, None)
else:
if value not in self._valid_values: if value not in self._valid_values:
raise StyleValueError( raise StyleValueError(
f"{self._name} must be one of {friendly_list(self._valid_values)}" f"{self.name} must be one of {friendly_list(self._valid_values)}"
) )
setattr(obj, self._internal_name, value) obj._rules[self.name] = value
class NameProperty: class NameProperty:
@@ -617,7 +593,6 @@ class NameProperty:
def __set_name__(self, owner: Styles, name: str) -> None: def __set_name__(self, owner: Styles, name: str) -> None:
self._name = name self._name = name
self._internal_name = f"_rule_{name}"
def __get__(self, obj: Styles, objtype: type[Styles] | None) -> str: def __get__(self, obj: Styles, objtype: type[Styles] | None) -> str:
"""Get the name property """Get the name property
@@ -629,7 +604,7 @@ class NameProperty:
Returns: Returns:
str: The name str: The name
""" """
return getattr(obj, self._internal_name) or "" return obj.get(self.name, "")
def __set__(self, obj: Styles, name: str | None): def __set__(self, obj: Styles, name: str | None):
"""Set the name property """Set the name property
@@ -642,42 +617,42 @@ class NameProperty:
StyleTypeError: If the value is not a ``str``. StyleTypeError: If the value is not a ``str``.
""" """
obj.refresh(layout=True) obj.refresh(layout=True)
if name is None:
obj._rules.pop(self.name, None)
else:
if not isinstance(name, str): if not isinstance(name, str):
raise StyleTypeError(f"{self._name} must be a str") raise StyleTypeError(f"{self._name} must be a str")
setattr(obj, self._internal_name, name) obj._rules[self.name] = name
class NameListProperty: class NameListProperty:
def __set_name__(self, owner: Styles, name: str) -> None: def __set_name__(self, owner: Styles, name: str) -> None:
self._name = name self._name = name
self._internal_name = f"_rule_{name}"
def __get__( def __get__(
self, obj: Styles, objtype: type[Styles] | None = None self, obj: Styles, objtype: type[Styles] | None = None
) -> tuple[str, ...]: ) -> tuple[str, ...]:
return getattr(obj, self._internal_name, None) or () return obj._rules.get(self.name, ())
def __set__( def __set__(
self, obj: Styles, names: str | tuple[str] | None = None self, obj: Styles, names: str | tuple[str] | None = None
) -> str | tuple[str] | None: ) -> str | tuple[str] | None:
obj.refresh(layout=True) obj.refresh(layout=True)
names_value: tuple[str, ...] | None = None if names is None:
if isinstance(names, str): obj._rules.pop(self.name, None)
names_value = tuple(name.strip().lower() for name in names.split(" ")) elif isinstance(names, str):
obj._rules[self.name] = tuple(
name.strip().lower() for name in names.split(" ")
)
elif isinstance(names, tuple): elif isinstance(names, tuple):
names_value = names obj._rules[self.name] = names
elif names is None:
names_value = None
setattr(obj, self._internal_name, names_value)
return names
class ColorProperty: class ColorProperty:
"""Descriptor for getting and setting color properties.""" """Descriptor for getting and setting color properties."""
def __set_name__(self, owner: Styles, name: str) -> None: def __set_name__(self, owner: Styles, name: str) -> None:
self._name = name self.name = name
self._internal_name = f"_rule_{name}"
def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> Color: def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> Color:
"""Get the ``Color``, or ``Color.default()`` if no color is set. """Get the ``Color``, or ``Color.default()`` if no color is set.
@@ -689,7 +664,7 @@ class ColorProperty:
Returns: Returns:
Color: The Color Color: The Color
""" """
return getattr(obj, self._internal_name, None) or Color.default() return obj._rules.get(self.name) or Color.default()
def __set__(self, obj: Styles, color: Color | str | None): def __set__(self, obj: Styles, color: Color | str | None):
"""Set the Color """Set the Color
@@ -705,13 +680,11 @@ class ColorProperty:
""" """
obj.refresh() obj.refresh()
if color is None: if color is None:
setattr(self, self._internal_name, None) obj._rules.pop(self.name, None)
else: elif isinstance(color, Color):
if isinstance(color, Color): obj._rules[self.name] = color
setattr(self, self._internal_name, color)
elif isinstance(color, str): elif isinstance(color, str):
new_color = Color.parse(color) obj._rules[self.name] = Color.parse(color)
setattr(self, self._internal_name, new_color)
class StyleFlagsProperty: class StyleFlagsProperty:
@@ -731,8 +704,7 @@ class StyleFlagsProperty:
} }
def __set_name__(self, owner: Styles, name: str) -> None: def __set_name__(self, owner: Styles, name: str) -> None:
self._name = name self.name = name
self._internal_name = f"_rule_{name}"
def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> Style: def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> Style:
"""Get the ``Style`` """Get the ``Style``
@@ -744,7 +716,7 @@ class StyleFlagsProperty:
Returns: Returns:
Style: The ``Style`` object Style: The ``Style`` object
""" """
return getattr(obj, self._internal_name, None) or Style.null() return obj._rules.get(self.name, Style.null())
def __set__(self, obj: Styles, style_flags: str | None): def __set__(self, obj: Styles, style_flags: str | None):
"""Set the style using a style flag string """Set the style using a style flag string
@@ -759,7 +731,7 @@ class StyleFlagsProperty:
""" """
obj.refresh() obj.refresh()
if style_flags is None: if style_flags is None:
setattr(self, self._internal_name, None) obj._styles.pop(self.name, None)
else: else:
words = [word.strip() for word in style_flags.split(" ")] words = [word.strip() for word in style_flags.split(" ")]
valid_word = self._VALID_PROPERTIES.__contains__ valid_word = self._VALID_PROPERTIES.__contains__
@@ -770,15 +742,14 @@ class StyleFlagsProperty:
f"valid values are {friendly_list(self._VALID_PROPERTIES)}" f"valid values are {friendly_list(self._VALID_PROPERTIES)}"
) )
style = Style.parse(style_flags) style = Style.parse(style_flags)
setattr(obj, self._internal_name, style) obj._rules[self.name] = style
class TransitionsProperty: class TransitionsProperty:
"""Descriptor for getting transitions properties""" """Descriptor for getting transitions properties"""
def __set_name__(self, owner: Styles, name: str) -> None: def __set_name__(self, owner: Styles, name: str) -> None:
self._name = name self.name = name
self._internal_name = f"_rule_{name}"
def __get__( def __get__(
self, obj: Styles, objtype: type[Styles] | None = None self, obj: Styles, objtype: type[Styles] | None = None
@@ -794,4 +765,4 @@ class TransitionsProperty:
e.g. ``{"offset": Transition(...), ...}``. If no transitions have been set, an empty ``dict`` e.g. ``{"offset": Transition(...), ...}``. If no transitions have been set, an empty ``dict``
is returned. is returned.
""" """
return getattr(obj, self._internal_name, None) or {} return obj._rules.get(self.name, {})

View File

@@ -71,7 +71,7 @@ class StylesBuilder:
if name == "token": if name == "token":
value = value.lower() value = value.lower()
if value in VALID_DISPLAY: if value in VALID_DISPLAY:
self.styles._rule_display = cast(Display, value) self.styles._rules["display"] = cast(Display, value)
else: else:
self.error( self.error(
name, name,
@@ -85,7 +85,7 @@ class StylesBuilder:
if not tokens: if not tokens:
return return
if len(tokens) == 1: if len(tokens) == 1:
setattr(self.styles, name, Scalar.parse(tokens[0].value)) self.styles._rules[name] = Scalar.parse(tokens[0].value)
else: else:
self.error(name, tokens[0], "a single scalar is expected") self.error(name, tokens[0], "a single scalar is expected")
@@ -113,7 +113,7 @@ class StylesBuilder:
if name == "token": if name == "token":
value = value.lower() value = value.lower()
if value in VALID_VISIBILITY: if value in VALID_VISIBILITY:
self.styles._rule_visibility = cast(Visibility, value) self.styles._rules["visibility"] = cast(Visibility, value)
else: else:
self.error( self.error(
name, name,
@@ -139,11 +139,7 @@ class StylesBuilder:
self.error( self.error(
name, tokens[0], f"1, 2, or 4 values expected; received {len(space)}" name, tokens[0], f"1, 2, or 4 values expected; received {len(space)}"
) )
setattr( self.styles._rules[name] = Spacing.unpack(cast(SpacingDimensions, tuple(space)))
self.styles,
f"_rule_{name}",
Spacing.unpack(cast(SpacingDimensions, tuple(space))),
)
def process_padding(self, name: str, tokens: list[Token], important: bool) -> None: def process_padding(self, name: str, tokens: list[Token], important: bool) -> None:
self._process_space(name, tokens) self._process_space(name, tokens)
@@ -176,13 +172,13 @@ class StylesBuilder:
def _process_border(self, edge: str, name: str, tokens: list[Token]) -> None: def _process_border(self, edge: str, name: str, tokens: list[Token]) -> None:
border = self._parse_border("border", tokens) border = self._parse_border("border", tokens)
setattr(self.styles, f"_rule_border_{edge}", border) self.styles._rules[f"border_{edge}"] = border
def process_border(self, name: str, tokens: list[Token], important: bool) -> None: def process_border(self, name: str, tokens: list[Token], important: bool) -> None:
border = self._parse_border("border", tokens) border = self._parse_border("border", tokens)
styles = self.styles rules = self.styles._rules
styles._rule_border_top = styles._rule_border_right = border rules["border_top"] = rules["border_right"] = border
styles._rule_border_bottom = styles._rule_border_left = border rules["border_bottom"] = rules["border_left"] = border
def process_border_top( def process_border_top(
self, name: str, tokens: list[Token], important: bool self, name: str, tokens: list[Token], important: bool
@@ -206,13 +202,13 @@ class StylesBuilder:
def _process_outline(self, edge: str, name: str, tokens: list[Token]) -> None: def _process_outline(self, edge: str, name: str, tokens: list[Token]) -> None:
border = self._parse_border("outline", tokens) border = self._parse_border("outline", tokens)
setattr(self.styles, f"_rule_outline_{edge}", border) self.styles._rules[f"outline_{edge}"] = border
def process_outline(self, name: str, tokens: list[Token], important: bool) -> None: def process_outline(self, name: str, tokens: list[Token], important: bool) -> None:
border = self._parse_border("outline", tokens) border = self._parse_border("outline", tokens)
styles = self.styles rules = self.styles._rules
styles._rule_outline_top = styles._rule_outline_right = border rules["outline_top"] = rules["outline_right"] = border
styles._rule_outline_bottom = styles._rule_outline_left = border rules["outline_bottom"] = rules["outline_left"] = border
def process_outline_top( def process_outline_top(
self, name: str, tokens: list[Token], important: bool self, name: str, tokens: list[Token], important: bool
@@ -255,7 +251,7 @@ class StylesBuilder:
scalar_x = Scalar.parse(token1.value, Unit.WIDTH) scalar_x = Scalar.parse(token1.value, Unit.WIDTH)
scalar_y = Scalar.parse(token2.value, Unit.HEIGHT) scalar_y = Scalar.parse(token2.value, Unit.HEIGHT)
self.styles._rule_offset = ScalarOffset(scalar_x, scalar_y) self.styles._rules["offset"] = ScalarOffset(scalar_x, scalar_y)
def process_offset_x(self, name: str, tokens: list[Token], important: bool) -> None: def process_offset_x(self, name: str, tokens: list[Token], important: bool) -> None:
if not tokens: if not tokens:
@@ -268,7 +264,7 @@ class StylesBuilder:
self.error(name, token, f"expected a scalar; found {token.value!r}") self.error(name, token, f"expected a scalar; found {token.value!r}")
x = Scalar.parse(token.value, Unit.WIDTH) x = Scalar.parse(token.value, Unit.WIDTH)
y = self.styles.offset.y y = self.styles.offset.y
self.styles._rule_offset = ScalarOffset(x, y) self.styles._rules["offset"] = ScalarOffset(x, y)
def process_offset_y(self, name: str, tokens: list[Token], important: bool) -> None: def process_offset_y(self, name: str, tokens: list[Token], important: bool) -> None:
if not tokens: if not tokens:
@@ -281,7 +277,7 @@ class StylesBuilder:
self.error(name, token, f"expected a scalar; found {token.value!r}") self.error(name, token, f"expected a scalar; found {token.value!r}")
y = Scalar.parse(token.value, Unit.HEIGHT) y = Scalar.parse(token.value, Unit.HEIGHT)
x = self.styles.offset.x x = self.styles.offset.x
self.styles._rule_offset = ScalarOffset(x, y) self.styles._rules["offset"] = ScalarOffset(x, y)
def process_layout(self, name: str, tokens: list[Token], important: bool) -> None: def process_layout(self, name: str, tokens: list[Token], important: bool) -> None:
from ..layouts.factory import get_layout, MissingLayout, LAYOUT_MAP from ..layouts.factory import get_layout, MissingLayout, LAYOUT_MAP
@@ -293,7 +289,7 @@ class StylesBuilder:
value = tokens[0].value value = tokens[0].value
layout_name = value layout_name = value
try: try:
self.styles._rule_layout = get_layout(layout_name) self.styles._rules["layout"] = get_layout(layout_name)
except MissingLayout: except MissingLayout:
self.error( self.error(
name, name,
@@ -311,7 +307,7 @@ class StylesBuilder:
self.styles.important.update( self.styles.important.update(
{"text_style", "text_background", "text_color"} {"text_style", "text_background", "text_color"}
) )
self.styles.text = style self.styles._rules["text"] = style
def process_text_color( def process_text_color(
self, name: str, tokens: list[Token], important: bool self, name: str, tokens: list[Token], important: bool
@@ -319,7 +315,7 @@ class StylesBuilder:
for token in tokens: for token in tokens:
if token.name in ("color", "token"): if token.name in ("color", "token"):
try: try:
self.styles._rule_text_color = Color.parse(token.value) self.styles._rules["text_color"] = Color.parse(token.value)
except Exception as error: except Exception as error:
self.error( self.error(
name, token, f"failed to parse color {token.value!r}; {error}" name, token, f"failed to parse color {token.value!r}; {error}"
@@ -335,7 +331,7 @@ class StylesBuilder:
for token in tokens: for token in tokens:
if token.name in ("color", "token"): if token.name in ("color", "token"):
try: try:
self.styles._rule_text_background = Color.parse(token.value) self.styles._rules["text_background"] = Color.parse(token.value)
except Exception as error: except Exception as error:
self.error( self.error(
name, token, f"failed to parse color {token.value!r}; {error}" name, token, f"failed to parse color {token.value!r}; {error}"
@@ -359,7 +355,7 @@ class StylesBuilder:
tokens[1], tokens[1],
f"unexpected tokens in dock declaration", f"unexpected tokens in dock declaration",
) )
self.styles._rule_dock = tokens[0].value if tokens else "" self.styles._rules["dock"] = tokens[0].value if tokens else ""
def process_docks(self, name: str, tokens: list[Token], important: bool) -> None: def process_docks(self, name: str, tokens: list[Token], important: bool) -> None:
docks: list[DockGroup] = [] docks: list[DockGroup] = []
@@ -390,12 +386,12 @@ class StylesBuilder:
token, token,
f"unexpected token {token.value!r} in docks declaration", f"unexpected token {token.value!r} in docks declaration",
) )
self.styles._rule_docks = tuple(docks + [DockGroup("_default", "top", 0)]) self.styles._rules["docks"] = tuple(docks + [DockGroup("_default", "top", 0)])
def process_layer(self, name: str, tokens: list[Token], important: bool) -> None: def process_layer(self, name: str, tokens: list[Token], important: bool) -> None:
if len(tokens) > 1: if len(tokens) > 1:
self.error(name, tokens[1], f"unexpected tokens in dock-edge declaration") self.error(name, tokens[1], f"unexpected tokens in dock-edge declaration")
self.styles._rule_layer = tokens[0].value self.styles._rules["layer"] = tokens[0].value
def process_layers(self, name: str, tokens: list[Token], important: bool) -> None: def process_layers(self, name: str, tokens: list[Token], important: bool) -> None:
layers: list[str] = [] layers: list[str] = []
@@ -403,7 +399,7 @@ class StylesBuilder:
if token.name != "token": if token.name != "token":
self.error(name, token, "{token.name} not expected here") self.error(name, token, "{token.name} not expected here")
layers.append(token.value) layers.append(token.value)
self.styles._rule_layers = tuple(layers) self.styles._rules["layers"] = tuple(layers)
def process_transition( def process_transition(
self, name: str, tokens: list[Token], important: bool self, name: str, tokens: list[Token], important: bool
@@ -468,4 +464,4 @@ class StylesBuilder:
pass pass
transitions[css_property] = Transition(duration, easing, delay) transitions[css_property] = Transition(duration, easing, delay)
self.styles._rule_transitions = transitions self.styles._rules["transitions"] = transitions

View File

@@ -142,7 +142,7 @@ class DOMQuery:
layout (bool): Layout node(s). Defaults to False. layout (bool): Layout node(s). Defaults to False.
Returns: Returns:
[type]: [description] DOMQuery: Query for chaining.
""" """
for node in self._nodes: for node in self._nodes:
node.refresh(repaint=repaint, layout=layout) node.refresh(repaint=repaint, layout=layout)

View File

@@ -145,6 +145,10 @@ class ScalarOffset(NamedTuple):
x: Scalar x: Scalar
y: Scalar y: Scalar
@classmethod
def null(cls) -> ScalarOffset:
return cls(Scalar.from_number(0), Scalar.from_number(0))
def __bool__(self) -> bool: def __bool__(self) -> bool:
x, y = self x, y = self
return bool(x.value or y.value) return bool(x.value or y.value)

View File

@@ -60,7 +60,7 @@ class ScalarAnimation(Animation):
return True return True
offset = self.start + (self.destination - self.start) * eased_factor offset = self.start + (self.destination - self.start) * eased_factor
current = getattr(self.styles, f"_rule_{self.attribute}") current = self.styles._rules[self.attribute]
if current != offset: if current != offset:
setattr(self.styles, f"{self.attribute}", offset) setattr(self.styles, f"{self.attribute}", offset)

View File

@@ -2,8 +2,8 @@ from __future__ import annotations
from dataclasses import dataclass, field from dataclasses import dataclass, field
from functools import lru_cache from functools import lru_cache
from operator import attrgetter import sys
from typing import Any, Iterable, NamedTuple, TYPE_CHECKING from typing import Any, cast, Iterable, NamedTuple, TYPE_CHECKING
import rich.repr import rich.repr
from rich.color import Color from rich.color import Color
@@ -45,25 +45,56 @@ from ..geometry import Spacing, SpacingDimensions
from .._box import BoxType from .._box import BoxType
if sys.version_info >= (3, 8):
from typing import TypedDict
else:
from typing_extensions import TypedDict
if TYPE_CHECKING: if TYPE_CHECKING:
from ..layout import Layout from ..layout import Layout
from ..dom import DOMNode from ..dom import DOMNode
_text_getter = attrgetter( class RulesMap(TypedDict):
"_rule_text_color", "_rule_text_background", "_rule_text_style"
)
_border_getter = attrgetter( display: Display
"_rule_border_top", "_rule_border_right", "_rule_border_bottom", "_rule_border_left" visibility: Visibility
) layout: "Layout"
_outline_getter = attrgetter( text_color: Color
"_rule_outline_top", text_background: Color
"_rule_outline_right", text_style: Style
"_rule_outline_bottom",
"_rule_outline_left", padding: Spacing
) margin: Spacing
offset: ScalarOffset
border_top: tuple[str, Style]
border_right: tuple[str, Style]
border_bottom: tuple[str, Style]
border_left: tuple[str, Style]
outline_top: tuple[str, Style]
outline_right: tuple[str, Style]
outline_bottom: tuple[str, Style]
outline_left: tuple[str, Style]
width: Scalar
height: Scalar
min_width: Scalar
min_height: Scalar
dock: str
docks: tuple[DockGroup, ...]
layers: tuple[str, ...]
layer: str
transitions: dict[str, Transition]
RULE_NAMES = list(RulesMap.__annotations__.keys())
class DockGroup(NamedTuple): class DockGroup(NamedTuple):
@@ -78,40 +109,7 @@ class Styles:
node: DOMNode | None = None node: DOMNode | None = None
_rule_display: Display | None = None _rules: RulesMap = field(default_factory=dict)
_rule_visibility: Visibility | None = None
_rule_layout: "Layout" | None = None
_rule_text_color: Color | None = None
_rule_text_background: Color | None = None
_rule_text_style: Style | None = None
_rule_padding: Spacing | None = None
_rule_margin: Spacing | None = None
_rule_offset: ScalarOffset | None = None
_rule_border_top: tuple[str, Style] | None = None
_rule_border_right: tuple[str, Style] | None = None
_rule_border_bottom: tuple[str, Style] | None = None
_rule_border_left: tuple[str, Style] | None = None
_rule_outline_top: tuple[str, Style] | None = None
_rule_outline_right: tuple[str, Style] | None = None
_rule_outline_bottom: tuple[str, Style] | None = None
_rule_outline_left: tuple[str, Style] | None = None
_rule_width: Scalar | None = None
_rule_height: Scalar | None = None
_rule_min_width: Scalar | None = None
_rule_min_height: Scalar | None = None
_rule_dock: str | None = None
_rule_docks: tuple[DockGroup, ...] | None = None
_rule_layers: tuple[str, ...] | None = None
_rule_layer: str | None = None
_rule_transitions: dict[str, Transition] | None = None
_layout_required: bool = False _layout_required: bool = False
_repaint_required: bool = False _repaint_required: bool = False
@@ -120,16 +118,26 @@ class Styles:
def has_rule(self, rule: str) -> bool: def has_rule(self, rule: str) -> bool:
"""Check if a rule has been set.""" """Check if a rule has been set."""
if rule in RULE_NAMES and getattr(self, f"_rule_{rule}") is not None: if rule in RULE_NAMES and rule in self._rules:
return True return True
has_rule = self._rules.__contains__
if rule == "text": if rule == "text":
return not all(rule is None for rule in _text_getter(self)) return (
has_rule("text_color")
or has_rule("text_bgcolor")
or has_rule("text_style")
)
if rule == "border" and any(self.border): if rule == "border" and any(self.border):
return not all(rule is None for rule in _border_getter(self)) return True
if rule == "outline" and any(self.outline): if rule == "outline" and any(self.outline):
return not all(rule is None for rule in _outline_getter(self)) return True
return False return False
def get_rules(self) -> RulesMap:
"""Get rules as a dictionary."""
rules = self._rules.copy()
return rules
display = StringEnumProperty(VALID_DISPLAY, "block") display = StringEnumProperty(VALID_DISPLAY, "block")
visibility = StringEnumProperty(VALID_VISIBILITY, "visible") visibility = StringEnumProperty(VALID_VISIBILITY, "visible")
layout = LayoutProperty() layout = LayoutProperty()
@@ -196,31 +204,6 @@ class Styles:
styles.node = node styles.node = node
return styles return styles
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 refresh(self, layout: bool = False) -> None: def refresh(self, layout: bool = False) -> None:
self._repaint_required = True self._repaint_required = True
self._layout_required = layout self._layout_required = layout
@@ -245,54 +228,49 @@ class Styles:
""" """
Reset internal style rules to ``None``, reverting to default styles. Reset internal style rules to ``None``, reverting to default styles.
""" """
for rule_name in INTERNAL_RULE_NAMES: self._rules.clear()
setattr(self, rule_name, None)
def extract_rules( def extract_rules(
self, specificity: Specificity3 self, specificity: Specificity3
) -> list[tuple[str, Specificity4, Any]]: ) -> list[tuple[str, Specificity4, Any]]:
is_important = self.important.__contains__ is_important = self.important.__contains__
rules = [ rules = [
( (rule_name, (int(is_important(rule_name)), *specificity), rule_value)
rule_name, for rule_name, rule_value in self._rules.items()
(int(is_important(rule_name)), *specificity),
getattr(self, f"_rule_{rule_name}"),
)
for rule_name in RULE_NAMES
if getattr(self, f"_rule_{rule_name}") is not None
] ]
return rules return rules
def apply_rules(self, rules: Iterable[tuple[str, object]], animate: bool = False): def apply_rules(self, rules: RulesMap, animate: bool = False):
if animate or self.node is None: if animate or self.node is None:
for key, value in rules: self._rules.update(rules)
setattr(self, f"_rule_{key}", value)
else: else:
styles = self styles = self
is_animatable = styles.ANIMATABLE.__contains__ is_animatable = styles.ANIMATABLE.__contains__
for key, value in rules: _rules = self._rules
current = getattr(styles, f"_rule_{key}") for key, value in rules.items():
current = _rules.get(key)
if current == value: if current == value:
continue continue
if is_animatable(key): if is_animatable(key):
transition = styles.get_transition(key) transition = styles.get_transition(key)
if transition is None: if transition is None:
setattr(styles, f"_rule_{key}", value) _rules[key] = value
else: else:
duration, easing, delay = transition duration, easing, delay = transition
self.node.app.animator.animate( self.node.app.animator.animate(
styles, key, value, duration=duration, easing=easing styles, key, value, duration=duration, easing=easing
) )
else: else:
setattr(styles, f"_rule_{key}", value) rules[key] = value
if self.node is not None: if self.node is not None:
self.node.on_style_change() self.node.on_style_change()
def __rich_repr__(self) -> rich.repr.Result: def __rich_repr__(self) -> rich.repr.Result:
for rule_name, internal_rule_name in zip(RULE_NAMES, INTERNAL_RULE_NAMES): for name, value in self._rules.items():
if getattr(self, internal_rule_name) is not None: yield name, value
yield rule_name, getattr(self, rule_name)
if self.important: if self.important:
yield "important", self.important yield "important", self.important
@@ -302,10 +280,63 @@ class Styles:
Args: Args:
other (Styles): A Styles object. other (Styles): A Styles object.
""" """
for name in INTERNAL_RULE_NAMES:
value = getattr(other, name) self._rules.update(other._rules)
if value is not None:
setattr(self, name, value) def _get_border_css_lines(
self, rules: RulesMap, name: str
) -> Iterable[tuple[str, str]]:
"""Get CSS lines for border / outline
Args:
rules (RulesMap): A rules map.
name (str): Name of rules (border or outline)
Returns:
Iterable[tuple[str, str]]: An iterable of CSS declarations.
"""
has_rule = rules.__contains__
get_rule = rules.__getitem__
has_top = has_rule(f"{name}_top")
has_right = has_rule(f"{name}_right")
has_bottom = has_rule(f"{name}_bottom")
has_left = has_rule(f"{name}_left")
if not any((has_top, has_right, has_bottom, has_left)):
# No border related rules
return
if all((has_top, has_right, has_bottom, has_left)):
# All rules are set
# See if we can set them with a single border: declaration
top = get_rule(f"{name}_top")
right = get_rule(f"{name}_right")
bottom = get_rule(f"{name}_bottom")
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}"
return
# Check for edges
if has_top:
border_type, border_style = rules[f"{name}_top"]
yield f"{name}-top", f"{border_type} {border_style}"
if has_right:
border_type, border_style = rules[f"{name}_right"]
yield f"{name}-right", f"{border_type} {border_style}"
if has_bottom:
border_type, border_style = rules[f"{name}_bottom"]
yield f"{name}-bottom", f"{border_type} {border_style}"
if has_left:
border_type, border_style = rules[f"{name}_left"]
yield f"{name}-left", f"{border_type} {border_style}"
@property @property
def css_lines(self) -> list[str]: def css_lines(self) -> list[str]:
@@ -318,91 +349,69 @@ class Styles:
else: else:
append(f"{name}: {value};") append(f"{name}: {value};")
if self._rule_display is not None: rules = self.get_rules()
append_declaration("display", self._rule_display) get_rule = rules.get
if self._rule_visibility is not None: has_rule = rules.__contains__
append_declaration("visibility", self._rule_visibility)
if self._rule_padding is not None:
append_declaration("padding", self._rule_padding.packed)
if self._rule_margin is not None:
append_declaration("margin", self._rule_margin.packed)
if ( if has_rule("display"):
self._rule_border_top is not None append_declaration("display", rules["display"])
and self._rule_border_top == self._rule_border_right if has_rule("visibility"):
and self._rule_border_right == self._rule_border_bottom append_declaration("visibility", rules["visibility"])
and self._rule_border_bottom == self._rule_border_left if has_rule("padding"):
): append_declaration("padding", rules["padding"].css)
_type, style = self._rule_border_top if has_rule("margin"):
append_declaration("border", f"{_type} {style}") append_declaration("margin", rules["margin"].css)
else:
if self._rule_border_top is not None:
_type, style = self._rule_border_top
append_declaration("border-top", f"{_type} {style}")
if self._rule_border_right is not None:
_type, style = self._rule_border_right
append_declaration("border-right", f"{_type} {style}")
if self._rule_border_bottom is not None:
_type, style = self._rule_border_bottom
append_declaration("border-bottom", f"{_type} {style}")
if self._rule_border_left is not None:
_type, style = self._rule_border_left
append_declaration("border-left", f"{_type} {style}")
if ( for name, rule in self._get_border_css_lines(rules, "border"):
self._rule_outline_top is not None append_declaration(name, rule)
and self._rule_outline_top == self._rule_outline_right
and self._rule_outline_right == self._rule_outline_bottom
and self._rule_outline_bottom == self._rule_outline_left
):
_type, style = self._rule_outline_top
append_declaration("outline", f"{_type} {style}")
else:
if self._rule_outline_top is not None:
_type, style = self._rule_outline_top
append_declaration("outline-top", f"{_type} {style}")
if self._rule_outline_right is not None:
_type, style = self._rule_outline_right
append_declaration("outline-right", f"{_type} {style}")
if self._rule_outline_bottom is not None:
_type, style = self._rule_outline_bottom
append_declaration("outline-bottom", f"{_type} {style}")
if self._rule_outline_left is not None:
_type, style = self._rule_outline_left
append_declaration("outline-left", f"{_type} {style}")
if self._rule_offset is not None: for name, rule in self._get_border_css_lines(rules, "outline"):
append_declaration(name, rule)
if has_rule("offset"):
x, y = self.offset x, y = self.offset
append_declaration("offset", f"{x} {y}") append_declaration("offset", f"{x} {y}")
if self._rule_dock: if has_rule("dock"):
append_declaration("dock", self._rule_dock) append_declaration("dock", rules["dock"])
if self._rule_docks: if has_rule("docks"):
append_declaration( append_declaration(
"docks", "docks",
" ".join( " ".join(
(f"{name}={edge}/{z}" if z else f"{name}={edge}") (f"{name}={edge}/{z}" if z else f"{name}={edge}")
for name, edge, z in self._rule_docks for name, edge, z in rules["docks"]
), ),
) )
if self._rule_layers is not None: if has_rule("layers"):
append_declaration("layers", " ".join(self.layers)) append_declaration("layers", " ".join(self.layers))
if self._rule_layer is not None: if has_rule("layer"):
append_declaration("layer", self.layer) append_declaration("layer", self.layer)
if self._rule_layout is not None: if has_rule("layout"):
assert self.layout is not None assert self.layout is not None
append_declaration("layout", self.layout.name) append_declaration("layout", self.layout.name)
if self._rule_text_color or self._rule_text_background or self._rule_text_style:
append_declaration("text", str(self.text))
if self._rule_width is not None: if (
has_rule("text_color")
and has_rule("text_bgcolor")
and has_rule("text_style")
):
append_declaration("text", str(self.text))
else:
if has_rule("text_color"):
append_declaration("text-color", str(get_rule("text_color")))
if has_rule("text_bgcolor"):
append_declaration("text-bgcolor", str(get_rule("text_bgcolor")))
if has_rule("text_style"):
append_declaration("text-style", str(get_rule("text_style")))
if has_rule("width"):
append_declaration("width", str(self.width)) append_declaration("width", str(self.width))
if self._rule_height is not None: if has_rule("height"):
append_declaration("height", str(self.height)) append_declaration("height", str(self.height))
if self._rule_min_width is not None: if has_rule("min-width"):
append_declaration("min-width", str(self.min_width)) append_declaration("min-width", str(self.min_width))
if self._rule_min_height is not None: if has_rule("min_height"):
append_declaration("min-height", str(self.min_height)) append_declaration("min-height", str(self.min_height))
if self._rule_transitions is not None: if has_rule("transitions"):
append_declaration( append_declaration(
"transition", "transition",
", ".join( ", ".join(
@@ -419,10 +428,6 @@ class Styles:
return "\n".join(self.css_lines) return "\n".join(self.css_lines)
RULE_NAMES = [name[6:] for name in dir(Styles) if name.startswith("_rule_")]
INTERNAL_RULE_NAMES = [f"_rule_{name}" for name in RULE_NAMES]
from typing import Generic, TypeVar from typing import Generic, TypeVar
GetType = TypeVar("GetType") GetType = TypeVar("GetType")
@@ -433,25 +438,35 @@ class StyleViewProperty(Generic[GetType, SetType]):
"""Presents a view of a base Styles object, plus inline styles.""" """Presents a view of a base Styles object, plus inline styles."""
def __set_name__(self, owner: StylesView, name: str) -> None: def __set_name__(self, owner: StylesView, name: str) -> None:
self._name = name self.name = name
self._internal_name = f"_rule_{name}"
def __set__(self, obj: StylesView, value: SetType) -> None: def __set__(self, obj: StylesView, value: SetType) -> None:
setattr(obj._inline_styles, self._name, value) setattr(obj._inline_styles, self.name, value)
def __get__( def __get__(
self, obj: StylesView, objtype: type[StylesView] | None = None self, obj: StylesView, objtype: type[StylesView] | None = None
) -> GetType: ) -> GetType:
if obj._inline_styles.has_rule(self._name): if obj._inline_styles.has_rule(self.name):
return getattr(obj._inline_styles, self._name) return getattr(obj._inline_styles, self.name)
return getattr(obj._base_styles, self._name) return getattr(obj._base_styles, self.name)
@rich.repr.auto @rich.repr.auto
class StylesView: class StylesView:
"""Presents a combined view of two Styles object: a base Styles and inline Styles.""" """Presents a combined view of two Styles object: a base Styles and inline Styles."""
def __init__(self, base: Styles, inline_styles: Styles) -> None: ANIMATABLE = {
"offset",
"padding",
"margin",
"width",
"height",
"min_width",
"min_height",
}
def __init__(self, node: DOMNode, base: Styles, inline_styles: Styles) -> None:
self.node = node
self._base_styles = base self._base_styles = base
self._inline_styles = inline_styles self._inline_styles = inline_styles
@@ -500,6 +515,42 @@ class StylesView:
"""Check if a rule has been set.""" """Check if a rule has been set."""
return self._inline_styles.has_rule(rule) or self._base_styles.has_rule(rule) return self._inline_styles.has_rule(rule) or self._base_styles.has_rule(rule)
def get_rules(self) -> RulesMap:
"""Get rules as a dictionary"""
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 @property
def css(self) -> str: def css(self) -> str:
"""Get the CSS for the combined styles.""" """Get the CSS for the combined styles."""
@@ -511,7 +562,7 @@ class StylesView:
display: StyleViewProperty[str, str | None] = StyleViewProperty() display: StyleViewProperty[str, str | None] = StyleViewProperty()
visibility: StyleViewProperty[str, str | None] = StyleViewProperty() visibility: StyleViewProperty[str, str | None] = StyleViewProperty()
layout: StyleViewProperty[Layout | None, str | Layout] = StyleViewProperty() layout: StyleViewProperty[Layout | None, str | Layout | None] = StyleViewProperty()
text: StyleViewProperty[Style, Style | str | None] = StyleViewProperty() text: StyleViewProperty[Style, Style | str | None] = StyleViewProperty()
text_color: StyleViewProperty[Color, Color | str | None] = StyleViewProperty() text_color: StyleViewProperty[Color, Color | str | None] = StyleViewProperty()

View File

@@ -148,10 +148,10 @@ class Stylesheet:
# For each rule declared for this node, keep only the most specific one # For each rule declared for this node, keep only the most specific one
get_first_item = itemgetter(0) get_first_item = itemgetter(0)
node_rules = [ node_rules = {
(name, max(specificity_rules, key=get_first_item)[1]) name: max(specificity_rules, key=get_first_item)[1]
for name, specificity_rules in rule_attributes.items() for name, specificity_rules in rule_attributes.items()
] }
node._css_styles.apply_rules(node_rules) node._css_styles.apply_rules(node_rules)

View File

@@ -42,7 +42,7 @@ class DOMNode(MessagePump):
self.children = NodeList() self.children = NodeList()
self._css_styles: Styles = Styles(self) self._css_styles: Styles = Styles(self)
self._inline_styles: Styles = Styles.parse(self.STYLES, repr(self), node=self) self._inline_styles: Styles = Styles.parse(self.STYLES, repr(self), node=self)
self.styles = StylesView(self._css_styles, self._inline_styles) self.styles = StylesView(self, self._css_styles, self._inline_styles)
super().__init__() super().__init__()
self.default_styles = Styles.parse(self.DEFAULT_STYLES, repr(self)) self.default_styles = Styles.parse(self.DEFAULT_STYLES, repr(self))
self._default_rules = self.default_styles.extract_rules((0, 0, 0)) self._default_rules = self.default_styles.extract_rules((0, 0, 0))
@@ -53,10 +53,6 @@ class DOMNode(MessagePump):
if self._classes: if self._classes:
yield "classes", self._classes yield "classes", self._classes
@property
def inline_styles(self) -> Styles:
return self._inline_styles
@property @property
def parent(self) -> DOMNode: def parent(self) -> DOMNode:
"""Get the parent node. """Get the parent node.
@@ -310,18 +306,41 @@ class DOMNode(MessagePump):
self.refresh() self.refresh()
def has_class(self, *class_names: str) -> bool: def has_class(self, *class_names: str) -> bool:
"""Check if the Node has all the given class names.
Args:
*class_names (str): CSS class names to check.
Returns:
bool: ``True`` if the node has all the given class names, otherwise ``False``.
"""
return self._classes.issuperset(class_names) return self._classes.issuperset(class_names)
def add_class(self, *class_names: str) -> None: def add_class(self, *class_names: str) -> None:
"""Add class names.""" """Add class names to this Node.
Args:
*class_names (str): CSS class names to add.
"""
self._classes.update(class_names) self._classes.update(class_names)
def remove_class(self, *class_names: str) -> None: def remove_class(self, *class_names: str) -> None:
"""Remove class names""" """Remove class names from this Node.
Args:
*class_names (str): CSS class names to remove.
"""
self._classes.difference_update(class_names) self._classes.difference_update(class_names)
def toggle_class(self, *class_names: str) -> None: def toggle_class(self, *class_names: str) -> None:
"""Toggle class names""" """Toggle class names on this Node.
Args:
*class_names (str): CSS class names to toggle.
"""
self._classes.symmetric_difference_update(class_names) self._classes.symmetric_difference_update(class_names)
self.app.stylesheet.update(self.app) self.app.stylesheet.update(self.app)

View File

@@ -514,6 +514,16 @@ class Spacing(NamedTuple):
else: else:
return f"{top}, {right}, {bottom}, {left}" return f"{top}, {right}, {bottom}, {left}"
@property
def css(self) -> str:
top, right, bottom, left = self
if top == right == bottom == left:
return f"{top}"
if (top, right) == (bottom, left):
return f"{top} {right}"
else:
return f"{top} {right} {bottom} {left}"
@classmethod @classmethod
def unpack(cls, pad: SpacingDimensions) -> Spacing: def unpack(cls, pad: SpacingDimensions) -> Spacing:
"""Unpack padding specified in CSS style.""" """Unpack padding specified in CSS style."""

View File

@@ -32,13 +32,6 @@ class View(Widget):
) )
super().__init__(name=name, id=id) super().__init__(name=name, id=id)
# def __init_subclass__(
# cls, layout: Callable[[], Layout] | None = None, **kwargs
# ) -> None:
# if layout is not None:
# cls.layout_factory = layout
# super().__init_subclass__(**kwargs)
background: Reactive[str] = Reactive("") background: Reactive[str] = Reactive("")
scroll_x: Reactive[int] = Reactive(0) scroll_x: Reactive[int] = Reactive(0)
scroll_y: Reactive[int] = Reactive(0) scroll_y: Reactive[int] = Reactive(0)