diff --git a/Libraries/LibWeb/CMakeLists.txt b/Libraries/LibWeb/CMakeLists.txt index a0e35b16b34..b04be4db6b2 100644 --- a/Libraries/LibWeb/CMakeLists.txt +++ b/Libraries/LibWeb/CMakeLists.txt @@ -872,6 +872,7 @@ set(SOURCES SVG/SVGTransformList.cpp SVG/SVGTSpanElement.cpp SVG/SVGUseElement.cpp + SVG/SVGViewElement.cpp SVG/TagNames.cpp SVG/ViewBox.cpp UIEvents/CompositionEvent.cpp diff --git a/Libraries/LibWeb/DOM/ElementFactory.cpp b/Libraries/LibWeb/DOM/ElementFactory.cpp index e8c3d319c4a..54c2b79c924 100644 --- a/Libraries/LibWeb/DOM/ElementFactory.cpp +++ b/Libraries/LibWeb/DOM/ElementFactory.cpp @@ -117,6 +117,7 @@ #include #include #include +#include #include #include @@ -512,6 +513,8 @@ static GC::Ref create_svg_element(JS::Realm& realm, Document& d return realm.create(document, move(qualified_name)); if (local_name == SVG::TagNames::script) return realm.create(document, move(qualified_name)); + if (local_name == SVG::TagNames::view) + return realm.create(document, move(qualified_name)); if (local_name == SVG::TagNames::a) return realm.create(document, move(qualified_name)); if (local_name == SVG::TagNames::image) diff --git a/Libraries/LibWeb/DOM/Node.h b/Libraries/LibWeb/DOM/Node.h index c455c71db06..7a9b16fc107 100644 --- a/Libraries/LibWeb/DOM/Node.h +++ b/Libraries/LibWeb/DOM/Node.h @@ -171,6 +171,7 @@ public: virtual bool is_svg_style_element() const { return false; } virtual bool is_svg_svg_element() const { return false; } virtual bool is_svg_use_element() const { return false; } + virtual bool is_svg_view_element() const { return false; } virtual bool is_svg_a_element() const { return false; } virtual bool is_svg_foreign_object_element() const { return false; } diff --git a/Libraries/LibWeb/Forward.h b/Libraries/LibWeb/Forward.h index a1f3b601475..a0090df82ed 100644 --- a/Libraries/LibWeb/Forward.h +++ b/Libraries/LibWeb/Forward.h @@ -984,6 +984,7 @@ class SVGAnimatedLength; class SVGAnimatedRect; class SVGCircleElement; class SVGClipPathElement; +class SVGDecodedImageData; class SVGDefsElement; class SVGDescElement; class SVGElement; @@ -1007,6 +1008,7 @@ class SVGRectElement; class SVGScriptElement; class SVGSVGElement; class SVGTitleElement; +class SVGViewElement; } diff --git a/Libraries/LibWeb/SVG/SVGSVGElement.cpp b/Libraries/LibWeb/SVG/SVGSVGElement.cpp index e7ba29f41c6..1a552f64951 100644 --- a/Libraries/LibWeb/SVG/SVGSVGElement.cpp +++ b/Libraries/LibWeb/SVG/SVGSVGElement.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include namespace Web::SVG { @@ -39,6 +40,7 @@ void SVGSVGElement::initialize(JS::Realm& realm) void SVGSVGElement::visit_edges(Visitor& visitor) { Base::visit_edges(visitor); + visitor.visit(m_active_view_element); visitor.visit(m_view_box_for_bindings); } @@ -137,6 +139,21 @@ void SVGSVGElement::attribute_changed(FlyString const& name, Optional co update_fallback_view_box_for_svg_as_image(); } +void SVGSVGElement::children_changed(ChildrenChangedMetadata const*) +{ + // FIXME: Add support for all types of SVG fragment identifier. + // See: https://svgwg.org/svg2-draft/linking.html#LinksIntoSVG + if (auto url = document().url(); url.fragment().has_value()) { + if (auto referenced_element = get_element_by_id(*url.fragment())) { + if (auto* view_element = as_if(*referenced_element)) { + set_active_view_element(*view_element); + return; + } + } + set_active_view_element({}); + } +} + void SVGSVGElement::update_fallback_view_box_for_svg_as_image() { // AD-HOC: This creates a fallback viewBox for SVGs used as images. @@ -173,6 +190,9 @@ void SVGSVGElement::set_fallback_view_box_for_svg_as_image(Optional vie Optional SVGSVGElement::view_box() const { + if (m_active_view_element && m_active_view_element->view_box().has_value()) + return m_active_view_element->view_box().value(); + if (m_view_box.has_value()) return m_view_box; @@ -309,9 +329,18 @@ SVGSVGElement::NaturalMetrics SVGSVGElement::negotiate_natural_metrics(SVG::SVGS return {}; } - // FIXME: 2. If an SVG View is active: - // FIXME: 1. let viewbox be the viewbox defined by the active SVG View - // FIXME: 2. return viewbox.width / viewbox.height + // 2. If an SVG View is active: + if (auto active_view_element = svg_root.active_view_element(); active_view_element && active_view_element->view_box().has_value()) { + // 1. let viewbox be the viewbox defined by the active SVG View + auto view_box = active_view_element->view_box().value(); + dbgln("SVG View is active"); + + // 2. return viewbox.width / viewbox.height + if (view_box.width != 0 || view_box.height != 0) + return view_box.width / view_box.height; + + return {}; + } // 3. If the ‘viewBox’ on the ‘svg’ element is correctly specified: if (svg_root.view_box().has_value()) { diff --git a/Libraries/LibWeb/SVG/SVGSVGElement.h b/Libraries/LibWeb/SVG/SVGSVGElement.h index 88ec8eee11b..b7b8f704fcd 100644 --- a/Libraries/LibWeb/SVG/SVGSVGElement.h +++ b/Libraries/LibWeb/SVG/SVGSVGElement.h @@ -35,6 +35,9 @@ public: virtual bool is_svg_container() const override { return true; } virtual Optional view_box() const override; + + void set_active_view_element(GC::Ptr view_element) { m_active_view_element = view_element; } + virtual Optional preserve_aspect_ratio() const override { return m_preserve_aspect_ratio; } void set_fallback_view_box_for_svg_as_image(Optional); @@ -97,7 +100,10 @@ private: virtual bool is_svg_svg_element() const override { return true; } + GC::Ptr active_view_element() const { return m_active_view_element; } + virtual void attribute_changed(FlyString const& name, Optional const& old_value, Optional const& value, Optional const& namespace_) override; + virtual void children_changed(ChildrenChangedMetadata const*) override; void update_fallback_view_box_for_svg_as_image(); @@ -107,6 +113,8 @@ private: Optional m_fallback_view_box_for_svg_as_image; GC::Ptr m_view_box_for_bindings; + + GC::Ptr m_active_view_element; }; } diff --git a/Libraries/LibWeb/SVG/SVGViewElement.cpp b/Libraries/LibWeb/SVG/SVGViewElement.cpp new file mode 100644 index 00000000000..b1ab75e8de6 --- /dev/null +++ b/Libraries/LibWeb/SVG/SVGViewElement.cpp @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2025, Tim Ledbetter + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "SVGViewElement.h" +#include +#include +#include +#include +#include + +namespace Web::SVG { + +GC_DEFINE_ALLOCATOR(SVGViewElement); + +SVGViewElement::SVGViewElement(DOM::Document& document, DOM::QualifiedName qualified_name) + : SVGGraphicsElement(document, move(qualified_name)) +{ +} + +void SVGViewElement::initialize(JS::Realm& realm) +{ + WEB_SET_PROTOTYPE_FOR_INTERFACE(SVGViewElement); + Base::initialize(realm); + m_view_box_for_bindings = realm.create(realm); +} + +void SVGViewElement::visit_edges(Visitor& visitor) +{ + Base::visit_edges(visitor); + visitor.visit(m_view_box_for_bindings); +} + +bool SVGViewElement::is_presentational_hint(FlyString const& name) const +{ + if (Base::is_presentational_hint(name)) + return true; + + return first_is_one_of(name, + SVG::AttributeNames::viewBox, + SVG::AttributeNames::preserveAspectRatio); +} + +void SVGViewElement::apply_presentational_hints(GC::Ref cascaded_properties) const +{ + Base::apply_presentational_hints(cascaded_properties); + auto parsing_context = CSS::Parser::ParsingParams { document(), CSS::Parser::ParsingMode::SVGPresentationAttribute }; +} + +void SVGViewElement::attribute_changed(FlyString const& name, Optional const& old_value, Optional const& value, Optional const& namespace_) +{ + Base::attribute_changed(name, old_value, value, namespace_); + + if (name.equals_ignoring_ascii_case(SVG::AttributeNames::viewBox)) { + if (!value.has_value()) { + m_view_box_for_bindings->set_nulled(true); + } else { + m_view_box = try_parse_view_box(value.value_or(String {})); + m_view_box_for_bindings->set_nulled(!m_view_box.has_value()); + if (m_view_box.has_value()) { + m_view_box_for_bindings->set_base_val(Gfx::DoubleRect { m_view_box->min_x, m_view_box->min_y, m_view_box->width, m_view_box->height }); + m_view_box_for_bindings->set_anim_val(Gfx::DoubleRect { m_view_box->min_x, m_view_box->min_y, m_view_box->width, m_view_box->height }); + } + } + } + if (name.equals_ignoring_ascii_case(SVG::AttributeNames::preserveAspectRatio)) + m_preserve_aspect_ratio = AttributeParser::parse_preserve_aspect_ratio(value.value_or(String {})); +} + +} diff --git a/Libraries/LibWeb/SVG/SVGViewElement.h b/Libraries/LibWeb/SVG/SVGViewElement.h new file mode 100644 index 00000000000..d1f29f50d67 --- /dev/null +++ b/Libraries/LibWeb/SVG/SVGViewElement.h @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2025, Tim Ledbetter + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include + +namespace Web::SVG { + +class SVGViewElement final : public SVGGraphicsElement + , public SVGViewport { + WEB_PLATFORM_OBJECT(SVGViewElement, SVGGraphicsElement); + GC_DECLARE_ALLOCATOR(SVGViewElement); + +public: + virtual bool is_presentational_hint(FlyString const&) const override; + virtual void apply_presentational_hints(GC::Ref) const override; + + virtual Optional view_box() const override { return m_view_box; } + virtual Optional preserve_aspect_ratio() const override { return m_preserve_aspect_ratio; } + + GC::Ref view_box_for_bindings() { return *m_view_box_for_bindings; } + +private: + SVGViewElement(DOM::Document&, DOM::QualifiedName); + + virtual void initialize(JS::Realm&) override; + virtual void visit_edges(Visitor&) override; + + virtual bool is_svg_view_element() const override { return true; } + + virtual void attribute_changed(FlyString const& name, Optional const& old_value, Optional const& value, Optional const& namespace_) override; + + Optional m_view_box; + Optional m_preserve_aspect_ratio; + GC::Ptr m_view_box_for_bindings; +}; + +} + +namespace Web::DOM { + +template<> +inline bool Node::fast_is() const { return is_svg_view_element(); } + +} diff --git a/Libraries/LibWeb/SVG/SVGViewElement.idl b/Libraries/LibWeb/SVG/SVGViewElement.idl new file mode 100644 index 00000000000..cc2616c2919 --- /dev/null +++ b/Libraries/LibWeb/SVG/SVGViewElement.idl @@ -0,0 +1,8 @@ +#import +#import + +// https://svgwg.org/svg2-draft/linking.html#InterfaceSVGViewElement +[Exposed=Window] +interface SVGViewElement : SVGElement {}; + +SVGViewElement includes SVGFitToViewBox; diff --git a/Libraries/LibWeb/SVG/TagNames.h b/Libraries/LibWeb/SVG/TagNames.h index 4a253e7c96f..e75415dddbc 100644 --- a/Libraries/LibWeb/SVG/TagNames.h +++ b/Libraries/LibWeb/SVG/TagNames.h @@ -42,7 +42,8 @@ namespace Web::SVG::TagNames { __ENUMERATE_SVG_TAG(textPath) \ __ENUMERATE_SVG_TAG(title) \ __ENUMERATE_SVG_TAG(tspan) \ - __ENUMERATE_SVG_TAG(use) + __ENUMERATE_SVG_TAG(use) \ + __ENUMERATE_SVG_TAG(view) #define __ENUMERATE_SVG_TAG(name) extern FlyString name; ENUMERATE_SVG_TAGS diff --git a/Libraries/LibWeb/idl_files.cmake b/Libraries/LibWeb/idl_files.cmake index a2f04c610c0..0d1515449e7 100644 --- a/Libraries/LibWeb/idl_files.cmake +++ b/Libraries/LibWeb/idl_files.cmake @@ -370,6 +370,7 @@ libweb_js_bindings(SVG/SVGTransform) libweb_js_bindings(SVG/SVGTransformList) libweb_js_bindings(SVG/SVGTSpanElement) libweb_js_bindings(SVG/SVGUseElement) +libweb_js_bindings(SVG/SVGViewElement) libweb_js_bindings(Selection/Selection) libweb_js_bindings(StorageAPI/StorageManager) libweb_js_bindings(UIEvents/CompositionEvent) diff --git a/Tests/LibWeb/Ref/expected/wpt-import/svg/embedded/reference/green-rect-100x100.svg b/Tests/LibWeb/Ref/expected/wpt-import/svg/embedded/reference/green-rect-100x100.svg new file mode 100644 index 00000000000..120941444a4 --- /dev/null +++ b/Tests/LibWeb/Ref/expected/wpt-import/svg/embedded/reference/green-rect-100x100.svg @@ -0,0 +1,3 @@ + + + diff --git a/Tests/LibWeb/Ref/input/wpt-import/svg/embedded/image-embedding-svg-viewref-with-viewbox.svg b/Tests/LibWeb/Ref/input/wpt-import/svg/embedded/image-embedding-svg-viewref-with-viewbox.svg new file mode 100644 index 00000000000..4940cc4da3b --- /dev/null +++ b/Tests/LibWeb/Ref/input/wpt-import/svg/embedded/image-embedding-svg-viewref-with-viewbox.svg @@ -0,0 +1,14 @@ + + <image> referencing SVG image with <view> with 'viewBox' + + + + + + diff --git a/Tests/LibWeb/Text/expected/all-window-properties.txt b/Tests/LibWeb/Text/expected/all-window-properties.txt index a79498ab377..97089a31a4d 100644 --- a/Tests/LibWeb/Text/expected/all-window-properties.txt +++ b/Tests/LibWeb/Text/expected/all-window-properties.txt @@ -376,6 +376,7 @@ SVGTitleElement SVGTransform SVGTransformList SVGUseElement +SVGViewElement Screen ScreenOrientation SecurityPolicyViolationEvent diff --git a/Tests/LibWeb/Text/expected/wpt-import/svg/idlharness.window.txt b/Tests/LibWeb/Text/expected/wpt-import/svg/idlharness.window.txt index 194f1c0959b..2eba4dcfd1c 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/svg/idlharness.window.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/svg/idlharness.window.txt @@ -2,8 +2,8 @@ Harness status: OK Found 1781 tests -942 Pass -839 Fail +952 Pass +829 Fail Pass idl_test setup Pass idl_test validation Pass Partial interface Document: original interface defined @@ -1606,17 +1606,17 @@ Pass SVGElement interface: objects.a must inherit property "ownerSVGElement" wit Pass SVGElement interface: objects.a must inherit property "viewportElement" with the proper type Fail SVGElement interface: objects.a must inherit property "correspondingElement" with the proper type Fail SVGElement interface: objects.a must inherit property "correspondingUseElement" with the proper type -Fail SVGViewElement interface: existence and properties of interface object -Fail SVGViewElement interface object length -Fail SVGViewElement interface object name -Fail SVGViewElement interface: existence and properties of interface prototype object -Fail SVGViewElement interface: existence and properties of interface prototype object's "constructor" property -Fail SVGViewElement interface: existence and properties of interface prototype object's @@unscopables property -Fail SVGViewElement interface: attribute viewBox +Pass SVGViewElement interface: existence and properties of interface object +Pass SVGViewElement interface object length +Pass SVGViewElement interface object name +Pass SVGViewElement interface: existence and properties of interface prototype object +Pass SVGViewElement interface: existence and properties of interface prototype object's "constructor" property +Pass SVGViewElement interface: existence and properties of interface prototype object's @@unscopables property +Pass SVGViewElement interface: attribute viewBox Fail SVGViewElement interface: attribute preserveAspectRatio -Fail SVGViewElement must be primary interface of objects.view -Fail Stringification of objects.view -Fail SVGViewElement interface: objects.view must inherit property "viewBox" with the proper type +Pass SVGViewElement must be primary interface of objects.view +Pass Stringification of objects.view +Pass SVGViewElement interface: objects.view must inherit property "viewBox" with the proper type Fail SVGViewElement interface: objects.view must inherit property "preserveAspectRatio" with the proper type Pass SVGElement interface: objects.view must inherit property "className" with the proper type Pass SVGElement interface: objects.view must inherit property "ownerSVGElement" with the proper type