stylesheet

This commit is contained in:
Will McGugan
2021-11-15 21:03:39 +00:00
parent 9856f2a1c3
commit 21d8e04067
6 changed files with 132 additions and 21 deletions

View File

@@ -6,6 +6,7 @@ import rich.repr
from rich.color import Color
from rich.style import Style
from .scalar import Scalar, ScalarParseError
from ..geometry import Offset, Spacing, SpacingDimensions
from .constants import NULL_SPACING, VALID_EDGE
from .errors import StyleTypeError, StyleValueError
@@ -15,6 +16,33 @@ if TYPE_CHECKING:
from .styles import Styles
class ScalarProperty:
def __set_name__(self, owner: Styles, name: str) -> None:
self.internal_name = f"_rule_{name}"
def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> Scalar:
value = getattr(obj, self.internal_name)
return value
def __set__(
self, obj: Styles, value: float | Scalar | str | None
) -> float | Scalar | str | None:
if value is None:
setattr(obj, self.internal_name, None)
elif isinstance(value, float):
setattr(obj, self.internal_name, Scalar(value, "cells"))
elif isinstance(value, Scalar):
setattr(obj, self.internal_name, value)
elif isinstance(value, str):
try:
setattr(obj, self.internal_name, Scalar.parse(value))
except ScalarParseError:
raise StyleValueError("unable to parse scalar from {value!r}")
else:
raise StyleValueError("expected float, Scalar, or None")
return value
class BoxProperty:
DEFAULT = ("", Style())
@@ -25,7 +53,9 @@ class BoxProperty:
self._type = _type
self.edge = edge
def __get__(self, obj: Styles, objtype=None) -> tuple[str, Style]:
def __get__(
self, obj: Styles, objtype: type[Styles] | None = None
) -> tuple[str, Style]:
value = getattr(obj, self.internal_name)
return value or self.DEFAULT
@@ -141,12 +171,12 @@ class StyleProperty:
def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> Style:
color = getattr(obj, self._color_name) or Color.default()
bgcolor = getattr(obj, self._bgcolor_name) or Color.default()
color = getattr(obj, self._color_name)
bgcolor = getattr(obj, self._bgcolor_name)
style = Style.from_color(color, bgcolor)
style_flags = getattr(obj, self._style_name)
if style_flags is not None:
style += style_flags
style += Style.parse(style_flags)
return style
def __set__(self, obj: Styles, style: Style | str | None) -> Style | str | None:

View File

@@ -11,6 +11,7 @@ from .errors import DeclarationError, StyleValueError
from ._error_tools import friendly_list
from ..geometry import Offset, Spacing, SpacingDimensions
from .model import Declaration
from .scalar import Scalar
from .styles import Styles
from .types import Display, Visibility
from .tokenize import Token
@@ -67,6 +68,26 @@ class StylesBuilder:
else:
self.error(name, token, f"invalid token {value!r} in this context")
def _process_scalar(self, name: str, tokens: list[Token]) -> None:
if not tokens:
return
if len(tokens) == 1:
setattr(self.styles, f"_rule_{name}", Scalar.parse(tokens[0].value))
else:
self.error(name, tokens[0], "a single scalar is expected")
def process_width(self, name: str, tokens: list[Token]) -> None:
self._process_scalar(name, tokens)
def process_height(self, name: str, tokens: list[Token]) -> None:
self._process_scalar(name, tokens)
def process_min_width(self, name: str, tokens: list[Token]) -> None:
self._process_scalar(name, tokens)
def process_min_height(self, name: str, tokens: list[Token]) -> None:
self._process_scalar(name, tokens)
def process_visibility(self, name: str, tokens: list[Token]) -> None:
for token in tokens:
_, _, location, name, value = token
@@ -87,8 +108,8 @@ class StylesBuilder:
space: list[int] = []
append = space.append
for token in tokens:
_, _, location, toke_name, value = token
if toke_name == "number":
_, _, location, token_name, value = token
if token_name == "number":
append(int(value))
else:
self.error(name, token, f"unexpected token {value!r} in declaration")

43
src/textual/css/scalar.py Normal file
View File

