diff --git a/src/browser/Frame.zig b/src/browser/Frame.zig index a1072e995..f0dd75881 100644 --- a/src/browser/Frame.zig +++ b/src/browser/Frame.zig @@ -64,6 +64,7 @@ const NavigationKind = @import("webapi/navigation/root.zig").NavigationKind; const PointList = @import("webapi/svg/PointList.zig"); const StringList = @import("webapi/svg/StringList.zig"); +const AnimatedEnumeration = @import("webapi/svg/AnimatedEnumeration.zig"); const AnimatedLength = @import("webapi/svg/AnimatedLength.zig"); const AnimatedNumber = @import("webapi/svg/AnimatedNumber.zig"); const AnimatedString = @import("webapi/svg/AnimatedString.zig"); @@ -150,6 +151,7 @@ _element_shadow_roots: Element.ShadowRootLookup = .empty, _node_owner_documents: Node.OwnerDocumentLookup = .empty, _element_scroll_positions: Element.ScrollPositionLookup = .empty, _element_namespace_uris: Element.NamespaceUriLookup = .empty, +_svg_animated_enumerations: AnimatedEnumeration.Lookup = .empty, _svg_animated_lengths: AnimatedLength.Lookup = .empty, _svg_animated_numbers: AnimatedNumber.Lookup = .empty, _svg_animated_preserve_aspect_ratios: AnimatedPreserveAspectRatio.Lookup = .empty, diff --git a/src/browser/frame/node_factory.zig b/src/browser/frame/node_factory.zig index 024b6614e..6fd085403 100644 --- a/src/browser/frame/node_factory.zig +++ b/src/browser/frame/node_factory.zig @@ -902,7 +902,9 @@ pub fn createElementNS(frame: *Frame, namespace: Element.Namespace, name: []cons 4 => switch (@as(u32, @bitCast(name[0..4].*))) { asUint("defs") => return createSvgElementT(frame, Graphics.Defs, name, attribute_iterator, .{ ._proto = undefined }), asUint("desc") => return createSvgElementT(frame, Element.Svg.Desc, name, attribute_iterator, .{ ._proto = undefined }), + asUint("mask") => return createSvgElementT(frame, Element.Svg.Mask, name, attribute_iterator, .{ ._proto = undefined }), asUint("rect") => return createSvgElementT(frame, Geometry.Rect, name, attribute_iterator, .{ ._proto = undefined }), + asUint("stop") => return createSvgElementT(frame, Element.Svg.Stop, name, attribute_iterator, .{ ._proto = undefined }), asUint("line") => return createSvgElementT(frame, Geometry.Line, name, attribute_iterator, .{ ._proto = undefined }), asUint("path") => return createSvgElementT(frame, Geometry.Path, name, attribute_iterator, .{ ._proto = undefined }), asUint("view") => return createSvgElementT(frame, Element.Svg.View, name, attribute_iterator, .{ ._proto = undefined }), @@ -915,16 +917,19 @@ pub fn createElementNS(frame: *Frame, namespace: Element.Namespace, name: []cons }, 6 => switch (@as(u48, @bitCast(name[0..6].*))) { asUint("circle") => return createSvgElementT(frame, Geometry.Circle, name, attribute_iterator, .{ ._proto = undefined }), + asUint("marker") => return createSvgElementT(frame, Element.Svg.Marker, name, attribute_iterator, .{ ._proto = undefined }), asUint("switch") => return createSvgElementT(frame, Graphics.Switch, name, attribute_iterator, .{ ._proto = undefined }), asUint("symbol") => return createSvgElementT(frame, Graphics.Symbol, name, attribute_iterator, .{ ._proto = undefined }), else => {}, }, 7 => switch (@as(u56, @bitCast(name[0..7].*))) { asUint("ellipse") => return createSvgElementT(frame, Geometry.Ellipse, name, attribute_iterator, .{ ._proto = undefined }), + asUint("pattern") => return createSvgElementT(frame, Element.Svg.Pattern, name, attribute_iterator, .{ ._proto = undefined }), asUint("polygon") => return createSvgElementT(frame, Geometry.Polygon, name, attribute_iterator, .{ ._proto = undefined }), else => {}, }, 8 => switch (@as(u64, @bitCast(name[0..8].*))) { + asUint("clipPath") => return createSvgElementT(frame, Element.Svg.ClipPath, name, attribute_iterator, .{ ._proto = undefined }), asUint("metadata") => return createSvgElementT(frame, Element.Svg.Metadata, name, attribute_iterator, .{ ._proto = undefined }), asUint("polyline") => return createSvgElementT(frame, Geometry.Polyline, name, attribute_iterator, .{ ._proto = undefined }), else => {}, @@ -933,6 +938,14 @@ pub fn createElementNS(frame: *Frame, namespace: Element.Namespace, name: []cons asUint("foreignObject") => return createSvgElementT(frame, Graphics.ForeignObject, name, attribute_iterator, .{ ._proto = undefined }), else => {}, }, + 14 => { + if (std.mem.eql(u8, name, "linearGradient")) { + return createSvgElementT(frame, Element.Svg.GradientElement.LinearGradient, name, attribute_iterator, .{ ._proto = undefined }); + } + if (std.mem.eql(u8, name, "radialGradient")) { + return createSvgElementT(frame, Element.Svg.GradientElement.RadialGradient, name, attribute_iterator, .{ ._proto = undefined }); + } + }, else => {}, } diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index 953d0bf97..53af562df 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -1068,6 +1068,14 @@ pub const PageJsApis = flattenTypes(&.{ @import("../webapi/element/svg/Title.zig"), @import("../webapi/element/svg/Desc.zig"), @import("../webapi/element/svg/Metadata.zig"), + @import("../webapi/element/svg/GradientElement.zig"), + @import("../webapi/element/svg/LinearGradient.zig"), + @import("../webapi/element/svg/RadialGradient.zig"), + @import("../webapi/element/svg/ClipPath.zig"), + @import("../webapi/element/svg/Marker.zig"), + @import("../webapi/element/svg/Mask.zig"), + @import("../webapi/element/svg/Pattern.zig"), + @import("../webapi/element/svg/Stop.zig"), @import("../webapi/element/svg/Rect.zig"), @import("../webapi/element/svg/Circle.zig"), @import("../webapi/element/svg/Ellipse.zig"), @@ -1082,6 +1090,7 @@ pub const PageJsApis = flattenTypes(&.{ @import("../webapi/svg/Transform.zig"), @import("../webapi/svg/AnimatedLength.zig"), @import("../webapi/svg/AnimatedNumber.zig"), + @import("../webapi/svg/AnimatedEnumeration.zig"), @import("../webapi/svg/PreserveAspectRatio.zig"), @import("../webapi/svg/AnimatedPreserveAspectRatio.zig"), @import("../webapi/svg/PointList.zig"), diff --git a/src/browser/tests/element/svg/hierarchy.html b/src/browser/tests/element/svg/hierarchy.html index 3a1e8a795..32ab775ca 100644 --- a/src/browser/tests/element/svg/hierarchy.html +++ b/src/browser/tests/element/svg/hierarchy.html @@ -39,6 +39,7 @@ testing.expectEqual('function', typeof SVGAnimatedString); testing.expectEqual('function', typeof SVGNumber); testing.expectEqual('function', typeof SVGAnimatedNumber); + testing.expectEqual('function', typeof SVGAnimatedEnumeration); testing.expectEqual('function', typeof SVGSymbolElement); testing.expectEqual('function', typeof SVGSwitchElement); testing.expectEqual('function', typeof SVGForeignObjectElement); @@ -46,6 +47,14 @@ testing.expectEqual('function', typeof SVGTitleElement); testing.expectEqual('function', typeof SVGDescElement); testing.expectEqual('function', typeof SVGMetadataElement); + testing.expectEqual('function', typeof SVGGradientElement); + testing.expectEqual('function', typeof SVGLinearGradientElement); + testing.expectEqual('function', typeof SVGRadialGradientElement); + testing.expectEqual('function', typeof SVGClipPathElement); + testing.expectEqual('function', typeof SVGMarkerElement); + testing.expectEqual('function', typeof SVGMaskElement); + testing.expectEqual('function', typeof SVGPatternElement); + testing.expectEqual('function', typeof SVGStopElement); } diff --git a/src/browser/tests/element/svg/resources.html b/src/browser/tests/element/svg/resources.html new file mode 100644 index 000000000..aadb8b233 --- /dev/null +++ b/src/browser/tests/element/svg/resources.html @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/browser/tests/element/svg/svgsvg.html b/src/browser/tests/element/svg/svgsvg.html index 8fa1379f8..c81845a9b 100644 --- a/src/browser/tests/element/svg/svgsvg.html +++ b/src/browser/tests/element/svg/svgsvg.html @@ -178,6 +178,10 @@ testing.expectError('NoModificationAllowedError', () => { aspect.animVal.meetOrSlice = 2; }); const empty = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + testing.expectEqual('100%', empty.width.baseVal.valueAsString); + testing.expectEqual('100%', empty.height.baseVal.valueAsString); + empty.setAttribute('width', 'invalid'); + testing.expectEqual('100%', empty.width.baseVal.valueAsString); testing.expectEqual(SVGPreserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMIDYMID, empty.preserveAspectRatio.baseVal.align); testing.expectEqual(SVGPreserveAspectRatio.SVG_MEETORSLICE_MEET, empty.preserveAspectRatio.baseVal.meetOrSlice); testing.expectEqual(null, empty.getAttribute('preserveAspectRatio')); diff --git a/src/browser/webapi/element/Svg.zig b/src/browser/webapi/element/Svg.zig index 442d60402..f53a16160 100644 --- a/src/browser/webapi/element/Svg.zig +++ b/src/browser/webapi/element/Svg.zig @@ -31,6 +31,12 @@ pub const View = @import("svg/View.zig"); pub const Title = @import("svg/Title.zig"); pub const Desc = @import("svg/Desc.zig"); pub const Metadata = @import("svg/Metadata.zig"); +pub const GradientElement = @import("svg/GradientElement.zig"); +pub const ClipPath = @import("svg/ClipPath.zig"); +pub const Marker = @import("svg/Marker.zig"); +pub const Mask = @import("svg/Mask.zig"); +pub const Pattern = @import("svg/Pattern.zig"); +pub const Stop = @import("svg/Stop.zig"); const String = lp.String; @@ -45,6 +51,12 @@ pub const Type = union(enum) { title: *Title, desc: *Desc, metadata: *Metadata, + gradient: *GradientElement, + clip_path: *ClipPath, + marker: *Marker, + mask: *Mask, + pattern: *Pattern, + stop: *Stop, generic: *Generic, }; @@ -59,6 +71,9 @@ pub fn is(self: *Svg, comptime T: type) ?*T { if (self._type == .graphics) { return self._type.graphics.is(T); } + if (self._type == .gradient) { + return self._type.gradient.is(T); + } return null; } @@ -82,7 +97,7 @@ pub fn getTag(self: *const Svg) Element.Tag { }, .generic => |g| g._tag, .title => .title, - .view, .desc, .metadata => .unknown, + .view, .desc, .metadata, .gradient, .clip_path, .marker, .mask, .pattern, .stop => .unknown, }; } diff --git a/src/browser/webapi/element/svg/ClipPath.zig b/src/browser/webapi/element/svg/ClipPath.zig new file mode 100644 index 000000000..d80155d1e --- /dev/null +++ b/src/browser/webapi/element/svg/ClipPath.zig @@ -0,0 +1,42 @@ +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) +// SPDX-License-Identifier: AGPL-3.0-or-later + +const js = @import("../../../js/js.zig"); +const Frame = @import("../../../Frame.zig"); +const Node = @import("../../Node.zig"); +const Element = @import("../../Element.zig"); +const Svg = @import("../Svg.zig"); +const AnimatedEnumeration = @import("../../svg/AnimatedEnumeration.zig"); +const AnimatedTransformList = @import("../../svg/AnimatedTransformList.zig"); + +const ClipPath = @This(); +_proto: *Svg, + +pub fn asElement(self: *ClipPath) *Element { + return self._proto.asElement(); +} +pub fn asNode(self: *ClipPath) *Node { + return self.asElement().asNode(); +} + +fn getClipPathUnits(self: *ClipPath, frame: *Frame) !*AnimatedEnumeration { + return AnimatedEnumeration.getOrCreate(self.asElement(), .clip_path_units, frame); +} + +fn getTransform(self: *ClipPath, frame: *Frame) !*AnimatedTransformList { + return AnimatedTransformList.getOrCreate(self.asElement(), .transform, frame); +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(ClipPath); + pub const Meta = struct { + pub const name = "SVGClipPathElement"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + pub const SVG_UNIT_TYPE_UNKNOWN = bridge.property(0, .{ .template = true }); + pub const SVG_UNIT_TYPE_USERSPACEONUSE = bridge.property(1, .{ .template = true }); + pub const SVG_UNIT_TYPE_OBJECTBOUNDINGBOX = bridge.property(2, .{ .template = true }); + pub const clipPathUnits = bridge.accessor(ClipPath.getClipPathUnits, null, .{}); + pub const transform = bridge.accessor(ClipPath.getTransform, null, .{}); +}; diff --git a/src/browser/webapi/element/svg/GradientElement.zig b/src/browser/webapi/element/svg/GradientElement.zig new file mode 100644 index 000000000..a36f350d4 --- /dev/null +++ b/src/browser/webapi/element/svg/GradientElement.zig @@ -0,0 +1,79 @@ +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) +// SPDX-License-Identifier: AGPL-3.0-or-later + +const js = @import("../../../js/js.zig"); +const Frame = @import("../../../Frame.zig"); +const Node = @import("../../Node.zig"); +const Element = @import("../../Element.zig"); +const Svg = @import("../Svg.zig"); +const AnimatedEnumeration = @import("../../svg/AnimatedEnumeration.zig"); +const AnimatedString = @import("../../svg/AnimatedString.zig"); +const AnimatedTransformList = @import("../../svg/AnimatedTransformList.zig"); + +pub const LinearGradient = @import("LinearGradient.zig"); +pub const RadialGradient = @import("RadialGradient.zig"); + +const GradientElement = @This(); +_proto: *Svg, +_type: Type, + +pub const Type = union(enum) { + linear: *LinearGradient, + radial: *RadialGradient, +}; + +pub fn is(self: *GradientElement, comptime T: type) ?*T { + inline for (@typeInfo(Type).@"union".fields) |field| { + if (@field(Type, field.name) == self._type) { + if (field.type == *T) { + return @field(self._type, field.name); + } + } + } + return null; +} + +pub fn asElement(self: *GradientElement) *Element { + return self._proto.asElement(); +} +pub fn asNode(self: *GradientElement) *Node { + return self.asElement().asNode(); +} + +fn getGradientUnits(self: *GradientElement, frame: *Frame) !*AnimatedEnumeration { + return AnimatedEnumeration.getOrCreate(self.asElement(), .gradient_units, frame); +} + +fn getSpreadMethod(self: *GradientElement, frame: *Frame) !*AnimatedEnumeration { + return AnimatedEnumeration.getOrCreate(self.asElement(), .spread_method, frame); +} + +fn getGradientTransform(self: *GradientElement, frame: *Frame) !*AnimatedTransformList { + return AnimatedTransformList.getOrCreate(self.asElement(), .gradient_transform, frame); +} + +fn getHref(self: *GradientElement, frame: *Frame) !*AnimatedString { + return AnimatedString.getOrCreate(self.asElement(), .href, frame); +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(GradientElement); + pub const Meta = struct { + pub const name = "SVGGradientElement"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const SVG_UNIT_TYPE_UNKNOWN = bridge.property(0, .{ .template = true }); + pub const SVG_UNIT_TYPE_USERSPACEONUSE = bridge.property(1, .{ .template = true }); + pub const SVG_UNIT_TYPE_OBJECTBOUNDINGBOX = bridge.property(2, .{ .template = true }); + pub const SVG_SPREADMETHOD_UNKNOWN = bridge.property(0, .{ .template = true }); + pub const SVG_SPREADMETHOD_PAD = bridge.property(1, .{ .template = true }); + pub const SVG_SPREADMETHOD_REFLECT = bridge.property(2, .{ .template = true }); + pub const SVG_SPREADMETHOD_REPEAT = bridge.property(3, .{ .template = true }); + + pub const gradientUnits = bridge.accessor(GradientElement.getGradientUnits, null, .{}); + pub const spreadMethod = bridge.accessor(GradientElement.getSpreadMethod, null, .{}); + pub const gradientTransform = bridge.accessor(GradientElement.getGradientTransform, null, .{}); + pub const href = bridge.accessor(GradientElement.getHref, null, .{}); +}; diff --git a/src/browser/webapi/element/svg/Graphics.zig b/src/browser/webapi/element/svg/Graphics.zig index d9b4dc269..cbdf90715 100644 --- a/src/browser/webapi/element/svg/Graphics.zig +++ b/src/browser/webapi/element/svg/Graphics.zig @@ -204,7 +204,7 @@ fn transformMatrix(element: *Element) PathData.Matrix { } pub fn getTransform(self: *Graphics, frame: *Frame) !*AnimatedTransformList { - return AnimatedTransformList.getOrCreate(self.asElement(), frame); + return AnimatedTransformList.getOrCreate(self.asElement(), .transform, frame); } pub fn getRequiredExtensions(self: *Graphics, frame: *Frame) !*StringList { diff --git a/src/browser/webapi/element/svg/LinearGradient.zig b/src/browser/webapi/element/svg/LinearGradient.zig new file mode 100644 index 000000000..8f79d3e2c --- /dev/null +++ b/src/browser/webapi/element/svg/LinearGradient.zig @@ -0,0 +1,45 @@ +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) +// SPDX-License-Identifier: AGPL-3.0-or-later + +const js = @import("../../../js/js.zig"); +const Frame = @import("../../../Frame.zig"); +const Node = @import("../../Node.zig"); +const Element = @import("../../Element.zig"); +const AnimatedLength = @import("../../svg/AnimatedLength.zig"); +const GradientElement = @import("GradientElement.zig"); + +const LinearGradient = @This(); +_proto: *GradientElement, + +pub fn asElement(self: *LinearGradient) *Element { + return self._proto.asElement(); +} +pub fn asNode(self: *LinearGradient) *Node { + return self.asElement().asNode(); +} + +fn getX1(self: *LinearGradient, frame: *Frame) !*AnimatedLength { + return AnimatedLength.getOrCreate(self.asElement(), .linear_gradient_x1, frame); +} +fn getY1(self: *LinearGradient, frame: *Frame) !*AnimatedLength { + return AnimatedLength.getOrCreate(self.asElement(), .linear_gradient_y1, frame); +} +fn getX2(self: *LinearGradient, frame: *Frame) !*AnimatedLength { + return AnimatedLength.getOrCreate(self.asElement(), .linear_gradient_x2, frame); +} +fn getY2(self: *LinearGradient, frame: *Frame) !*AnimatedLength { + return AnimatedLength.getOrCreate(self.asElement(), .linear_gradient_y2, frame); +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(LinearGradient); + pub const Meta = struct { + pub const name = "SVGLinearGradientElement"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + pub const x1 = bridge.accessor(LinearGradient.getX1, null, .{}); + pub const y1 = bridge.accessor(LinearGradient.getY1, null, .{}); + pub const x2 = bridge.accessor(LinearGradient.getX2, null, .{}); + pub const y2 = bridge.accessor(LinearGradient.getY2, null, .{}); +}; diff --git a/src/browser/webapi/element/svg/Marker.zig b/src/browser/webapi/element/svg/Marker.zig new file mode 100644 index 000000000..6e9c580a8 --- /dev/null +++ b/src/browser/webapi/element/svg/Marker.zig @@ -0,0 +1,53 @@ +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) +// SPDX-License-Identifier: AGPL-3.0-or-later + +const js = @import("../../../js/js.zig"); +const Frame = @import("../../../Frame.zig"); +const Node = @import("../../Node.zig"); +const Element = @import("../../Element.zig"); +const Svg = @import("../Svg.zig"); +const AnimatedEnumeration = @import("../../svg/AnimatedEnumeration.zig"); +const AnimatedLength = @import("../../svg/AnimatedLength.zig"); + +const Marker = @This(); +_proto: *Svg, + +pub fn asElement(self: *Marker) *Element { + return self._proto.asElement(); +} +pub fn asNode(self: *Marker) *Node { + return self.asElement().asNode(); +} + +fn getRefX(self: *Marker, frame: *Frame) !*AnimatedLength { + return AnimatedLength.getOrCreate(self.asElement(), .marker_ref_x, frame); +} +fn getRefY(self: *Marker, frame: *Frame) !*AnimatedLength { + return AnimatedLength.getOrCreate(self.asElement(), .marker_ref_y, frame); +} +fn getMarkerWidth(self: *Marker, frame: *Frame) !*AnimatedLength { + return AnimatedLength.getOrCreate(self.asElement(), .marker_width, frame); +} +fn getMarkerHeight(self: *Marker, frame: *Frame) !*AnimatedLength { + return AnimatedLength.getOrCreate(self.asElement(), .marker_height, frame); +} +fn getMarkerUnits(self: *Marker, frame: *Frame) !*AnimatedEnumeration { + return AnimatedEnumeration.getOrCreate(self.asElement(), .marker_units, frame); +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(Marker); + pub const Meta = struct { + pub const name = "SVGMarkerElement"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + pub const SVG_MARKERUNITS_UNKNOWN = bridge.property(0, .{ .template = true }); + pub const SVG_MARKERUNITS_USERSPACEONUSE = bridge.property(1, .{ .template = true }); + pub const SVG_MARKERUNITS_STROKEWIDTH = bridge.property(2, .{ .template = true }); + pub const refX = bridge.accessor(Marker.getRefX, null, .{}); + pub const refY = bridge.accessor(Marker.getRefY, null, .{}); + pub const markerWidth = bridge.accessor(Marker.getMarkerWidth, null, .{}); + pub const markerHeight = bridge.accessor(Marker.getMarkerHeight, null, .{}); + pub const markerUnits = bridge.accessor(Marker.getMarkerUnits, null, .{}); +}; diff --git a/src/browser/webapi/element/svg/Mask.zig b/src/browser/webapi/element/svg/Mask.zig new file mode 100644 index 000000000..4b397505f --- /dev/null +++ b/src/browser/webapi/element/svg/Mask.zig @@ -0,0 +1,57 @@ +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) +// SPDX-License-Identifier: AGPL-3.0-or-later + +const js = @import("../../../js/js.zig"); +const Frame = @import("../../../Frame.zig"); +const Node = @import("../../Node.zig"); +const Element = @import("../../Element.zig"); +const Svg = @import("../Svg.zig"); +const AnimatedEnumeration = @import("../../svg/AnimatedEnumeration.zig"); +const AnimatedLength = @import("../../svg/AnimatedLength.zig"); + +const Mask = @This(); +_proto: *Svg, + +pub fn asElement(self: *Mask) *Element { + return self._proto.asElement(); +} +pub fn asNode(self: *Mask) *Node { + return self.asElement().asNode(); +} + +fn getX(self: *Mask, frame: *Frame) !*AnimatedLength { + return AnimatedLength.getOrCreate(self.asElement(), .mask_x, frame); +} +fn getY(self: *Mask, frame: *Frame) !*AnimatedLength { + return AnimatedLength.getOrCreate(self.asElement(), .mask_y, frame); +} +fn getWidth(self: *Mask, frame: *Frame) !*AnimatedLength { + return AnimatedLength.getOrCreate(self.asElement(), .mask_width, frame); +} +fn getHeight(self: *Mask, frame: *Frame) !*AnimatedLength { + return AnimatedLength.getOrCreate(self.asElement(), .mask_height, frame); +} +fn getMaskUnits(self: *Mask, frame: *Frame) !*AnimatedEnumeration { + return AnimatedEnumeration.getOrCreate(self.asElement(), .mask_units, frame); +} +fn getMaskContentUnits(self: *Mask, frame: *Frame) !*AnimatedEnumeration { + return AnimatedEnumeration.getOrCreate(self.asElement(), .mask_content_units, frame); +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(Mask); + pub const Meta = struct { + pub const name = "SVGMaskElement"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + pub const SVG_UNIT_TYPE_UNKNOWN = bridge.property(0, .{ .template = true }); + pub const SVG_UNIT_TYPE_USERSPACEONUSE = bridge.property(1, .{ .template = true }); + pub const SVG_UNIT_TYPE_OBJECTBOUNDINGBOX = bridge.property(2, .{ .template = true }); + pub const x = bridge.accessor(Mask.getX, null, .{}); + pub const y = bridge.accessor(Mask.getY, null, .{}); + pub const width = bridge.accessor(Mask.getWidth, null, .{}); + pub const height = bridge.accessor(Mask.getHeight, null, .{}); + pub const maskUnits = bridge.accessor(Mask.getMaskUnits, null, .{}); + pub const maskContentUnits = bridge.accessor(Mask.getMaskContentUnits, null, .{}); +}; diff --git a/src/browser/webapi/element/svg/Pattern.zig b/src/browser/webapi/element/svg/Pattern.zig new file mode 100644 index 000000000..0f2a614bf --- /dev/null +++ b/src/browser/webapi/element/svg/Pattern.zig @@ -0,0 +1,67 @@ +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) +// SPDX-License-Identifier: AGPL-3.0-or-later + +const js = @import("../../../js/js.zig"); +const Frame = @import("../../../Frame.zig"); +const Node = @import("../../Node.zig"); +const Element = @import("../../Element.zig"); +const Svg = @import("../Svg.zig"); +const AnimatedEnumeration = @import("../../svg/AnimatedEnumeration.zig"); +const AnimatedLength = @import("../../svg/AnimatedLength.zig"); +const AnimatedString = @import("../../svg/AnimatedString.zig"); +const AnimatedTransformList = @import("../../svg/AnimatedTransformList.zig"); + +const Pattern = @This(); +_proto: *Svg, + +pub fn asElement(self: *Pattern) *Element { + return self._proto.asElement(); +} +pub fn asNode(self: *Pattern) *Node { + return self.asElement().asNode(); +} + +fn getX(self: *Pattern, frame: *Frame) !*AnimatedLength { + return AnimatedLength.getOrCreate(self.asElement(), .pattern_x, frame); +} +fn getY(self: *Pattern, frame: *Frame) !*AnimatedLength { + return AnimatedLength.getOrCreate(self.asElement(), .pattern_y, frame); +} +fn getWidth(self: *Pattern, frame: *Frame) !*AnimatedLength { + return AnimatedLength.getOrCreate(self.asElement(), .pattern_width, frame); +} +fn getHeight(self: *Pattern, frame: *Frame) !*AnimatedLength { + return AnimatedLength.getOrCreate(self.asElement(), .pattern_height, frame); +} +fn getPatternUnits(self: *Pattern, frame: *Frame) !*AnimatedEnumeration { + return AnimatedEnumeration.getOrCreate(self.asElement(), .pattern_units, frame); +} +fn getPatternContentUnits(self: *Pattern, frame: *Frame) !*AnimatedEnumeration { + return AnimatedEnumeration.getOrCreate(self.asElement(), .pattern_content_units, frame); +} +fn getPatternTransform(self: *Pattern, frame: *Frame) !*AnimatedTransformList { + return AnimatedTransformList.getOrCreate(self.asElement(), .pattern_transform, frame); +} +fn getHref(self: *Pattern, frame: *Frame) !*AnimatedString { + return AnimatedString.getOrCreate(self.asElement(), .href, frame); +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(Pattern); + pub const Meta = struct { + pub const name = "SVGPatternElement"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + pub const SVG_UNIT_TYPE_UNKNOWN = bridge.property(0, .{ .template = true }); + pub const SVG_UNIT_TYPE_USERSPACEONUSE = bridge.property(1, .{ .template = true }); + pub const SVG_UNIT_TYPE_OBJECTBOUNDINGBOX = bridge.property(2, .{ .template = true }); + pub const x = bridge.accessor(Pattern.getX, null, .{}); + pub const y = bridge.accessor(Pattern.getY, null, .{}); + pub const width = bridge.accessor(Pattern.getWidth, null, .{}); + pub const height = bridge.accessor(Pattern.getHeight, null, .{}); + pub const patternUnits = bridge.accessor(Pattern.getPatternUnits, null, .{}); + pub const patternContentUnits = bridge.accessor(Pattern.getPatternContentUnits, null, .{}); + pub const patternTransform = bridge.accessor(Pattern.getPatternTransform, null, .{}); + pub const href = bridge.accessor(Pattern.getHref, null, .{}); +}; diff --git a/src/browser/webapi/element/svg/RadialGradient.zig b/src/browser/webapi/element/svg/RadialGradient.zig new file mode 100644 index 000000000..ef8522545 --- /dev/null +++ b/src/browser/webapi/element/svg/RadialGradient.zig @@ -0,0 +1,53 @@ +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) +// SPDX-License-Identifier: AGPL-3.0-or-later + +const js = @import("../../../js/js.zig"); +const Frame = @import("../../../Frame.zig"); +const Node = @import("../../Node.zig"); +const Element = @import("../../Element.zig"); +const AnimatedLength = @import("../../svg/AnimatedLength.zig"); +const GradientElement = @import("GradientElement.zig"); + +const RadialGradient = @This(); +_proto: *GradientElement, + +pub fn asElement(self: *RadialGradient) *Element { + return self._proto.asElement(); +} +pub fn asNode(self: *RadialGradient) *Node { + return self.asElement().asNode(); +} + +fn getCx(self: *RadialGradient, frame: *Frame) !*AnimatedLength { + return AnimatedLength.getOrCreate(self.asElement(), .radial_gradient_cx, frame); +} +fn getCy(self: *RadialGradient, frame: *Frame) !*AnimatedLength { + return AnimatedLength.getOrCreate(self.asElement(), .radial_gradient_cy, frame); +} +fn getR(self: *RadialGradient, frame: *Frame) !*AnimatedLength { + return AnimatedLength.getOrCreate(self.asElement(), .radial_gradient_r, frame); +} +fn getFx(self: *RadialGradient, frame: *Frame) !*AnimatedLength { + return AnimatedLength.getOrCreate(self.asElement(), .radial_gradient_fx, frame); +} +fn getFy(self: *RadialGradient, frame: *Frame) !*AnimatedLength { + return AnimatedLength.getOrCreate(self.asElement(), .radial_gradient_fy, frame); +} +fn getFr(self: *RadialGradient, frame: *Frame) !*AnimatedLength { + return AnimatedLength.getOrCreate(self.asElement(), .radial_gradient_fr, frame); +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(RadialGradient); + pub const Meta = struct { + pub const name = "SVGRadialGradientElement"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + pub const cx = bridge.accessor(RadialGradient.getCx, null, .{}); + pub const cy = bridge.accessor(RadialGradient.getCy, null, .{}); + pub const r = bridge.accessor(RadialGradient.getR, null, .{}); + pub const fx = bridge.accessor(RadialGradient.getFx, null, .{}); + pub const fy = bridge.accessor(RadialGradient.getFy, null, .{}); + pub const fr = bridge.accessor(RadialGradient.getFr, null, .{}); +}; diff --git a/src/browser/webapi/element/svg/Stop.zig b/src/browser/webapi/element/svg/Stop.zig new file mode 100644 index 000000000..1ca7a1059 --- /dev/null +++ b/src/browser/webapi/element/svg/Stop.zig @@ -0,0 +1,33 @@ +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) +// SPDX-License-Identifier: AGPL-3.0-or-later + +const js = @import("../../../js/js.zig"); +const Frame = @import("../../../Frame.zig"); +const Node = @import("../../Node.zig"); +const Element = @import("../../Element.zig"); +const Svg = @import("../Svg.zig"); +const AnimatedNumber = @import("../../svg/AnimatedNumber.zig"); + +const Stop = @This(); +_proto: *Svg, + +pub fn asElement(self: *Stop) *Element { + return self._proto.asElement(); +} +pub fn asNode(self: *Stop) *Node { + return self.asElement().asNode(); +} + +fn getOffset(self: *Stop, frame: *Frame) !*AnimatedNumber { + return AnimatedNumber.getOrCreate(self.asElement(), .offset, frame); +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(Stop); + pub const Meta = struct { + pub const name = "SVGStopElement"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + pub const offset = bridge.accessor(Stop.getOffset, null, .{}); +}; diff --git a/src/browser/webapi/element/svg/Svg.zig b/src/browser/webapi/element/svg/Svg.zig index ab60357f9..17d3db27e 100644 --- a/src/browser/webapi/element/svg/Svg.zig +++ b/src/browser/webapi/element/svg/Svg.zig @@ -70,11 +70,11 @@ pub fn getY(self: *Svg, frame: *Frame) !*AnimatedLength { } pub fn getWidth(self: *Svg, frame: *Frame) !*AnimatedLength { - return AnimatedLength.getOrCreate(self.asElement(), .width, frame); + return AnimatedLength.getOrCreate(self.asElement(), .svg_width, frame); } pub fn getHeight(self: *Svg, frame: *Frame) !*AnimatedLength { - return AnimatedLength.getOrCreate(self.asElement(), .height, frame); + return AnimatedLength.getOrCreate(self.asElement(), .svg_height, frame); } pub fn getPreserveAspectRatio(self: *Svg, frame: *Frame) !*AnimatedPreserveAspectRatio { diff --git a/src/browser/webapi/svg/AnimatedEnumeration.zig b/src/browser/webapi/svg/AnimatedEnumeration.zig new file mode 100644 index 000000000..0a042e77c --- /dev/null +++ b/src/browser/webapi/svg/AnimatedEnumeration.zig @@ -0,0 +1,153 @@ +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) +// SPDX-License-Identifier: AGPL-3.0-or-later + +const std = @import("std"); +const lp = @import("lightpanda"); + +const js = @import("../../js/js.zig"); +const Frame = @import("../../Frame.zig"); +const Element = @import("../Element.zig"); + +const AnimatedEnumeration = @This(); + +_element: *Element, +_attr_name: lp.String, +_entries: []const Entry, +_default_value: u16, + +pub const Entry = struct { + keyword: []const u8, + value: u16, +}; + +const unit_entries = [_]Entry{ + .{ .keyword = "userSpaceOnUse", .value = 1 }, + .{ .keyword = "objectBoundingBox", .value = 2 }, +}; +const marker_unit_entries = [_]Entry{ + .{ .keyword = "userSpaceOnUse", .value = 1 }, + .{ .keyword = "strokeWidth", .value = 2 }, +}; +const spread_entries = [_]Entry{ + .{ .keyword = "pad", .value = 1 }, + .{ .keyword = "reflect", .value = 2 }, + .{ .keyword = "repeat", .value = 3 }, +}; + +pub const Kind = enum { + clip_path_units, + gradient_units, + spread_method, + marker_units, + mask_units, + mask_content_units, + pattern_units, + pattern_content_units, + + fn attributeName(self: Kind, frame: *Frame) !lp.String { + const name = switch (self) { + .clip_path_units => "clipPathUnits", + .gradient_units => "gradientUnits", + .spread_method => "spreadMethod", + .marker_units => "markerUnits", + .mask_units => "maskUnits", + .mask_content_units => "maskContentUnits", + .pattern_units => "patternUnits", + .pattern_content_units => "patternContentUnits", + }; + return lp.String.init(frame.arena, name, .{ .dupe = false }); + } + + fn entries(self: Kind) []const Entry { + return switch (self) { + .marker_units => &marker_unit_entries, + .spread_method => &spread_entries, + else => &unit_entries, + }; + } + + fn defaultValue(self: Kind) u16 { + return switch (self) { + .clip_path_units, + .mask_content_units, + .pattern_content_units, + .spread_method, + => 1, + else => 2, + }; + } +}; + +pub const Key = struct { + element: *Element, + kind: Kind, +}; + +pub const Lookup = std.AutoHashMapUnmanaged(Key, *AnimatedEnumeration); + +pub fn getOrCreate(element: *Element, kind: Kind, frame: *Frame) !*AnimatedEnumeration { + const key: Key = .{ .element = element, .kind = kind }; + const gop = try frame._svg_animated_enumerations.getOrPut(frame.arena, key); + if (!gop.found_existing) { + errdefer _ = frame._svg_animated_enumerations.remove(key); + gop.value_ptr.* = try create( + element, + try kind.attributeName(frame), + kind.entries(), + kind.defaultValue(), + frame, + ); + } + return gop.value_ptr.*; +} + +pub fn create( + element: *Element, + attr_name: lp.String, + entries: []const Entry, + default_value: u16, + frame: *Frame, +) !*AnimatedEnumeration { + return frame._factory.create(AnimatedEnumeration{ + ._element = element, + ._attr_name = attr_name, + ._entries = entries, + ._default_value = default_value, + }); +} + +pub fn getBaseVal(self: *const AnimatedEnumeration) u16 { + const raw = self._element.getAttributeSafe(self._attr_name) orelse return self._default_value; + const trimmed = std.mem.trim(u8, raw, " \t\r\n\x0c"); + for (self._entries) |entry| { + if (std.mem.eql(u8, trimmed, entry.keyword)) return entry.value; + } + return 0; +} + +pub fn setBaseVal(self: *AnimatedEnumeration, value: u16, frame: *Frame) !void { + for (self._entries) |entry| { + if (entry.value == value) { + try self._element.setAttributeSafe(self._attr_name, lp.String.wrap(entry.keyword), frame); + return; + } + } + return error.TypeError; +} + +pub fn getAnimVal(self: *const AnimatedEnumeration) u16 { + return self.getBaseVal(); +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(AnimatedEnumeration); + + pub const Meta = struct { + pub const name = "SVGAnimatedEnumeration"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const baseVal = bridge.accessor(AnimatedEnumeration.getBaseVal, AnimatedEnumeration.setBaseVal, .{}); + pub const animVal = bridge.accessor(AnimatedEnumeration.getAnimVal, null, .{}); +}; diff --git a/src/browser/webapi/svg/AnimatedLength.zig b/src/browser/webapi/svg/AnimatedLength.zig index 47fb4927a..bab50c803 100644 --- a/src/browser/webapi/svg/AnimatedLength.zig +++ b/src/browser/webapi/svg/AnimatedLength.zig @@ -34,6 +34,8 @@ pub const Kind = enum { y, width, height, + svg_width, + svg_height, cx, cy, r, @@ -43,30 +45,141 @@ pub const Kind = enum { y1, x2, y2, + linear_gradient_x1, + linear_gradient_y1, + linear_gradient_x2, + linear_gradient_y2, + radial_gradient_cx, + radial_gradient_cy, + radial_gradient_r, + radial_gradient_fx, + radial_gradient_fy, + radial_gradient_fr, + marker_ref_x, + marker_ref_y, + marker_width, + marker_height, + mask_x, + mask_y, + mask_width, + mask_height, + pattern_x, + pattern_y, + pattern_width, + pattern_height, fn attributeName(self: Kind) lp.String { return switch (self) { - .x => comptime .wrap("x"), - .y => comptime .wrap("y"), - .width => comptime .wrap("width"), - .height => comptime .wrap("height"), - .cx => comptime .wrap("cx"), - .cy => comptime .wrap("cy"), - .r => comptime .wrap("r"), + .x, .mask_x, .pattern_x => comptime .wrap("x"), + .y, .mask_y, .pattern_y => comptime .wrap("y"), + .width, .svg_width, .mask_width, .pattern_width => comptime .wrap("width"), + .height, .svg_height, .mask_height, .pattern_height => comptime .wrap("height"), + .cx, .radial_gradient_cx => comptime .wrap("cx"), + .cy, .radial_gradient_cy => comptime .wrap("cy"), + .r, .radial_gradient_r => comptime .wrap("r"), .rx => comptime .wrap("rx"), .ry => comptime .wrap("ry"), - .x1 => comptime .wrap("x1"), - .y1 => comptime .wrap("y1"), - .x2 => comptime .wrap("x2"), - .y2 => comptime .wrap("y2"), + .x1, .linear_gradient_x1 => comptime .wrap("x1"), + .y1, .linear_gradient_y1 => comptime .wrap("y1"), + .x2, .linear_gradient_x2 => comptime .wrap("x2"), + .y2, .linear_gradient_y2 => comptime .wrap("y2"), + .radial_gradient_fx => comptime .wrap("fx"), + .radial_gradient_fy => comptime .wrap("fy"), + .radial_gradient_fr => comptime .wrap("fr"), + .marker_ref_x => comptime .wrap("refX"), + .marker_ref_y => comptime .wrap("refY"), + .marker_width => comptime .wrap("markerWidth"), + .marker_height => comptime .wrap("markerHeight"), }; } fn direction(self: Kind) Length.Direction { return switch (self) { - .x, .width, .cx, .rx, .x1, .x2 => .horizontal, - .y, .height, .cy, .ry, .y1, .y2 => .vertical, - .r => .unspecified, + .x, + .width, + .svg_width, + .cx, + .rx, + .x1, + .x2, + .linear_gradient_x1, + .linear_gradient_x2, + .radial_gradient_cx, + .radial_gradient_fx, + .marker_ref_x, + .marker_width, + .mask_x, + .mask_width, + .pattern_x, + .pattern_width, + => .horizontal, + .y, + .height, + .svg_height, + .cy, + .ry, + .y1, + .y2, + .linear_gradient_y1, + .linear_gradient_y2, + .radial_gradient_cy, + .radial_gradient_fy, + .marker_ref_y, + .marker_height, + .mask_y, + .mask_height, + .pattern_y, + .pattern_height, + => .vertical, + .r, .radial_gradient_r, .radial_gradient_fr => .unspecified, + }; + } + + fn defaultValue(self: Kind) f64 { + return switch (self) { + .svg_width, .svg_height => 100, + .linear_gradient_x2 => 100, + .radial_gradient_cx, + .radial_gradient_cy, + .radial_gradient_r, + .radial_gradient_fx, + .radial_gradient_fy, + => 50, + .marker_width, .marker_height => 3, + .mask_x, .mask_y => -10, + .mask_width, .mask_height => 120, + else => 0, + }; + } + + fn defaultUnit(self: Kind) Length.Unit { + return switch (self) { + .svg_width, + .svg_height, + .linear_gradient_x1, + .linear_gradient_y1, + .linear_gradient_x2, + .linear_gradient_y2, + .radial_gradient_cx, + .radial_gradient_cy, + .radial_gradient_r, + .radial_gradient_fx, + .radial_gradient_fy, + .radial_gradient_fr, + .mask_x, + .mask_y, + .mask_width, + .mask_height, + => .percentage, + else => .number, + }; + } + + fn fallbackAttributeName(self: Kind) ?lp.String { + return switch (self) { + .radial_gradient_fx => comptime .wrap("cx"), + .radial_gradient_fy => comptime .wrap("cy"), + else => null, }; } }; @@ -86,7 +199,15 @@ pub fn getOrCreate(element: *Element, kind: Kind, frame: *Frame) !*AnimatedLengt const gop = try frame._svg_animated_lengths.getOrPut(frame.arena, key); if (!gop.found_existing) { errdefer _ = frame._svg_animated_lengths.remove(key); - gop.value_ptr.* = try create(element, kind.attributeName(), kind.direction(), frame); + gop.value_ptr.* = try createConfigured( + element, + kind.attributeName(), + kind.direction(), + kind.defaultValue(), + kind.defaultUnit(), + kind.fallbackAttributeName(), + frame, + ); } return gop.value_ptr.*; } @@ -100,6 +221,41 @@ pub fn create(element: *Element, attr_name: lp.String, direction: Length.Directi }); } +pub fn createConfigured( + element: *Element, + attr_name: lp.String, + direction: Length.Direction, + default_value: f64, + default_unit: Length.Unit, + fallback_attr_name: ?lp.String, + frame: *Frame, +) !*AnimatedLength { + const base_val = try Length.reflectedConfigured( + element, + attr_name, + direction, + default_value, + default_unit, + fallback_attr_name, + false, + frame, + ); + const anim_val = try Length.reflectedConfigured( + element, + attr_name, + direction, + default_value, + default_unit, + fallback_attr_name, + true, + frame, + ); + return frame._factory.create(AnimatedLength{ + ._base_val = base_val, + ._anim_val = anim_val, + }); +} + pub fn getBaseVal(self: *AnimatedLength) *Length { return self._base_val; } diff --git a/src/browser/webapi/svg/AnimatedNumber.zig b/src/browser/webapi/svg/AnimatedNumber.zig index e96684772..7d0e97906 100644 --- a/src/browser/webapi/svg/AnimatedNumber.zig +++ b/src/browser/webapi/svg/AnimatedNumber.zig @@ -27,13 +27,23 @@ const AnimatedNumber = @This(); _element: *Element, _attr_name: lp.String, +_allow_percentage: bool, pub const Kind = enum { path_length, + offset, fn attributeName(self: Kind) lp.String { return switch (self) { .path_length => comptime .wrap("pathLength"), + .offset => comptime .wrap("offset"), + }; + } + + fn allowsPercentage(self: Kind) bool { + return switch (self) { + .path_length => false, + .offset => true, }; } }; @@ -53,6 +63,7 @@ pub fn getOrCreate(element: *Element, kind: Kind, frame: *Frame) !*AnimatedNumbe gop.value_ptr.* = try frame._factory.create(AnimatedNumber{ ._element = element, ._attr_name = kind.attributeName(), + ._allow_percentage = kind.allowsPercentage(), }); } return gop.value_ptr.*; @@ -77,7 +88,10 @@ pub fn getAnimVal(self: *const AnimatedNumber) f32 { fn currentValue(self: *const AnimatedNumber) f32 { const raw = self._element.getAttributeSafe(self._attr_name) orelse return 0; const trimmed = std.mem.trim(u8, raw, " \t\r\n\x0c"); - const value = std.fmt.parseFloat(f32, trimmed) catch return 0; + const value = if (self._allow_percentage and std.mem.endsWith(u8, trimmed, "%")) + (std.fmt.parseFloat(f32, trimmed[0 .. trimmed.len - 1]) catch return 0) / 100 + else + std.fmt.parseFloat(f32, trimmed) catch return 0; return if (std.math.isFinite(value)) value else 0; } diff --git a/src/browser/webapi/svg/AnimatedTransformList.zig b/src/browser/webapi/svg/AnimatedTransformList.zig index 0ec5670f6..04a26992d 100644 --- a/src/browser/webapi/svg/AnimatedTransformList.zig +++ b/src/browser/webapi/svg/AnimatedTransformList.zig @@ -6,6 +6,7 @@ // (at your option) any later version. const std = @import("std"); +const lp = @import("lightpanda"); const js = @import("../../js/js.zig"); const Frame = @import("../../Frame.zig"); @@ -18,20 +19,45 @@ const AnimatedTransformList = @This(); _base_val: *TransformList, _anim_val: *TransformList, -pub const Lookup = std.AutoHashMapUnmanaged(*Element, *AnimatedTransformList); +pub const Kind = enum { + transform, + gradient_transform, + pattern_transform, -pub fn getOrCreate(element: *Element, frame: *Frame) !*AnimatedTransformList { - const gop = try frame._svg_animated_transform_lists.getOrPut(frame.arena, element); + fn attributeName(self: Kind, frame: *Frame) !lp.String { + const name = switch (self) { + .transform => "transform", + .gradient_transform => "gradientTransform", + .pattern_transform => "patternTransform", + }; + return lp.String.init(frame.arena, name, .{ .dupe = false }); + } +}; + +pub const Key = struct { + element: *Element, + kind: Kind, +}; + +pub const Lookup = std.AutoHashMapUnmanaged(Key, *AnimatedTransformList); + +pub fn getOrCreate(element: *Element, kind: Kind, frame: *Frame) !*AnimatedTransformList { + const key: Key = .{ .element = element, .kind = kind }; + const gop = try frame._svg_animated_transform_lists.getOrPut(frame.arena, key); if (!gop.found_existing) { - errdefer _ = frame._svg_animated_transform_lists.remove(element); - gop.value_ptr.* = try create(element, frame); + errdefer _ = frame._svg_animated_transform_lists.remove(key); + gop.value_ptr.* = try createForAttribute(element, try kind.attributeName(frame), frame); } return gop.value_ptr.*; } pub fn create(element: *Element, frame: *Frame) !*AnimatedTransformList { - const base_val = try TransformList.create(element, false, frame); - const anim_val = try TransformList.create(element, true, frame); + return createForAttribute(element, comptime .wrap("transform"), frame); +} + +pub fn createForAttribute(element: *Element, attr_name: lp.String, frame: *Frame) !*AnimatedTransformList { + const base_val = try TransformList.createForAttribute(element, attr_name, false, frame); + const anim_val = try TransformList.createForAttribute(element, attr_name, true, frame); return frame._factory.create(AnimatedTransformList{ ._base_val = base_val, ._anim_val = anim_val, diff --git a/src/browser/webapi/svg/Length.zig b/src/browser/webapi/svg/Length.zig index 86274696d..84f94fc02 100644 --- a/src/browser/webapi/svg/Length.zig +++ b/src/browser/webapi/svg/Length.zig @@ -32,6 +32,9 @@ _element: ?*Element = null, _attr_name: String = .empty, _direction: Direction = .unspecified, _read_only: bool = false, +_default_value: f64 = 0, +_default_unit: Unit = .number, +_fallback_attr_name: ?String = null, pub const Direction = enum { horizontal, @@ -39,7 +42,7 @@ pub const Direction = enum { unspecified, }; -const Unit = enum(u16) { +pub const Unit = enum(u16) { unknown = 0, number = 1, percentage = 2, @@ -68,6 +71,28 @@ pub fn reflected(element: *Element, attr_name: String, direction: Direction, rea }); } +pub fn reflectedConfigured( + element: *Element, + attr_name: String, + direction: Direction, + default_value: f64, + default_unit: Unit, + fallback_attr_name: ?String, + read_only: bool, + frame: *Frame, +) !*Length { + try ensureFinite(default_value); + return frame._factory.create(Length{ + ._element = element, + ._attr_name = attr_name, + ._direction = direction, + ._read_only = read_only, + ._default_value = default_value, + ._default_unit = default_unit, + ._fallback_attr_name = fallback_attr_name, + }); +} + pub fn getUnitType(self: *Length) u16 { self.syncFromAttribute(); return @intFromEnum(self._unit); @@ -155,14 +180,17 @@ fn ensureFinite(value: f64) !void { fn syncFromAttribute(self: *Length) void { const element = self._element orelse return; - const raw = element.getAttributeSafe(self._attr_name) orelse { - self._value = 0; - self._unit = .number; + const raw = element.getAttributeSafe(self._attr_name) orelse raw: { + if (self._fallback_attr_name) |fallback_attr_name| { + if (element.getAttributeSafe(fallback_attr_name)) |fallback| break :raw fallback; + } + self._value = self._default_value; + self._unit = self._default_unit; return; }; const parsed = parse(raw) catch { - self._value = 0; - self._unit = .unknown; + self._value = self._default_value; + self._unit = self._default_unit; return; }; self._value = parsed.value; diff --git a/src/browser/webapi/svg/TransformList.zig b/src/browser/webapi/svg/TransformList.zig index 8baf90052..c882f0690 100644 --- a/src/browser/webapi/svg/TransformList.zig +++ b/src/browser/webapi/svg/TransformList.zig @@ -6,6 +6,7 @@ // (at your option) any later version. const std = @import("std"); +const lp = @import("lightpanda"); const js = @import("../../js/js.zig"); const Frame = @import("../../Frame.zig"); @@ -17,6 +18,7 @@ const Transform = @import("Transform.zig"); const TransformList = @This(); _frame: *Frame, +_attr_name: lp.String, _read_only: bool, _element: *Element, _synced: bool = false, @@ -25,9 +27,14 @@ _items: std.ArrayList(*Transform) = .empty, _retired: std.ArrayList(*Transform) = .empty, pub fn create(element: *Element, read_only: bool, frame: *Frame) !*TransformList { + return createForAttribute(element, comptime .wrap("transform"), read_only, frame); +} + +pub fn createForAttribute(element: *Element, attr_name: lp.String, read_only: bool, frame: *Frame) !*TransformList { return frame._factory.create(TransformList{ ._frame = frame, ._element = element, + ._attr_name = attr_name, ._read_only = read_only, }); } @@ -202,7 +209,7 @@ fn mutateTransform(context: *anyopaque, transform: *Transform, state: Transform. fn sync(self: *TransformList, frame: *Frame) !void { self.releaseRetired(frame._page); - const raw = self._element.getAttributeSafe(comptime .wrap("transform")) orelse ""; + const raw = self._element.getAttributeSafe(self._attr_name) orelse ""; if (self._synced and std.mem.eql(u8, self._snapshot.items, raw)) return; self._synced = false; @@ -285,7 +292,7 @@ fn setAttributeWithOverride(self: *TransformList, index: usize, state: Transform fn commitAttribute(self: *TransformList, serialized: []const u8, frame: *Frame) !void { self._synced = false; - try self._element.setAttributeSafe(comptime .wrap("transform"), .wrap(serialized), frame); + try self._element.setAttributeSafe(self._attr_name, .wrap(serialized), frame); self._snapshot.clearRetainingCapacity(); try self._snapshot.appendSlice(frame.arena, serialized); self._synced = true;