diff options
author | PrestonLTaylor <95388976+PrestonLTaylor@users.noreply.github.com> | 2023-05-30 21:23:52 +0100 |
---|---|---|
committer | Andreas Kling <kling@serenityos.org> | 2023-06-03 05:58:00 +0200 |
commit | c7c3043aa2bdf98b0e733286ef2d5547546ba87e (patch) | |
tree | 5a38849618f982e6b68a12ca8ca632e27131af43 | |
parent | b322abd8d0881003fcf84bed9c1fe6c30fcff191 (diff) | |
download | serenity-c7c3043aa2bdf98b0e733286ef2d5547546ba87e.zip |
LibWeb: Implement the `<use>` SVG element
The SVG <use> element is used to be able to reuse other SVG graphics
without having to re-write the svg element.
We now support this feature! :^)
-rw-r--r-- | Userland/Libraries/LibWeb/CMakeLists.txt | 1 | ||||
-rw-r--r-- | Userland/Libraries/LibWeb/DOM/ElementFactory.cpp | 3 | ||||
-rw-r--r-- | Userland/Libraries/LibWeb/SVG/AttributeNames.h | 1 | ||||
-rw-r--r-- | Userland/Libraries/LibWeb/SVG/SVGElement.cpp | 55 | ||||
-rw-r--r-- | Userland/Libraries/LibWeb/SVG/SVGElement.h | 9 | ||||
-rw-r--r-- | Userland/Libraries/LibWeb/SVG/SVGUseElement.cpp | 177 | ||||
-rw-r--r-- | Userland/Libraries/LibWeb/SVG/SVGUseElement.h | 58 | ||||
-rw-r--r-- | Userland/Libraries/LibWeb/SVG/SVGUseElement.idl | 16 | ||||
-rw-r--r-- | Userland/Libraries/LibWeb/SVG/TagNames.h | 3 | ||||
-rw-r--r-- | Userland/Libraries/LibWeb/idl_files.cmake | 1 |
10 files changed, 323 insertions, 1 deletions
diff --git a/Userland/Libraries/LibWeb/CMakeLists.txt b/Userland/Libraries/LibWeb/CMakeLists.txt index 4439811ef8..0e79a2caa9 100644 --- a/Userland/Libraries/LibWeb/CMakeLists.txt +++ b/Userland/Libraries/LibWeb/CMakeLists.txt @@ -523,6 +523,7 @@ set(SOURCES SVG/SVGSVGElement.cpp SVG/SVGStopElement.cpp SVG/SVGTextContentElement.cpp + SVG/SVGUseElement.cpp SVG/TagNames.cpp SVG/ViewBox.cpp Selection/Selection.cpp diff --git a/Userland/Libraries/LibWeb/DOM/ElementFactory.cpp b/Userland/Libraries/LibWeb/DOM/ElementFactory.cpp index 10867906ae..00e2802f9c 100644 --- a/Userland/Libraries/LibWeb/DOM/ElementFactory.cpp +++ b/Userland/Libraries/LibWeb/DOM/ElementFactory.cpp @@ -98,6 +98,7 @@ #include <LibWeb/SVG/SVGSVGElement.h> #include <LibWeb/SVG/SVGStopElement.h> #include <LibWeb/SVG/SVGTextContentElement.h> +#include <LibWeb/SVG/SVGUseElement.h> #include <LibWeb/SVG/TagNames.h> #include <LibWeb/WebIDL/AbstractOperations.h> @@ -455,6 +456,8 @@ static WebIDL::ExceptionOr<JS::GCPtr<SVG::SVGElement>> create_svg_element(JS::Re return MUST_OR_THROW_OOM(realm.heap().allocate<SVG::SVGStopElement>(realm, document, move(qualified_name))); if (local_name == SVG::TagNames::text) return MUST_OR_THROW_OOM(realm.heap().allocate<SVG::SVGTextContentElement>(realm, document, move(qualified_name))); + if (local_name == SVG::TagNames::use) + return MUST_OR_THROW_OOM(realm.heap().allocate<SVG::SVGUseElement>(realm, document, move(qualified_name))); return nullptr; } diff --git a/Userland/Libraries/LibWeb/SVG/AttributeNames.h b/Userland/Libraries/LibWeb/SVG/AttributeNames.h index 96413616e1..7594e138b3 100644 --- a/Userland/Libraries/LibWeb/SVG/AttributeNames.h +++ b/Userland/Libraries/LibWeb/SVG/AttributeNames.h @@ -32,6 +32,7 @@ namespace Web::SVG::AttributeNames { E(gradientTransform) \ E(gradientUnits) \ E(height) \ + E(href) \ E(kernelMatrix) \ E(kernelUnitLength) \ E(keyPoints) \ diff --git a/Userland/Libraries/LibWeb/SVG/SVGElement.cpp b/Userland/Libraries/LibWeb/SVG/SVGElement.cpp index 09da38721d..a09e81c0af 100644 --- a/Userland/Libraries/LibWeb/SVG/SVGElement.cpp +++ b/Userland/Libraries/LibWeb/SVG/SVGElement.cpp @@ -1,13 +1,16 @@ /* * Copyright (c) 2020, Matthew Olsson <mattco@serenityos.org> + * Copyright (c) 2023, Preston Taylor <95388976+PrestonLTaylor@users.noreply.github.com> * * SPDX-License-Identifier: BSD-2-Clause */ #include <LibWeb/Bindings/ExceptionOrUtils.h> #include <LibWeb/Bindings/Intrinsics.h> +#include <LibWeb/DOM/Document.h> #include <LibWeb/HTML/DOMStringMap.h> #include <LibWeb/SVG/SVGElement.h> +#include <LibWeb/SVG/SVGUseElement.h> namespace Web::SVG { @@ -34,4 +37,56 @@ void SVGElement::visit_edges(Cell::Visitor& visitor) visitor.visit(m_dataset); } +void SVGElement::parse_attribute(DeprecatedFlyString const& name, DeprecatedString const& value) +{ + Base::parse_attribute(name, value); + + update_use_elements_that_reference_this(); +} + +void SVGElement::inserted() +{ + Base::inserted(); + + update_use_elements_that_reference_this(); +} + +void SVGElement::children_changed() +{ + Base::children_changed(); + + update_use_elements_that_reference_this(); +} + +void SVGElement::update_use_elements_that_reference_this() +{ + if (is<SVGUseElement>(this)) { + return; + } + + document().for_each_in_subtree_of_type<SVGUseElement>([this](SVGUseElement& use_element) { + use_element.svg_element_changed(*this); + return IterationDecision::Continue; + }); +} + +void SVGElement::removed_from(Node* parent) +{ + Base::removed_from(parent); + + remove_from_use_element_that_reference_this(); +} + +void SVGElement::remove_from_use_element_that_reference_this() +{ + if (is<SVGUseElement>(this)) { + return; + } + + document().for_each_in_subtree_of_type<SVGUseElement>([this](SVGUseElement& use_element) { + use_element.svg_element_removed(*this); + return IterationDecision::Continue; + }); +} + } diff --git a/Userland/Libraries/LibWeb/SVG/SVGElement.h b/Userland/Libraries/LibWeb/SVG/SVGElement.h index 1329d86814..93fd507634 100644 --- a/Userland/Libraries/LibWeb/SVG/SVGElement.h +++ b/Userland/Libraries/LibWeb/SVG/SVGElement.h @@ -16,6 +16,12 @@ class SVGElement : public DOM::Element { public: virtual bool requires_svg_container() const override { return true; } + virtual void parse_attribute(DeprecatedFlyString const& name, DeprecatedString const& value) override; + + virtual void children_changed() override; + virtual void inserted() override; + virtual void removed_from(Node*) override; + HTML::DOMStringMap* dataset() { return m_dataset.ptr(); } HTML::DOMStringMap const* dataset() const { return m_dataset.ptr(); } @@ -25,6 +31,9 @@ protected: virtual JS::ThrowCompletionOr<void> initialize(JS::Realm&) override; virtual void visit_edges(Cell::Visitor&) override; + void update_use_elements_that_reference_this(); + void remove_from_use_element_that_reference_this(); + JS::GCPtr<HTML::DOMStringMap> m_dataset; private: diff --git a/Userland/Libraries/LibWeb/SVG/SVGUseElement.cpp b/Userland/Libraries/LibWeb/SVG/SVGUseElement.cpp new file mode 100644 index 0000000000..fcad33e765 --- /dev/null +++ b/Userland/Libraries/LibWeb/SVG/SVGUseElement.cpp @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2023, Preston Taylor <95388976+PrestonLTaylor@users.noreply.github.com> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include <LibWeb/Bindings/Intrinsics.h> +#include <LibWeb/DOM/Document.h> +#include <LibWeb/DOM/ElementFactory.h> +#include <LibWeb/DOM/Event.h> +#include <LibWeb/DOM/ShadowRoot.h> +#include <LibWeb/Namespace.h> +#include <LibWeb/SVG/AttributeNames.h> +#include <LibWeb/SVG/SVGSVGElement.h> +#include <LibWeb/SVG/SVGUseElement.h> + +namespace Web::SVG { + +SVGUseElement::SVGUseElement(DOM::Document& document, DOM::QualifiedName qualified_name) + : SVGGraphicsElement(document, qualified_name) +{ +} + +JS::ThrowCompletionOr<void> SVGUseElement::initialize(JS::Realm& realm) +{ + MUST_OR_THROW_OOM(Base::initialize(realm)); + set_prototype(&Bindings::ensure_web_prototype<Bindings::SVGUseElementPrototype>(realm, "SVGUseElement")); + + // The shadow tree is open (inspectable by script), but read-only. + auto shadow_root = TRY(heap().allocate<DOM::ShadowRoot>(realm, document(), *this, Bindings::ShadowRootMode::Open)); + + // The user agent must create a use-element shadow tree whose host is the ‘use’ element itself + set_shadow_root(shadow_root); + + m_document_observer = TRY(realm.heap().allocate<DOM::DocumentObserver>(realm, realm, document())); + m_document_observer->document_fully_loaded = [this]() { + clone_element_tree_as_our_shadow_tree(referenced_element()); + }; + + return {}; +} + +void SVGUseElement::visit_edges(Cell::Visitor& visitor) +{ + Base::visit_edges(visitor); + visitor.visit(m_document_observer); +} + +void SVGUseElement::parse_attribute(DeprecatedFlyString const& name, DeprecatedString const& value) +{ + Base::parse_attribute(name, value); + + // https://svgwg.org/svg2-draft/struct.html#UseLayout + if (name == SVG::AttributeNames::x) { + m_x = AttributeParser::parse_coordinate(value); + } else if (name == SVG::AttributeNames::y) { + m_y = AttributeParser::parse_coordinate(value); + } else if (name == SVG::AttributeNames::href) { + // FIXME: Support the xlink:href attribute as a fallback + m_referenced_id = parse_id_from_href(value); + } +} + +Optional<StringView> SVGUseElement::parse_id_from_href(DeprecatedString const& href) +{ + auto id_seperator = href.find('#'); + if (!id_seperator.has_value()) { + return {}; + } + + return href.substring_view(id_seperator.value() + 1); +} + +void SVGUseElement::inserted() +{ + Base::inserted(); + + // The x and y properties define an additional transformation (translate(x,y), where x and y represent the computed value of the corresponding property) + // to be applied to the ‘use’ element, after any transformations specified with other properties + m_transform.translate(m_x.value_or(0), m_y.value_or(0)); +} + +void SVGUseElement::svg_element_changed(SVGElement& svg_element) +{ + auto to_clone = referenced_element(); + if (!to_clone) { + return; + } + + // NOTE: We need to check the ancestor because parse_attribute of a child doesn't call children_changed on the parent(s) + if (to_clone == &svg_element || to_clone->is_ancestor_of(svg_element)) { + clone_element_tree_as_our_shadow_tree(to_clone); + } +} + +void SVGUseElement::svg_element_removed(SVGElement& svg_element) +{ + if (!m_referenced_id.has_value()) { + return; + } + + if (svg_element.attribute("id"sv).matches(m_referenced_id.value())) { + shadow_root()->remove_all_children(); + } +} + +JS::GCPtr<DOM::Element> SVGUseElement::referenced_element() +{ + if (!m_referenced_id.has_value()) { + return nullptr; + } + + // FIXME: Support loading of external svg documents + return document().get_element_by_id(m_referenced_id.value()); +} + +// https://svgwg.org/svg2-draft/struct.html#UseShadowTree +void SVGUseElement::clone_element_tree_as_our_shadow_tree(Element* to_clone) const +{ + shadow_root()->remove_all_children(); + + if (to_clone && is_valid_reference_element(to_clone)) { + // The ‘use’ element references another element, a copy of which is rendered in place of the ‘use’ in the document. + auto cloned_reference_node = to_clone->clone_node(nullptr, true); + shadow_root()->append_child(cloned_reference_node).release_value_but_fixme_should_propagate_errors(); + } +} + +bool SVGUseElement::is_valid_reference_element(Element* reference_element) const +{ + // If the referenced element that results from resolving the URL is not an SVG element, then the reference is invalid and the ‘use’ element is in error. + // If the referenced element is a (shadow-including) ancestor of the ‘use’ element, then this is an invalid circular reference and the ‘use’ element is in error. + return reference_element->is_svg_element() && !reference_element->is_ancestor_of(*this); +} + +// https://www.w3.org/TR/SVG11/shapes.html#RectElementXAttribute +JS::NonnullGCPtr<SVGAnimatedLength> SVGUseElement::x() const +{ + // FIXME: Populate the unit type when it is parsed (0 here is "unknown"). + // FIXME: Create a proper animated value when animations are supported. + auto base_length = SVGLength::create(realm(), 0, m_x.value_or(0)).release_value_but_fixme_should_propagate_errors(); + auto anim_length = SVGLength::create(realm(), 0, m_x.value_or(0)).release_value_but_fixme_should_propagate_errors(); + return SVGAnimatedLength::create(realm(), move(base_length), move(anim_length)).release_value_but_fixme_should_propagate_errors(); +} + +// https://www.w3.org/TR/SVG11/shapes.html#RectElementYAttribute +JS::NonnullGCPtr<SVGAnimatedLength> SVGUseElement::y() const +{ + // FIXME: Populate the unit type when it is parsed (0 here is "unknown"). + // FIXME: Create a proper animated value when animations are supported. + auto base_length = SVGLength::create(realm(), 0, m_y.value_or(0)).release_value_but_fixme_should_propagate_errors(); + auto anim_length = SVGLength::create(realm(), 0, m_y.value_or(0)).release_value_but_fixme_should_propagate_errors(); + return SVGAnimatedLength::create(realm(), move(base_length), move(anim_length)).release_value_but_fixme_should_propagate_errors(); +} + +JS::NonnullGCPtr<SVGAnimatedLength> SVGUseElement::width() const +{ + TODO(); +} + +JS::NonnullGCPtr<SVGAnimatedLength> SVGUseElement::height() const +{ + TODO(); +} + +// https://svgwg.org/svg2-draft/struct.html#TermInstanceRoot +JS::GCPtr<SVGElement> SVGUseElement::instance_root() const +{ + return shadow_root()->first_child_of_type<SVGElement>(); +} + +JS::GCPtr<SVGElement> SVGUseElement::animated_instance_root() const +{ + return instance_root(); +} + +} diff --git a/Userland/Libraries/LibWeb/SVG/SVGUseElement.h b/Userland/Libraries/LibWeb/SVG/SVGUseElement.h new file mode 100644 index 0000000000..eb118db513 --- /dev/null +++ b/Userland/Libraries/LibWeb/SVG/SVGUseElement.h @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2023, Preston Taylor <95388976+PrestonLTaylor@users.noreply.github.com> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include <LibGfx/Path.h> +#include <LibWeb/DOM/DocumentObserver.h> +#include <LibWeb/SVG/SVGAnimatedLength.h> +#include <LibWeb/SVG/SVGGraphicsElement.h> + +namespace Web::SVG { + +class SVGUseElement final : public SVGGraphicsElement { + WEB_PLATFORM_OBJECT(SVGUseElement, SVGGraphicsElement); + +public: + virtual ~SVGUseElement() override = default; + + virtual void parse_attribute(DeprecatedFlyString const& name, DeprecatedString const& value) override; + + virtual void inserted() override; + + void svg_element_changed(SVGElement&); + void svg_element_removed(SVGElement&); + + JS::NonnullGCPtr<SVGAnimatedLength> x() const; + JS::NonnullGCPtr<SVGAnimatedLength> y() const; + JS::NonnullGCPtr<SVGAnimatedLength> width() const; + JS::NonnullGCPtr<SVGAnimatedLength> height() const; + + JS::GCPtr<SVGElement> instance_root() const; + JS::GCPtr<SVGElement> animated_instance_root() const; + +private: + SVGUseElement(DOM::Document&, DOM::QualifiedName); + + virtual JS::ThrowCompletionOr<void> initialize(JS::Realm&) override; + virtual void visit_edges(Cell::Visitor&) override; + + Optional<StringView> parse_id_from_href(DeprecatedString const& href); + + JS::GCPtr<DOM::Element> referenced_element(); + + void clone_element_tree_as_our_shadow_tree(Element* to_clone) const; + bool is_valid_reference_element(Element* reference_element) const; + + Optional<float> m_x; + Optional<float> m_y; + + Optional<StringView> m_referenced_id; + + JS::GCPtr<DOM::DocumentObserver> m_document_observer; +}; + +} diff --git a/Userland/Libraries/LibWeb/SVG/SVGUseElement.idl b/Userland/Libraries/LibWeb/SVG/SVGUseElement.idl new file mode 100644 index 0000000000..37de82fefd --- /dev/null +++ b/Userland/Libraries/LibWeb/SVG/SVGUseElement.idl @@ -0,0 +1,16 @@ +#import <SVG/SVGAnimatedLength.idl> +#import <SVG/SVGElement.idl> +#import <SVG/SVGGraphicsElement.idl> + +// https://svgwg.org/svg2-draft/struct.html#InterfaceSVGUseElement +[Exposed=Window] +interface SVGUseElement : SVGGraphicsElement { + [SameObject] readonly attribute SVGAnimatedLength x; + [SameObject] readonly attribute SVGAnimatedLength y; + [SameObject] readonly attribute SVGAnimatedLength width; + [SameObject] readonly attribute SVGAnimatedLength height; + [SameObject] readonly attribute SVGElement? instanceRoot; + [SameObject] readonly attribute SVGElement? animatedInstanceRoot; +}; + +// FIXME: SVGUseElement includes SVGURIReference; diff --git a/Userland/Libraries/LibWeb/SVG/TagNames.h b/Userland/Libraries/LibWeb/SVG/TagNames.h index 85712cad39..9034987c81 100644 --- a/Userland/Libraries/LibWeb/SVG/TagNames.h +++ b/Userland/Libraries/LibWeb/SVG/TagNames.h @@ -33,7 +33,8 @@ namespace Web::SVG::TagNames { __ENUMERATE_SVG_TAG(radialGradient) \ __ENUMERATE_SVG_TAG(script) \ __ENUMERATE_SVG_TAG(stop) \ - __ENUMERATE_SVG_TAG(title) + __ENUMERATE_SVG_TAG(title) \ + __ENUMERATE_SVG_TAG(use) #define __ENUMERATE_SVG_TAG(name) extern DeprecatedFlyString name; ENUMERATE_SVG_TAGS diff --git a/Userland/Libraries/LibWeb/idl_files.cmake b/Userland/Libraries/LibWeb/idl_files.cmake index 69da343304..53a5076bbb 100644 --- a/Userland/Libraries/LibWeb/idl_files.cmake +++ b/Userland/Libraries/LibWeb/idl_files.cmake @@ -212,6 +212,7 @@ libweb_js_bindings(SVG/SVGRectElement) libweb_js_bindings(SVG/SVGSVGElement) libweb_js_bindings(SVG/SVGStopElement) libweb_js_bindings(SVG/SVGTextContentElement) +libweb_js_bindings(SVG/SVGUseElement) libweb_js_bindings(Selection/Selection) libweb_js_bindings(UIEvents/FocusEvent) libweb_js_bindings(UIEvents/KeyboardEvent) |