Add Specificity5 for user defined CSS

This commit is contained in:
Darren Burns
2022-06-28 17:26:18 +01:00
parent aef7270863
commit 972aeece64
8 changed files with 69 additions and 26 deletions

View File

@@ -1,8 +1,3 @@
Screen {
layout: dock;
height: 100%;
}
#box {
height: 50%;
width: 50%;

View File

@@ -1,10 +1,27 @@
from __future__ import annotations
from rich.console import RenderableType
from rich.panel import Panel
from textual.app import App, ComposeResult
from textual.widgets import Static
from textual.widget import Widget
class Box(Widget):
CSS = "#box {background: blue;}"
def __init__(
self, id: str | None = None, classes: str | None = None, *children: Widget
):
super().__init__(*children, id=id, classes=classes)
def render(self) -> RenderableType:
return Panel("Box")
class JustABox(App):
def compose(self) -> ComposeResult:
yield Static("Hello, World!", id="box")
yield Box(id="box")
if __name__ == "__main__":

View File

@@ -156,6 +156,7 @@ class RuleSet:
styles: Styles = field(default_factory=Styles)
errors: list[tuple[Token, str]] = field(default_factory=list)
classes: set[str] = field(default_factory=set)
is_widget_rule_set: bool = False
@classmethod
def _selector_to_css(cls, selectors: list[Selector]) -> str:

View File

@@ -56,6 +56,7 @@ from .types import (
AlignVertical,
Visibility,
ScrollbarGutter,
Specificity5,
)
if sys.version_info >= (3, 8):
@@ -511,20 +512,27 @@ class Styles(StylesBase):
self._rules.update(rules)
def extract_rules(
self, specificity: Specificity3
) -> list[tuple[str, Specificity4, Any]]:
"""Extract rules from Styles object, and apply !important css specificity.
self,
specificity: Specificity3,
is_widget_rule: bool = False,
) -> list[tuple[str, Specificity5, Any]]:
"""Extract rules from Styles object, and apply !important css specificity as
well as higher specificity of user CSS vs widget CSS.
Args:
specificity (Specificity3): A node specificity.
Returns:
list[tuple[str, Specificity4, Any]]]: A list containing a tuple of <RULE NAME>, <SPECIFICITY> <RULE VALUE>.
list[tuple[str, Specificity5, Any]]]: A list containing a tuple of <RULE NAME>, <SPECIFICITY> <RULE VALUE>.
"""
is_important = self.important.__contains__
rules = [
(rule_name, (int(is_important(rule_name)), *specificity), rule_value)
(
rule_name,
(int(not is_widget_rule), int(is_important(rule_name)), *specificity),
rule_value,
)
for rule_name, rule_value in self._rules.items()
]

View File

@@ -122,6 +122,8 @@ class Stylesheet:
self._rules: list[RuleSet] = []
self.variables = variables or {}
self.source: dict[str, str] = {}
# Records which of the source keys represent CSS defined at the widget level
self._widget_css_paths: set[str] = set()
self._require_parse = False
def __rich_repr__(self) -> rich.repr.Result:
@@ -147,6 +149,7 @@ class Stylesheet:
"""
stylesheet = Stylesheet(variables=self.variables.copy())
stylesheet.source = self.source.copy()
stylesheet._widget_css_paths = self._widget_css_paths.copy()
return stylesheet
def set_variables(self, variables: dict[str, str]) -> None:
@@ -160,7 +163,6 @@ class Stylesheet:
def _parse_rules(self, css: str, path: str | PurePath) -> list[RuleSet]:
"""Parse CSS and return rules.
Args:
css (str): String containing Textual CSS.
path (str | PurePath): Path to CSS or unique identifier
@@ -177,6 +179,11 @@ class Stylesheet:
raise
except Exception as error:
raise StylesheetError(f"failed to parse css; {error}")
if path in self._widget_css_paths:
for rule in rules:
rule.is_widget_rule_set = True
return rules
def read(self, filename: str | PurePath) -> None:
@@ -199,13 +206,17 @@ class Stylesheet:
self.source[str(path)] = css
self._require_parse = True
def add_source(self, css: str, path: str | PurePath | None = None) -> None:
def add_source(
self, css: str, path: str | PurePath | None = None, is_widget_css: bool = False
) -> None:
"""Parse CSS from a string.
Args:
css (str): String with CSS source.
path (str | PurePath, optional): The path of the source if a file, or some other identifier.
Defaults to None.
is_widget_css (bool): True if the CSS is defined in the Widget, False if the CSS is defined
in a user stylesheet.
Raises:
StylesheetError: If the CSS could not be read.
@@ -220,6 +231,11 @@ class Stylesheet:
# Path already in source, and CSS is identical
return
# Record any CSS defined at the widget level for
# different specificity treatment from user CSS.
if is_widget_css:
self._widget_css_paths.add(path)
self.source[path] = css
self._require_parse = True
@@ -286,9 +302,10 @@ class Stylesheet:
# Collect the rules defined in the stylesheet
for rule in reversed(self.rules):
for specificity in _check_rule(rule, node):
is_widget_rule = rule.is_widget_rule_set
for base_specificity in _check_rule(rule, node):
for key, rule_specificity, value in rule.styles.extract_rules(
specificity
base_specificity, is_widget_rule
):
rule_attributes[key].append((rule_specificity, value))

