scalars in CSS

This commit is contained in:
Will McGugan
2021-11-26 14:13:01 +00:00
parent 6706746548
commit 079d41473f
9 changed files with 89 additions and 54 deletions

View File

@@ -11,6 +11,7 @@ App > DockView {
height: 1fr;
layer: panels;
border-right: outer #09312e;
offset-x: -50%;
}
#header {

View File

@@ -16,4 +16,4 @@ class BasicApp(App):
)
BasicApp.run(css_file="basic.css")
BasicApp.run(log="textual.log", css_file="basic.css")

View File

@@ -6,7 +6,14 @@ import rich.repr
from rich.color import Color
from rich.style import Style
from .scalar import get_symbols, UNIT_SYMBOL, Unit, Scalar, ScalarParseError
from .scalar import (
get_symbols,
UNIT_SYMBOL,
Unit,
Scalar,
ScalarOffset,
ScalarParseError,
)
from ..geometry import Offset, Spacing, SpacingDimensions
from .constants import NULL_SPACING, VALID_EDGE
from .errors import StyleTypeError, StyleValueError
@@ -42,7 +49,7 @@ class ScalarProperty:
if value is None:
new_value = None
elif isinstance(value, float):
new_value = Scalar(value, Unit.CELLS)
new_value = Scalar(value, Unit.CELLS, Unit.WIDTH)
elif isinstance(value, Scalar):
new_value = value
elif isinstance(value, str):
@@ -57,7 +64,7 @@ class ScalarProperty:
f"{self.name} units must be one of {friendly_list(get_symbols(self.units))}"
)
if new_value is not None and new_value.is_percent:
new_value = Scalar(new_value.value, self.percent_unit)
new_value = Scalar(new_value.value, self.percent_unit, Unit.WIDTH)
setattr(obj, self.internal_name, new_value)
return value
@@ -258,11 +265,26 @@ class OffsetProperty:
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) -> Offset:
return getattr(obj, self._internal_name) or Offset()
def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> ScalarOffset:
return getattr(obj, self._internal_name) or ScalarOffset(
Scalar.from_number(0), Scalar.from_number(0)
)
def __set__(self, obj: Styles, offset: tuple[int, int]) -> tuple[int, int]:
_offset = Offset(*offset)
def __set__(
self, obj: Styles, offset: tuple[int | str, int | str]
) -> tuple[int | str, int | str]:
x, y = offset
scalar_x = (
Scalar.parse(x, Unit.WIDTH)
if isinstance(x, str)
else Scalar(x, Unit.CELLS, Unit.WIDTH)
)
scalar_y = (
Scalar.parse(y, Unit.HEIGHT)
if isinstance(y, str)
else Scalar(y, Unit.CELLS, Unit.HEIGHT)
)
_offset = ScalarOffset(scalar_x, scalar_y)
setattr(obj, self._internal_name, _offset)
return offset

View File

