mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
convert styles to using sipler dict
This commit is contained in:
@@ -17,6 +17,7 @@ class BasicApp(App):
|
||||
footer=Widget(),
|
||||
sidebar=Widget(),
|
||||
)
|
||||
self.panic(self.stylesheet.css)
|
||||
|
||||
|
||||
BasicApp.run(css_file="basic.css", watch_css=True, log="textual.log")
|
||||
|
||||
@@ -9,7 +9,8 @@ when setting and getting.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Iterable, NamedTuple, Sequence, TYPE_CHECKING
|
||||
|
||||
from typing import Iterable, Iterator, NamedTuple, Sequence, TYPE_CHECKING
|
||||
|
||||
import rich.repr
|
||||
from rich.color import Color
|
||||
@@ -54,7 +55,6 @@ class ScalarProperty:
|
||||
|
||||
def __set_name__(self, owner: Styles, name: str) -> None:
|
||||
self.name = name
|
||||
self.internal_name = f"_rule_{name}"
|
||||
|
||||
def __get__(
|
||||
self, obj: Styles, objtype: type[Styles] | None = None
|
||||
@@ -68,7 +68,7 @@ class ScalarProperty:
|
||||
Returns:
|
||||
The Scalar object or ``None`` if it's not set.
|
||||
"""
|
||||
value = getattr(obj, self.internal_name)
|
||||
value = obj._rules.get(self.name)
|
||||
return value
|
||||
|
||||
def __set__(self, obj: Styles, value: float | Scalar | str | None) -> None:
|
||||
@@ -88,8 +88,9 @@ class ScalarProperty:
|
||||
cannot be parsed for any other reason.
|
||||
"""
|
||||
if value is None:
|
||||
new_value = None
|
||||
elif isinstance(value, float):
|
||||
obj._rules.pop(self.name, None)
|
||||
return
|
||||
if isinstance(value, float):
|
||||
new_value = Scalar(float(value), Unit.CELLS, Unit.WIDTH)
|
||||
elif isinstance(value, Scalar):
|
||||
new_value = value
|
||||
@@ -106,7 +107,7 @@ class ScalarProperty:
|
||||
)
|
||||
if new_value is not None and new_value.is_percent:
|
||||
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()
|
||||
|
||||
|
||||
@@ -118,7 +119,7 @@ class BoxProperty:
|
||||
DEFAULT = ("", Style())
|
||||
|
||||
def __set_name__(self, owner: Styles, name: str) -> None:
|
||||
self.internal_name = f"_rule_{name}"
|
||||
self.name = name
|
||||
_type, edge = name.split("_")
|
||||
self._type = _type
|
||||
self.edge = edge
|
||||
@@ -136,8 +137,8 @@ class BoxProperty:
|
||||
A ``tuple[BoxType, Style]`` containing the string type of the box and
|
||||
it's style. Example types are "rounded", "solid", and "dashed".
|
||||
"""
|
||||
value = getattr(obj, self.internal_name)
|
||||
return value or self.DEFAULT
|
||||
value = obj._rules.get(self.name) or self.DEFAULT
|
||||
return value
|
||||
|
||||
def __set__(self, obj: Styles, border: tuple[BoxType, str | Color | Style] | None):
|
||||
"""Set the box property
|
||||
@@ -152,7 +153,7 @@ class BoxProperty:
|
||||
StyleSyntaxError: If the string supplied for the color has invalid syntax.
|
||||
"""
|
||||
if border is None:
|
||||
new_value = None
|
||||
obj._rules.pop(self.name, None)
|
||||
else:
|
||||
_type, color = border
|
||||
if isinstance(color, str):
|
||||
@@ -161,7 +162,7 @@ class BoxProperty:
|
||||
new_value = (_type, Style.from_color(color))
|
||||
else:
|
||||
new_value = (_type, Style.from_color(Color.parse(color)))
|
||||
setattr(obj, self.internal_name, new_value)
|
||||
obj._rules[self.name] = new_value
|
||||
obj.refresh()
|
||||
|
||||
|
||||
@@ -169,13 +170,14 @@ class BoxProperty:
|
||||
class Edges(NamedTuple):
|
||||
"""Stores edges for border / outline."""
|
||||
|
||||
css_name: str
|
||||
top: tuple[BoxType, Style]
|
||||
right: tuple[BoxType, Style]
|
||||
bottom: tuple[BoxType, Style]
|
||||
left: tuple[BoxType, Style]
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
top, right, bottom, left = self
|
||||
_, top, right, bottom, left = self
|
||||
if top[0]:
|
||||
yield "top", top
|
||||
if right[0]:
|
||||
@@ -191,7 +193,7 @@ class Edges(NamedTuple):
|
||||
Returns:
|
||||
tuple[int, int, int, int]: Spacing for top, right, bottom, and left.
|
||||
"""
|
||||
top, right, bottom, left = self
|
||||
_, top, right, bottom, left = self
|
||||
return (
|
||||
1 if top[0] else 0,
|
||||
1 if right[0] else 0,
|
||||
@@ -204,6 +206,7 @@ class BorderProperty:
|
||||
"""Descriptor for getting and setting full borders and outlines."""
|
||||
|
||||
def __set_name__(self, owner: Styles, name: str) -> None:
|
||||
self.name = name
|
||||
self._properties = (
|
||||
f"{name}_top",
|
||||
f"{name}_right",
|
||||
@@ -222,7 +225,9 @@ class BorderProperty:
|
||||
An ``Edges`` object describing the type and style of each edge.
|
||||
"""
|
||||
top, right, bottom, left = self._properties
|
||||
|
||||
border = Edges(
|
||||
self.name,
|
||||
getattr(obj, top),
|
||||
getattr(obj, right),
|
||||
getattr(obj, bottom),
|
||||
@@ -292,11 +297,6 @@ class StyleProperty:
|
||||
|
||||
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:
|
||||
"""Get the Style
|
||||
|
||||
@@ -307,12 +307,11 @@ class StyleProperty:
|
||||
Returns:
|
||||
A ``Style`` object.
|
||||
"""
|
||||
color = getattr(obj, self._color_name)
|
||||
bgcolor = getattr(obj, self._bgcolor_name)
|
||||
rules_get = obj._rules.get
|
||||
color = rules_get("text_color") or Color.default()
|
||||
bgcolor = rules_get("text_background") or Color.default()
|
||||
style = Style.from_color(color, bgcolor)
|
||||
style_flags = getattr(obj, self._style_name)
|
||||
if style_flags:
|
||||
style += style_flags
|
||||
style += rules_get("text_style") or Style()
|
||||
return style
|
||||
|
||||
def __set__(self, obj: Styles, style: Style | str | None):
|
||||
@@ -327,26 +326,28 @@ class StyleProperty:
|
||||
StyleSyntaxError: When the supplied style string has invalid syntax.
|
||||
"""
|
||||
obj.refresh()
|
||||
rules_set = obj._rules.__setitem__
|
||||
if style is None:
|
||||
setattr(obj, self._color_name, None)
|
||||
setattr(obj, self._bgcolor_name, None)
|
||||
setattr(obj, self._style_name, None)
|
||||
rules = obj._rules
|
||||
rules.pop("text_color")
|
||||
rules.pop("text_background")
|
||||
rules.pop("text_style")
|
||||
elif isinstance(style, Style):
|
||||
setattr(obj, self._color_name, style.color)
|
||||
setattr(obj, self._bgcolor_name, style.bgcolor)
|
||||
setattr(obj, self._style_name, style.without_color)
|
||||
rules_set("text_color", style.color)
|
||||
rules_set("text_background", style.bgcolor)
|
||||
rules_set("text_style", 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)
|
||||
setattr(obj, self._style_name, new_style.without_color)
|
||||
rules_set("text_color", new_style.color)
|
||||
rules_set("text_background", new_style.bgcolor)
|
||||
rules_set("text_style", new_style.without_color)
|
||||
|
||||
|
||||
class SpacingProperty:
|
||||
"""Descriptor for getting and setting spacing properties (e.g. padding and margin)."""
|
||||
|
||||
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:
|
||||
"""Get the Spacing
|
||||
@@ -358,9 +359,9 @@ class SpacingProperty:
|
||||
Returns:
|
||||
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
|
||||
|
||||
Args:
|
||||
@@ -373,8 +374,10 @@ class SpacingProperty:
|
||||
not 1, 2, or 4.
|
||||
"""
|
||||
obj.refresh(layout=True)
|
||||
spacing = Spacing.unpack(spacing)
|
||||
setattr(obj, self._internal_name, spacing)
|
||||
if spacing is None:
|
||||
obj._rules.pop(self.name)
|
||||
else:
|
||||
obj._rules[self.name] = Spacing.unpack(spacing)
|
||||
|
||||
|
||||
class DocksProperty:
|
||||
@@ -394,7 +397,7 @@ class DocksProperty:
|
||||
Returns:
|
||||
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):
|
||||
"""Set the Docks property
|
||||
@@ -405,9 +408,9 @@ class DocksProperty:
|
||||
"""
|
||||
obj.refresh(layout=True)
|
||||
if docks is None:
|
||||
obj._rule_docks = None
|
||||
obj._rules.pop("docks")
|
||||
else:
|
||||
obj._rule_docks = tuple(docks)
|
||||
obj._rules["docks"] = tuple(docks)
|
||||
|
||||
|
||||
class DockProperty:
|
||||
@@ -427,7 +430,7 @@ class DockProperty:
|
||||
Returns:
|
||||
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):
|
||||
"""Set the Dock property
|
||||
@@ -437,14 +440,17 @@ class DockProperty:
|
||||
spacing (str | None): The spacing to use.
|
||||
"""
|
||||
obj.refresh(layout=True)
|
||||
obj._rule_dock = spacing
|
||||
if spacing is None:
|
||||
obj._rules.pop("dock")
|
||||
else:
|
||||
obj._rules["dock"] = spacing
|
||||
|
||||
|
||||
class LayoutProperty:
|
||||
"""Descriptor for getting and setting layout."""
|
||||
|
||||
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
|
||||
@@ -456,9 +462,9 @@ class LayoutProperty:
|
||||
Returns:
|
||||
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:
|
||||
obj (Styles): The Styles object.
|
||||
@@ -469,11 +475,13 @@ class LayoutProperty:
|
||||
from ..layouts.factory import get_layout, Layout # Prevents circular import
|
||||
|
||||
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:
|
||||
new_layout = get_layout(layout)
|
||||
setattr(obj, self._internal_name, new_layout)
|
||||
obj._rules["layout"] = get_layout(layout)
|
||||
|
||||
|
||||
class OffsetProperty:
|
||||
@@ -483,7 +491,7 @@ class OffsetProperty:
|
||||
"""
|
||||
|
||||
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:
|
||||
"""Get the offset
|
||||
@@ -496,11 +504,11 @@ class OffsetProperty:
|
||||
ScalarOffset: The ``ScalarOffset`` indicating the adjustment that
|
||||
will be made to widget position prior to it being rendered.
|
||||
"""
|
||||
return getattr(obj, self._internal_name) or ScalarOffset(
|
||||
Scalar.from_number(0), Scalar.from_number(0)
|
||||
)
|
||||
return obj._rules.get(self.name, ScalarOffset.null())
|
||||
|
||||
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
|
||||
|
||||
Args:
|
||||
@@ -515,57 +523,24 @@ class OffsetProperty:
|
||||
be parsed into a Scalar. For example, if you specify an non-existent unit.
|
||||
"""
|
||||
obj.refresh(layout=True)
|
||||
if isinstance(offset, ScalarOffset):
|
||||
setattr(obj, self._internal_name, offset)
|
||||
return offset
|
||||
x, y = offset
|
||||
scalar_x = (
|
||||
Scalar.parse(x, Unit.WIDTH)
|
||||
if isinstance(x, str)
|
||||
else Scalar(float(x), Unit.CELLS, Unit.WIDTH)
|
||||
)
|
||||
scalar_y = (
|
||||
Scalar.parse(y, Unit.HEIGHT)
|
||||
if isinstance(y, str)
|
||||
else Scalar(float(y), Unit.CELLS, Unit.HEIGHT)
|
||||
)
|
||||
_offset = ScalarOffset(scalar_x, scalar_y)
|
||||
setattr(obj, self._internal_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)
|
||||
if offset is None:
|
||||
obj._rules.pop(self.name, None)
|
||||
elif isinstance(offset, ScalarOffset):
|
||||
obj._rules[self.name] = offset
|
||||
else:
|
||||
x, y = offset
|
||||
scalar_x = (
|
||||
Scalar.parse(x, Unit.WIDTH)
|
||||
if isinstance(x, str)
|
||||
else Scalar(float(x), Unit.CELLS, Unit.WIDTH)
|
||||
)
|
||||
scalar_y = (
|
||||
Scalar.parse(y, Unit.HEIGHT)
|
||||
if isinstance(y, str)
|
||||
else Scalar(float(y), Unit.CELLS, Unit.HEIGHT)
|
||||
)
|
||||
_offset = ScalarOffset(scalar_x, scalar_y)
|
||||
obj._rules[self.name] = _offset
|
||||
|
||||
|
||||
class StringEnumProperty:
|
||||
@@ -578,8 +553,7 @@ class StringEnumProperty:
|
||||
self._default = default
|
||||
|
||||
def __set_name__(self, owner: Styles, name: str) -> None:
|
||||
self._name = name
|
||||
self._internal_name = f"_rule_{name}"
|
||||
self.name = name
|
||||
|
||||
def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> str:
|
||||
"""Get the string property, or the default value if it's not set
|
||||
@@ -591,7 +565,7 @@ class StringEnumProperty:
|
||||
Returns:
|
||||
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):
|
||||
"""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.
|
||||
"""
|
||||
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:
|
||||
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:
|
||||
@@ -617,7 +593,6 @@ class NameProperty:
|
||||
|
||||
def __set_name__(self, owner: Styles, name: str) -> None:
|
||||
self._name = name
|
||||
self._internal_name = f"_rule_{name}"
|
||||
|
||||
def __get__(self, obj: Styles, objtype: type[Styles] | None) -> str:
|
||||
"""Get the name property
|
||||
@@ -629,7 +604,7 @@ class NameProperty:
|
||||
Returns:
|
||||
str: The name
|
||||
"""
|
||||
return getattr(obj, self._internal_name) or ""
|
||||
return obj.get(self.name, "")
|
||||
|
||||
def __set__(self, obj: Styles, name: str | None):
|
||||
"""Set the name property
|
||||
@@ -642,42 +617,42 @@ class NameProperty:
|
||||
StyleTypeError: If the value is not a ``str``.
|
||||
"""
|
||||
obj.refresh(layout=True)
|
||||
if not isinstance(name, str):
|
||||
raise StyleTypeError(f"{self._name} must be a str")
|
||||
setattr(obj, self._internal_name, name)
|
||||
if name is None:
|
||||
obj._rules.pop(self.name, None)
|
||||
else:
|
||||
if not isinstance(name, str):
|
||||
raise StyleTypeError(f"{self._name} must be a str")
|
||||
obj._rules[self.name] = name
|
||||
|
||||
|
||||
class NameListProperty:
|
||||
def __set_name__(self, owner: Styles, name: str) -> None:
|
||||
self._name = name
|
||||
self._internal_name = f"_rule_{name}"
|
||||
|
||||
def __get__(
|
||||
self, obj: Styles, objtype: type[Styles] | None = None
|
||||
) -> tuple[str, ...]:
|
||||
return getattr(obj, self._internal_name, None) or ()
|
||||
return obj._rules.get(self.name, ())
|
||||
|
||||
def __set__(
|
||||
self, obj: Styles, names: str | tuple[str] | None = None
|
||||
) -> str | tuple[str] | None:
|
||||
obj.refresh(layout=True)
|
||||
names_value: tuple[str, ...] | None = None
|
||||
if isinstance(names, str):
|
||||
names_value = tuple(name.strip().lower() for name in names.split(" "))
|
||||
if names is None:
|
||||
obj._rules.pop(self.name, None)
|
||||
elif isinstance(names, str):
|
||||
obj._rules[self.name] = tuple(
|
||||
name.strip().lower() for name in names.split(" ")
|
||||
)
|
||||
elif isinstance(names, tuple):
|
||||
names_value = names
|
||||
elif names is None:
|
||||
names_value = None
|
||||
setattr(obj, self._internal_name, names_value)
|
||||
return names
|
||||
obj._rules[self.name] = names
|
||||
|
||||
|
||||
class ColorProperty:
|
||||
"""Descriptor for getting and setting color properties."""
|
||||
|
||||
def __set_name__(self, owner: Styles, name: str) -> None:
|
||||
self._name = name
|
||||
self._internal_name = f"_rule_{name}"
|
||||
self.name = name
|
||||
|
||||
def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> Color:
|
||||
"""Get the ``Color``, or ``Color.default()`` if no color is set.
|
||||
@@ -689,7 +664,7 @@ class ColorProperty:
|
||||
Returns:
|
||||
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):
|
||||
"""Set the Color
|
||||
@@ -705,13 +680,11 @@ class ColorProperty:
|
||||
"""
|
||||
obj.refresh()
|
||||
if color is None:
|
||||
setattr(self, self._internal_name, None)
|
||||
else:
|
||||
if isinstance(color, Color):
|
||||
setattr(self, self._internal_name, color)
|
||||
elif isinstance(color, str):
|
||||
new_color = Color.parse(color)
|
||||
setattr(self, self._internal_name, new_color)
|
||||
obj._rules.pop(self.name, None)
|
||||
elif isinstance(color, Color):
|
||||
obj._rules[self.name] = color
|
||||
elif isinstance(color, str):
|
||||
obj._rules[self.name] = Color.parse(color)
|
||||
|
||||
|
||||
class StyleFlagsProperty:
|
||||
@@ -731,8 +704,7 @@ class StyleFlagsProperty:
|
||||
}
|
||||
|
||||
def __set_name__(self, owner: Styles, name: str) -> None:
|
||||
self._name = name
|
||||
self._internal_name = f"_rule_{name}"
|
||||
self.name = name
|
||||
|
||||
def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> Style:
|
||||
"""Get the ``Style``
|
||||
@@ -744,7 +716,7 @@ class StyleFlagsProperty:
|
||||
Returns:
|
||||
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):
|
||||
"""Set the style using a style flag string
|
||||
@@ -759,7 +731,7 @@ class StyleFlagsProperty:
|
||||
"""
|
||||
obj.refresh()
|
||||
if style_flags is None:
|
||||
setattr(self, self._internal_name, None)
|
||||
obj._styles.pop(self.name, None)
|
||||
else:
|
||||
words = [word.strip() for word in style_flags.split(" ")]
|
||||
valid_word = self._VALID_PROPERTIES.__contains__
|
||||
@@ -770,15 +742,14 @@ class StyleFlagsProperty:
|
||||
f"valid values are {friendly_list(self._VALID_PROPERTIES)}"
|
||||
)
|
||||
style = Style.parse(style_flags)
|
||||
setattr(obj, self._internal_name, style)
|
||||
obj._rules[self.name] = style
|
||||
|
||||
|
||||
class TransitionsProperty:
|
||||
"""Descriptor for getting transitions properties"""
|
||||
|
||||
def __set_name__(self, owner: Styles, name: str) -> None:
|
||||
self._name = name
|
||||
self._internal_name = f"_rule_{name}"
|
||||
self.name = name
|
||||
|
||||
def __get__(
|
||||
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``
|
||||
is returned.
|
||||
"""
|
||||
return getattr(obj, self._internal_name, None) or {}
|
||||
return obj._rules.get(self.name, {})
|
||||
|
||||
@@ -71,7 +71,7 @@ class StylesBuilder:
|
||||
if name == "token":
|
||||
value = value.lower()
|
||||
if value in VALID_DISPLAY:
|
||||
self.styles._rule_display = cast(Display, value)
|
||||
self.styles._rules["display"] = cast(Display, value)
|
||||
else:
|
||||
self.error(
|
||||
name,
|
||||
@@ -85,7 +85,7 @@ class StylesBuilder:
|
||||
if not tokens:
|
||||
return
|
||||
if len(tokens) == 1:
|
||||
setattr(self.styles, name, Scalar.parse(tokens[0].value))
|
||||
self.styles._rules[name] = Scalar.parse(tokens[0].value)
|
||||
else:
|
||||
self.error(name, tokens[0], "a single scalar is expected")
|
||||
|
||||
@@ -113,7 +113,7 @@ class StylesBuilder:
|
||||
if name == "token":
|
||||
value = value.lower()
|
||||
if value in VALID_VISIBILITY:
|
||||
self.styles._rule_visibility = cast(Visibility, value)
|
||||
self.styles._rules["visibility"] = cast(Visibility, value)
|
||||
else:
|
||||
self.error(
|
||||
name,
|
||||
@@ -139,11 +139,7 @@ class StylesBuilder:
|
||||
self.error(
|
||||
name, tokens[0], f"1, 2, or 4 values expected; received {len(space)}"
|
||||
)
|
||||
setattr(
|
||||
self.styles,
|
||||
f"_rule_{name}",
|
||||
Spacing.unpack(cast(SpacingDimensions, tuple(space))),
|
||||
)
|
||||
self.styles._rules[name] = Spacing.unpack(cast(SpacingDimensions, tuple(space)))
|
||||
|
||||
def process_padding(self, name: str, tokens: list[Token], important: bool) -> None:
|
||||
self._process_space(name, tokens)
|
||||
@@ -176,13 +172,13 @@ class StylesBuilder:
|
||||
|
||||
def _process_border(self, edge: str, name: str, tokens: list[Token]) -> None:
|
||||
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:
|
||||
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
|
||||
rules = self.styles._rules
|
||||
rules["border_top"] = rules["border_right"] = border
|
||||
rules["border_bottom"] = rules["border_left"] = border
|
||||
|
||||
def process_border_top(
|
||||
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:
|
||||
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:
|
||||
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
|
||||
rules = self.styles._rules
|
||||
rules["outline_top"] = rules["outline_right"] = border
|
||||
rules["outline_bottom"] = rules["outline_left"] = border
|
||||
|
||||
def process_outline_top(
|
||||
self, name: str, tokens: list[Token], important: bool
|
||||
@@ -255,7 +251,7 @@ class StylesBuilder:
|
||||
|
||||
scalar_x = Scalar.parse(token1.value, Unit.WIDTH)
|
||||
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:
|
||||
if not tokens:
|
||||
@@ -268,7 +264,7 @@ class StylesBuilder:
|
||||
self.error(name, token, f"expected a scalar; found {token.value!r}")
|
||||
x = Scalar.parse(token.value, Unit.WIDTH)
|
||||
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:
|
||||
if not tokens:
|
||||
@@ -281,7 +277,7 @@ class StylesBuilder:
|
||||
self.error(name, token, f"expected a scalar; found {token.value!r}")
|
||||
y = Scalar.parse(token.value, Unit.HEIGHT)
|
||||
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:
|
||||
from ..layouts.factory import get_layout, MissingLayout, LAYOUT_MAP
|
||||
@@ -293,7 +289,7 @@ class StylesBuilder:
|
||||
value = tokens[0].value
|
||||
layout_name = value
|
||||
try:
|
||||
self.styles._rule_layout = get_layout(layout_name)
|
||||
self.styles._rules["layout"] = get_layout(layout_name)
|
||||
except MissingLayout:
|
||||
self.error(
|
||||
name,
|
||||
@@ -311,7 +307,7 @@ class StylesBuilder:
|
||||
self.styles.important.update(
|
||||
{"text_style", "text_background", "text_color"}
|
||||
)
|
||||
self.styles.text = style
|
||||
self.styles._rules["text"] = style
|
||||
|
||||
def process_text_color(
|
||||
self, name: str, tokens: list[Token], important: bool
|
||||
@@ -319,7 +315,7 @@ class StylesBuilder:
|
||||
for token in tokens:
|
||||
if token.name in ("color", "token"):
|
||||
try:
|
||||
self.styles._rule_text_color = Color.parse(token.value)
|
||||
self.styles._rules["text_color"] = Color.parse(token.value)
|
||||
except Exception as error:
|
||||
self.error(
|
||||
name, token, f"failed to parse color {token.value!r}; {error}"
|
||||
@@ -335,7 +331,7 @@ class StylesBuilder:
|
||||
for token in tokens:
|
||||
if token.name in ("color", "token"):
|
||||
try:
|
||||
self.styles._rule_text_background = Color.parse(token.value)
|
||||
self.styles._rules["text_background"] = Color.parse(token.value)
|
||||
except Exception as error:
|
||||
self.error(
|
||||
name, token, f"failed to parse color {token.value!r}; {error}"
|
||||
@@ -359,7 +355,7 @@ class StylesBuilder:
|
||||
tokens[1],
|
||||
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:
|
||||
docks: list[DockGroup] = []
|
||||
@@ -390,12 +386,12 @@ class StylesBuilder:
|
||||
token,
|
||||
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:
|
||||
if len(tokens) > 1:
|
||||
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:
|
||||
layers: list[str] = []
|
||||
@@ -403,7 +399,7 @@ class StylesBuilder:
|
||||
if token.name != "token":
|
||||
self.error(name, token, "{token.name} not expected here")
|
||||
layers.append(token.value)
|
||||
self.styles._rule_layers = tuple(layers)
|
||||
self.styles._rules["layers"] = tuple(layers)
|
||||
|
||||
def process_transition(
|
||||
self, name: str, tokens: list[Token], important: bool
|
||||
@@ -468,4 +464,4 @@ class StylesBuilder:
|
||||
pass
|
||||
transitions[css_property] = Transition(duration, easing, delay)
|
||||
|
||||
self.styles._rule_transitions = transitions
|
||||
self.styles._rules["transitions"] = transitions
|
||||
|
||||
@@ -142,7 +142,7 @@ class DOMQuery:
|
||||
layout (bool): Layout node(s). Defaults to False.
|
||||
|
||||
Returns:
|
||||
[type]: [description]
|
||||
DOMQuery: Query for chaining.
|
||||
"""
|
||||
for node in self._nodes:
|
||||
node.refresh(repaint=repaint, layout=layout)
|
||||
|
||||
@@ -145,6 +145,10 @@ class ScalarOffset(NamedTuple):
|
||||
x: Scalar
|
||||
y: Scalar
|
||||
|
||||
@classmethod
|
||||
def null(cls) -> ScalarOffset:
|
||||
return cls(Scalar.from_number(0), Scalar.from_number(0))
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
x, y = self
|
||||
return bool(x.value or y.value)
|
||||
|
||||
@@ -60,7 +60,7 @@ class ScalarAnimation(Animation):
|
||||
return True
|
||||
|
||||
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:
|
||||
setattr(self.styles, f"{self.attribute}", offset)
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from functools import lru_cache
|
||||
from operator import attrgetter
|
||||
from typing import Any, Iterable, NamedTuple, TYPE_CHECKING
|
||||
import sys
|
||||
from typing import Any, cast, Iterable, NamedTuple, TYPE_CHECKING
|
||||
|
||||
import rich.repr
|
||||
from rich.color import Color
|
||||
@@ -45,25 +45,56 @@ from ..geometry import Spacing, SpacingDimensions
|
||||
from .._box import BoxType
|
||||
|
||||
|
||||
if sys.version_info >= (3, 8):
|
||||
from typing import TypedDict
|
||||
else:
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..layout import Layout
|
||||
from ..dom import DOMNode
|
||||
|
||||
|
||||
_text_getter = attrgetter(
|
||||
"_rule_text_color", "_rule_text_background", "_rule_text_style"
|
||||
)
|
||||
class RulesMap(TypedDict):
|
||||
|
||||
_border_getter = attrgetter(
|
||||
"_rule_border_top", "_rule_border_right", "_rule_border_bottom", "_rule_border_left"
|
||||
)
|
||||
display: Display
|
||||
visibility: Visibility
|
||||
layout: "Layout"
|
||||
|
||||
_outline_getter = attrgetter(
|
||||
"_rule_outline_top",
|
||||
"_rule_outline_right",
|
||||
"_rule_outline_bottom",
|
||||
"_rule_outline_left",
|
||||
)
|
||||
text_color: Color
|
||||
text_background: Color
|
||||
text_style: Style
|
||||
|
||||
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):
|
||||
@@ -78,40 +109,7 @@ class Styles:
|
||||
|
||||
node: DOMNode | None = None
|
||||
|
||||
_rule_display: Display | None = None
|
||||
_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
|
||||
_rules: RulesMap = field(default_factory=dict)
|
||||
|
||||
_layout_required: bool = False
|
||||
_repaint_required: bool = False
|
||||
@@ -120,16 +118,26 @@ class Styles:
|
||||
|
||||
def has_rule(self, rule: str) -> bool:
|
||||
"""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
|
||||
has_rule = self._rules.__contains__
|
||||
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):
|
||||
return not all(rule is None for rule in _border_getter(self))
|
||||
return True
|
||||
if rule == "outline" and any(self.outline):
|
||||
return not all(rule is None for rule in _outline_getter(self))
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_rules(self) -> RulesMap:
|
||||
"""Get rules as a dictionary."""
|
||||
rules = self._rules.copy()
|
||||
return rules
|
||||
|
||||
display = StringEnumProperty(VALID_DISPLAY, "block")
|
||||
visibility = StringEnumProperty(VALID_VISIBILITY, "visible")
|
||||
layout = LayoutProperty()
|
||||
@@ -196,31 +204,6 @@ class Styles:
|
||||
styles.node = node
|
||||
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:
|
||||
self._repaint_required = True
|
||||
self._layout_required = layout
|
||||
@@ -245,54 +228,49 @@ class Styles:
|
||||
"""
|
||||
Reset internal style rules to ``None``, reverting to default styles.
|
||||
"""
|
||||
for rule_name in INTERNAL_RULE_NAMES:
|
||||
setattr(self, rule_name, None)
|
||||
self._rules.clear()
|
||||
|
||||
def extract_rules(
|
||||
self, specificity: Specificity3
|
||||
) -> list[tuple[str, Specificity4, Any]]:
|
||||
is_important = self.important.__contains__
|
||||
|
||||
rules = [
|
||||
(
|
||||
rule_name,
|
||||
(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
|
||||
(rule_name, (int(is_important(rule_name)), *specificity), rule_value)
|
||||
for rule_name, rule_value in self._rules.items()
|
||||
]
|
||||
|
||||
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:
|
||||
for key, value in rules:
|
||||
setattr(self, f"_rule_{key}", value)
|
||||
self._rules.update(rules)
|
||||
else:
|
||||
styles = self
|
||||
is_animatable = styles.ANIMATABLE.__contains__
|
||||
for key, value in rules:
|
||||
current = getattr(styles, f"_rule_{key}")
|
||||
_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:
|
||||
setattr(styles, f"_rule_{key}", value)
|
||||
_rules[key] = value
|
||||
else:
|
||||
duration, easing, delay = transition
|
||||
self.node.app.animator.animate(
|
||||
styles, key, value, duration=duration, easing=easing
|
||||
)
|
||||
else:
|
||||
setattr(styles, f"_rule_{key}", value)
|
||||
rules[key] = value
|
||||
|
||||
if self.node is not None:
|
||||
self.node.on_style_change()
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
for rule_name, internal_rule_name in zip(RULE_NAMES, INTERNAL_RULE_NAMES):
|
||||
if getattr(self, internal_rule_name) is not None:
|
||||
yield rule_name, getattr(self, rule_name)
|
||||
for name, value in self._rules.items():
|
||||
yield name, value
|
||||
if self.important:
|
||||
yield "important", self.important
|
||||
|
||||
@@ -302,10 +280,63 @@ class Styles:
|
||||
Args:
|
||||
other (Styles): A Styles object.
|
||||
"""
|
||||
for name in INTERNAL_RULE_NAMES:
|
||||
value = getattr(other, name)
|
||||
if value is not None:
|
||||
setattr(self, name, value)
|
||||
|
||||
self._rules.update(other._rules)
|
||||
|
||||
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
|
||||
def css_lines(self) -> list[str]:
|
||||
@@ -318,91 +349,69 @@ class Styles:
|
||||
else:
|
||||
append(f"{name}: {value};")
|
||||
|
||||
if self._rule_display is not None:
|
||||
append_declaration("display", self._rule_display)
|
||||
if self._rule_visibility is not None:
|
||||
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)
|
||||
rules = self.get_rules()
|
||||
get_rule = rules.get
|
||||
has_rule = rules.__contains__
|
||||
|
||||
if (
|
||||
self._rule_border_top is not None
|
||||
and self._rule_border_top == self._rule_border_right
|
||||
and self._rule_border_right == self._rule_border_bottom
|
||||
and self._rule_border_bottom == self._rule_border_left
|
||||
):
|
||||
_type, style = self._rule_border_top
|
||||
append_declaration("border", f"{_type} {style}")
|
||||
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 has_rule("display"):
|
||||
append_declaration("display", rules["display"])
|
||||
if has_rule("visibility"):
|
||||
append_declaration("visibility", rules["visibility"])
|
||||
if has_rule("padding"):
|
||||
append_declaration("padding", rules["padding"].css)
|
||||
if has_rule("margin"):
|
||||
append_declaration("margin", rules["margin"].css)
|
||||
|
||||
if (
|
||||
self._rule_outline_top is not None
|
||||
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}")
|
||||
for name, rule in self._get_border_css_lines(rules, "border"):
|
||||
append_declaration(name, rule)
|
||||
|
||||
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
|
||||
append_declaration("offset", f"{x} {y}")
|
||||
if self._rule_dock:
|
||||
append_declaration("dock", self._rule_dock)
|
||||
if self._rule_docks:
|
||||
if has_rule("dock"):
|
||||
append_declaration("dock", rules["dock"])
|
||||
if has_rule("docks"):
|
||||
append_declaration(
|
||||
"docks",
|
||||
" ".join(
|
||||
(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))
|
||||
if self._rule_layer is not None:
|
||||
if has_rule("layer"):
|
||||
append_declaration("layer", self.layer)
|
||||
if self._rule_layout is not None:
|
||||
if has_rule("layout"):
|
||||
assert self.layout is not None
|
||||
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))
|
||||
if self._rule_height is not None:
|
||||
if has_rule("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))
|
||||
if self._rule_min_height is not None:
|
||||
if has_rule("min_height"):
|
||||
append_declaration("min-height", str(self.min_height))
|
||||
if self._rule_transitions is not None:
|
||||
if has_rule("transitions"):
|
||||
append_declaration(
|
||||
"transition",
|
||||
", ".join(
|
||||
@@ -419,10 +428,6 @@ class Styles:
|
||||
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
|
||||
|
||||
GetType = TypeVar("GetType")
|
||||
@@ -433,25 +438,35 @@ class StyleViewProperty(Generic[GetType, SetType]):
|
||||
"""Presents a view of a base Styles object, plus inline styles."""
|
||||
|
||||
def __set_name__(self, owner: StylesView, name: str) -> None:
|
||||
self._name = name
|
||||
self._internal_name = f"_rule_{name}"
|
||||
self.name = name
|
||||
|
||||
def __set__(self, obj: StylesView, value: SetType) -> None:
|
||||
setattr(obj._inline_styles, self._name, value)
|
||||
setattr(obj._inline_styles, self.name, value)
|
||||
|
||||
def __get__(
|
||||
self, obj: StylesView, objtype: type[StylesView] | 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)
|
||||
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 StylesView:
|
||||
"""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._inline_styles = inline_styles
|
||||
|
||||
@@ -500,6 +515,42 @@ class StylesView:
|
||||
"""Check if a rule has been set."""
|
||||
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
|
||||
def css(self) -> str:
|
||||
"""Get the CSS for the combined styles."""
|
||||
@@ -511,7 +562,7 @@ class StylesView:
|
||||
|
||||
display: 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_color: StyleViewProperty[Color, Color | str | None] = StyleViewProperty()
|
||||
|
||||
@@ -148,10 +148,10 @@ class Stylesheet:
|
||||
|
||||
# For each rule declared for this node, keep only the most specific one
|
||||
get_first_item = itemgetter(0)
|
||||
node_rules = [
|
||||
(name, max(specificity_rules, key=get_first_item)[1])
|
||||
node_rules = {
|
||||
name: max(specificity_rules, key=get_first_item)[1]
|
||||
for name, specificity_rules in rule_attributes.items()
|
||||
]
|
||||
}
|
||||
|
||||
node._css_styles.apply_rules(node_rules)
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ class DOMNode(MessagePump):
|
||||
self.children = NodeList()
|
||||
self._css_styles: Styles = Styles(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__()
|
||||
self.default_styles = Styles.parse(self.DEFAULT_STYLES, repr(self))
|
||||
self._default_rules = self.default_styles.extract_rules((0, 0, 0))
|
||||
@@ -53,10 +53,6 @@ class DOMNode(MessagePump):
|
||||
if self._classes:
|
||||
yield "classes", self._classes
|
||||
|
||||
@property
|
||||
def inline_styles(self) -> Styles:
|
||||
return self._inline_styles
|
||||
|
||||
@property
|
||||
def parent(self) -> DOMNode:
|
||||
"""Get the parent node.
|
||||
@@ -310,18 +306,41 @@ class DOMNode(MessagePump):
|
||||
self.refresh()
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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.app.stylesheet.update(self.app)
|
||||
|
||||
|
||||
@@ -514,6 +514,16 @@ class Spacing(NamedTuple):
|
||||
else:
|
||||
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
|
||||
def unpack(cls, pad: SpacingDimensions) -> Spacing:
|
||||
"""Unpack padding specified in CSS style."""
|
||||
|
||||
@@ -32,13 +32,6 @@ class View(Widget):
|
||||
)
|
||||
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("")
|
||||
scroll_x: Reactive[int] = Reactive(0)
|
||||
scroll_y: Reactive[int] = Reactive(0)
|
||||
|
||||
Reference in New Issue
Block a user