View File

@@ -37,5 +37,7 @@ ScrollbarGutter = Literal["auto", "stable"]
BoxSizing = Literal["border-box", "content-box"]
Overflow = Literal["scroll", "hidden", "auto"]
EdgeStyle = Tuple[EdgeType, Color]
Specificity3 = Tuple[int, int, int]
Specificity4 = Tuple[int, int, int, int]
Specificity5 = Tuple[int, int, int, int, int]

View File

@@ -74,12 +74,12 @@ class Widget(DOMNode):
CSS = """
Widget{
scrollbar-background: $panel-darken-2;
scrollbar-background-hover: $panel-darken-3;
scrollbar-background-hover: $panel-darken-3;
scrollbar-color: $system;
scrollbar-color-active: $secondary-darken-1;
scrollbar-color-active: $secondary-darken-1;
scrollbar-size-vertical: 2;
scrollbar-size-horizontal: 1;
}
"""
@@ -194,7 +194,7 @@ class Widget(DOMNode):
"""
# Parse the Widget's CSS
for path, css in self.css:
self.app.stylesheet.add_source(css, path=path)
self.app.stylesheet.add_source(css, path=path, is_widget_css=True)
def get_box_model(
self, container: Size, viewport: Size, fraction_unit: Fraction

View File

@@ -104,22 +104,25 @@ def test_stylesheet_apply_empty_rulesets():
stylesheet.apply(node)
@pytest.mark.xfail(reason="wip")
def test_stylesheet_apply_user_css_over_widget_css():
user_css = ".a {color: red;}"
user_css = ".a {color: red; tint: yellow;}"
class MyWidget(Widget):
CSS = ".a {color: blue;}"
CSS = ".a {color: blue; background: lime;}"
node = MyWidget()
node.add_class("a")
print(node.styles.color)
stylesheet = _make_stylesheet(user_css)
stylesheet.add_source(MyWidget.CSS, "widget.py:MyWidget", is_widget_css=True)
stylesheet.apply(node)
assert node.styles.background == Color(0, 0, 255)
# TODO: On Tuesday - writing the tests for prioritising user CSS above widget CSS.
# The node is red because user CSS overrides Widget.CSS
assert node.styles.color == Color(255, 0, 0)
# The background colour defined in the Widget still applies, since user CSS doesn't override it
assert node.styles.background == Color(0, 255, 0)
# As expected, the tint colour is yellow, since there's no competition between user or widget CSS
assert node.styles.tint == Color(255, 255, 0)
@pytest.mark.parametrize(