@@ -1,6 +1,6 @@
from __future__ import annotations
from typing import cast
from typing import cast, NoReturn
import rich.repr
from rich.color import ANSI_COLOR_NAMES, Color
@@ -11,7 +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 .scalar import Scalar, ScalarOffset, Unit
from .styles import DockGroup, Styles
from .types import Edge, Display, Visibility
from .tokenize import Token
@@ -27,7 +27,7 @@ class StylesBuilder:
def __repr__(self) -> str:
return "StylesBuilder()"
def error(self, name: str, token: Token, message: str) -> None:
def error(self, name: str, token: Token, message: str) -> NoReturn:
raise DeclarationError(name, token, message)
def add_declaration(self, declaration: Declaration) -> None:
@@ -210,13 +210,14 @@ class StylesBuilder:
self.error(name, tokens[0], "expected two numbers in declaration")
else:
token1, token2 = tokens
if token1.name != "number":
self.error(name, token1, f"expected a number (found {token1.value!r})")
if token2.name != "number":
self.error(name, token2, f"expected a number (found {token1.value!r})")
self.styles._rule_offset = Offset(
int(float(token1.value)), int(float(token2.value))
)
if token1.name != "scalar":
self.error(name, token1, f"expected a scalar; found {token1.value!r}")
if token2.name != "scalar":
self.error(name, token2, f"expected a scalar; found {token1.value!r}")
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)
def process_offset_x(self, name: str, tokens: list[Token]) -> None:
if not tokens:
@@ -224,9 +225,12 @@ class StylesBuilder:
if len(tokens) != 1:
self.error(name, tokens[0], f"expected a single number")
else:
x = int(float(tokens[0].value))
token = tokens[0]
if token.name != "scalar":
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 = Offset(x, y)
self.styles._rule_offset = ScalarOffset(x, y)
def process_offset_y(self, name: str, tokens: list[Token]) -> None:
if not tokens:
@@ -234,9 +238,12 @@ class StylesBuilder:
if len(tokens) != 1:
self.error(name, tokens[0], f"expected a single number")
else:
y = int(float(tokens[0].value))
token = tokens[0]
if token.name != "scalar":
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 = Offset(x, y)
self.styles._rule_offset = ScalarOffset(x, y)
def process_layout(self, name: str, tokens: list[Token]) -> None:
if tokens:
@@ -247,7 +254,10 @@ class StylesBuilder:
def process_text(self, name: str, tokens: list[Token]) -> None:
style_definition = " ".join(token.value for token in tokens)
style = Style.parse(style_definition)
try:
style = Style.parse(style_definition)
except Exception as error:
self.error(name, tokens[0], f"failed to parse style; {error}")
self.styles.text = style
def process_text_color(self, name: str, tokens: list[Token]) -> None:

View File

