optimize stylesheet apply

This commit is contained in:
Will McGugan
2022-08-25 16:10:40 +01:00
parent 390853e1de
commit 6879f91989
3 changed files with 53 additions and 10 deletions

View File

@@ -160,6 +160,8 @@ class RuleSet:
styles: Styles = field(default_factory=Styles) styles: Styles = field(default_factory=Styles)
errors: list[tuple[Token, str]] = field(default_factory=list) errors: list[tuple[Token, str]] = field(default_factory=list)
classes: set[str] = field(default_factory=set) classes: set[str] = field(default_factory=set)
pseudo_classes: set[str] = field(default_factory=set)
ids: set[str] = field(default_factory=set)
is_default_rules: bool = False is_default_rules: bool = False
tie_breaker: int = 0 tie_breaker: int = 0
@@ -195,11 +197,25 @@ class RuleSet:
def _post_parse(self) -> None: def _post_parse(self) -> None:
"""Called after the RuleSet is parsed.""" """Called after the RuleSet is parsed."""
# Build a set of the class names that have been updated # Build a set of the class names that have been updated
update = self.classes.update update_classes = self.classes.update
update_ids = self.ids.update
update_pseudo_classes = self.pseudo_classes.update
class_type = SelectorType.CLASS class_type = SelectorType.CLASS
id_type = SelectorType.ID
for selector_set in self.selector_set: for selector_set in self.selector_set:
update( update_classes(
selector.name selector.name
for selector in selector_set.selectors for selector in selector_set.selectors
if selector.type == class_type if selector.type == class_type
) )
update_ids(
selector.name
for selector in selector_set.selectors
if selector.type == id_type
)
update_pseudo_classes(
pseudo_class
for selector in selector_set.selectors
for pseudo_class in selector.pseudo_classes
)

View File

@@ -305,9 +305,11 @@ class Stylesheet:
self.source = stylesheet.source self.source = stylesheet.source
@classmethod @classmethod
def _check_rule(cls, rule: RuleSet, node: DOMNode) -> Iterable[Specificity3]: def _check_rule(
cls, rule: RuleSet, css_path_nodes: list[DOMNode]
) -> Iterable[Specificity3]:
for selector_set in rule.selector_set: for selector_set in rule.selector_set:
if _check_selectors(selector_set.selectors, node.css_path_nodes): if _check_selectors(selector_set.selectors, css_path_nodes):
yield selector_set.specificity yield selector_set.specificity
def apply(self, node: DOMNode, animate: bool = False) -> None: def apply(self, node: DOMNode, animate: bool = False) -> None:
@@ -332,21 +334,47 @@ class Stylesheet:
# same attribute, then we can choose the most specific rule and use that. # same attribute, then we can choose the most specific rule and use that.
rule_attributes: dict[str, list[tuple[Specificity6, object]]] rule_attributes: dict[str, list[tuple[Specificity6, object]]]
rule_attributes = {} rule_attributes = {}
rule_attributes_setdefault = rule_attributes.setdefault
_check_rule = self._check_rule _check_rule = self._check_rule
css_path_nodes = node.css_path_nodes
path_ids = {path_node._id for path_node in css_path_nodes if path_node._id}
path_classes = {
class_name
for path_node in css_path_nodes
for class_name in path_node.classes
}
path_pseudo_classes = {
pseudo_class
for path_node in css_path_nodes
for pseudo_class in path_node.get_pseudo_classes()
}
# Collect the rules defined in the stylesheet # Collect the rules defined in the stylesheet
for rule in reversed(self.rules): for rule in reversed(self.rules):
if rule.ids and not rule.ids.issubset(path_ids):
continue
if rule.classes and not rule.classes.issubset(path_classes):
continue
if rule.pseudo_classes and not rule.pseudo_classes.issubset(
path_pseudo_classes
):
continue
is_default_rules = rule.is_default_rules is_default_rules = rule.is_default_rules
tie_breaker = rule.tie_breaker tie_breaker = rule.tie_breaker
for base_specificity in _check_rule(rule, node): for base_specificity in _check_rule(rule, css_path_nodes):
for key, rule_specificity, value in rule.styles.extract_rules( for key, rule_specificity, value in rule.styles.extract_rules(
base_specificity, is_default_rules, tie_breaker base_specificity, is_default_rules, tie_breaker
): ):
rule_attributes.setdefault(key, []).append( rule_attributes_setdefault(key, []).append(
(rule_specificity, value) (rule_specificity, value)
) )
if not rule_attributes:
return
# For each rule declared for this node, keep only the most specific one # For each rule declared for this node, keep only the most specific one
get_first_item = itemgetter(0) get_first_item = itemgetter(0)
node_rules: RulesMap = cast( node_rules: RulesMap = cast(
@@ -382,7 +410,7 @@ class Stylesheet:
base_styles = styles.base base_styles = styles.base
# Styles currently used on new rules # Styles currently used on new rules
modified_rule_keys = {*base_styles.get_rules().keys(), *rules.keys()} modified_rule_keys = base_styles.get_rules().keys() | rules.keys()
# Current render rules (missing rules are filled with default) # Current render rules (missing rules are filled with default)
current_render_rules = styles.get_render_rules() current_render_rules = styles.get_render_rules()
@@ -442,10 +470,9 @@ class Stylesheet:
root (DOMNode): Root note to update. root (DOMNode): Root note to update.
animate (bool, optional): Enable CSS animation. Defaults to False. animate (bool, optional): Enable CSS animation. Defaults to False.
""" """
print("update", root)
self.update_nodes(root.walk_children(), animate=animate) self.update_nodes(root.walk_children(), animate=animate)
@timer("update_nodes")
def update_nodes(self, nodes: Iterable[DOMNode], animate: bool = False) -> None: def update_nodes(self, nodes: Iterable[DOMNode], animate: bool = False) -> None:
"""Update styles for nodes. """Update styles for nodes.
@@ -455,7 +482,6 @@ class Stylesheet:
""" """
apply = self.apply apply = self.apply
nodes = list(nodes) nodes = list(nodes)
print(len(nodes), "NODES")
for node in nodes: for node in nodes:
apply(node, animate=animate) apply(node, animate=animate)
if isinstance(node, Widget) and node.is_scrollable: if isinstance(node, Widget) and node.is_scrollable:

View File

@@ -163,6 +163,7 @@ class DevtoolsClient:
if isinstance(log, str): if isinstance(log, str):
await websocket.send_str(log) await websocket.send_str(log)
else: else:
assert isinstance(log, bytes)
await websocket.send_bytes(log) await websocket.send_bytes(log)
log_queue.task_done() log_queue.task_done()