mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Add Specificity5 for user defined CSS
This commit is contained in:
@@ -1,8 +1,3 @@
|
||||
Screen {
|
||||
layout: dock;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#box {
|
||||
height: 50%;
|
||||
width: 50%;
|
||||
|
||||
@@ -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__":
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
]
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user