@@ -40,15 +40,15 @@ UNIT_SYMBOL = {
SYMBOL_UNIT = {v: k for k, v in UNIT_SYMBOL.items()}
_MATCH_SCALAR = re.compile(r"^(\d+\.?\d*)(fr|%|w|h|vw|vh)?$").match
_MATCH_SCALAR = re.compile(r"^(\-?\d+\.?\d*)(fr|%|w|h|vw|vh)?$").match
RESOLVE_MAP = {
Unit.CELLS: lambda value, size, viewport: value,
Unit.WIDTH: lambda value, size, viewport: size[0],
Unit.HEIGHT: lambda value, size, viewport: size[1],
Unit.VIEW_WIDTH: lambda value, size, viewport: viewport[0],
Unit.VIEW_HEIGHT: lambda value, size, viewport: viewport[1],
Unit.WIDTH: lambda value, size, viewport: size[0] * value / 100,
Unit.HEIGHT: lambda value, size, viewport: size[1] * value / 100,
Unit.VIEW_WIDTH: lambda value, size, viewport: viewport[0] * value / 100,
Unit.VIEW_HEIGHT: lambda value, size, viewport: viewport[1] * value / 100,
}
@@ -69,9 +69,10 @@ class Scalar(NamedTuple):
value: float
unit: Unit
percent_unit: Unit
def __str__(self) -> str:
value, _unit = self
value, _unit, _ = self
return f"{int(value) if value.is_integer() else value}{self.symbol}"
@property
@@ -80,12 +81,12 @@ class Scalar(NamedTuple):
@property
def cells(self) -> int | None:
value, unit = self
value, unit, _ = self
return int(value) if unit == Unit.CELLS else None
@property
def fraction(self) -> int | None:
value, unit = self
value, unit, _ = self
return int(value) if unit == Unit.FRACTION else None
@property
@@ -93,7 +94,11 @@ class Scalar(NamedTuple):
return UNIT_SYMBOL[self.unit]
@classmethod
def parse(cls, token: str) -> Scalar:
def from_number(cls, value: float) -> Scalar:
return cls(value, Unit.CELLS, Unit.WIDTH)
@classmethod
def parse(cls, token: str, percent_unit: Unit = Unit.WIDTH) -> Scalar:
"""Parse a string in to a Scalar
Args:
@@ -109,16 +114,11 @@ class Scalar(NamedTuple):
if match is None:
raise ScalarParseError(f"{token!r} is not a valid scalar")
value, unit_name = match.groups()
scalar = cls(float(value), SYMBOL_UNIT[unit_name or ""])
scalar = cls(float(value), SYMBOL_UNIT[unit_name or ""], percent_unit)
return scalar
def resolve(
self,
size: tuple[int, int],
viewport: tuple[int, int],
percent_unit: Unit = Unit.WIDTH,
) -> float:
value, unit = self
def resolve(self, size: tuple[int, int], viewport: tuple[int, int]) -> float:
value, unit, percent_unit = self
if unit == Unit.PERCENT:
unit = percent_unit
try:
@@ -139,8 +139,7 @@ class ScalarOffset(NamedTuple):
def resolve(self, size: tuple[int, int], viewport: tuple[int, int]) -> Offset:
x, y = self
return Offset(
round(x.resolve(size, viewport, percent_unit=Unit.WIDTH)),
round(y.resolve(size, viewport, percent_unit=Unit.HEIGHT)),
round(x.resolve(size, viewport)), round(y.resolve(size, viewport))
)

View File

@@ -18,7 +18,7 @@ from .constants import (
NULL_SPACING,
)
from ..geometry import NULL_OFFSET, Offset, Spacing
from .scalar import Scalar, Unit
from .scalar import Scalar, ScalarOffset, Unit
from ._style_properties import (
BorderProperty,
BoxProperty,
@@ -63,7 +63,7 @@ class Styles:
_rule_padding: Spacing | None = None
_rule_margin: Spacing | None = None
_rule_offset: Offset | None = None
_rule_offset: ScalarOffset | None = None
_rule_border_top: tuple[str, Style] | None = None
_rule_border_right: tuple[str, Style] | None = None
@@ -144,6 +144,10 @@ class Styles:
"""Check if an outline is present."""
return any(edge for edge, _style in self.outline)
@property
def has_offset(self) -> bool:
return self._rule_offset is not None
def extract_rules(
self, specificity: tuple[int, int, int]
) -> list[tuple[str, tuple[int, int, int, int], Any]]:

View File

@@ -44,8 +44,7 @@ expect_declaration_content = Expect(
declaration_end=r"\n|;",
whitespace=r"\s+",
comment_start=r"\/\*",
percentage=r"\d+\%",
scalar=r"\-?\d+\.?\d*(?:fr|%)?",
scalar=r"\-?\d+\.?\d*(?:fr|%|w|h|vw|vh)?",
color=r"\#[0-9a-fA-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_-]*=[0-9a-zA-Z_\-\/]+",
token="[a-zA-Z_-]+",

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
from typing import ItemsView, KeysView, ValuesView, NamedTuple
from . import log
from .geometry import Region, Size
from .geometry import Offset, Region, Size
from operator import attrgetter
from .widget import Widget
@@ -47,7 +47,13 @@ class LayoutMap:
if widget in self.widgets:
return
self.widgets[widget] = RenderRegion(region + widget.layout_offset, order, clip)
layout_offset = Offset(0, 0)
if widget.styles.has_offset:
log("r", region, "c", clip.size)
layout_offset = widget.styles.offset.resolve(region.size, clip.size)
log("layout_offset", layout_offset)
self.widgets[widget] = RenderRegion(region + layout_offset, order, clip)
if isinstance(widget, View):
view: View = widget

View File

@@ -185,12 +185,6 @@ class Widget(DOMNode):
assert self._animate is not None
return self._animate
@property
def layout_offset(self) -> tuple[int, int]:
"""Get the layout offset as a tuple."""
x, y = self.styles.offset
return round(x), round(y)
@property
def gutter(self) -> Spacing:
mt, mr, mb, bl = self.margin or (0, 0, 0, 0)