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(),
sidebar=Widget(),
)
self.panic(self.stylesheet.css)
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 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, {})

View File

@@ -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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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."""

View File

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