@@ -0,0 +1,43 @@
from __future__ import annotations
import re
from typing import NamedTuple
_MATCH_SCALAR = re.compile(r"^(\d+\.?\d*)(fr|%)?$").match
class ScalarParseError(Exception):
pass
class Scalar(NamedTuple):
"""A numeric value and a unit."""
value: float
unit: str
@classmethod
def parse(cls, token: str) -> Scalar:
"""Parse a string in to a Scalar
Args:
token (str): A string containing a scalar, e.g. "3.14fr"
Raises:
ScalarParseError: If the value is not a valid scalar
Returns:
Scalar: New scalar
"""
match = _MATCH_SCALAR(token)
if match is None:
raise ScalarParseError(f"{token!r} is not a valid scalar")
value, unit = match.groups()
scalar = cls(float(value), unit or "")
return scalar
if __name__ == "__main__":
print(Scalar.parse("3.14"))

View File

@@ -17,6 +17,7 @@ from .constants import (
NULL_SPACING,
)
from ..geometry import NULL_OFFSET, Offset, Spacing
from .scalar import Scalar
from ._style_properties import (
BorderProperty,
BoxProperty,
@@ -24,10 +25,10 @@ from ._style_properties import (
DockEdgeProperty,
DocksProperty,
DockGroupProperty,
IntegerProperty,
OffsetProperty,
NameProperty,
NameListProperty,
ScalarProperty,
SpacingProperty,
StringProperty,
StyleProperty,
@@ -61,9 +62,10 @@ class Styles:
_rule_outline_bottom: tuple[str, Style] | None = None
_rule_outline_left: tuple[str, Style] | None = None
_rule_size: int | None = None
_rule_fraction: int | None = None
_rule_min_size: int | 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_layout: str | None = None
@@ -101,9 +103,10 @@ class Styles:
outline_bottom = BoxProperty()
outline_left = BoxProperty()
size = IntegerProperty()
fraction = IntegerProperty()
min_size = IntegerProperty()
width = ScalarProperty()
height = ScalarProperty()
min_width = ScalarProperty()
min_height = ScalarProperty()
dock_group = DockGroupProperty()
docks = DocksProperty()
@@ -148,12 +151,16 @@ class Styles:
yield "dock_edge", self.dock_edge, ""
yield "dock_group", self.dock_group, ""
yield "docks", self.docks, ()
yield "width", self.width, None
yield "height", self.height, None
yield "min_width", self.min_width, None
yield "min_height", self.min_height, None
yield "margin", self.margin, NULL_SPACING
yield "offset", self.offset, NULL_OFFSET
if self.has_outline:
yield "outline", self.outline
yield "padding", self.padding, NULL_SPACING
yield "text", self.text, ""
yield "text", self.text, Style()
yield "visibility", self.visibility, "visible"
yield "layers", self.layers, ()
yield "layer", self.layer, ""
@@ -257,6 +264,15 @@ class Styles:
if self._rule_text_color or self._rule_text_bgcolor or self._rule_text_style:
append_declaration("text", str(self.text))
if self._rule_width is not None:
append_declaration("width", str(self.width))
if self._rule_height is not None:
append_declaration("height", str(self.height))
if self._rule_min_width is not None:
append_declaration("min-width", str(self.min_width))
if self._rule_min_height is not None:
append_declaration("min-height", str(self.min_height))
lines.sort()
return lines

View File

@@ -179,8 +179,8 @@ if __name__ == "__main__":
CSS = """
App > View {
layout: dock;
docks: sidebar=left | widgets=top;
fart: poo
/* docks: sidebar=left | widgets=top; */
}
#sidebar {
@@ -190,6 +190,7 @@ if __name__ == "__main__":
#widget1 {
text: on blue;
dock-group: widgets;
width: 10%
}
#widget2 {
@@ -200,15 +201,15 @@ if __name__ == "__main__":
"""
stylesheet = Stylesheet()
stylesheet.read("~/example.css")
stylesheet.parse(CSS)
print(stylesheet.error_renderable)
# print(widget1.styles)
print(widget1.styles)
# stylesheet.apply(widget1)
stylesheet.apply(widget1)
# print(widget1.styles)
print(widget1.styles)
# from .query import DOMQuery

View File

@@ -45,7 +45,7 @@ expect_declaration_content = Expect(
whitespace=r"\s+",
comment_start=r"\/\*",
percentage=r"\d+\%",
number=r"\d+\.?\d*",
scalar=r"\d+\.?\d*(?:fr|%)?",
color=r"\#[0-9a-f]{6}|color\([0-9]{1,3}\)|rgb\(\d{1,3}\,\s?\d{1,3}\,\s?\d{1,3}\)",
key_value=r"[a-zA-Z_-][a-zA-Z0-9_-]*=[a-zA-Z_-]+",
token="[a-zA-Z_-]+",