
I'm about to make StackingContext traverse the paintable tree instead of actually traversing the layout tree, and it turns out we were not creating paintables for these SVG elements. Also switch them to Layout::Box instead of the default InlineNode to make the trees look a bit less weird. Ultimately, we should do something specialized for these subtrees, but for now this'll do.
181 lines
6.3 KiB
C++
181 lines
6.3 KiB
C++
/*
|
||
* 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/Layout/Box.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)
|
||
{
|
||
}
|
||
|
||
void SVGUseElement::initialize(JS::Realm& realm)
|
||
{
|
||
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 = 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 = realm.heap().allocate<DOM::DocumentObserver>(realm, realm, document());
|
||
m_document_observer->document_completely_loaded = [this]() {
|
||
clone_element_tree_as_our_shadow_tree(referenced_element());
|
||
};
|
||
}
|
||
|
||
void SVGUseElement::visit_edges(Cell::Visitor& visitor)
|
||
{
|
||
Base::visit_edges(visitor);
|
||
visitor.visit(m_document_observer);
|
||
}
|
||
|
||
void SVGUseElement::attribute_changed(DeprecatedFlyString const& name, DeprecatedString const& value)
|
||
{
|
||
Base::attribute_changed(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 attribute_changed 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));
|
||
auto anim_length = SVGLength::create(realm(), 0, m_x.value_or(0));
|
||
return SVGAnimatedLength::create(realm(), move(base_length), move(anim_length));
|
||
}
|
||
|
||
// 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));
|
||
auto anim_length = SVGLength::create(realm(), 0, m_y.value_or(0));
|
||
return SVGAnimatedLength::create(realm(), move(base_length), move(anim_length));
|
||
}
|
||
|
||
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();
|
||
}
|
||
|
||
JS::GCPtr<Layout::Node> SVGUseElement::create_layout_node(NonnullRefPtr<CSS::StyleProperties> style)
|
||
{
|
||
return heap().allocate_without_realm<Layout::Box>(document(), this, move(style));
|
||
}
|
||
|
||
}
|