From 31301ef08b6c798dc59f19f5672405a4ef6005f3 Mon Sep 17 00:00:00 2001 From: Psychpsyo Date: Tue, 25 Mar 2025 18:13:18 +0100 Subject: [PATCH] LibWeb: Add support for trees of pseudo-elements This is needed for CSS view transitions. --- Documentation/CSSGeneratedFiles.md | 6 ++- Libraries/LibWeb/CSS/PseudoElements.json | 3 +- Libraries/LibWeb/DOM/Element.cpp | 52 +++++++++++++------ Libraries/LibWeb/DOM/Element.h | 24 +++++++-- .../LibWeb/GenerateCSSPseudoElement.cpp | 28 ++++++++++ 5 files changed, 88 insertions(+), 25 deletions(-) diff --git a/Documentation/CSSGeneratedFiles.md b/Documentation/CSSGeneratedFiles.md index f6cfa808ab3..d28b9f4d1ee 100644 --- a/Documentation/CSSGeneratedFiles.md +++ b/Documentation/CSSGeneratedFiles.md @@ -169,8 +169,9 @@ Each entry has the following properties: |----------------------|----------|----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `alias-for` | No | Nothing | Use to specify that this should be treated as an alias for the named pseudo-element. | | `function-syntax` | No | Nothing | Syntax for the function arguments if this is a function-type pseudo-element. Copied directly from the spec. | -| `is-generated` | No | `false` | Whether this is a [generated pseudo-element.](https://drafts.csswg.org/css-pseudo-4/#generated-content) | -| `is-allowed-in-has` | No | `false` | Whether this is a [`:has`-allowed pseudo-element.](https://drafts.csswg.org/selectors/#has-allowed-pseudo-element) | +| `is-generated` | No | `false` | Whether this is a [generated pseudo-element](https://drafts.csswg.org/css-pseudo-4/#generated-content). | +| `is-allowed-in-has` | No | `false` | Whether this is a [`:has`-allowed pseudo-element](https://drafts.csswg.org/selectors/#has-allowed-pseudo-element). | +| `is-pseudo-root` | No | `false` | Whether this is a [pseudo-element root](https://drafts.csswg.org/css-view-transitions/#pseudo-element-root). | | `property-whitelist` | No | Nothing | Some pseudo-elements only permit certain properties. If so, name them in an array here. Some special values are allowed here for categories of properties - see below. | | `spec` | No | Nothing | Link to the spec definition, for reference. Not used in generated code. | | `type` | No | `"identifier"` | What type of pseudo-element is this. Either "identifier", "function", or "both". | @@ -181,6 +182,7 @@ The generated code provides: - `Optional aliased_pseudo_element_from_string(StringView)` is similar, but returns the `PseudoElement` this name is an alias for - `StringView pseudo_element_name(PseudoElement)` to convert a `PseudoElement` back into a string - `bool is_has_allowed_pseudo_element(PseudoElement)` returns whether the pseudo-element is valid inside `:has()` +- `bool is_pseudo_element_root(PseudoElement)` returns whether the pseudo-element is a [pseudo-element root](https://drafts.csswg.org/css-view-transitions/#pseudo-element-root) - `bool pseudo_element_supports_property(PseudoElement, PropertyID)` returns whether the property can be applied to this pseudo-element - A `GeneratedPseudoElement` enum listing only the pseudo-elements that are [generated content](https://drafts.csswg.org/css-pseudo-4/#generated-content) - `Optional to_generated_pseudo_element(PseudoElement)` for converting from `PseudoElement` to `GeneratedPseudoElement`. Returns nothing if it's not a generated pseudo-element diff --git a/Libraries/LibWeb/CSS/PseudoElements.json b/Libraries/LibWeb/CSS/PseudoElements.json index 1e702281795..01c532fbbfc 100644 --- a/Libraries/LibWeb/CSS/PseudoElements.json +++ b/Libraries/LibWeb/CSS/PseudoElements.json @@ -114,7 +114,8 @@ "spec": "https://drafts.csswg.org/css-forms-1/#selectordef-slider-track" }, "view-transition": { - "spec": "https://drafts.csswg.org/css-view-transitions-1/#selectordef-view-transition" + "spec": "https://drafts.csswg.org/css-view-transitions-1/#selectordef-view-transition", + "is-pseudo-root": true }, "view-transition-group": { "spec": "https://drafts.csswg.org/css-view-transitions-1/#selectordef-view-transition-group", diff --git a/Libraries/LibWeb/DOM/Element.cpp b/Libraries/LibWeb/DOM/Element.cpp index 0931824fc01..a280eb32ae4 100644 --- a/Libraries/LibWeb/DOM/Element.cpp +++ b/Libraries/LibWeb/DOM/Element.cpp @@ -87,6 +87,9 @@ namespace Web::DOM { +GC_DEFINE_ALLOCATOR(Element::PseudoElement); +GC_DEFINE_ALLOCATOR(Element::PseudoElementTreeNode); + Element::Element(Document& document, DOM::QualifiedName qualified_name) : ParentNode(document, NodeType::ELEMENT_NODE) , m_qualified_name(move(qualified_name)) @@ -117,9 +120,7 @@ void Element::visit_edges(Cell::Visitor& visitor) visitor.visit(m_computed_properties); if (m_pseudo_element_data) { for (auto& pseudo_element : *m_pseudo_element_data) { - visitor.visit(pseudo_element.cascaded_properties); - visitor.visit(pseudo_element.computed_properties); - visitor.visit(pseudo_element.layout_node); + visitor.visit(pseudo_element.value); } } if (m_registered_intersection_observers) { @@ -128,6 +129,15 @@ void Element::visit_edges(Cell::Visitor& visitor) } } +void Element::PseudoElement::visit_edges(Cell::Visitor& visitor) +{ + Base::visit_edges(visitor); + + visitor.visit(cascaded_properties); + visitor.visit(computed_properties); + visitor.visit(layout_node); +} + // https://dom.spec.whatwg.org/#dom-element-getattribute Optional Element::get_attribute(FlyString const& name) const { @@ -1337,9 +1347,9 @@ bool Element::affected_by_pseudo_class(CSS::PseudoClass pseudo_class) const } if (m_pseudo_element_data) { for (auto& pseudo_element : *m_pseudo_element_data) { - if (!pseudo_element.computed_properties) + if (!pseudo_element.value->computed_properties) continue; - if (pseudo_element.computed_properties->has_attempted_match_against_pseudo_class(pseudo_class)) + if (pseudo_element.value->computed_properties->has_attempted_match_against_pseudo_class(pseudo_class)) return true; } } @@ -1506,7 +1516,7 @@ bool Element::has_pseudo_elements() const { if (m_pseudo_element_data) { for (auto& pseudo_element : *m_pseudo_element_data) { - if (pseudo_element.layout_node) + if (pseudo_element.value->layout_node) return true; } } @@ -1517,7 +1527,7 @@ void Element::clear_pseudo_element_nodes(Badge) { if (m_pseudo_element_data) { for (auto& pseudo_element : *m_pseudo_element_data) { - pseudo_element.layout_node = nullptr; + pseudo_element.value->layout_node = nullptr; } } } @@ -1526,15 +1536,12 @@ void Element::serialize_pseudo_elements_as_json(JsonArraySerializersize(); ++i) { - auto& pseudo_element = (*m_pseudo_element_data)[i].layout_node; - if (!pseudo_element) - continue; + for (auto& pseudo_element : m_pseudo_element_data->keys()) { auto object = MUST(children_array.add_object()); - MUST(object.add("name"sv, MUST(String::formatted("::{}", CSS::pseudo_element_name(static_cast(i)))))); + MUST(object.add("name"sv, MUST(String::formatted("::{}", CSS::pseudo_element_name(pseudo_element))))); MUST(object.add("type"sv, "pseudo-element")); MUST(object.add("parent-id"sv, unique_id().value())); - MUST(object.add("pseudo-element"sv, i)); + MUST(object.add("pseudo-element"sv, to_underlying(pseudo_element))); MUST(object.finish()); } } @@ -2857,9 +2864,8 @@ void Element::set_pseudo_element_computed_properties(CSS::PseudoElement pseudo_e if (!m_pseudo_element_data && !style) return; - if (!CSS::Selector::PseudoElementSelector::is_known_pseudo_element_type(pseudo_element)) { + if (!CSS::Selector::PseudoElementSelector::is_known_pseudo_element_type(pseudo_element)) return; - } ensure_pseudo_element(pseudo_element).computed_properties = style; } @@ -2881,7 +2887,11 @@ Optional Element::get_pseudo_element(CSS::PseudoElement return {}; } - return m_pseudo_element_data->at(to_underlying(type)); + auto pseudo_element = m_pseudo_element_data->get(type); + if (!pseudo_element.has_value()) + return {}; + + return *(pseudo_element.value()); } Element::PseudoElement& Element::ensure_pseudo_element(CSS::PseudoElement type) const @@ -2891,7 +2901,15 @@ Element::PseudoElement& Element::ensure_pseudo_element(CSS::PseudoElement type) VERIFY(CSS::Selector::PseudoElementSelector::is_known_pseudo_element_type(type)); - return m_pseudo_element_data->at(to_underlying(type)); + if (!m_pseudo_element_data->get(type).has_value()) { + if (is_pseudo_element_root(type)) { + m_pseudo_element_data->set(type, heap().allocate()); + } else { + m_pseudo_element_data->set(type, heap().allocate()); + } + } + + return *(m_pseudo_element_data->get(type).value()); } void Element::set_custom_properties(Optional pseudo_element, HashMap custom_properties) diff --git a/Libraries/LibWeb/DOM/Element.h b/Libraries/LibWeb/DOM/Element.h index a6011739106..00b083f2d53 100644 --- a/Libraries/LibWeb/DOM/Element.h +++ b/Libraries/LibWeb/DOM/Element.h @@ -527,15 +527,26 @@ private: GC::Ptr m_computed_properties; HashMap m_custom_properties; - struct PseudoElement { + struct PseudoElement : public JS::Cell { + GC_CELL(PseudoElement, JS::Cell); + GC_DECLARE_ALLOCATOR(PseudoElement); + GC::Ptr layout_node; GC::Ptr cascaded_properties; GC::Ptr computed_properties; HashMap custom_properties; + + private: + virtual void visit_edges(JS::Cell::Visitor&) override; }; - // TODO: CSS::Selector::PseudoElement includes a lot of pseudo-elements that exist in shadow trees, - // and so we don't want to include data for them here. - using PseudoElementData = Array; + // https://drafts.csswg.org/css-view-transitions/#pseudo-element-tree + struct PseudoElementTreeNode + : public PseudoElement + , TreeNode { + GC_CELL(PseudoElementTreeNode, PseudoElement); + GC_DECLARE_ALLOCATOR(PseudoElementTreeNode); + }; + using PseudoElementData = HashMap>; mutable OwnPtr m_pseudo_element_data; Optional get_pseudo_element(CSS::PseudoElement) const; PseudoElement& ensure_pseudo_element(CSS::PseudoElement) const; @@ -618,7 +629,10 @@ inline bool Element::has_pseudo_element(CSS::PseudoElement type) const return false; if (!CSS::Selector::PseudoElementSelector::is_known_pseudo_element_type(type)) return false; - return m_pseudo_element_data->at(to_underlying(type)).layout_node; + auto pseudo_element = m_pseudo_element_data->get(type); + if (!pseudo_element.has_value()) + return false; + return pseudo_element.value()->layout_node; } WebIDL::ExceptionOr validate_and_extract(JS::Realm&, Optional namespace_, FlyString const& qualified_name); diff --git a/Meta/Lagom/Tools/CodeGenerators/LibWeb/GenerateCSSPseudoElement.cpp b/Meta/Lagom/Tools/CodeGenerators/LibWeb/GenerateCSSPseudoElement.cpp index 70cb12e5f1d..fcd26433bcc 100644 --- a/Meta/Lagom/Tools/CodeGenerators/LibWeb/GenerateCSSPseudoElement.cpp +++ b/Meta/Lagom/Tools/CodeGenerators/LibWeb/GenerateCSSPseudoElement.cpp @@ -86,6 +86,7 @@ Optional aliased_pseudo_element_from_string(StringView); StringView pseudo_element_name(PseudoElement); bool is_has_allowed_pseudo_element(PseudoElement); +bool is_pseudo_element_root(PseudoElement); bool pseudo_element_supports_property(PseudoElement, PropertyID); struct PseudoElementMetadata { @@ -234,6 +235,33 @@ bool is_has_allowed_pseudo_element(PseudoElement pseudo_element) } } +bool is_pseudo_element_root(PseudoElement pseudo_element) +{ + switch (pseudo_element) { +)~~~"); + + pseudo_elements_data.for_each_member([&](auto& name, JsonValue const& value) { + auto& pseudo_element = value.as_object(); + if (pseudo_element.has("alias-for"sv)) + return; + if (!pseudo_element.get_bool("is-pseudo-root"sv).value_or(false)) + return; + + auto member_generator = generator.fork(); + member_generator.set("name:titlecase", title_casify(name)); + + member_generator.append(R"~~~( + case PseudoElement::@name:titlecase@: + return true; +)~~~"); + }); + + generator.append(R"~~~( + default: + return false; + } +} + bool pseudo_element_supports_property(PseudoElement pseudo_element, PropertyID property_id) { switch (pseudo_element) {