mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-07-30 09:16:07 -04:00
Merge pull request #3030 from mh0pe/codex/svg-03-collections
webapi: add live SVG collections
This commit is contained in:
@@ -42,9 +42,6 @@ const Event = @import("webapi/Event.zig");
|
||||
const EventTarget = @import("webapi/EventTarget.zig");
|
||||
const Element = @import("webapi/Element.zig");
|
||||
const HtmlElement = @import("webapi/element/Html.zig");
|
||||
const AnimatedLength = @import("webapi/svg/AnimatedLength.zig");
|
||||
const AnimatedPreserveAspectRatio = @import("webapi/svg/AnimatedPreserveAspectRatio.zig");
|
||||
const AnimatedString = @import("webapi/svg/AnimatedString.zig");
|
||||
const Window = @import("webapi/Window.zig");
|
||||
const Location = @import("webapi/Location.zig");
|
||||
const Document = @import("webapi/Document.zig");
|
||||
@@ -65,8 +62,15 @@ const popover = @import("webapi/element/popover.zig");
|
||||
const slotting = @import("webapi/element/slotting.zig");
|
||||
const NavigationKind = @import("webapi/navigation/root.zig").NavigationKind;
|
||||
|
||||
const HttpClient = @import("../network/HttpClient.zig");
|
||||
const PointList = @import("webapi/svg/PointList.zig");
|
||||
const StringList = @import("webapi/svg/StringList.zig");
|
||||
const AnimatedLength = @import("webapi/svg/AnimatedLength.zig");
|
||||
const AnimatedString = @import("webapi/svg/AnimatedString.zig");
|
||||
const AnimatedTransformList = @import("webapi/svg/AnimatedTransformList.zig");
|
||||
const AnimatedPreserveAspectRatio = @import("webapi/svg/AnimatedPreserveAspectRatio.zig");
|
||||
|
||||
const sys_url = @import("../sys/url.zig");
|
||||
const HttpClient = @import("../network/HttpClient.zig");
|
||||
|
||||
const timestamp = @import("../datetime.zig").timestamp;
|
||||
const milliTimestamp = @import("../datetime.zig").milliTimestamp;
|
||||
@@ -148,6 +152,9 @@ _element_namespace_uris: Element.NamespaceUriLookup = .empty,
|
||||
_svg_animated_lengths: AnimatedLength.Lookup = .empty,
|
||||
_svg_animated_preserve_aspect_ratios: AnimatedPreserveAspectRatio.Lookup = .empty,
|
||||
_svg_animated_strings: AnimatedString.Lookup = .empty,
|
||||
_svg_animated_transform_lists: AnimatedTransformList.Lookup = .empty,
|
||||
_svg_point_lists: PointList.Lookup = .empty,
|
||||
_svg_string_lists: StringList.Lookup = .empty,
|
||||
|
||||
// Same as above, but for Nodes (slot assigments apply to both Element AND
|
||||
// Text nodes)
|
||||
@@ -503,6 +510,16 @@ pub fn deinit(self: *Frame) void {
|
||||
|
||||
observers.deinit(self, page);
|
||||
|
||||
var svg_point_lists = self._svg_point_lists.valueIterator();
|
||||
while (svg_point_lists.next()) |list| {
|
||||
list.*.deinit(page);
|
||||
}
|
||||
|
||||
var svg_transform_lists = self._svg_animated_transform_lists.valueIterator();
|
||||
while (svg_transform_lists.next()) |list| {
|
||||
list.*.deinit(page);
|
||||
}
|
||||
|
||||
var document = self.window._document;
|
||||
document._selection.releaseRef(page);
|
||||
|
||||
|
||||
@@ -1076,6 +1076,10 @@ pub const PageJsApis = flattenTypes(&.{
|
||||
@import("../webapi/svg/AnimatedLength.zig"),
|
||||
@import("../webapi/svg/PreserveAspectRatio.zig"),
|
||||
@import("../webapi/svg/AnimatedPreserveAspectRatio.zig"),
|
||||
@import("../webapi/svg/PointList.zig"),
|
||||
@import("../webapi/svg/TransformList.zig"),
|
||||
@import("../webapi/svg/AnimatedTransformList.zig"),
|
||||
@import("../webapi/svg/StringList.zig"),
|
||||
@import("../webapi/encoding/TextDecoder.zig"),
|
||||
@import("../webapi/encoding/TextEncoder.zig"),
|
||||
@import("../webapi/encoding/TextEncoderStream.zig"),
|
||||
|
||||
720
src/browser/tests/element/svg/collections.html
Normal file
720
src/browser/tests/element/svg/collections.html
Normal file
@@ -0,0 +1,720 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../../testing.js"></script>
|
||||
|
||||
<svg id=root>
|
||||
<polygon id=polygon points="0,0 10,20"></polygon>
|
||||
<polyline id=polylineLive points="0,0 10,20"></polyline>
|
||||
<polyline id=polylineExternal points="0,0 10,20"></polyline>
|
||||
<polyline id=polylineInvalid points="0,0 10,20"></polyline>
|
||||
<polyline id=polylineIdentity points="1 2 3 4"></polyline>
|
||||
<polyline id=polylineMutators points="1 2 3 4"></polyline>
|
||||
<polyline id=polylineStale points="1 2 3 4"></polyline>
|
||||
<polyline id=polylineErrors points="1 2 3 4"></polyline>
|
||||
<polygon id=polygonReadOnly points="1 2 3 4"></polygon>
|
||||
<polyline id=many></polyline>
|
||||
|
||||
<g id=group transform="translate(10 20) rotate(45 1 2)"></g>
|
||||
<g id=transformLive transform="translate(10 20) rotate(45 1 2)"></g>
|
||||
<g id=transformExternal transform="translate(10 20)"></g>
|
||||
<g id=transformIdentity transform="translate(1 2)"></g>
|
||||
<g id=transformMutators transform="translate(1 2)"></g>
|
||||
<g id=transformStale transform="translate(10 20)"></g>
|
||||
<g id=transformConsolidate transform="translate(10 20) scale(2)"></g>
|
||||
<g id=transformConsolidateCommit transform="rotate(30 5 6) scale(2 3)"></g>
|
||||
<g id=matrixIs2D transform="translate(1 2)"></g>
|
||||
<g id=matrixStaleMatrix transform="translate(1 2)"></g>
|
||||
<g id=transformReadOnly transform="translate(1 2)"></g>
|
||||
<g id=transformParse transform="translate(1 2) scale(3,4) rotate(30 5 6)"></g>
|
||||
<g id=matrixGroup transform="translate(1 2)"></g>
|
||||
<g id=matrixReadOnly transform="translate(1 2)"></g>
|
||||
|
||||
<g id=strings requiredExtensions="urn:one urn:two" systemLanguage="en, fr"></g>
|
||||
<g id=stringMutators requiredExtensions="urn:one urn:two"></g>
|
||||
<g id=stringReturns requiredExtensions="urn:one urn:two"></g>
|
||||
<g id=stringClear requiredExtensions="urn:one urn:two"></g>
|
||||
<g id=stringLanguages systemLanguage="en, fr"></g>
|
||||
<g id=stringExternal systemLanguage="en, fr"></g>
|
||||
<g id=stringTokens systemLanguage="en, fr"></g>
|
||||
<g id=stringNoValidation requiredExtensions="urn:one"></g>
|
||||
</svg>
|
||||
|
||||
<!--
|
||||
These fail in Chrome, deliberately. SVG 2 builds the list interfaces over a
|
||||
list of real objects — getItem returns "a reference to that object and not a
|
||||
copy of it" — so two getItem calls hand back the same object. Firefox does
|
||||
this; Chrome creates a fresh tear-off per call and per mutator return, and
|
||||
honours identity only where the IDL says [SameObject] (SVGTransform.matrix,
|
||||
asserted in transformMatrixLive).
|
||||
|
||||
WPT sits this one out: every list test in svg/types/scripted compares .value
|
||||
rather than the objects. We follow the spec and Firefox. Kept in its own
|
||||
script so the rest of the file still runs in Chrome.
|
||||
-->
|
||||
<script id=pointListIdentity>
|
||||
{
|
||||
const root = $('#root');
|
||||
const polygon = $('#polygon');
|
||||
const points = polygon.points;
|
||||
|
||||
testing.expectTrue(points === polygon.points);
|
||||
testing.expectTrue(polygon.animatedPoints === polygon.animatedPoints);
|
||||
testing.expectFalse(points === polygon.animatedPoints);
|
||||
testing.expectTrue(points.getItem(0) === points.getItem(0));
|
||||
|
||||
// "The inserted item is the item itself and not a copy", so every mutator
|
||||
// hands back the object it was given.
|
||||
const list = $('#polylineIdentity').points;
|
||||
const appended = root.createSVGPoint();
|
||||
testing.expectEqual(appended, list.appendItem(appended));
|
||||
const inserted = root.createSVGPoint();
|
||||
testing.expectEqual(inserted, list.insertItemBefore(inserted, 0));
|
||||
const replacement = root.createSVGPoint();
|
||||
testing.expectEqual(replacement, list.replaceItem(replacement, 1));
|
||||
testing.expectEqual(replacement, list.removeItem(1));
|
||||
const initialized = root.createSVGPoint();
|
||||
testing.expectEqual(initialized, list.initialize(initialized));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=pointListBasics>
|
||||
{
|
||||
const polygon = $('#polygon');
|
||||
const points = polygon.points;
|
||||
|
||||
testing.expectTrue(points instanceof SVGPointList);
|
||||
testing.expectEqual(2, points.length);
|
||||
testing.expectEqual(2, points.numberOfItems);
|
||||
testing.expectEqual(0, points.getItem(0).x);
|
||||
testing.expectEqual(0, points.getItem(0).y);
|
||||
testing.expectEqual(10, points.getItem(1).x);
|
||||
testing.expectEqual(20, points.getItem(1).y);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=pointListLive>
|
||||
{
|
||||
const polyline = $('#polylineLive');
|
||||
const points = polyline.points;
|
||||
|
||||
points.getItem(0).x = 5;
|
||||
testing.expectEqual('5 0 10 20', polyline.getAttribute('points'));
|
||||
testing.expectEqual(5, polyline.animatedPoints.getItem(0).x);
|
||||
|
||||
points.getItem(1).y = -1;
|
||||
testing.expectEqual('5 0 10 -1', polyline.getAttribute('points'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=pointListExternalMutation>
|
||||
{
|
||||
const polyline = $('#polylineExternal');
|
||||
const points = polyline.points;
|
||||
|
||||
polyline.setAttribute('points', '1 2, 3 4');
|
||||
testing.expectEqual(2, points.length);
|
||||
testing.expectEqual(1, points.getItem(0).x);
|
||||
testing.expectEqual(2, points.getItem(0).y);
|
||||
testing.expectEqual(3, points.getItem(1).x);
|
||||
testing.expectEqual('1 2, 3 4', polyline.getAttribute('points'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=pointListInvalidAttribute>
|
||||
{
|
||||
const polyline = $('#polylineInvalid');
|
||||
const points = polyline.points;
|
||||
|
||||
// A malformed number invalidates the whole attribute...
|
||||
polyline.setAttribute('points', '0,0 100,0 INVALID');
|
||||
testing.expectEqual(0, points.length);
|
||||
testing.expectEqual('0,0 100,0 INVALID', polyline.getAttribute('points'));
|
||||
|
||||
// ...but a trailing coordinate with no pair only truncates it.
|
||||
polyline.setAttribute('points', '0,0 100,0 20');
|
||||
testing.expectEqual(2, points.length);
|
||||
polyline.setAttribute('points', '0,0 100,0 20,');
|
||||
testing.expectEqual(2, points.length);
|
||||
polyline.setAttribute('points', '1,2,3');
|
||||
testing.expectEqual(1, points.length);
|
||||
testing.expectEqual('1,2,3', polyline.getAttribute('points'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=pointListMutators>
|
||||
{
|
||||
const root = $('#root');
|
||||
const polyline = $('#polylineMutators');
|
||||
const points = polyline.points;
|
||||
|
||||
const detached = root.createSVGPoint();
|
||||
detached.x = 7;
|
||||
detached.y = 8;
|
||||
points.appendItem(detached);
|
||||
testing.expectEqual('1 2 3 4 7 8', polyline.getAttribute('points'));
|
||||
|
||||
// Already in a list, so this one gets copied.
|
||||
points.appendItem(detached);
|
||||
testing.expectEqual(4, points.length);
|
||||
detached.x = 9;
|
||||
testing.expectEqual(9, points.getItem(2).x);
|
||||
testing.expectEqual(7, points.getItem(3).x);
|
||||
|
||||
const removed = points.removeItem(2);
|
||||
testing.expectEqual(9, removed.x);
|
||||
testing.expectEqual('1 2 3 4 7 8', polyline.getAttribute('points'));
|
||||
|
||||
const inserted = root.createSVGPoint();
|
||||
inserted.x = 100;
|
||||
inserted.y = 200;
|
||||
points.insertItemBefore(inserted, 0);
|
||||
testing.expectEqual('100 200 1 2 3 4 7 8', polyline.getAttribute('points'));
|
||||
|
||||
const replacement = root.createSVGPoint();
|
||||
replacement.x = 5;
|
||||
replacement.y = 6;
|
||||
points.replaceItem(replacement, 1);
|
||||
testing.expectEqual('100 200 5 6 3 4 7 8', polyline.getAttribute('points'));
|
||||
|
||||
const initialized = root.createSVGPoint();
|
||||
initialized.x = 4;
|
||||
initialized.y = 5;
|
||||
points.initialize(initialized);
|
||||
testing.expectEqual('4 5', polyline.getAttribute('points'));
|
||||
|
||||
points.clear();
|
||||
testing.expectEqual('', polyline.getAttribute('points'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=pointListErrors>
|
||||
{
|
||||
const root = $('#root');
|
||||
const points = $('#polylineErrors').points;
|
||||
|
||||
testing.expectError('IndexSizeError', () => points.getItem(99));
|
||||
testing.expectError('IndexSizeError', () => points.replaceItem(root.createSVGPoint(), 99));
|
||||
testing.expectError('IndexSizeError', () => points.removeItem(99));
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- SVGPoint's coordinates are a restricted float, unlike DOMPoint's. -->
|
||||
<script id=pointCoordinatesAreRestricted>
|
||||
{
|
||||
const point = $('#root').createSVGPoint();
|
||||
point.x = 100;
|
||||
testing.expectError('TypeError', () => { point.x = NaN; });
|
||||
testing.expectError('TypeError', () => { point.x = Infinity; });
|
||||
testing.expectError('TypeError', () => { point.x = point; });
|
||||
testing.expectEqual(100, point.x);
|
||||
|
||||
point.y = null;
|
||||
testing.expectEqual(0, point.y);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=pointListReadOnly>
|
||||
{
|
||||
const root = $('#root');
|
||||
const polygon = $('#polygonReadOnly');
|
||||
const animated = polygon.animatedPoints;
|
||||
|
||||
testing.expectError('NoModificationAllowedError', () => animated.clear());
|
||||
testing.expectError('NoModificationAllowedError', () => animated.appendItem(root.createSVGPoint()));
|
||||
testing.expectError('NoModificationAllowedError', () => { animated.getItem(0).x = 4; });
|
||||
|
||||
// Read-only is sticky: an external attribute change drops the item from the
|
||||
// list, but it must not become a writable orphan.
|
||||
const stale = animated.getItem(0);
|
||||
polygon.setAttribute('points', '9 9');
|
||||
testing.expectEqual(1, animated.length);
|
||||
testing.expectError('NoModificationAllowedError', () => { stale.x = 4; });
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=pointListCapacity>
|
||||
{
|
||||
const root = $('#root');
|
||||
const points = $('#many').points;
|
||||
for (let i = 0; i < 70; i++) {
|
||||
const point = root.createSVGPoint();
|
||||
point.x = i;
|
||||
point.y = -i;
|
||||
points.appendItem(point);
|
||||
}
|
||||
testing.expectEqual(70, points.numberOfItems);
|
||||
testing.expectEqual(69, points.getItem(69).x);
|
||||
testing.expectEqual(-69, points.getItem(69).y);
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Fails in Chrome, passes in Firefox; see pointListIdentity. -->
|
||||
<script id=transformListIdentity>
|
||||
{
|
||||
const root = $('#root');
|
||||
const group = $('#group');
|
||||
const animated = group.transform;
|
||||
|
||||
testing.expectTrue(animated === group.transform);
|
||||
testing.expectTrue(animated.baseVal === animated.baseVal);
|
||||
testing.expectTrue(animated.animVal === animated.animVal);
|
||||
testing.expectFalse(animated.baseVal === animated.animVal);
|
||||
testing.expectTrue(animated.baseVal.getItem(1) === animated.baseVal.getItem(1));
|
||||
testing.expectTrue(animated.baseVal.getItem(1).matrix === animated.baseVal.getItem(1).matrix);
|
||||
|
||||
const list = $('#transformIdentity').transform.baseVal;
|
||||
const appended = root.createSVGTransform();
|
||||
testing.expectEqual(appended, list.appendItem(appended));
|
||||
const inserted = root.createSVGTransform();
|
||||
testing.expectEqual(inserted, list.insertItemBefore(inserted, 0));
|
||||
const replacement = root.createSVGTransform();
|
||||
testing.expectEqual(replacement, list.replaceItem(replacement, 1));
|
||||
testing.expectEqual(replacement, list.removeItem(1));
|
||||
const initialized = root.createSVGTransform();
|
||||
testing.expectEqual(initialized, list.initialize(initialized));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=transformListBasics>
|
||||
{
|
||||
const group = $('#group');
|
||||
const animated = group.transform;
|
||||
const base = animated.baseVal;
|
||||
|
||||
testing.expectTrue(animated instanceof SVGAnimatedTransformList);
|
||||
testing.expectTrue(base instanceof SVGTransformList);
|
||||
testing.expectEqual(2, base.numberOfItems);
|
||||
testing.expectEqual(2, base.length);
|
||||
testing.expectEqual(SVGTransform.SVG_TRANSFORM_TRANSLATE, base.getItem(0).type);
|
||||
testing.expectEqual(10, base.getItem(0).matrix.e);
|
||||
testing.expectEqual(20, base.getItem(0).matrix.f);
|
||||
testing.expectEqual(SVGTransform.SVG_TRANSFORM_ROTATE, base.getItem(1).type);
|
||||
testing.expectEqual(45, base.getItem(1).angle);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=transformListLive>
|
||||
{
|
||||
const group = $('#transformLive');
|
||||
const base = group.transform.baseVal;
|
||||
|
||||
base.getItem(0).setTranslate(3, 4);
|
||||
testing.expectEqual('translate(3 4)', group.getAttribute('transform').slice(0, 14));
|
||||
testing.expectEqual(3, group.transform.animVal.getItem(0).matrix.e);
|
||||
|
||||
base.getItem(1).setSkewX(15);
|
||||
testing.expectEqual(SVGTransform.SVG_TRANSFORM_SKEWX, base.getItem(1).type);
|
||||
testing.expectEqual('translate(3 4) skewX(15)', group.getAttribute('transform'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=transformListExternalMutation>
|
||||
{
|
||||
const group = $('#transformExternal');
|
||||
const base = group.transform.baseVal;
|
||||
|
||||
group.setAttribute('transform', 'scale(2)');
|
||||
testing.expectEqual(1, base.length);
|
||||
testing.expectEqual(SVGTransform.SVG_TRANSFORM_SCALE, base.getItem(0).type);
|
||||
testing.expectEqual(2, base.getItem(0).matrix.a);
|
||||
testing.expectEqual(2, base.getItem(0).matrix.d);
|
||||
}
|
||||
</script>
|
||||
|
||||
<!--
|
||||
All three engines differ on writing an item that an external attribute change
|
||||
has dropped from its list:
|
||||
|
||||
us the write lands on the orphan; the list and attribute are untouched
|
||||
Chrome lands on the orphan, but the list is re-committed anyway, so the
|
||||
attribute comes back reserialized ("scale(2)" -> "scale(2 2)")
|
||||
Firefox lands on the *list* at that index, because its tear-off proxies
|
||||
slot n rather than the object ("scale(2)" -> "translate(8, 9)")
|
||||
|
||||
SVG 2 reparses the attribute into a fresh set of objects, so the item that was
|
||||
removed is no longer in the list and a write to it should not reach it.
|
||||
-->
|
||||
<script id=staleItemWrites>
|
||||
{
|
||||
const group = $('#transformStale');
|
||||
const base = group.transform.baseVal;
|
||||
const stale = base.getItem(0);
|
||||
|
||||
group.setAttribute('transform', 'scale(2)');
|
||||
testing.expectEqual(1, base.length);
|
||||
stale.setTranslate(8, 9);
|
||||
testing.expectEqual('scale(2)', group.getAttribute('transform'));
|
||||
testing.expectEqual(SVGTransform.SVG_TRANSFORM_SCALE, base.getItem(0).type);
|
||||
testing.expectEqual(2, base.getItem(0).matrix.a);
|
||||
|
||||
const polyline = $('#polylineStale');
|
||||
const points = polyline.points;
|
||||
const stalePoint = points.getItem(0);
|
||||
polyline.setAttribute('points', '9 9');
|
||||
testing.expectEqual(1, points.length);
|
||||
stalePoint.x = 5;
|
||||
testing.expectEqual('9 9', polyline.getAttribute('points'));
|
||||
testing.expectEqual(9, points.getItem(0).x);
|
||||
|
||||
// A live matrix orphaned the same way keeps working as a plain matrix.
|
||||
const matrixGroup = $('#matrixStaleMatrix');
|
||||
const matrixBase = matrixGroup.transform.baseVal;
|
||||
const matrix = matrixBase.getItem(0).matrix;
|
||||
matrixGroup.setAttribute('transform', 'scale(4)');
|
||||
testing.expectEqual(1, matrixBase.length);
|
||||
matrix.e = 11;
|
||||
testing.expectEqual(11, matrix.e);
|
||||
testing.expectEqual(0, matrixBase.getItem(0).matrix.e);
|
||||
testing.expectEqual(4, matrixBase.getItem(0).matrix.a);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=transformListMutators>
|
||||
{
|
||||
const root = $('#root');
|
||||
const group = $('#transformMutators');
|
||||
const base = group.transform.baseVal;
|
||||
|
||||
const scale = root.createSVGTransform();
|
||||
scale.setScale(2, 3);
|
||||
base.appendItem(scale);
|
||||
testing.expectEqual('translate(1 2) scale(2 3)', group.getAttribute('transform'));
|
||||
|
||||
const removed = base.removeItem(0);
|
||||
testing.expectEqual(SVGTransform.SVG_TRANSFORM_TRANSLATE, removed.type);
|
||||
testing.expectEqual('scale(2 3)', group.getAttribute('transform'));
|
||||
|
||||
const rotate = root.createSVGTransform();
|
||||
rotate.setRotate(30, 5, 6);
|
||||
base.insertItemBefore(rotate, 0);
|
||||
testing.expectEqual('rotate(30 5 6) scale(2 3)', group.getAttribute('transform'));
|
||||
|
||||
const initialized = root.createSVGTransform();
|
||||
initialized.setTranslate(7, 8);
|
||||
base.initialize(initialized);
|
||||
testing.expectEqual('translate(7 8)', group.getAttribute('transform'));
|
||||
|
||||
base.clear();
|
||||
testing.expectEqual('', group.getAttribute('transform'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=transformListConsolidate>
|
||||
{
|
||||
const base = $('#transformConsolidate').transform.baseVal;
|
||||
|
||||
testing.expectEqual(2, base.numberOfItems);
|
||||
const consolidated = base.consolidate();
|
||||
testing.expectEqual(SVGTransform.SVG_TRANSFORM_MATRIX, consolidated.type);
|
||||
testing.expectEqual(1, base.numberOfItems);
|
||||
testing.expectEqual(2, consolidated.matrix.a);
|
||||
testing.expectEqual(2, consolidated.matrix.d);
|
||||
testing.expectEqual(10, consolidated.matrix.e);
|
||||
testing.expectEqual(20, consolidated.matrix.f);
|
||||
}
|
||||
</script>
|
||||
|
||||
<!--
|
||||
Chrome updates the list but leaves the attribute alone, so after consolidating
|
||||
to a single matrix the attribute still reads "rotate(...)" while the DOM says
|
||||
one SVG_TRANSFORM_MATRIX item. We commit, like every other mutator does: our
|
||||
list is rebuilt from the attribute on the next read, so not committing would
|
||||
undo the consolidate on the very next access.
|
||||
-->
|
||||
<script id=transformListConsolidateCommits>
|
||||
{
|
||||
const group = $('#transformConsolidateCommit');
|
||||
group.transform.baseVal.consolidate();
|
||||
testing.expectEqual('matrix(', group.getAttribute('transform').slice(0, 7));
|
||||
testing.expectEqual(1, group.transform.baseVal.numberOfItems);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=transformListReadOnly>
|
||||
{
|
||||
const root = $('#root');
|
||||
const group = $('#transformReadOnly');
|
||||
const anim = group.transform.animVal;
|
||||
|
||||
testing.expectError('NoModificationAllowedError', () => anim.clear());
|
||||
testing.expectError('NoModificationAllowedError', () => anim.appendItem(root.createSVGTransform()));
|
||||
testing.expectError('NoModificationAllowedError', () => anim.getItem(0).setTranslate(1, 1));
|
||||
|
||||
// Read-only is sticky, see pointListReadOnly.
|
||||
const stale = anim.getItem(0);
|
||||
group.setAttribute('transform', 'scale(3)');
|
||||
testing.expectEqual(1, anim.length);
|
||||
testing.expectError('NoModificationAllowedError', () => stale.setTranslate(1, 1));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=transformMatrixLive>
|
||||
{
|
||||
const group = $('#matrixGroup');
|
||||
const base = group.transform.baseVal;
|
||||
const anim = group.transform.animVal;
|
||||
const transform = base.getItem(0);
|
||||
const matrix = transform.matrix;
|
||||
|
||||
testing.expectTrue(matrix === transform.matrix);
|
||||
|
||||
matrix.e = 5;
|
||||
testing.expectEqual(SVGTransform.SVG_TRANSFORM_MATRIX, transform.type);
|
||||
testing.expectEqual(0, transform.angle);
|
||||
testing.expectEqual('matrix(1 0 0 1 5 2)', group.getAttribute('transform'));
|
||||
testing.expectEqual(5, anim.getItem(0).matrix.e);
|
||||
testing.expectEqual(1, base.length);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=transformMatrixReadOnly>
|
||||
{
|
||||
const group = $('#matrixReadOnly');
|
||||
const animMatrix = group.transform.animVal.getItem(0).matrix;
|
||||
|
||||
testing.expectError('NoModificationAllowedError', () => { animMatrix.e = 99; });
|
||||
testing.expectEqual('translate(1 2)', group.getAttribute('transform'));
|
||||
testing.expectEqual(1, animMatrix.e);
|
||||
}
|
||||
</script>
|
||||
|
||||
<!--
|
||||
SVG 2 types SVGTransform.matrix as a DOMMatrix (SVGMatrix is a typedef for it).
|
||||
Chrome still returns the legacy SVGMatrix, which has none of these members, so
|
||||
this block and the next one only run here.
|
||||
-->
|
||||
<script id=transformMatrixDomMatrixApi>
|
||||
{
|
||||
const root = $('#root');
|
||||
const transform = root.createSVGTransform();
|
||||
const matrix = transform.matrix;
|
||||
|
||||
testing.expectEqual(matrix, matrix.translateSelf(2, 3));
|
||||
testing.expectEqual(2, matrix.e);
|
||||
testing.expectEqual(3, matrix.f);
|
||||
testing.expectEqual(SVGTransform.SVG_TRANSFORM_MATRIX, transform.type);
|
||||
|
||||
testing.expectEqual(matrix, matrix.setMatrixValue('matrix(1, 0, 0, 1, 9, 10)'));
|
||||
testing.expectEqual(9, matrix.e);
|
||||
testing.expectEqual(10, matrix.f);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=transformMatrixIs2D>
|
||||
{
|
||||
const root = $('#root');
|
||||
const group = $('#matrixIs2D');
|
||||
const transform = root.createSVGTransform();
|
||||
const matrix = transform.matrix;
|
||||
|
||||
// An SVGTransform is a 2D affine transform and the `transform` attribute has
|
||||
// no syntax for anything else, so the 3D components are dropped.
|
||||
matrix.m13 = 2;
|
||||
testing.expectEqual(SVGTransform.SVG_TRANSFORM_MATRIX, transform.type);
|
||||
testing.expectTrue(matrix.is2D);
|
||||
testing.expectEqual(0, matrix.m13);
|
||||
|
||||
matrix.setMatrixValue('translate3d(1px, 2px, 3px)');
|
||||
testing.expectTrue(matrix.is2D);
|
||||
testing.expectEqual(1, matrix.e);
|
||||
testing.expectEqual(2, matrix.f);
|
||||
testing.expectEqual(0, matrix.m43);
|
||||
|
||||
group.transform.baseVal.initialize(transform);
|
||||
testing.expectEqual('matrix(1 0 0 1 1 2)', group.getAttribute('transform'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=transformParser>
|
||||
{
|
||||
const group = $('#transformParse');
|
||||
const base = group.transform.baseVal;
|
||||
|
||||
testing.expectEqual(3, base.length);
|
||||
testing.expectEqual(2, base.getItem(0).matrix.f);
|
||||
testing.expectEqual(4, base.getItem(1).matrix.d);
|
||||
testing.expectEqual(30, base.getItem(2).angle);
|
||||
|
||||
group.setAttribute('transform', 'translate()');
|
||||
testing.expectEqual(0, base.length);
|
||||
testing.expectEqual('translate()', group.getAttribute('transform'));
|
||||
|
||||
// No 3D function is part of the SVG transform grammar.
|
||||
group.setAttribute('transform', 'matrix3d(1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1)');
|
||||
testing.expectEqual(0, base.length);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=domMatrixTransformString>
|
||||
{
|
||||
testing.expectError('SyntaxError', () => new DOMMatrix('translate()'));
|
||||
testing.expectError('SyntaxError', () => new DOMMatrix('scale()'));
|
||||
|
||||
const matrix = new DOMMatrix('translate(3px, 4px) scale(2, 3)');
|
||||
testing.expectEqual(3, matrix.e);
|
||||
testing.expectEqual(4, matrix.f);
|
||||
testing.expectEqual(2, matrix.a);
|
||||
}
|
||||
</script>
|
||||
|
||||
<!--
|
||||
The SVG grammar's separators are not CSS syntax: a DOMMatrix string wants
|
||||
commas between arguments, even though `transform="translate(3 4)"` does not.
|
||||
-->
|
||||
<script id=domMatrixRejectsSvgSeparators>
|
||||
{
|
||||
testing.expectError('SyntaxError', () => new DOMMatrix('translate(3px 4px) scale(2 3)'));
|
||||
testing.expectError('SyntaxError', () => new DOMMatrix('translate(3 4)'));
|
||||
testing.expectError('SyntaxError', () => new DOMMatrix('matrix(1 0 0 1 5 6)'));
|
||||
testing.expectError('SyntaxError', () => new DOMMatrix('translate(3px-4px)'));
|
||||
|
||||
const matrix = new DOMMatrix('translate(3px, 4px) scale(2, 3)');
|
||||
testing.expectEqual(3, matrix.e);
|
||||
testing.expectEqual(2, matrix.a);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=stringListBasics>
|
||||
{
|
||||
const group = $('#strings');
|
||||
const required = group.requiredExtensions;
|
||||
const languages = group.systemLanguage;
|
||||
|
||||
testing.expectTrue(required instanceof SVGStringList);
|
||||
testing.expectTrue(required === group.requiredExtensions);
|
||||
testing.expectTrue(languages === group.systemLanguage);
|
||||
testing.expectEqual(2, required.numberOfItems);
|
||||
testing.expectEqual('urn:one', required.getItem(0));
|
||||
testing.expectEqual('urn:two', required.getItem(1));
|
||||
testing.expectEqual(2, languages.length);
|
||||
testing.expectEqual('en', languages.getItem(0));
|
||||
testing.expectEqual('fr', languages.getItem(1));
|
||||
testing.expectError('IndexSizeError', () => required.getItem(99));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=stringListMutators>
|
||||
{
|
||||
const group = $('#stringMutators');
|
||||
const required = group.requiredExtensions;
|
||||
|
||||
required.appendItem('urn:three');
|
||||
testing.expectEqual('urn:one urn:two urn:three', group.getAttribute('requiredExtensions'));
|
||||
required.removeItem(1);
|
||||
testing.expectEqual('urn:one urn:three', group.getAttribute('requiredExtensions'));
|
||||
required.insertItemBefore('urn:zero', 0);
|
||||
testing.expectEqual('urn:zero urn:one urn:three', group.getAttribute('requiredExtensions'));
|
||||
required.replaceItem('urn:x', 1);
|
||||
testing.expectEqual('urn:zero urn:x urn:three', group.getAttribute('requiredExtensions'));
|
||||
required.initialize('urn:only');
|
||||
testing.expectEqual('urn:only', group.getAttribute('requiredExtensions'));
|
||||
|
||||
required.clear();
|
||||
testing.expectEqual(0, required.length);
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Emptying a string list removes the attribute in Chrome but blanks it in
|
||||
Firefox, which blanks it for point and transform lists too. Chrome is the
|
||||
inconsistent one here; we follow it. -->
|
||||
<script id=stringListClearRemovesAttribute>
|
||||
{
|
||||
const group = $('#stringClear');
|
||||
group.requiredExtensions.clear();
|
||||
testing.expectEqual(null, group.getAttribute('requiredExtensions'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Firefox returns "" from SVGStringList.removeItem; Chrome and the spec
|
||||
return the removed item. -->
|
||||
<script id=stringListMutatorReturns>
|
||||
{
|
||||
const required = $('#stringReturns').requiredExtensions;
|
||||
|
||||
testing.expectEqual('urn:three', required.appendItem('urn:three'));
|
||||
testing.expectEqual('urn:two', required.removeItem(1));
|
||||
testing.expectEqual('urn:zero', required.insertItemBefore('urn:zero', 0));
|
||||
testing.expectEqual('urn:x', required.replaceItem('urn:x', 1));
|
||||
testing.expectEqual('urn:only', required.initialize('urn:only'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=stringListDelimiter>
|
||||
{
|
||||
const group = $('#stringLanguages');
|
||||
const languages = group.systemLanguage;
|
||||
|
||||
languages.insertItemBefore('de', 1);
|
||||
testing.expectEqual('en,de,fr', group.getAttribute('systemLanguage'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<!--
|
||||
systemLanguage is a set of comma-separated tokens: every segment is a token,
|
||||
including an empty one, and each is trimmed. Mirrors the cases in
|
||||
wpt/svg/struct/systemLanguage-parsing.html, which we can't run yet because
|
||||
<text> is not an SVGGraphicsElement here.
|
||||
-->
|
||||
<script id=stringListCommaTokens>
|
||||
{
|
||||
const group = $('#stringTokens');
|
||||
const languages = group.systemLanguage;
|
||||
|
||||
const parsesAs = (input, expected) => {
|
||||
group.setAttribute('systemLanguage', input);
|
||||
testing.expectEqual(expected.length, languages.length);
|
||||
for (let i = 0; i < expected.length; i++) {
|
||||
testing.expectEqual(expected[i], languages.getItem(i));
|
||||
}
|
||||
};
|
||||
|
||||
parsesAs('en,fr,de', ['en', 'fr', 'de']);
|
||||
parsesAs('en , fr , de', ['en', 'fr', 'de']);
|
||||
parsesAs(' en, fr ', ['en', 'fr']);
|
||||
parsesAs(' \t\nen, fr\t\n ', ['en', 'fr']);
|
||||
parsesAs('en', ['en']);
|
||||
parsesAs('en-US, zh-Hans', ['en-US', 'zh-Hans']);
|
||||
parsesAs('en,,fr', ['en', '', 'fr']);
|
||||
parsesAs('', ['']);
|
||||
parsesAs(',', ['', '']);
|
||||
parsesAs('not-a-lang, ???', ['not-a-lang', '???']);
|
||||
|
||||
// An absent attribute is an empty list, unlike an empty one.
|
||||
group.removeAttribute('systemLanguage');
|
||||
testing.expectEqual(0, languages.length);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=stringListExternalMutation>
|
||||
{
|
||||
const group = $('#stringExternal');
|
||||
const languages = group.systemLanguage;
|
||||
|
||||
group.setAttribute('systemLanguage', 'ja, ko');
|
||||
testing.expectEqual(2, languages.length);
|
||||
testing.expectEqual('ko', languages.getItem(1));
|
||||
|
||||
group.setAttribute('systemLanguage', 'it, pt');
|
||||
testing.expectEqual(2, languages.length);
|
||||
testing.expectEqual('pt', languages.getItem(1));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=stringListNoValidation>
|
||||
{
|
||||
const group = $('#stringNoValidation');
|
||||
const required = group.requiredExtensions;
|
||||
|
||||
// Neither the spec nor Chrome validates the inserted string against the
|
||||
// list's delimiter, and the list stays authoritative over its own
|
||||
// serialization, so this remains one item.
|
||||
required.appendItem('two words');
|
||||
testing.expectEqual('urn:one two words', group.getAttribute('requiredExtensions'));
|
||||
testing.expectEqual(2, required.length);
|
||||
testing.expectEqual('two words', required.getItem(1));
|
||||
|
||||
// Until the attribute is written from the outside, which reparses.
|
||||
group.setAttribute('requiredExtensions', 'urn:one three more words');
|
||||
testing.expectEqual(4, required.length);
|
||||
testing.expectEqual('three', required.getItem(1));
|
||||
}
|
||||
</script>
|
||||
@@ -59,6 +59,11 @@
|
||||
const loc_pathname = loc.pathname;
|
||||
const loc_to_string = String(loc);
|
||||
|
||||
// DOMPoint mutation (setters must not require a Frame in a worker)
|
||||
const point = new DOMPoint(1, 2);
|
||||
point.x = 5;
|
||||
point.w = 0.5;
|
||||
|
||||
postMessage({
|
||||
ok: true,
|
||||
results: {
|
||||
@@ -90,6 +95,8 @@
|
||||
loc_protocol,
|
||||
loc_pathname,
|
||||
loc_to_string,
|
||||
point_x: point.x,
|
||||
point_w: point.w,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
|
||||
@@ -247,6 +247,10 @@
|
||||
testing.expectEqual(r.loc_href, r.loc_to_string);
|
||||
testing.expectEqual('http:', r.loc_protocol);
|
||||
testing.expectTrue(r.loc_pathname.endsWith('api-worker.js'));
|
||||
|
||||
// DOMPoint
|
||||
testing.expectEqual(5, r.point_x);
|
||||
testing.expectEqual(0.5, r.point_w);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -84,137 +84,158 @@ pub fn getF(self: *const DOMMatrix) f64 {
|
||||
return self._proto._m[13];
|
||||
}
|
||||
|
||||
pub fn setA(self: *DOMMatrix, v: f64) void {
|
||||
self._proto._m[0] = v;
|
||||
pub fn setA(self: *DOMMatrix, v: f64) !void {
|
||||
try self.setElement(0, v);
|
||||
}
|
||||
pub fn setB(self: *DOMMatrix, v: f64) void {
|
||||
self._proto._m[1] = v;
|
||||
pub fn setB(self: *DOMMatrix, v: f64) !void {
|
||||
try self.setElement(1, v);
|
||||
}
|
||||
pub fn setC(self: *DOMMatrix, v: f64) void {
|
||||
self._proto._m[4] = v;
|
||||
pub fn setC(self: *DOMMatrix, v: f64) !void {
|
||||
try self.setElement(4, v);
|
||||
}
|
||||
pub fn setD(self: *DOMMatrix, v: f64) void {
|
||||
self._proto._m[5] = v;
|
||||
pub fn setD(self: *DOMMatrix, v: f64) !void {
|
||||
try self.setElement(5, v);
|
||||
}
|
||||
pub fn setE(self: *DOMMatrix, v: f64) void {
|
||||
self._proto._m[12] = v;
|
||||
pub fn setE(self: *DOMMatrix, v: f64) !void {
|
||||
try self.setElement(12, v);
|
||||
}
|
||||
pub fn setF(self: *DOMMatrix, v: f64) void {
|
||||
self._proto._m[13] = v;
|
||||
pub fn setF(self: *DOMMatrix, v: f64) !void {
|
||||
try self.setElement(13, v);
|
||||
}
|
||||
|
||||
pub fn translateSelf(self: *DOMMatrix, tx_: ?f64, ty_: ?f64, tz_: ?f64) *DOMMatrix {
|
||||
pub fn translateSelf(self: *DOMMatrix, tx_: ?f64, ty_: ?f64, tz_: ?f64) !*DOMMatrix {
|
||||
const tz = tz_ orelse 0;
|
||||
const p = self._proto;
|
||||
p._m = RO.multiplyMatrix(p._m, RO.translationMatrix(tx_ orelse 0, ty_ orelse 0, tz));
|
||||
if (tz != 0) p._is_2d = false;
|
||||
return self;
|
||||
var state = self._proto.getState();
|
||||
state.matrix = RO.multiplyMatrix(state.matrix, RO.translationMatrix(tx_ orelse 0, ty_ orelse 0, tz));
|
||||
if (tz != 0) state.is_2d = false;
|
||||
return self.applyState(state);
|
||||
}
|
||||
|
||||
pub fn scaleSelf(self: *DOMMatrix, sx_: ?f64, sy_: ?f64, sz_: ?f64, ox_: ?f64, oy_: ?f64, oz_: ?f64) *DOMMatrix {
|
||||
pub fn scaleSelf(self: *DOMMatrix, sx_: ?f64, sy_: ?f64, sz_: ?f64, ox_: ?f64, oy_: ?f64, oz_: ?f64) !*DOMMatrix {
|
||||
const sx = sx_ orelse 1;
|
||||
const sy = sy_ orelse sx;
|
||||
const sz = sz_ orelse 1;
|
||||
const ox = ox_ orelse 0;
|
||||
const oy = oy_ orelse 0;
|
||||
const oz = oz_ orelse 0;
|
||||
const p = self._proto;
|
||||
var m = RO.multiplyMatrix(p._m, RO.translationMatrix(ox, oy, oz));
|
||||
var state = self._proto.getState();
|
||||
var m = RO.multiplyMatrix(state.matrix, RO.translationMatrix(ox, oy, oz));
|
||||
m = RO.multiplyMatrix(m, RO.scaleMatrix(sx, sy, sz));
|
||||
m = RO.multiplyMatrix(m, RO.translationMatrix(-ox, -oy, -oz));
|
||||
p._m = m;
|
||||
if (sz != 1 or oz != 0) p._is_2d = false;
|
||||
return self;
|
||||
state.matrix = m;
|
||||
if (sz != 1 or oz != 0) state.is_2d = false;
|
||||
return self.applyState(state);
|
||||
}
|
||||
|
||||
pub fn scale3dSelf(self: *DOMMatrix, scale_: ?f64, ox_: ?f64, oy_: ?f64, oz_: ?f64) *DOMMatrix {
|
||||
pub fn scale3dSelf(self: *DOMMatrix, scale_: ?f64, ox_: ?f64, oy_: ?f64, oz_: ?f64) !*DOMMatrix {
|
||||
const s = scale_ orelse 1;
|
||||
const ox = ox_ orelse 0;
|
||||
const oy = oy_ orelse 0;
|
||||
const oz = oz_ orelse 0;
|
||||
const p = self._proto;
|
||||
var m = RO.multiplyMatrix(p._m, RO.translationMatrix(ox, oy, oz));
|
||||
var state = self._proto.getState();
|
||||
var m = RO.multiplyMatrix(state.matrix, RO.translationMatrix(ox, oy, oz));
|
||||
m = RO.multiplyMatrix(m, RO.scaleMatrix(s, s, s));
|
||||
m = RO.multiplyMatrix(m, RO.translationMatrix(-ox, -oy, -oz));
|
||||
p._m = m;
|
||||
if (s != 1) p._is_2d = false;
|
||||
return self;
|
||||
state.matrix = m;
|
||||
if (s != 1) state.is_2d = false;
|
||||
return self.applyState(state);
|
||||
}
|
||||
|
||||
pub fn rotateSelf(self: *DOMMatrix, rx_: ?f64, ry_: ?f64, rz_: ?f64) *DOMMatrix {
|
||||
const p = self._proto;
|
||||
pub fn rotateSelf(self: *DOMMatrix, rx_: ?f64, ry_: ?f64, rz_: ?f64) !*DOMMatrix {
|
||||
var state = self._proto.getState();
|
||||
if (ry_ == null and rz_ == null) {
|
||||
p._m = RO.multiplyMatrix(p._m, RO.rotateZMatrix(RO.toRadians(rx_ orelse 0, .deg)));
|
||||
state.matrix = RO.multiplyMatrix(state.matrix, RO.rotateZMatrix(RO.toRadians(rx_ orelse 0, .deg)));
|
||||
} else {
|
||||
p._m = RO.multiplyMatrix(p._m, RO.rotateXMatrix(RO.toRadians(rx_ orelse 0, .deg)));
|
||||
p._m = RO.multiplyMatrix(p._m, RO.rotateYMatrix(RO.toRadians(ry_ orelse 0, .deg)));
|
||||
p._m = RO.multiplyMatrix(p._m, RO.rotateZMatrix(RO.toRadians(rz_ orelse 0, .deg)));
|
||||
p._is_2d = false;
|
||||
state.matrix = RO.multiplyMatrix(state.matrix, RO.rotateXMatrix(RO.toRadians(rx_ orelse 0, .deg)));
|
||||
state.matrix = RO.multiplyMatrix(state.matrix, RO.rotateYMatrix(RO.toRadians(ry_ orelse 0, .deg)));
|
||||
state.matrix = RO.multiplyMatrix(state.matrix, RO.rotateZMatrix(RO.toRadians(rz_ orelse 0, .deg)));
|
||||
state.is_2d = false;
|
||||
}
|
||||
return self;
|
||||
return self.applyState(state);
|
||||
}
|
||||
|
||||
pub fn rotateFromVectorSelf(self: *DOMMatrix, x_: ?f64, y_: ?f64) *DOMMatrix {
|
||||
pub fn rotateFromVectorSelf(self: *DOMMatrix, x_: ?f64, y_: ?f64) !*DOMMatrix {
|
||||
const x = x_ orelse 0;
|
||||
const y = y_ orelse 0;
|
||||
const rad = if (x == 0 and y == 0) 0 else std.math.atan2(y, x);
|
||||
const p = self._proto;
|
||||
p._m = RO.multiplyMatrix(p._m, RO.rotateZMatrix(rad));
|
||||
return self;
|
||||
var state = self._proto.getState();
|
||||
state.matrix = RO.multiplyMatrix(state.matrix, RO.rotateZMatrix(rad));
|
||||
return self.applyState(state);
|
||||
}
|
||||
|
||||
pub fn rotateAxisAngleSelf(self: *DOMMatrix, x_: ?f64, y_: ?f64, z_: ?f64, angle_: ?f64) *DOMMatrix {
|
||||
const p = self._proto;
|
||||
p._m = RO.multiplyMatrix(p._m, RO.axisAngleMatrix(x_ orelse 0, y_ orelse 0, z_ orelse 0, RO.toRadians(angle_ orelse 0, .deg)));
|
||||
if ((x_ orelse 0) != 0 or (y_ orelse 0) != 0) p._is_2d = false;
|
||||
return self;
|
||||
pub fn rotateAxisAngleSelf(self: *DOMMatrix, x_: ?f64, y_: ?f64, z_: ?f64, angle_: ?f64) !*DOMMatrix {
|
||||
var state = self._proto.getState();
|
||||
state.matrix = RO.multiplyMatrix(state.matrix, RO.axisAngleMatrix(x_ orelse 0, y_ orelse 0, z_ orelse 0, RO.toRadians(angle_ orelse 0, .deg)));
|
||||
if ((x_ orelse 0) != 0 or (y_ orelse 0) != 0) state.is_2d = false;
|
||||
return self.applyState(state);
|
||||
}
|
||||
|
||||
pub fn skewXSelf(self: *DOMMatrix, sx_: ?f64) *DOMMatrix {
|
||||
const p = self._proto;
|
||||
p._m = RO.multiplyMatrix(p._m, RO.skewMatrix(RO.toRadians(sx_ orelse 0, .deg), 0));
|
||||
return self;
|
||||
pub fn skewXSelf(self: *DOMMatrix, sx_: ?f64) !*DOMMatrix {
|
||||
var state = self._proto.getState();
|
||||
state.matrix = RO.multiplyMatrix(state.matrix, RO.skewMatrix(RO.toRadians(sx_ orelse 0, .deg), 0));
|
||||
return self.applyState(state);
|
||||
}
|
||||
|
||||
pub fn skewYSelf(self: *DOMMatrix, sy_: ?f64) *DOMMatrix {
|
||||
const p = self._proto;
|
||||
p._m = RO.multiplyMatrix(p._m, RO.skewMatrix(0, RO.toRadians(sy_ orelse 0, .deg)));
|
||||
return self;
|
||||
pub fn skewYSelf(self: *DOMMatrix, sy_: ?f64) !*DOMMatrix {
|
||||
var state = self._proto.getState();
|
||||
state.matrix = RO.multiplyMatrix(state.matrix, RO.skewMatrix(0, RO.toRadians(sy_ orelse 0, .deg)));
|
||||
return self.applyState(state);
|
||||
}
|
||||
|
||||
pub fn multiplySelf(self: *DOMMatrix, other_: ?RO.DOMMatrixInit) !*DOMMatrix {
|
||||
const p = self._proto;
|
||||
const other = try RO.fixupDict(other_ orelse .{});
|
||||
p._m = RO.multiplyMatrix(p._m, other.m);
|
||||
p._is_2d = p._is_2d and other.is_2d;
|
||||
return self;
|
||||
var state = self._proto.getState();
|
||||
state.matrix = RO.multiplyMatrix(state.matrix, other.m);
|
||||
state.is_2d = state.is_2d and other.is_2d;
|
||||
return self.applyState(state);
|
||||
}
|
||||
|
||||
pub fn preMultiplySelf(self: *DOMMatrix, other_: ?RO.DOMMatrixInit) !*DOMMatrix {
|
||||
const p = self._proto;
|
||||
const other = try RO.fixupDict(other_ orelse .{});
|
||||
p._m = RO.multiplyMatrix(other.m, p._m);
|
||||
p._is_2d = p._is_2d and other.is_2d;
|
||||
return self;
|
||||
var state = self._proto.getState();
|
||||
state.matrix = RO.multiplyMatrix(other.m, state.matrix);
|
||||
state.is_2d = state.is_2d and other.is_2d;
|
||||
return self.applyState(state);
|
||||
}
|
||||
|
||||
pub fn invertSelf(self: *DOMMatrix) *DOMMatrix {
|
||||
const p = self._proto;
|
||||
if (RO.invertMatrix(p._m)) |v| {
|
||||
p._m = v;
|
||||
pub fn invertSelf(self: *DOMMatrix) !*DOMMatrix {
|
||||
var state = self._proto.getState();
|
||||
if (RO.invertMatrix(state.matrix)) |v| {
|
||||
state.matrix = v;
|
||||
} else {
|
||||
p._m = .{std.math.nan(f64)} ** 16;
|
||||
p._is_2d = false;
|
||||
state.matrix = .{std.math.nan(f64)} ** 16;
|
||||
state.is_2d = false;
|
||||
}
|
||||
return self;
|
||||
return self.applyState(state);
|
||||
}
|
||||
|
||||
pub fn setMatrixValue(self: *DOMMatrix, transform: []const u8) !*DOMMatrix {
|
||||
var m = RO.identity();
|
||||
var is_2d = true;
|
||||
try RO.parseTransformList(transform, &m, &is_2d);
|
||||
self._proto._m = m;
|
||||
self._proto._is_2d = is_2d;
|
||||
var state: RO.State = .{ .matrix = RO.identity(), .is_2d = true };
|
||||
try RO.parseTransformList(transform, &state.matrix, &state.is_2d);
|
||||
try self._proto.applyState(state);
|
||||
return self;
|
||||
}
|
||||
|
||||
fn setElement(self: *DOMMatrix, comptime index: usize, value: f64) !void {
|
||||
var state = self._proto.getState();
|
||||
state.matrix[index] = value;
|
||||
|
||||
// Assigning a z/w element a value other than its identity drops the
|
||||
// 2D flag. Setting it back does not restore is2D.
|
||||
switch (index) {
|
||||
2, 3, 6, 7, 8, 9, 11, 14 => if (value != 0) {
|
||||
state.is_2d = false;
|
||||
},
|
||||
10, 15 => if (value != 1) {
|
||||
state.is_2d = false;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
try self._proto.applyState(state);
|
||||
}
|
||||
|
||||
fn applyState(self: *DOMMatrix, state: RO.State) !*DOMMatrix {
|
||||
try self._proto.applyState(state);
|
||||
return self;
|
||||
}
|
||||
|
||||
@@ -281,23 +302,10 @@ pub const JsApi = struct {
|
||||
}.get;
|
||||
}
|
||||
|
||||
fn setM(comptime idx: usize) fn (*DOMMatrix, f64) void {
|
||||
fn setM(comptime idx: usize) fn (*DOMMatrix, f64) anyerror!void {
|
||||
return struct {
|
||||
fn set(self: *DOMMatrix, v: f64) void {
|
||||
self._proto._m[idx] = v;
|
||||
// Assigning a z/w element a value other than its identity drops the
|
||||
// 2D flag. Setting it back to the identity value (0 for the
|
||||
// off-diagonal elements, 1 for m33/m44) preserves is2D. Note `-0`
|
||||
// compares equal to `0`, so it preserves it too, per spec.
|
||||
switch (idx) {
|
||||
2, 3, 6, 7, 8, 9, 11, 14 => if (v != 0) {
|
||||
self._proto._is_2d = false;
|
||||
},
|
||||
10, 15 => if (v != 1) {
|
||||
self._proto._is_2d = false;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
fn set(self: *DOMMatrix, v: f64) !void {
|
||||
try self.setElement(idx, v);
|
||||
}
|
||||
}.set;
|
||||
}
|
||||
|
||||
@@ -44,12 +44,25 @@ _arena: Allocator,
|
||||
// out[row] = sum_col _m[col*4 + row] * in[col]
|
||||
_m: [16]f64,
|
||||
_is_2d: bool,
|
||||
_attachment: ?Attachment = null,
|
||||
|
||||
pub const Type = union(enum) {
|
||||
generic,
|
||||
mutable: *DOMMatrix,
|
||||
};
|
||||
|
||||
pub const State = struct {
|
||||
matrix: [16]f64,
|
||||
is_2d: bool,
|
||||
};
|
||||
|
||||
// The owner decides whether a write is allowed; a matrix that belongs to an
|
||||
// animVal SVGTransform is rejected by the transform, not here.
|
||||
pub const Attachment = struct {
|
||||
owner: *anyopaque,
|
||||
mutate: *const fn (*anyopaque, *DOMMatrixReadOnly, State) anyerror!void,
|
||||
};
|
||||
|
||||
pub fn init(init_: ?js.Value, exec: *const js.Execution) !*DOMMatrixReadOnly {
|
||||
const parsed = try Parsed.init(init_, exec);
|
||||
return createBare(parsed.m, parsed.is_2d, exec.page);
|
||||
@@ -82,6 +95,29 @@ pub fn createBare(m: [16]f64, is_2d: bool, page: *Page) !*DOMMatrixReadOnly {
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn getState(self: *const DOMMatrixReadOnly) State {
|
||||
return .{
|
||||
.matrix = self._m,
|
||||
.is_2d = self._is_2d,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn applyState(self: *DOMMatrixReadOnly, state: State) !void {
|
||||
if (self._attachment) |attachment| {
|
||||
return attachment.mutate(attachment.owner, self, state);
|
||||
}
|
||||
self.applyStateRaw(state);
|
||||
}
|
||||
|
||||
pub fn applyStateRaw(self: *DOMMatrixReadOnly, state: State) void {
|
||||
self._m = state.matrix;
|
||||
self._is_2d = state.is_2d;
|
||||
}
|
||||
|
||||
pub fn attach(self: *DOMMatrixReadOnly, attachment: Attachment) void {
|
||||
self._attachment = attachment;
|
||||
}
|
||||
|
||||
pub const DOMMatrixInit = struct {
|
||||
a: ?f64 = null,
|
||||
b: ?f64 = null,
|
||||
@@ -312,178 +348,274 @@ pub fn invertMatrix(m: [16]f64) ?[16]f64 {
|
||||
return out;
|
||||
}
|
||||
|
||||
pub const TransformSyntax = enum { css, svg };
|
||||
|
||||
pub const TransformKind = enum {
|
||||
matrix,
|
||||
matrix3d,
|
||||
translate,
|
||||
translate_x,
|
||||
translate_y,
|
||||
translate_z,
|
||||
translate_3d,
|
||||
scale,
|
||||
scale_x,
|
||||
scale_y,
|
||||
scale_z,
|
||||
scale_3d,
|
||||
rotate,
|
||||
rotate_x,
|
||||
rotate_y,
|
||||
rotate_z,
|
||||
rotate_3d,
|
||||
skew,
|
||||
skew_x,
|
||||
skew_y,
|
||||
perspective,
|
||||
};
|
||||
|
||||
pub const ParsedTransform = struct {
|
||||
kind: TransformKind,
|
||||
matrix: [16]f64,
|
||||
values: [16]f64,
|
||||
count: usize,
|
||||
is_2d: bool,
|
||||
};
|
||||
|
||||
pub const TransformFunction = struct {
|
||||
name: []const u8,
|
||||
arguments: []const u8,
|
||||
};
|
||||
|
||||
pub const TransformFunctionIterator = struct {
|
||||
input: []const u8,
|
||||
index: usize = 0,
|
||||
allow_comma: bool = false,
|
||||
|
||||
pub fn next(self: *TransformFunctionIterator) !?TransformFunction {
|
||||
while (self.index < self.input.len and std.ascii.isWhitespace(self.input[self.index])) self.index += 1;
|
||||
if (self.index == self.input.len) return null;
|
||||
if (self.input[self.index] == ',') return error.SyntaxError;
|
||||
|
||||
const name_start = self.index;
|
||||
while (self.index < self.input.len and
|
||||
(std.ascii.isAlphabetic(self.input[self.index]) or std.ascii.isDigit(self.input[self.index])))
|
||||
{
|
||||
self.index += 1;
|
||||
}
|
||||
if (self.index == name_start or self.index >= self.input.len or self.input[self.index] != '(') {
|
||||
return error.SyntaxError;
|
||||
}
|
||||
const name = self.input[name_start..self.index];
|
||||
self.index += 1;
|
||||
|
||||
const arguments_start = self.index;
|
||||
while (self.index < self.input.len and self.input[self.index] != ')') {
|
||||
if (self.input[self.index] == '(') return error.SyntaxError;
|
||||
self.index += 1;
|
||||
}
|
||||
if (self.index == self.input.len) return error.SyntaxError;
|
||||
const arguments = self.input[arguments_start..self.index];
|
||||
self.index += 1;
|
||||
|
||||
var had_whitespace = false;
|
||||
while (self.index < self.input.len and std.ascii.isWhitespace(self.input[self.index])) {
|
||||
had_whitespace = true;
|
||||
self.index += 1;
|
||||
}
|
||||
if (self.index < self.input.len and self.input[self.index] == ',') {
|
||||
if (!self.allow_comma) return error.SyntaxError;
|
||||
self.index += 1;
|
||||
while (self.index < self.input.len and std.ascii.isWhitespace(self.input[self.index])) self.index += 1;
|
||||
if (self.index == self.input.len) return error.SyntaxError;
|
||||
} else if (self.index < self.input.len and !had_whitespace) {
|
||||
return error.SyntaxError;
|
||||
}
|
||||
return .{ .name = name, .arguments = arguments };
|
||||
}
|
||||
};
|
||||
|
||||
// Parses a CSS <transform-list> (e.g. "matrix(1,0,0,1,10,20) scale(2)") and
|
||||
// accumulates it into `m`. "none"/empty leave the matrix as identity.
|
||||
pub fn parseTransformList(input: []const u8, m: *[16]f64, is_2d: *bool) !void {
|
||||
const trimmed = std.mem.trim(u8, input, " \t\r\n");
|
||||
if (trimmed.len == 0 or std.mem.eql(u8, trimmed, "none")) {
|
||||
return;
|
||||
}
|
||||
if (trimmed.len == 0 or std.mem.eql(u8, trimmed, "none")) return;
|
||||
|
||||
var i: usize = 0;
|
||||
while (i < trimmed.len) {
|
||||
// skip whitespace and separating commas
|
||||
while (i < trimmed.len and (std.ascii.isWhitespace(trimmed[i]) or trimmed[i] == ',')) : (i += 1) {}
|
||||
if (i >= trimmed.len) {
|
||||
break;
|
||||
}
|
||||
|
||||
const name_start = i;
|
||||
while (i < trimmed.len and trimmed[i] != '(') : (i += 1) {}
|
||||
if (i >= trimmed.len) {
|
||||
return error.SyntaxError;
|
||||
}
|
||||
const name = std.mem.trim(u8, trimmed[name_start..i], " \t\r\n");
|
||||
|
||||
i += 1; // consume '('
|
||||
const args_start = i;
|
||||
while (i < trimmed.len and trimmed[i] != ')') : (i += 1) {}
|
||||
if (i >= trimmed.len) {
|
||||
return error.SyntaxError;
|
||||
}
|
||||
const args = trimmed[args_start..i];
|
||||
i += 1; // consume ')'
|
||||
|
||||
const func = try parseFunction(name, args, is_2d);
|
||||
m.* = multiplyMatrix(m.*, func);
|
||||
var iterator = TransformFunctionIterator{ .input = trimmed };
|
||||
while (try iterator.next()) |function| {
|
||||
const parsed = try parseTransformFunction(function, .css);
|
||||
m.* = multiplyMatrix(m.*, parsed.matrix);
|
||||
if (!parsed.is_2d) is_2d.* = false;
|
||||
}
|
||||
}
|
||||
|
||||
fn parseFunction(name: []const u8, args: []const u8, is_2d: *bool) ![16]f64 {
|
||||
var nums: [16]f64 = undefined;
|
||||
pub fn parseTransformFunction(function: TransformFunction, syntax: TransformSyntax) !ParsedTransform {
|
||||
var values: [16]f64 = undefined;
|
||||
var units: [16]ParsedValue.Unit = undefined;
|
||||
var count: usize = 0;
|
||||
|
||||
var it = std.mem.splitScalar(u8, args, ',');
|
||||
while (it.next()) |raw| {
|
||||
const tok = std.mem.trim(u8, raw, " \t\r\n");
|
||||
if (tok.len == 0) {
|
||||
continue;
|
||||
}
|
||||
if (count >= 16) {
|
||||
return error.SyntaxError;
|
||||
}
|
||||
const parsed = try ParsedValue.parse(tok);
|
||||
nums[count] = parsed.value;
|
||||
units[count] = parsed.unit;
|
||||
count += 1;
|
||||
}
|
||||
|
||||
const count = try parseArguments(function.arguments, syntax, &values, &units);
|
||||
const name = function.name;
|
||||
const Eql = std.mem.eql;
|
||||
|
||||
if (Eql(u8, name, "matrix")) {
|
||||
if (count != 6) {
|
||||
return error.SyntaxError;
|
||||
}
|
||||
return .{
|
||||
nums[0], nums[1], 0, 0,
|
||||
nums[2], nums[3], 0, 0,
|
||||
0, 0, 1, 0,
|
||||
nums[4], nums[5], 0, 1,
|
||||
};
|
||||
try requireCount(count, 6, 6);
|
||||
try requireUnitless(units[0..count]);
|
||||
return makeParsedTransform(.matrix, .{
|
||||
values[0], values[1], 0, 0,
|
||||
values[2], values[3], 0, 0,
|
||||
0, 0, 1, 0,
|
||||
values[4], values[5], 0, 1,
|
||||
}, values, count, true);
|
||||
}
|
||||
|
||||
if (Eql(u8, name, "matrix3d")) {
|
||||
if (count != 16) {
|
||||
return error.SyntaxError;
|
||||
}
|
||||
is_2d.* = false;
|
||||
return nums;
|
||||
if (syntax == .svg) return error.SyntaxError;
|
||||
try requireCount(count, 16, 16);
|
||||
try requireUnitless(units[0..count]);
|
||||
return makeParsedTransform(.matrix3d, values, values, count, false);
|
||||
}
|
||||
|
||||
if (Eql(u8, name, "translate")) {
|
||||
const tx = nums[0];
|
||||
const ty = if (count > 1) nums[1] else 0;
|
||||
return translationMatrix(tx, ty, 0);
|
||||
try requireCount(count, 1, 2);
|
||||
if (syntax == .svg) try requireUnitless(units[0..count]);
|
||||
const ty = if (count == 2) values[1] else 0;
|
||||
return makeParsedTransform(.translate, translationMatrix(values[0], ty, 0), values, count, true);
|
||||
}
|
||||
|
||||
if (Eql(u8, name, "translateX")) {
|
||||
return translationMatrix(nums[0], 0, 0);
|
||||
if (syntax == .svg) return error.SyntaxError;
|
||||
try requireCount(count, 1, 1);
|
||||
return makeParsedTransform(.translate_x, translationMatrix(values[0], 0, 0), values, count, true);
|
||||
}
|
||||
|
||||
if (Eql(u8, name, "translateY")) {
|
||||
return translationMatrix(0, nums[0], 0);
|
||||
if (syntax == .svg) return error.SyntaxError;
|
||||
try requireCount(count, 1, 1);
|
||||
return makeParsedTransform(.translate_y, translationMatrix(0, values[0], 0), values, count, true);
|
||||
}
|
||||
|
||||
if (Eql(u8, name, "translateZ")) {
|
||||
is_2d.* = false;
|
||||
return translationMatrix(0, 0, nums[0]);
|
||||
if (syntax == .svg) return error.SyntaxError;
|
||||
try requireCount(count, 1, 1);
|
||||
return makeParsedTransform(.translate_z, translationMatrix(0, 0, values[0]), values, count, false);
|
||||
}
|
||||
|
||||
if (Eql(u8, name, "translate3d")) {
|
||||
is_2d.* = false;
|
||||
return translationMatrix(nums[0], nums[1], nums[2]);
|
||||
if (syntax == .svg) return error.SyntaxError;
|
||||
try requireCount(count, 3, 3);
|
||||
return makeParsedTransform(.translate_3d, translationMatrix(values[0], values[1], values[2]), values, count, false);
|
||||
}
|
||||
|
||||
if (Eql(u8, name, "scale")) {
|
||||
const sx = nums[0];
|
||||
const sy = if (count > 1) nums[1] else sx;
|
||||
return scaleMatrix(sx, sy, 1);
|
||||
try requireCount(count, 1, 2);
|
||||
try requireUnitless(units[0..count]);
|
||||
const sy = if (count == 2) values[1] else values[0];
|
||||
return makeParsedTransform(.scale, scaleMatrix(values[0], sy, 1), values, count, true);
|
||||
}
|
||||
|
||||
if (Eql(u8, name, "scaleX")) {
|
||||
return scaleMatrix(nums[0], 1, 1);
|
||||
if (syntax == .svg) return error.SyntaxError;
|
||||
try requireCount(count, 1, 1);
|
||||
try requireUnitless(units[0..count]);
|
||||
return makeParsedTransform(.scale_x, scaleMatrix(values[0], 1, 1), values, count, true);
|
||||
}
|
||||
|
||||
if (Eql(u8, name, "scaleY")) {
|
||||
return scaleMatrix(1, nums[0], 1);
|
||||
if (syntax == .svg) return error.SyntaxError;
|
||||
try requireCount(count, 1, 1);
|
||||
try requireUnitless(units[0..count]);
|
||||
return makeParsedTransform(.scale_y, scaleMatrix(1, values[0], 1), values, count, true);
|
||||
}
|
||||
|
||||
if (Eql(u8, name, "scaleZ")) {
|
||||
is_2d.* = false;
|
||||
return scaleMatrix(1, 1, nums[0]);
|
||||
if (syntax == .svg) return error.SyntaxError;
|
||||
try requireCount(count, 1, 1);
|
||||
try requireUnitless(units[0..count]);
|
||||
return makeParsedTransform(.scale_z, scaleMatrix(1, 1, values[0]), values, count, false);
|
||||
}
|
||||
|
||||
if (Eql(u8, name, "scale3d")) {
|
||||
is_2d.* = false;
|
||||
return scaleMatrix(nums[0], nums[1], nums[2]);
|
||||
}
|
||||
if (Eql(u8, name, "rotate") or Eql(u8, name, "rotateZ")) {
|
||||
if (Eql(u8, name, "rotateZ")) is_2d.* = false;
|
||||
return rotateZMatrix(toRadians(nums[0], units[0]));
|
||||
if (syntax == .svg) return error.SyntaxError;
|
||||
try requireCount(count, 3, 3);
|
||||
try requireUnitless(units[0..count]);
|
||||
return makeParsedTransform(.scale_3d, scaleMatrix(values[0], values[1], values[2]), values, count, false);
|
||||
}
|
||||
|
||||
if (Eql(u8, name, "rotateX")) {
|
||||
is_2d.* = false;
|
||||
return rotateXMatrix(toRadians(nums[0], units[0]));
|
||||
}
|
||||
|
||||
if (Eql(u8, name, "rotateY")) {
|
||||
is_2d.* = false;
|
||||
return rotateYMatrix(toRadians(nums[0], units[0]));
|
||||
}
|
||||
|
||||
if (Eql(u8, name, "rotate3d")) {
|
||||
is_2d.* = false;
|
||||
if (count != 4) {
|
||||
return error.SyntaxError;
|
||||
if (Eql(u8, name, "rotate")) {
|
||||
try requireCount(count, 1, if (syntax == .svg) 3 else 1);
|
||||
if (count != 1 and count != 3) return error.SyntaxError;
|
||||
try requireAngle(units[0]);
|
||||
if (syntax == .svg and units[0] != .none) return error.SyntaxError;
|
||||
if (count == 3) try requireUnitless(units[1..3]);
|
||||
var matrix = rotateZMatrix(toRadians(values[0], units[0]));
|
||||
if (count == 3) {
|
||||
matrix = multiplyMatrix(translationMatrix(values[1], values[2], 0), matrix);
|
||||
matrix = multiplyMatrix(matrix, translationMatrix(-values[1], -values[2], 0));
|
||||
}
|
||||
return axisAngleMatrix(nums[0], nums[1], nums[2], toRadians(nums[3], units[3]));
|
||||
return makeParsedTransform(.rotate, matrix, values, count, true);
|
||||
}
|
||||
if (Eql(u8, name, "rotateX") or Eql(u8, name, "rotateY") or Eql(u8, name, "rotateZ")) {
|
||||
if (syntax == .svg) return error.SyntaxError;
|
||||
try requireCount(count, 1, 1);
|
||||
try requireAngle(units[0]);
|
||||
if (Eql(u8, name, "rotateX")) return makeParsedTransform(.rotate_x, rotateXMatrix(toRadians(values[0], units[0])), values, count, false);
|
||||
if (Eql(u8, name, "rotateY")) return makeParsedTransform(.rotate_y, rotateYMatrix(toRadians(values[0], units[0])), values, count, false);
|
||||
return makeParsedTransform(.rotate_z, rotateZMatrix(toRadians(values[0], units[0])), values, count, false);
|
||||
}
|
||||
if (Eql(u8, name, "rotate3d")) {
|
||||
if (syntax == .svg) return error.SyntaxError;
|
||||
try requireCount(count, 4, 4);
|
||||
try requireUnitless(units[0..3]);
|
||||
try requireAngle(units[3]);
|
||||
return makeParsedTransform(.rotate_3d, axisAngleMatrix(values[0], values[1], values[2], toRadians(values[3], units[3])), values, count, false);
|
||||
}
|
||||
|
||||
if (Eql(u8, name, "skew")) {
|
||||
const ax = toRadians(nums[0], units[0]);
|
||||
const ay = if (count > 1) toRadians(nums[1], units[1]) else 0;
|
||||
return skewMatrix(ax, ay);
|
||||
if (syntax == .svg) return error.SyntaxError;
|
||||
try requireCount(count, 1, 2);
|
||||
try requireAngle(units[0]);
|
||||
if (count == 2) try requireAngle(units[1]);
|
||||
const ay = if (count == 2) toRadians(values[1], units[1]) else 0;
|
||||
return makeParsedTransform(.skew, skewMatrix(toRadians(values[0], units[0]), ay), values, count, true);
|
||||
}
|
||||
|
||||
if (Eql(u8, name, "skewX")) {
|
||||
return skewMatrix(toRadians(nums[0], units[0]), 0);
|
||||
}
|
||||
|
||||
if (Eql(u8, name, "skewY")) {
|
||||
return skewMatrix(0, toRadians(nums[0], units[0]));
|
||||
if (Eql(u8, name, "skewX") or Eql(u8, name, "skewY")) {
|
||||
try requireCount(count, 1, 1);
|
||||
try requireAngle(units[0]);
|
||||
if (syntax == .svg and units[0] != .none) return error.SyntaxError;
|
||||
if (Eql(u8, name, "skewX")) return makeParsedTransform(.skew_x, skewMatrix(toRadians(values[0], units[0]), 0), values, count, true);
|
||||
return makeParsedTransform(.skew_y, skewMatrix(0, toRadians(values[0], units[0])), values, count, true);
|
||||
}
|
||||
|
||||
if (Eql(u8, name, "perspective")) {
|
||||
is_2d.* = false;
|
||||
var out = identity();
|
||||
if (nums[0] != 0) out[11] = -1.0 / nums[0];
|
||||
return out;
|
||||
if (syntax == .svg) return error.SyntaxError;
|
||||
try requireCount(count, 1, 1);
|
||||
var matrix = identity();
|
||||
if (values[0] != 0) matrix[11] = -1.0 / values[0];
|
||||
return makeParsedTransform(.perspective, matrix, values, count, false);
|
||||
}
|
||||
|
||||
return error.SyntaxError;
|
||||
}
|
||||
|
||||
fn makeParsedTransform(kind: TransformKind, matrix: [16]f64, values: [16]f64, count: usize, is_2d: bool) ParsedTransform {
|
||||
return .{ .kind = kind, .matrix = matrix, .values = values, .count = count, .is_2d = is_2d };
|
||||
}
|
||||
|
||||
fn requireCount(count: usize, minimum: usize, maximum: usize) !void {
|
||||
if (count < minimum or count > maximum) return error.SyntaxError;
|
||||
}
|
||||
|
||||
fn requireUnitless(units: []const ParsedValue.Unit) !void {
|
||||
for (units) |unit| if (unit != .none) return error.SyntaxError;
|
||||
}
|
||||
|
||||
fn requireAngle(unit: ParsedValue.Unit) !void {
|
||||
if (unit == .other) return error.SyntaxError;
|
||||
}
|
||||
|
||||
fn parseArguments(arguments: []const u8, syntax: TransformSyntax, values: *[16]f64, units: *[16]ParsedValue.Unit) !usize {
|
||||
var scanner = ArgumentScanner{ .input = arguments, .syntax = syntax };
|
||||
var count: usize = 0;
|
||||
while (try scanner.next()) |value| {
|
||||
if (count == values.len) return error.SyntaxError;
|
||||
values[count] = value.value;
|
||||
units[count] = value.unit;
|
||||
count += 1;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
pub fn toRadians(value: f64, unit: ParsedValue.Unit) f64 {
|
||||
return switch (unit) {
|
||||
.rad => value,
|
||||
@@ -491,6 +623,7 @@ pub fn toRadians(value: f64, unit: ParsedValue.Unit) f64 {
|
||||
.turn => value * std.math.tau,
|
||||
// bare numbers in rotate()/skew() are interpreted as degrees
|
||||
.deg, .none => value * std.math.pi / 180.0,
|
||||
.other => value,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -725,43 +858,80 @@ const ParsedValue = struct {
|
||||
rad,
|
||||
grad,
|
||||
turn,
|
||||
other,
|
||||
};
|
||||
};
|
||||
|
||||
// Parses a single CSS dimension token: a number with an optional unit suffix.
|
||||
// Length units are ignored (we don't resolve layout), so the numeric part is
|
||||
// taken verbatim; angle units are recorded so they can be normalised.
|
||||
fn parse(tok: []const u8) !ParsedValue {
|
||||
var end: usize = 0;
|
||||
while (end < tok.len) : (end += 1) {
|
||||
const c = tok[end];
|
||||
if ((c >= '0' and c <= '9') or c == '.' or c == '+' or c == '-' or c == 'e' or c == 'E') {
|
||||
// 'e'/'E' is ambiguous with exponents; only treat as exponent when
|
||||
// followed by a digit/sign.
|
||||
if ((c == 'e' or c == 'E') and end > 0) {
|
||||
if (end + 1 >= tok.len) break;
|
||||
const nx = tok[end + 1];
|
||||
if (!((nx >= '0' and nx <= '9') or nx == '+' or nx == '-')) break;
|
||||
}
|
||||
continue;
|
||||
const ArgumentScanner = struct {
|
||||
input: []const u8,
|
||||
syntax: TransformSyntax,
|
||||
index: usize = 0,
|
||||
first: bool = true,
|
||||
|
||||
fn next(self: *ArgumentScanner) !?ParsedValue {
|
||||
var had_whitespace = false;
|
||||
while (self.index < self.input.len and std.ascii.isWhitespace(self.input[self.index])) {
|
||||
had_whitespace = true;
|
||||
self.index += 1;
|
||||
}
|
||||
|
||||
if (!self.first and self.index < self.input.len and self.input[self.index] == ',') {
|
||||
self.index += 1;
|
||||
while (self.index < self.input.len and std.ascii.isWhitespace(self.input[self.index])) self.index += 1;
|
||||
if (self.index == self.input.len) return error.SyntaxError;
|
||||
} else if (!self.first and self.index < self.input.len) {
|
||||
// CSS separates arguments with a comma and nothing else. The SVG
|
||||
// grammar also takes whitespace, or no separator at all when the
|
||||
// next number carries its own sign.
|
||||
if (self.syntax == .css) return error.SyntaxError;
|
||||
if (!had_whitespace and self.input[self.index] != '+' and self.input[self.index] != '-') {
|
||||
return error.SyntaxError;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (end == 0) {
|
||||
return error.SyntaxError;
|
||||
}
|
||||
const value = try std.fmt.parseFloat(f64, tok[0..end]);
|
||||
const suffix = tok[end..];
|
||||
|
||||
var unit: Unit = .none;
|
||||
if (std.ascii.eqlIgnoreCase(suffix, "deg")) {
|
||||
unit = .deg;
|
||||
} else if (std.ascii.eqlIgnoreCase(suffix, "rad")) {
|
||||
unit = .rad;
|
||||
} else if (std.ascii.eqlIgnoreCase(suffix, "grad")) {
|
||||
unit = .grad;
|
||||
} else if (std.ascii.eqlIgnoreCase(suffix, "turn")) {
|
||||
unit = .turn;
|
||||
if (self.index == self.input.len) return null;
|
||||
const start = self.index;
|
||||
if (self.input[self.index] == '+' or self.input[self.index] == '-') self.index += 1;
|
||||
|
||||
var digits: usize = 0;
|
||||
while (self.index < self.input.len and std.ascii.isDigit(self.input[self.index])) : (self.index += 1) digits += 1;
|
||||
if (self.index < self.input.len and self.input[self.index] == '.') {
|
||||
self.index += 1;
|
||||
while (self.index < self.input.len and std.ascii.isDigit(self.input[self.index])) : (self.index += 1) digits += 1;
|
||||
}
|
||||
if (digits == 0) return error.SyntaxError;
|
||||
|
||||
if (self.index < self.input.len and (self.input[self.index] == 'e' or self.input[self.index] == 'E')) {
|
||||
self.index += 1;
|
||||
if (self.index < self.input.len and (self.input[self.index] == '+' or self.input[self.index] == '-')) self.index += 1;
|
||||
const exponent_start = self.index;
|
||||
while (self.index < self.input.len and std.ascii.isDigit(self.input[self.index])) self.index += 1;
|
||||
if (self.index == exponent_start) return error.SyntaxError;
|
||||
}
|
||||
|
||||
const number_end = self.index;
|
||||
while (self.index < self.input.len and
|
||||
(std.ascii.isAlphabetic(self.input[self.index]) or self.input[self.index] == '%'))
|
||||
{
|
||||
self.index += 1;
|
||||
}
|
||||
const suffix = self.input[number_end..self.index];
|
||||
const unit: ParsedValue.Unit = if (suffix.len == 0)
|
||||
.none
|
||||
else if (std.ascii.eqlIgnoreCase(suffix, "deg"))
|
||||
.deg
|
||||
else if (std.ascii.eqlIgnoreCase(suffix, "rad"))
|
||||
.rad
|
||||
else if (std.ascii.eqlIgnoreCase(suffix, "grad"))
|
||||
.grad
|
||||
else if (std.ascii.eqlIgnoreCase(suffix, "turn"))
|
||||
.turn
|
||||
else
|
||||
.other;
|
||||
|
||||
const value = std.fmt.parseFloat(f64, self.input[start..number_end]) catch return error.SyntaxError;
|
||||
if (!std.math.isFinite(value)) return error.SyntaxError;
|
||||
self.first = false;
|
||||
return .{ .value = value, .unit = unit };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -77,17 +77,17 @@ pub fn getW(self: *const DOMPoint) f64 {
|
||||
return self._proto._w;
|
||||
}
|
||||
|
||||
pub fn setX(self: *DOMPoint, v: f64) void {
|
||||
self._proto._x = v;
|
||||
pub fn setX(self: *DOMPoint, v: f64) !void {
|
||||
try self._proto.setCoordinate(.x, v);
|
||||
}
|
||||
pub fn setY(self: *DOMPoint, v: f64) void {
|
||||
self._proto._y = v;
|
||||
pub fn setY(self: *DOMPoint, v: f64) !void {
|
||||
try self._proto.setCoordinate(.y, v);
|
||||
}
|
||||
pub fn setZ(self: *DOMPoint, v: f64) void {
|
||||
self._proto._z = v;
|
||||
pub fn setZ(self: *DOMPoint, v: f64) !void {
|
||||
try self._proto.setCoordinate(.z, v);
|
||||
}
|
||||
pub fn setW(self: *DOMPoint, v: f64) void {
|
||||
self._proto._w = v;
|
||||
pub fn setW(self: *DOMPoint, v: f64) !void {
|
||||
try self._proto.setCoordinate(.w, v);
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
|
||||
@@ -38,6 +38,23 @@ _x: f64,
|
||||
_y: f64,
|
||||
_z: f64,
|
||||
_w: f64,
|
||||
_attachment: ?Attachment = null,
|
||||
|
||||
// Sticky. An animVal item that a later external attribute change detaches from
|
||||
// its list must not turn into a writable orphan.
|
||||
_read_only: bool = false,
|
||||
|
||||
// SVGPoint's coordinates are a restricted float: NaN and infinity are a
|
||||
// TypeError rather than a stored value. Only points that reach an SVG list are
|
||||
// restricted; a plain DOMPoint takes an unrestricted double.
|
||||
_restricted: bool = false,
|
||||
|
||||
pub const Coordinate = enum { x, y, z, w };
|
||||
|
||||
pub const Attachment = struct {
|
||||
owner: *anyopaque,
|
||||
mutate: *const fn (*anyopaque, *DOMPointReadOnly, Coordinate, f64) anyerror!void,
|
||||
};
|
||||
|
||||
pub const Type = union(enum) {
|
||||
generic,
|
||||
@@ -132,6 +149,47 @@ pub fn getW(self: *const DOMPointReadOnly) f64 {
|
||||
return self._w;
|
||||
}
|
||||
|
||||
pub fn setCoordinate(self: *DOMPointReadOnly, coordinate: Coordinate, value: f64) !void {
|
||||
if (self._read_only) return error.NoModificationAllowed;
|
||||
if (self._restricted and !std.math.isFinite(value)) return error.TypeError;
|
||||
if (self._attachment) |attachment| {
|
||||
return attachment.mutate(attachment.owner, self, coordinate, value);
|
||||
}
|
||||
self.setCoordinateRaw(coordinate, value);
|
||||
}
|
||||
|
||||
pub fn setCoordinateRaw(self: *DOMPointReadOnly, coordinate: Coordinate, value: f64) void {
|
||||
switch (coordinate) {
|
||||
.x => self._x = value,
|
||||
.y => self._y = value,
|
||||
.z => self._z = value,
|
||||
.w => self._w = value,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn restrict(self: *DOMPointReadOnly) void {
|
||||
self._restricted = true;
|
||||
}
|
||||
|
||||
pub fn attach(self: *DOMPointReadOnly, attachment: Attachment, read_only: bool) void {
|
||||
self._attachment = attachment;
|
||||
if (read_only) self._read_only = true;
|
||||
}
|
||||
|
||||
pub fn detach(self: *DOMPointReadOnly, owner: *anyopaque) void {
|
||||
const attachment = self._attachment orelse return;
|
||||
if (attachment.owner == owner) self._attachment = null;
|
||||
}
|
||||
|
||||
pub fn isAttached(self: *const DOMPointReadOnly) bool {
|
||||
return self._attachment != null;
|
||||
}
|
||||
|
||||
pub fn isAttachedTo(self: *const DOMPointReadOnly, owner: *anyopaque) bool {
|
||||
const attachment = self._attachment orelse return false;
|
||||
return attachment.owner == owner;
|
||||
}
|
||||
|
||||
pub fn toJSON(self: *const DOMPointReadOnly) struct {
|
||||
x: f64,
|
||||
y: f64,
|
||||
|
||||
@@ -856,6 +856,10 @@ pub fn removeAttribute(self: *Element, name: String, frame: *Frame) !void {
|
||||
return self._attributes.delete(name, self, frame);
|
||||
}
|
||||
|
||||
pub fn removeAttributeSafe(self: *Element, name: String, frame: *Frame) void {
|
||||
self._attributes.deleteSafe(name, self, frame);
|
||||
}
|
||||
|
||||
pub fn toggleAttribute(self: *Element, name: String, force: ?bool, frame: *Frame) !bool {
|
||||
try Attribute.validateAttributeName(name);
|
||||
const has = try self.hasAttribute(name, frame);
|
||||
|
||||
@@ -315,9 +315,17 @@ pub const List = struct {
|
||||
pub fn delete(self: *List, name: String, element: *Element, frame: *Frame) !void {
|
||||
const result = try self.getEntryAndNormalizedName(name, frame);
|
||||
const entry = result.entry orelse return;
|
||||
return self._delete(entry, result.normalized, element, frame);
|
||||
}
|
||||
|
||||
pub fn deleteSafe(self: *List, name: String, element: *Element, frame: *Frame) void {
|
||||
const entry = self.getEntryWithNormalizedName(name) orelse return;
|
||||
self._delete(entry, name, element, frame);
|
||||
}
|
||||
|
||||
fn _delete(self: *List, entry: *Entry, normalized: String, element: *Element, frame: *Frame) void {
|
||||
const owner = element.ownerFrame(frame);
|
||||
const is_id = shouldAddToIdMap(result.normalized, element);
|
||||
const is_id = shouldAddToIdMap(normalized, element);
|
||||
const old_value = entry.value();
|
||||
|
||||
if (is_id) {
|
||||
@@ -333,7 +341,7 @@ pub const List = struct {
|
||||
self._len -= 1;
|
||||
|
||||
owner.domChanged();
|
||||
owner.attributeRemove(element, result.normalized, .wrap(old_value));
|
||||
owner.attributeRemove(element, normalized, .wrap(old_value));
|
||||
}
|
||||
|
||||
pub fn getNames(self: *const List, allocator: Allocator) ![][]const u8 {
|
||||
|
||||
@@ -17,9 +17,12 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Frame = @import("../../../Frame.zig");
|
||||
const Node = @import("../../Node.zig");
|
||||
const Element = @import("../../Element.zig");
|
||||
const SvgElement = @import("../Svg.zig");
|
||||
const AnimatedTransformList = @import("../../svg/AnimatedTransformList.zig");
|
||||
const StringList = @import("../../svg/StringList.zig");
|
||||
|
||||
pub const Svg = @import("Svg.zig");
|
||||
pub const G = @import("G.zig");
|
||||
@@ -72,4 +75,20 @@ pub const JsApi = struct {
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const transform = bridge.accessor(Graphics.getTransform, null, .{});
|
||||
pub const requiredExtensions = bridge.accessor(Graphics.getRequiredExtensions, null, .{});
|
||||
pub const systemLanguage = bridge.accessor(Graphics.getSystemLanguage, null, .{});
|
||||
};
|
||||
|
||||
pub fn getTransform(self: *Graphics, frame: *Frame) !*AnimatedTransformList {
|
||||
return AnimatedTransformList.getOrCreate(self.asElement(), frame);
|
||||
}
|
||||
|
||||
pub fn getRequiredExtensions(self: *Graphics, frame: *Frame) !*StringList {
|
||||
return StringList.getOrCreate(self.asElement(), .required_extensions, frame);
|
||||
}
|
||||
|
||||
pub fn getSystemLanguage(self: *Graphics, frame: *Frame) !*StringList {
|
||||
return StringList.getOrCreate(self.asElement(), .system_language, frame);
|
||||
}
|
||||
|
||||
@@ -17,11 +17,13 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Frame = @import("../../../Frame.zig");
|
||||
|
||||
const Node = @import("../../Node.zig");
|
||||
const Element = @import("../../Element.zig");
|
||||
|
||||
const Geometry = @import("Geometry.zig");
|
||||
const PointList = @import("../../svg/PointList.zig");
|
||||
|
||||
const Polygon = @This();
|
||||
_proto: *Geometry,
|
||||
@@ -41,4 +43,15 @@ pub const JsApi = struct {
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const points = bridge.accessor(Polygon.getPoints, null, .{});
|
||||
pub const animatedPoints = bridge.accessor(Polygon.getAnimatedPoints, null, .{});
|
||||
};
|
||||
|
||||
pub fn getPoints(self: *Polygon, frame: *Frame) !*PointList {
|
||||
return PointList.getOrCreate(self.asElement(), .base, frame);
|
||||
}
|
||||
|
||||
pub fn getAnimatedPoints(self: *Polygon, frame: *Frame) !*PointList {
|
||||
return PointList.getOrCreate(self.asElement(), .animated, frame);
|
||||
}
|
||||
|
||||
@@ -17,11 +17,13 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Frame = @import("../../../Frame.zig");
|
||||
|
||||
const Node = @import("../../Node.zig");
|
||||
const Element = @import("../../Element.zig");
|
||||
|
||||
const Geometry = @import("Geometry.zig");
|
||||
const PointList = @import("../../svg/PointList.zig");
|
||||
|
||||
const Polyline = @This();
|
||||
_proto: *Geometry,
|
||||
@@ -41,4 +43,15 @@ pub const JsApi = struct {
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const points = bridge.accessor(Polyline.getPoints, null, .{});
|
||||
pub const animatedPoints = bridge.accessor(Polyline.getAnimatedPoints, null, .{});
|
||||
};
|
||||
|
||||
pub fn getPoints(self: *Polyline, frame: *Frame) !*PointList {
|
||||
return PointList.getOrCreate(self.asElement(), .base, frame);
|
||||
}
|
||||
|
||||
pub fn getAnimatedPoints(self: *Polyline, frame: *Frame) !*PointList {
|
||||
return PointList.getOrCreate(self.asElement(), .animated, frame);
|
||||
}
|
||||
|
||||
@@ -82,7 +82,9 @@ pub fn getPreserveAspectRatio(self: *Svg, frame: *Frame) !*AnimatedPreserveAspec
|
||||
}
|
||||
|
||||
pub fn createSVGPoint(_: *Svg, frame: *Frame) !*DOMPoint {
|
||||
return DOMPoint.create(0, 0, 0, 1, frame._page);
|
||||
const point = try DOMPoint.create(0, 0, 0, 1, frame._page);
|
||||
point._proto.restrict();
|
||||
return point;
|
||||
}
|
||||
|
||||
pub fn createSVGMatrix(_: *Svg, frame: *Frame) !*DOMMatrix {
|
||||
|
||||
65
src/browser/webapi/svg/AnimatedTransformList.zig
Normal file
65
src/browser/webapi/svg/AnimatedTransformList.zig
Normal file
@@ -0,0 +1,65 @@
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const js = @import("../../js/js.zig");
|
||||
const Frame = @import("../../Frame.zig");
|
||||
const Page = @import("../../Page.zig");
|
||||
const Element = @import("../Element.zig");
|
||||
const TransformList = @import("TransformList.zig");
|
||||
|
||||
const AnimatedTransformList = @This();
|
||||
|
||||
_base_val: *TransformList,
|
||||
_anim_val: *TransformList,
|
||||
|
||||
pub const Lookup = std.AutoHashMapUnmanaged(*Element, *AnimatedTransformList);
|
||||
|
||||
pub fn getOrCreate(element: *Element, frame: *Frame) !*AnimatedTransformList {
|
||||
const gop = try frame._svg_animated_transform_lists.getOrPut(frame.arena, element);
|
||||
if (!gop.found_existing) {
|
||||
errdefer _ = frame._svg_animated_transform_lists.remove(element);
|
||||
gop.value_ptr.* = try create(element, 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 frame._factory.create(AnimatedTransformList{
|
||||
._base_val = base_val,
|
||||
._anim_val = anim_val,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn deinit(self: *AnimatedTransformList, page: *Page) void {
|
||||
self._base_val.deinit(page);
|
||||
self._anim_val.deinit(page);
|
||||
}
|
||||
|
||||
pub fn getBaseVal(self: *AnimatedTransformList) *TransformList {
|
||||
return self._base_val;
|
||||
}
|
||||
|
||||
pub fn getAnimVal(self: *AnimatedTransformList) *TransformList {
|
||||
return self._anim_val;
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(AnimatedTransformList);
|
||||
|
||||
pub const Meta = struct {
|
||||
pub const name = "SVGAnimatedTransformList";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const baseVal = bridge.accessor(AnimatedTransformList.getBaseVal, null, .{});
|
||||
pub const animVal = bridge.accessor(AnimatedTransformList.getAnimVal, null, .{});
|
||||
};
|
||||
381
src/browser/webapi/svg/PointList.zig
Normal file
381
src/browser/webapi/svg/PointList.zig
Normal file
@@ -0,0 +1,381 @@
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (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");
|
||||
const Page = @import("../../Page.zig");
|
||||
const DOMPoint = @import("../DOMPoint.zig");
|
||||
const DOMPointReadOnly = @import("../DOMPointReadOnly.zig");
|
||||
const Element = @import("../Element.zig");
|
||||
|
||||
const PointList = @This();
|
||||
|
||||
_frame: *Frame,
|
||||
_element: *Element,
|
||||
_read_only: bool,
|
||||
_synced: bool = false,
|
||||
_snapshot: std.ArrayList(u8) = .empty,
|
||||
_items: std.ArrayList(*DOMPoint) = .empty,
|
||||
_retired: std.ArrayList(*DOMPoint) = .empty,
|
||||
|
||||
pub const Kind = enum { base, animated };
|
||||
|
||||
pub const Key = struct {
|
||||
element: *Element,
|
||||
kind: Kind,
|
||||
};
|
||||
|
||||
pub const Lookup = std.AutoHashMapUnmanaged(Key, *PointList);
|
||||
|
||||
pub fn getOrCreate(element: *Element, kind: Kind, frame: *Frame) !*PointList {
|
||||
const key: Key = .{
|
||||
.element = element,
|
||||
.kind = kind,
|
||||
};
|
||||
const gop = try frame._svg_point_lists.getOrPut(frame.arena, key);
|
||||
if (!gop.found_existing) {
|
||||
errdefer _ = frame._svg_point_lists.remove(key);
|
||||
gop.value_ptr.* = try frame._factory.create(PointList{
|
||||
._frame = frame,
|
||||
._element = element,
|
||||
._read_only = kind == .animated,
|
||||
});
|
||||
}
|
||||
return gop.value_ptr.*;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *PointList, page: *Page) void {
|
||||
for (self._items.items) |point| {
|
||||
point._proto.detach(self);
|
||||
point._proto.releaseRef(page);
|
||||
}
|
||||
self._items.clearRetainingCapacity();
|
||||
self.releaseRetired(page);
|
||||
}
|
||||
|
||||
pub fn getLength(self: *PointList, frame: *Frame) !u32 {
|
||||
try self.sync(frame);
|
||||
return @intCast(self._items.items.len);
|
||||
}
|
||||
|
||||
pub fn getNumberOfItems(self: *PointList, frame: *Frame) !u32 {
|
||||
return self.getLength(frame);
|
||||
}
|
||||
|
||||
pub fn clear(self: *PointList, frame: *Frame) !void {
|
||||
try self.requireMutable();
|
||||
try self.sync(frame);
|
||||
try self.retireAll(frame);
|
||||
try self.setAttribute(&.{}, frame);
|
||||
}
|
||||
|
||||
pub fn initialize(self: *PointList, item: *DOMPoint, frame: *Frame) !*DOMPoint {
|
||||
try self.requireMutable();
|
||||
try self.sync(frame);
|
||||
|
||||
const prepared = try self.prepareItem(item, frame);
|
||||
errdefer prepared._proto.releaseRef(frame._page);
|
||||
|
||||
try self.retireAll(frame);
|
||||
try self._items.ensureTotalCapacity(frame.arena, 1);
|
||||
try self.setAttribute(&.{prepared}, frame);
|
||||
self._items.appendAssumeCapacity(prepared);
|
||||
self.attach(prepared);
|
||||
return prepared;
|
||||
}
|
||||
|
||||
pub fn getItem(self: *PointList, index: u32, frame: *Frame) !*DOMPoint {
|
||||
try self.sync(frame);
|
||||
if (index >= self._items.items.len) return error.IndexSizeError;
|
||||
return self._items.items[index];
|
||||
}
|
||||
|
||||
pub fn insertItemBefore(self: *PointList, item: *DOMPoint, index: u32, frame: *Frame) !*DOMPoint {
|
||||
try self.requireMutable();
|
||||
try self.sync(frame);
|
||||
|
||||
const prepared = try self.prepareItem(item, frame);
|
||||
errdefer prepared._proto.releaseRef(frame._page);
|
||||
const at = @min(@as(usize, index), self._items.items.len);
|
||||
const next = try frame.local_arena.alloc(*DOMPoint, self._items.items.len + 1);
|
||||
@memcpy(next[0..at], self._items.items[0..at]);
|
||||
next[at] = prepared;
|
||||
@memcpy(next[at + 1 ..], self._items.items[at..]);
|
||||
|
||||
try self._items.ensureUnusedCapacity(frame.arena, 1);
|
||||
try self.setAttribute(next, frame);
|
||||
self._items.insertAssumeCapacity(at, prepared);
|
||||
self.attach(prepared);
|
||||
return prepared;
|
||||
}
|
||||
|
||||
pub fn replaceItem(self: *PointList, item: *DOMPoint, index: u32, frame: *Frame) !*DOMPoint {
|
||||
try self.requireMutable();
|
||||
try self.sync(frame);
|
||||
if (index >= self._items.items.len) return error.IndexSizeError;
|
||||
|
||||
const prepared = try self.prepareItem(item, frame);
|
||||
errdefer prepared._proto.releaseRef(frame._page);
|
||||
const next = try frame.local_arena.dupe(*DOMPoint, self._items.items);
|
||||
next[index] = prepared;
|
||||
|
||||
try self._retired.ensureUnusedCapacity(frame.arena, 1);
|
||||
try self.setAttribute(next, frame);
|
||||
const replaced = self._items.items[index];
|
||||
replaced._proto.detach(self);
|
||||
self._retired.appendAssumeCapacity(replaced);
|
||||
self._items.items[index] = prepared;
|
||||
self.attach(prepared);
|
||||
return prepared;
|
||||
}
|
||||
|
||||
pub fn removeItem(self: *PointList, index: u32, frame: *Frame) !*DOMPoint {
|
||||
try self.requireMutable();
|
||||
try self.sync(frame);
|
||||
if (index >= self._items.items.len) return error.IndexSizeError;
|
||||
|
||||
const next = try frame.local_arena.alloc(*DOMPoint, self._items.items.len - 1);
|
||||
@memcpy(next[0..index], self._items.items[0..index]);
|
||||
@memcpy(next[index..], self._items.items[index + 1 ..]);
|
||||
|
||||
try self._retired.ensureUnusedCapacity(frame.arena, 1);
|
||||
try self.setAttribute(next, frame);
|
||||
const removed = self._items.orderedRemove(index);
|
||||
removed._proto.detach(self);
|
||||
self._retired.appendAssumeCapacity(removed);
|
||||
return removed;
|
||||
}
|
||||
|
||||
pub fn appendItem(self: *PointList, item: *DOMPoint, frame: *Frame) !*DOMPoint {
|
||||
return self.insertItemBefore(item, std.math.maxInt(u32), frame);
|
||||
}
|
||||
|
||||
fn requireMutable(self: *const PointList) !void {
|
||||
if (self._read_only) return error.NoModificationAllowed;
|
||||
}
|
||||
|
||||
fn prepareItem(_: *PointList, item: *DOMPoint, frame: *Frame) !*DOMPoint {
|
||||
if (!std.math.isFinite(item._proto._x) or !std.math.isFinite(item._proto._y)) return error.TypeError;
|
||||
const prepared = if (item._proto.isAttached())
|
||||
try DOMPoint.create(item._proto._x, item._proto._y, item._proto._z, item._proto._w, frame._page)
|
||||
else
|
||||
item;
|
||||
prepared._proto.acquireRef();
|
||||
return prepared;
|
||||
}
|
||||
|
||||
fn attach(self: *PointList, point: *DOMPoint) void {
|
||||
point._proto.restrict();
|
||||
point._proto.attach(.{
|
||||
.owner = self,
|
||||
.mutate = PointList.mutatePoint,
|
||||
}, self._read_only);
|
||||
}
|
||||
|
||||
fn mutatePoint(
|
||||
context: *anyopaque,
|
||||
point: *DOMPointReadOnly,
|
||||
coordinate: DOMPointReadOnly.Coordinate,
|
||||
value: f64,
|
||||
) anyerror!void {
|
||||
const self: *PointList = @ptrCast(@alignCast(context));
|
||||
const frame = self._frame;
|
||||
try self.sync(frame);
|
||||
|
||||
// An external attribute mutation detaches the old item during sync. The
|
||||
// caller still owns that DOMPoint identity, but it no longer mutates the list.
|
||||
if (!point.isAttachedTo(self)) {
|
||||
point.setCoordinateRaw(coordinate, value);
|
||||
return;
|
||||
}
|
||||
|
||||
if (coordinate == .z or coordinate == .w) {
|
||||
point.setCoordinateRaw(coordinate, value);
|
||||
return;
|
||||
}
|
||||
if (!std.math.isFinite(value)) return error.TypeError;
|
||||
|
||||
const index = for (self._items.items, 0..) |candidate, i| {
|
||||
if (candidate._proto == point) break i;
|
||||
} else unreachable;
|
||||
|
||||
try self.setAttributeWithOverride(index, coordinate, value, frame);
|
||||
point.setCoordinateRaw(coordinate, value);
|
||||
}
|
||||
|
||||
fn sync(self: *PointList, frame: *Frame) !void {
|
||||
self.releaseRetired(frame._page);
|
||||
|
||||
const raw = self._element.getAttributeSafe(comptime .wrap("points")) orelse "";
|
||||
if (self._synced and std.mem.eql(u8, self._snapshot.items, raw)) {
|
||||
return;
|
||||
}
|
||||
|
||||
self._synced = false;
|
||||
var parsed = parse(raw, frame) catch |err| switch (err) {
|
||||
error.SyntaxError => std.ArrayList(*DOMPoint).empty,
|
||||
else => return err,
|
||||
};
|
||||
errdefer for (parsed.items) |point| point._proto.releaseRef(frame._page);
|
||||
|
||||
self._snapshot.clearRetainingCapacity();
|
||||
try self._snapshot.appendSlice(frame.arena, raw);
|
||||
try self.retireAll(frame);
|
||||
try self._items.ensureTotalCapacity(frame.arena, parsed.items.len);
|
||||
for (parsed.items) |point| {
|
||||
self._items.appendAssumeCapacity(point);
|
||||
self.attach(point);
|
||||
}
|
||||
parsed.clearRetainingCapacity();
|
||||
self._synced = true;
|
||||
}
|
||||
|
||||
// A retired item must outlive the operation that retired it: removeItem's
|
||||
// return value has no JS wrapper until the bridge wraps it after we return.
|
||||
// By the next operation, anything still reachable holds its own ref.
|
||||
fn releaseRetired(self: *PointList, page: *Page) void {
|
||||
for (self._retired.items) |point| {
|
||||
point._proto.releaseRef(page);
|
||||
}
|
||||
self._retired.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
fn parse(raw: []const u8, frame: *Frame) !std.ArrayList(*DOMPoint) {
|
||||
var scanner = NumberScanner{ .input = raw };
|
||||
var parsed: std.ArrayList(*DOMPoint) = .empty;
|
||||
errdefer for (parsed.items) |point| point._proto.releaseRef(frame._page);
|
||||
|
||||
while (try scanner.next()) |x| {
|
||||
// A trailing coordinate with no pair truncates the list; only a
|
||||
// malformed number invalidates the whole attribute.
|
||||
const y = (try scanner.next()) orelse break;
|
||||
const point = try DOMPoint.create(x, y, 0, 1, frame._page);
|
||||
point._proto.acquireRef();
|
||||
parsed.append(frame.local_arena, point) catch |err| {
|
||||
point._proto.releaseRef(frame._page);
|
||||
return err;
|
||||
};
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
fn retireAll(self: *PointList, frame: *Frame) !void {
|
||||
self._synced = false;
|
||||
try self._retired.ensureUnusedCapacity(frame.arena, self._items.items.len);
|
||||
for (self._items.items) |point| {
|
||||
point._proto.detach(self);
|
||||
self._retired.appendAssumeCapacity(point);
|
||||
}
|
||||
self._items.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
fn setAttribute(self: *PointList, items: []const *DOMPoint, frame: *Frame) !void {
|
||||
var serialized: std.Io.Writer.Allocating = .init(frame.local_arena);
|
||||
const writer = &serialized.writer;
|
||||
for (items, 0..) |point, i| {
|
||||
if (i != 0) try writer.writeByte(' ');
|
||||
try writer.print("{d} {d}", .{ point._proto._x, point._proto._y });
|
||||
}
|
||||
try self.commitAttribute(serialized.written(), frame);
|
||||
}
|
||||
|
||||
fn setAttributeWithOverride(
|
||||
self: *PointList,
|
||||
index: usize,
|
||||
coordinate: DOMPointReadOnly.Coordinate,
|
||||
value: f64,
|
||||
frame: *Frame,
|
||||
) !void {
|
||||
var serialized: std.Io.Writer.Allocating = .init(frame.local_arena);
|
||||
const writer = &serialized.writer;
|
||||
for (self._items.items, 0..) |point, i| {
|
||||
if (i != 0) try writer.writeByte(' ');
|
||||
const x = if (i == index and coordinate == .x) value else point._proto._x;
|
||||
const y = if (i == index and coordinate == .y) value else point._proto._y;
|
||||
try writer.print("{d} {d}", .{ x, y });
|
||||
}
|
||||
try self.commitAttribute(serialized.written(), frame);
|
||||
}
|
||||
|
||||
fn commitAttribute(self: *PointList, serialized: []const u8, frame: *Frame) !void {
|
||||
self._synced = false;
|
||||
try self._element.setAttributeSafe(comptime .wrap("points"), .wrap(serialized), frame);
|
||||
self._snapshot.clearRetainingCapacity();
|
||||
try self._snapshot.appendSlice(frame.arena, serialized);
|
||||
self._synced = true;
|
||||
}
|
||||
|
||||
const NumberScanner = struct {
|
||||
input: []const u8,
|
||||
index: usize = 0,
|
||||
first: bool = true,
|
||||
|
||||
fn next(self: *NumberScanner) !?f64 {
|
||||
var had_whitespace = false;
|
||||
while (self.index < self.input.len and std.ascii.isWhitespace(self.input[self.index])) {
|
||||
had_whitespace = true;
|
||||
self.index += 1;
|
||||
}
|
||||
|
||||
if (!self.first and self.index < self.input.len and self.input[self.index] == ',') {
|
||||
self.index += 1;
|
||||
while (self.index < self.input.len and std.ascii.isWhitespace(self.input[self.index])) self.index += 1;
|
||||
} else if (!self.first and self.index < self.input.len and !had_whitespace and
|
||||
self.input[self.index] != '+' and self.input[self.index] != '-')
|
||||
{
|
||||
return error.SyntaxError;
|
||||
}
|
||||
|
||||
if (self.index == self.input.len) return null;
|
||||
const start = self.index;
|
||||
if (self.input[self.index] == '+' or self.input[self.index] == '-') self.index += 1;
|
||||
|
||||
var digits: usize = 0;
|
||||
while (self.index < self.input.len and std.ascii.isDigit(self.input[self.index])) : (self.index += 1) digits += 1;
|
||||
if (self.index < self.input.len and self.input[self.index] == '.') {
|
||||
self.index += 1;
|
||||
while (self.index < self.input.len and std.ascii.isDigit(self.input[self.index])) : (self.index += 1) digits += 1;
|
||||
}
|
||||
if (digits == 0) return error.SyntaxError;
|
||||
|
||||
if (self.index < self.input.len and (self.input[self.index] == 'e' or self.input[self.index] == 'E')) {
|
||||
self.index += 1;
|
||||
if (self.index < self.input.len and (self.input[self.index] == '+' or self.input[self.index] == '-')) self.index += 1;
|
||||
const exponent_start = self.index;
|
||||
while (self.index < self.input.len and std.ascii.isDigit(self.input[self.index])) self.index += 1;
|
||||
if (self.index == exponent_start) return error.SyntaxError;
|
||||
}
|
||||
|
||||
const value = std.fmt.parseFloat(f64, self.input[start..self.index]) catch return error.SyntaxError;
|
||||
if (!std.math.isFinite(value)) return error.SyntaxError;
|
||||
self.first = false;
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(PointList);
|
||||
|
||||
pub const Meta = struct {
|
||||
pub const name = "SVGPointList";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const length = bridge.accessor(PointList.getLength, null, .{});
|
||||
pub const numberOfItems = bridge.accessor(PointList.getNumberOfItems, null, .{});
|
||||
pub const clear = bridge.function(PointList.clear, .{});
|
||||
pub const initialize = bridge.function(PointList.initialize, .{});
|
||||
pub const getItem = bridge.function(PointList.getItem, .{});
|
||||
pub const insertItemBefore = bridge.function(PointList.insertItemBefore, .{});
|
||||
pub const replaceItem = bridge.function(PointList.replaceItem, .{});
|
||||
pub const removeItem = bridge.function(PointList.removeItem, .{});
|
||||
pub const appendItem = bridge.function(PointList.appendItem, .{});
|
||||
};
|
||||
225
src/browser/webapi/svg/StringList.zig
Normal file
225
src/browser/webapi/svg/StringList.zig
Normal file
@@ -0,0 +1,225 @@
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (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");
|
||||
const Element = @import("../Element.zig");
|
||||
|
||||
const StringList = @This();
|
||||
|
||||
pub const Delimiter = enum { whitespace, comma };
|
||||
|
||||
_element: *Element,
|
||||
_attribute_name: lp.String,
|
||||
_delimiter: Delimiter,
|
||||
_synced: bool = false,
|
||||
// An absent attribute is an empty list, but an empty attribute is not: a
|
||||
// comma-separated value parses "" as a single empty token.
|
||||
_present: bool = false,
|
||||
_snapshot: std.ArrayList(u8) = .empty,
|
||||
_items: std.ArrayList([]const u8) = .empty,
|
||||
|
||||
pub const Kind = enum {
|
||||
required_extensions,
|
||||
system_language,
|
||||
|
||||
fn attributeName(self: Kind) lp.String {
|
||||
return switch (self) {
|
||||
.required_extensions => .wrap("requiredExtensions"),
|
||||
.system_language => .wrap("systemLanguage"),
|
||||
};
|
||||
}
|
||||
|
||||
fn delimiter(self: Kind) Delimiter {
|
||||
return switch (self) {
|
||||
.required_extensions => .whitespace,
|
||||
.system_language => .comma,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const Key = struct {
|
||||
element: *Element,
|
||||
kind: Kind,
|
||||
};
|
||||
|
||||
pub const Lookup = std.AutoHashMapUnmanaged(Key, *StringList);
|
||||
|
||||
pub fn getOrCreate(element: *Element, kind: Kind, frame: *Frame) !*StringList {
|
||||
const key: Key = .{
|
||||
.element = element,
|
||||
.kind = kind,
|
||||
};
|
||||
const gop = try frame._svg_string_lists.getOrPut(frame.arena, key);
|
||||
if (!gop.found_existing) {
|
||||
errdefer _ = frame._svg_string_lists.remove(key);
|
||||
gop.value_ptr.* = try frame._factory.create(StringList{
|
||||
._element = element,
|
||||
._attribute_name = kind.attributeName(),
|
||||
._delimiter = kind.delimiter(),
|
||||
});
|
||||
}
|
||||
return gop.value_ptr.*;
|
||||
}
|
||||
|
||||
pub fn getLength(self: *StringList, frame: *Frame) !u32 {
|
||||
try self.sync(frame);
|
||||
return @intCast(self._items.items.len);
|
||||
}
|
||||
|
||||
pub fn getNumberOfItems(self: *StringList, frame: *Frame) !u32 {
|
||||
return self.getLength(frame);
|
||||
}
|
||||
|
||||
pub fn clear(self: *StringList, frame: *Frame) !void {
|
||||
try self.commit(&.{}, frame);
|
||||
}
|
||||
|
||||
pub fn initialize(self: *StringList, item: []const u8, frame: *Frame) ![]const u8 {
|
||||
try self.commit(&.{item}, frame);
|
||||
return item;
|
||||
}
|
||||
|
||||
pub fn getItem(self: *StringList, index: u32, frame: *Frame) ![]const u8 {
|
||||
try self.sync(frame);
|
||||
if (index >= self._items.items.len) return error.IndexSizeError;
|
||||
return self._items.items[index];
|
||||
}
|
||||
|
||||
pub fn insertItemBefore(self: *StringList, item: []const u8, index: u32, frame: *Frame) ![]const u8 {
|
||||
try self.sync(frame);
|
||||
const at = @min(@as(usize, index), self._items.items.len);
|
||||
const next = try frame.local_arena.alloc([]const u8, self._items.items.len + 1);
|
||||
@memcpy(next[0..at], self._items.items[0..at]);
|
||||
next[at] = item;
|
||||
@memcpy(next[at + 1 ..], self._items.items[at..]);
|
||||
try self.commit(next, frame);
|
||||
return item;
|
||||
}
|
||||
|
||||
pub fn replaceItem(self: *StringList, item: []const u8, index: u32, frame: *Frame) ![]const u8 {
|
||||
try self.sync(frame);
|
||||
if (index >= self._items.items.len) return error.IndexSizeError;
|
||||
const next = try frame.local_arena.dupe([]const u8, self._items.items);
|
||||
next[index] = item;
|
||||
try self.commit(next, frame);
|
||||
return item;
|
||||
}
|
||||
|
||||
pub fn removeItem(self: *StringList, index: u32, frame: *Frame) ![]const u8 {
|
||||
try self.sync(frame);
|
||||
if (index >= self._items.items.len) return error.IndexSizeError;
|
||||
const removed = try frame.local_arena.dupe(u8, self._items.items[index]);
|
||||
const next = try frame.local_arena.alloc([]const u8, self._items.items.len - 1);
|
||||
@memcpy(next[0..index], self._items.items[0..index]);
|
||||
@memcpy(next[index..], self._items.items[index + 1 ..]);
|
||||
try self.commit(next, frame);
|
||||
return removed;
|
||||
}
|
||||
|
||||
pub fn appendItem(self: *StringList, item: []const u8, frame: *Frame) ![]const u8 {
|
||||
return self.insertItemBefore(item, std.math.maxInt(u32), frame);
|
||||
}
|
||||
|
||||
fn sync(self: *StringList, frame: *Frame) !void {
|
||||
const raw = self._element.getAttributeSafe(self._attribute_name);
|
||||
if (self._synced and self._present == (raw != null) and
|
||||
std.mem.eql(u8, self._snapshot.items, raw orelse ""))
|
||||
{
|
||||
return;
|
||||
}
|
||||
return self.rebuild(raw, frame);
|
||||
}
|
||||
|
||||
// The list is authoritative: an item keeps its identity even when it contains
|
||||
// the delimiter, so we record where each one landed rather than reparsing our
|
||||
// own serialization.
|
||||
fn commit(self: *StringList, items: []const []const u8, frame: *Frame) !void {
|
||||
if (items.len == 0) {
|
||||
self._synced = false;
|
||||
self._element.removeAttributeSafe(self._attribute_name, frame);
|
||||
self._present = false;
|
||||
self._snapshot.clearRetainingCapacity();
|
||||
self._items.clearRetainingCapacity();
|
||||
self._synced = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const separator: []const u8 = if (self._delimiter == .comma) "," else " ";
|
||||
var serialized: std.Io.Writer.Allocating = .init(frame.local_arena);
|
||||
const writer = &serialized.writer;
|
||||
const bounds = try frame.local_arena.alloc([2]usize, items.len);
|
||||
for (items, bounds, 0..) |item, *bound, i| {
|
||||
if (i != 0) try writer.writeAll(separator);
|
||||
const start = serialized.written().len;
|
||||
try writer.writeAll(item);
|
||||
bound.* = .{ start, serialized.written().len };
|
||||
}
|
||||
const bytes = serialized.written();
|
||||
|
||||
self._synced = false;
|
||||
try self._element.setAttributeSafe(self._attribute_name, .wrap(bytes), frame);
|
||||
self._present = true;
|
||||
self._snapshot.clearRetainingCapacity();
|
||||
try self._snapshot.appendSlice(frame.arena, bytes);
|
||||
self._items.clearRetainingCapacity();
|
||||
for (bounds) |bound| {
|
||||
try self._items.append(frame.arena, self._snapshot.items[bound[0]..bound[1]]);
|
||||
}
|
||||
self._synced = true;
|
||||
}
|
||||
|
||||
fn rebuild(self: *StringList, raw: ?[]const u8, frame: *Frame) !void {
|
||||
self._synced = false;
|
||||
self._present = raw != null;
|
||||
self._snapshot.clearRetainingCapacity();
|
||||
self._items.clearRetainingCapacity();
|
||||
|
||||
if (raw) |value| {
|
||||
try self._snapshot.appendSlice(frame.arena, value);
|
||||
switch (self._delimiter) {
|
||||
.whitespace => {
|
||||
var iterator = std.mem.tokenizeAny(u8, self._snapshot.items, WHITESPACE);
|
||||
while (iterator.next()) |item| try self._items.append(frame.arena, item);
|
||||
},
|
||||
// A set of comma-separated tokens: every segment is a token, even
|
||||
// an empty one, and each is trimmed of surrounding whitespace.
|
||||
.comma => {
|
||||
var iterator = std.mem.splitScalar(u8, self._snapshot.items, ',');
|
||||
while (iterator.next()) |part| {
|
||||
try self._items.append(frame.arena, std.mem.trim(u8, part, WHITESPACE));
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
self._synced = true;
|
||||
}
|
||||
|
||||
const WHITESPACE = " \t\r\n\x0c";
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(StringList);
|
||||
|
||||
pub const Meta = struct {
|
||||
pub const name = "SVGStringList";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const length = bridge.accessor(StringList.getLength, null, .{});
|
||||
pub const numberOfItems = bridge.accessor(StringList.getNumberOfItems, null, .{});
|
||||
pub const clear = bridge.function(StringList.clear, .{});
|
||||
pub const initialize = bridge.function(StringList.initialize, .{});
|
||||
pub const getItem = bridge.function(StringList.getItem, .{});
|
||||
pub const insertItemBefore = bridge.function(StringList.insertItemBefore, .{});
|
||||
pub const replaceItem = bridge.function(StringList.replaceItem, .{});
|
||||
pub const removeItem = bridge.function(StringList.removeItem, .{});
|
||||
pub const appendItem = bridge.function(StringList.appendItem, .{});
|
||||
};
|
||||
@@ -43,7 +43,28 @@ pub const DOMMatrix2DInit = struct {
|
||||
|
||||
_type: u16 = 1,
|
||||
_angle: f64 = 0,
|
||||
_cx: f64 = 0,
|
||||
_cy: f64 = 0,
|
||||
_matrix: *DOMMatrix,
|
||||
_attachment: ?Attachment = null,
|
||||
|
||||
// Sticky. An animVal item that a later external attribute change detaches from
|
||||
// its list must not turn into a writable orphan.
|
||||
_read_only: bool = false,
|
||||
|
||||
pub const State = struct {
|
||||
typ: u16,
|
||||
angle: f64,
|
||||
cx: f64,
|
||||
cy: f64,
|
||||
matrix: [16]f64,
|
||||
is_2d: bool,
|
||||
};
|
||||
|
||||
pub const Attachment = struct {
|
||||
owner: *anyopaque,
|
||||
mutate: *const fn (*anyopaque, *Transform, State) anyerror!void,
|
||||
};
|
||||
|
||||
// The transform owns the matrix arena even when no JS wrapper currently
|
||||
// references `matrix`. Forwarding the transform's bridge lifetime keeps the
|
||||
@@ -58,13 +79,57 @@ pub fn releaseRef(self: *Transform, page: *Page) void {
|
||||
|
||||
pub fn detached(frame: *Frame) !*Transform {
|
||||
const matrix = try DOMMatrix.create(RO.identity(), true, frame._page);
|
||||
return frame._factory.create(Transform{ ._matrix = matrix });
|
||||
errdefer matrix._proto.deinit(frame._page);
|
||||
const self = try frame._factory.create(Transform{ ._matrix = matrix });
|
||||
self.attachMatrix();
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn fromMatrix(init: ?DOMMatrix2DInit, frame: *Frame) !*Transform {
|
||||
const parsed = try fixup2D(init orelse .{});
|
||||
const matrix = try DOMMatrix.create(parsed.m, true, frame._page);
|
||||
return frame._factory.create(Transform{ ._matrix = matrix });
|
||||
errdefer matrix._proto.deinit(frame._page);
|
||||
const self = try frame._factory.create(Transform{ ._matrix = matrix });
|
||||
self.attachMatrix();
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn fromParsed(parsed: RO.ParsedTransform, frame: *Frame) !*Transform {
|
||||
const typ: u16 = switch (parsed.kind) {
|
||||
.matrix => 1,
|
||||
.translate => 2,
|
||||
.scale => 3,
|
||||
.rotate => 4,
|
||||
.skew_x => 5,
|
||||
.skew_y => 6,
|
||||
else => return error.SyntaxError,
|
||||
};
|
||||
const matrix = try DOMMatrix.create(parsed.matrix, parsed.is_2d, frame._page);
|
||||
errdefer matrix._proto.deinit(frame._page);
|
||||
const self = try frame._factory.create(Transform{
|
||||
._type = typ,
|
||||
._angle = if (typ >= 4) parsed.values[0] else 0,
|
||||
._cx = if (typ == 4 and parsed.count == 3) parsed.values[1] else 0,
|
||||
._cy = if (typ == 4 and parsed.count == 3) parsed.values[2] else 0,
|
||||
._matrix = matrix,
|
||||
});
|
||||
self.attachMatrix();
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn clone(self: *const Transform, frame: *Frame) !*Transform {
|
||||
const current = self.getState();
|
||||
const matrix = try DOMMatrix.create(current.matrix, current.is_2d, frame._page);
|
||||
errdefer matrix._proto.deinit(frame._page);
|
||||
const cloned = try frame._factory.create(Transform{
|
||||
._type = current.typ,
|
||||
._angle = current.angle,
|
||||
._cx = current.cx,
|
||||
._cy = current.cy,
|
||||
._matrix = matrix,
|
||||
});
|
||||
cloned.attachMatrix();
|
||||
return cloned;
|
||||
}
|
||||
|
||||
pub fn getType(self: *const Transform) u16 {
|
||||
@@ -81,23 +146,17 @@ pub fn getAngle(self: *const Transform) f64 {
|
||||
|
||||
pub fn setMatrix(self: *Transform, init: ?DOMMatrix2DInit) !void {
|
||||
const parsed = try fixup2D(init orelse .{});
|
||||
self.replaceMatrix(parsed.m, true);
|
||||
self._type = 1;
|
||||
self._angle = 0;
|
||||
try self.applyState(.{ .typ = 1, .angle = 0, .cx = 0, .cy = 0, .matrix = parsed.m, .is_2d = true });
|
||||
}
|
||||
|
||||
pub fn setTranslate(self: *Transform, tx: f64, ty: f64) !void {
|
||||
try ensureFinite(&.{ tx, ty });
|
||||
self.replaceMatrix(RO.translationMatrix(tx, ty, 0), true);
|
||||
self._type = 2;
|
||||
self._angle = 0;
|
||||
try self.applyState(.{ .typ = 2, .angle = 0, .cx = 0, .cy = 0, .matrix = RO.translationMatrix(tx, ty, 0), .is_2d = true });
|
||||
}
|
||||
|
||||
pub fn setScale(self: *Transform, sx: f64, sy: f64) !void {
|
||||
try ensureFinite(&.{ sx, sy });
|
||||
self.replaceMatrix(RO.scaleMatrix(sx, sy, 1), true);
|
||||
self._type = 3;
|
||||
self._angle = 0;
|
||||
try self.applyState(.{ .typ = 3, .angle = 0, .cx = 0, .cy = 0, .matrix = RO.scaleMatrix(sx, sy, 1), .is_2d = true });
|
||||
}
|
||||
|
||||
pub fn setRotate(self: *Transform, angle: f64, cx: f64, cy: f64) !void {
|
||||
@@ -106,32 +165,116 @@ pub fn setRotate(self: *Transform, angle: f64, cx: f64, cy: f64) !void {
|
||||
var matrix = RO.translationMatrix(cx, cy, 0);
|
||||
matrix = RO.multiplyMatrix(matrix, RO.rotateZMatrix(radians));
|
||||
matrix = RO.multiplyMatrix(matrix, RO.translationMatrix(-cx, -cy, 0));
|
||||
self.replaceMatrix(matrix, true);
|
||||
self._type = 4;
|
||||
self._angle = angle;
|
||||
try self.applyState(.{ .typ = 4, .angle = angle, .cx = cx, .cy = cy, .matrix = matrix, .is_2d = true });
|
||||
}
|
||||
|
||||
pub fn setSkewX(self: *Transform, angle: f64) !void {
|
||||
try ensureFinite(&.{angle});
|
||||
self.replaceMatrix(RO.skewMatrix(angle * std.math.pi / 180.0, 0), true);
|
||||
self._type = 5;
|
||||
self._angle = angle;
|
||||
try self.applyState(.{ .typ = 5, .angle = angle, .cx = 0, .cy = 0, .matrix = RO.skewMatrix(angle * std.math.pi / 180.0, 0), .is_2d = true });
|
||||
}
|
||||
|
||||
pub fn setSkewY(self: *Transform, angle: f64) !void {
|
||||
try ensureFinite(&.{angle});
|
||||
self.replaceMatrix(RO.skewMatrix(0, angle * std.math.pi / 180.0), true);
|
||||
self._type = 6;
|
||||
self._angle = angle;
|
||||
try self.applyState(.{ .typ = 6, .angle = angle, .cx = 0, .cy = 0, .matrix = RO.skewMatrix(0, angle * std.math.pi / 180.0), .is_2d = true });
|
||||
}
|
||||
|
||||
fn replaceMatrix(self: *Transform, matrix: [16]f64, is_2d: bool) void {
|
||||
self._matrix._proto._m = matrix;
|
||||
self._matrix._proto._is_2d = is_2d;
|
||||
pub fn getState(self: *const Transform) State {
|
||||
return .{
|
||||
.typ = self._type,
|
||||
.angle = self._angle,
|
||||
.cx = self._cx,
|
||||
.cy = self._cy,
|
||||
.matrix = self._matrix._proto._m,
|
||||
.is_2d = self._matrix._proto._is_2d,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn applyStateRaw(self: *Transform, state: State) void {
|
||||
self._type = state.typ;
|
||||
self._angle = state.angle;
|
||||
self._cx = state.cx;
|
||||
self._cy = state.cy;
|
||||
self._matrix._proto.applyStateRaw(.{
|
||||
.matrix = state.matrix,
|
||||
.is_2d = state.is_2d,
|
||||
});
|
||||
}
|
||||
|
||||
fn applyState(self: *Transform, state: State) !void {
|
||||
if (self._read_only) return error.NoModificationAllowed;
|
||||
try ensureFinite(&state.matrix);
|
||||
if (self._attachment) |attachment| {
|
||||
return attachment.mutate(attachment.owner, self, state);
|
||||
}
|
||||
self.applyStateRaw(state);
|
||||
}
|
||||
|
||||
pub fn attach(self: *Transform, attachment: Attachment, read_only: bool) void {
|
||||
self._attachment = attachment;
|
||||
if (read_only) self._read_only = true;
|
||||
}
|
||||
|
||||
pub fn detach(self: *Transform, owner: *anyopaque) void {
|
||||
const attachment = self._attachment orelse return;
|
||||
if (attachment.owner == owner) self._attachment = null;
|
||||
}
|
||||
|
||||
pub fn isAttached(self: *const Transform) bool {
|
||||
return self._attachment != null;
|
||||
}
|
||||
|
||||
pub fn isAttachedTo(self: *const Transform, owner: *anyopaque) bool {
|
||||
const attachment = self._attachment orelse return false;
|
||||
return attachment.owner == owner;
|
||||
}
|
||||
|
||||
pub fn writeState(state: State, writer: anytype) !void {
|
||||
switch (state.typ) {
|
||||
1 => try writer.print("matrix({d} {d} {d} {d} {d} {d})", .{
|
||||
state.matrix[0], state.matrix[1], state.matrix[4], state.matrix[5], state.matrix[12], state.matrix[13],
|
||||
}),
|
||||
2 => try writer.print("translate({d} {d})", .{ state.matrix[12], state.matrix[13] }),
|
||||
3 => try writer.print("scale({d} {d})", .{ state.matrix[0], state.matrix[5] }),
|
||||
4 => if (state.cx == 0 and state.cy == 0)
|
||||
try writer.print("rotate({d})", .{state.angle})
|
||||
else
|
||||
try writer.print("rotate({d} {d} {d})", .{ state.angle, state.cx, state.cy }),
|
||||
5 => try writer.print("skewX({d})", .{state.angle}),
|
||||
6 => try writer.print("skewY({d})", .{state.angle}),
|
||||
else => return error.SyntaxError,
|
||||
}
|
||||
}
|
||||
|
||||
fn attachMatrix(self: *Transform) void {
|
||||
self._matrix._proto.attach(.{
|
||||
.owner = self,
|
||||
.mutate = Transform.mutateMatrix,
|
||||
});
|
||||
}
|
||||
|
||||
// An SVGTransform is a 2D affine transform, and the `transform` attribute has
|
||||
// no syntax for anything else, so a write through the live matrix keeps only
|
||||
// the six components the transform can represent.
|
||||
fn mutateMatrix(context: *anyopaque, _: *RO, state: RO.State) anyerror!void {
|
||||
const self: *Transform = @ptrCast(@alignCast(context));
|
||||
const m = state.matrix;
|
||||
try self.applyState(.{
|
||||
.typ = 1,
|
||||
.angle = 0,
|
||||
.cx = 0,
|
||||
.cy = 0,
|
||||
.matrix = .{
|
||||
m[0], m[1], 0, 0,
|
||||
m[4], m[5], 0, 0,
|
||||
0, 0, 1, 0,
|
||||
m[12], m[13], 0, 1,
|
||||
},
|
||||
.is_2d = true,
|
||||
});
|
||||
}
|
||||
|
||||
fn fixup2D(init: DOMMatrix2DInit) !RO.Parsed {
|
||||
return RO.fixupDict(.{
|
||||
const parsed = try RO.fixupDict(.{
|
||||
.a = init.a,
|
||||
.b = init.b,
|
||||
.c = init.c,
|
||||
@@ -146,6 +289,8 @@ fn fixup2D(init: DOMMatrix2DInit) !RO.Parsed {
|
||||
.m42 = init.m42,
|
||||
.is2D = true,
|
||||
});
|
||||
try ensureFinite(&parsed.m);
|
||||
return parsed;
|
||||
}
|
||||
|
||||
fn ensureFinite(values: []const f64) !void {
|
||||
|
||||
313
src/browser/webapi/svg/TransformList.zig
Normal file
313
src/browser/webapi/svg/TransformList.zig
Normal file
@@ -0,0 +1,313 @@
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const js = @import("../../js/js.zig");
|
||||
const Frame = @import("../../Frame.zig");
|
||||
const Page = @import("../../Page.zig");
|
||||
const DOMMatrixReadOnly = @import("../DOMMatrixReadOnly.zig");
|
||||
const Element = @import("../Element.zig");
|
||||
const Transform = @import("Transform.zig");
|
||||
|
||||
const TransformList = @This();
|
||||
|
||||
_frame: *Frame,
|
||||
_read_only: bool,
|
||||
_element: *Element,
|
||||
_synced: bool = false,
|
||||
_snapshot: std.ArrayList(u8) = .empty,
|
||||
_items: std.ArrayList(*Transform) = .empty,
|
||||
_retired: std.ArrayList(*Transform) = .empty,
|
||||
|
||||
pub fn create(element: *Element, read_only: bool, frame: *Frame) !*TransformList {
|
||||
return frame._factory.create(TransformList{
|
||||
._frame = frame,
|
||||
._element = element,
|
||||
._read_only = read_only,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn deinit(self: *TransformList, page: *Page) void {
|
||||
for (self._items.items) |transform| {
|
||||
transform.detach(self);
|
||||
transform.releaseRef(page);
|
||||
}
|
||||
self._items.clearRetainingCapacity();
|
||||
self.releaseRetired(page);
|
||||
}
|
||||
|
||||
pub fn getLength(self: *TransformList, frame: *Frame) !u32 {
|
||||
try self.sync(frame);
|
||||
return @intCast(self._items.items.len);
|
||||
}
|
||||
|
||||
pub fn getNumberOfItems(self: *TransformList, frame: *Frame) !u32 {
|
||||
return self.getLength(frame);
|
||||
}
|
||||
|
||||
pub fn clear(self: *TransformList, frame: *Frame) !void {
|
||||
try self.requireMutable();
|
||||
try self.sync(frame);
|
||||
try self.retireAll(frame);
|
||||
try self.setAttribute(&.{}, frame);
|
||||
}
|
||||
|
||||
pub fn initialize(self: *TransformList, item: *Transform, frame: *Frame) !*Transform {
|
||||
try self.requireMutable();
|
||||
try self.sync(frame);
|
||||
const prepared = try self.prepareItem(item, frame);
|
||||
errdefer prepared.releaseRef(frame._page);
|
||||
|
||||
try self.retireAll(frame);
|
||||
try self._items.ensureTotalCapacity(frame.arena, 1);
|
||||
try self.setAttribute(&.{prepared}, frame);
|
||||
self._items.appendAssumeCapacity(prepared);
|
||||
self.attach(prepared);
|
||||
return prepared;
|
||||
}
|
||||
|
||||
pub fn getItem(self: *TransformList, index: u32, frame: *Frame) !*Transform {
|
||||
try self.sync(frame);
|
||||
if (index >= self._items.items.len) return error.IndexSizeError;
|
||||
return self._items.items[index];
|
||||
}
|
||||
|
||||
pub fn insertItemBefore(self: *TransformList, item: *Transform, index: u32, frame: *Frame) !*Transform {
|
||||
try self.requireMutable();
|
||||
try self.sync(frame);
|
||||
const prepared = try self.prepareItem(item, frame);
|
||||
errdefer prepared.releaseRef(frame._page);
|
||||
const at = @min(@as(usize, index), self._items.items.len);
|
||||
const next = try frame.local_arena.alloc(*Transform, self._items.items.len + 1);
|
||||
@memcpy(next[0..at], self._items.items[0..at]);
|
||||
next[at] = prepared;
|
||||
@memcpy(next[at + 1 ..], self._items.items[at..]);
|
||||
|
||||
try self._items.ensureUnusedCapacity(frame.arena, 1);
|
||||
try self.setAttribute(next, frame);
|
||||
self._items.insertAssumeCapacity(at, prepared);
|
||||
self.attach(prepared);
|
||||
return prepared;
|
||||
}
|
||||
|
||||
pub fn replaceItem(self: *TransformList, item: *Transform, index: u32, frame: *Frame) !*Transform {
|
||||
try self.requireMutable();
|
||||
try self.sync(frame);
|
||||
if (index >= self._items.items.len) return error.IndexSizeError;
|
||||
const prepared = try self.prepareItem(item, frame);
|
||||
errdefer prepared.releaseRef(frame._page);
|
||||
const next = try frame.local_arena.dupe(*Transform, self._items.items);
|
||||
next[index] = prepared;
|
||||
|
||||
try self._retired.ensureUnusedCapacity(frame.arena, 1);
|
||||
try self.setAttribute(next, frame);
|
||||
const replaced = self._items.items[index];
|
||||
replaced.detach(self);
|
||||
self._retired.appendAssumeCapacity(replaced);
|
||||
self._items.items[index] = prepared;
|
||||
self.attach(prepared);
|
||||
return prepared;
|
||||
}
|
||||
|
||||
pub fn removeItem(self: *TransformList, index: u32, frame: *Frame) !*Transform {
|
||||
try self.requireMutable();
|
||||
try self.sync(frame);
|
||||
if (index >= self._items.items.len) return error.IndexSizeError;
|
||||
const next = try frame.local_arena.alloc(*Transform, self._items.items.len - 1);
|
||||
@memcpy(next[0..index], self._items.items[0..index]);
|
||||
@memcpy(next[index..], self._items.items[index + 1 ..]);
|
||||
|
||||
try self._retired.ensureUnusedCapacity(frame.arena, 1);
|
||||
try self.setAttribute(next, frame);
|
||||
const removed = self._items.orderedRemove(index);
|
||||
removed.detach(self);
|
||||
self._retired.appendAssumeCapacity(removed);
|
||||
return removed;
|
||||
}
|
||||
|
||||
pub fn appendItem(self: *TransformList, item: *Transform, frame: *Frame) !*Transform {
|
||||
return self.insertItemBefore(item, std.math.maxInt(u32), frame);
|
||||
}
|
||||
|
||||
pub fn consolidate(self: *TransformList, frame: *Frame) !?*Transform {
|
||||
try self.requireMutable();
|
||||
try self.sync(frame);
|
||||
if (self._items.items.len == 0) return null;
|
||||
|
||||
var matrix = DOMMatrixReadOnly.identity();
|
||||
for (self._items.items) |item| matrix = DOMMatrixReadOnly.multiplyMatrix(matrix, item.getState().matrix);
|
||||
for (matrix) |value| if (!std.math.isFinite(value)) return error.TypeError;
|
||||
var values: [16]f64 = undefined;
|
||||
values[0] = matrix[0];
|
||||
values[1] = matrix[1];
|
||||
values[2] = matrix[4];
|
||||
values[3] = matrix[5];
|
||||
values[4] = matrix[12];
|
||||
values[5] = matrix[13];
|
||||
const consolidated = try Transform.fromParsed(.{
|
||||
.kind = .matrix,
|
||||
.matrix = matrix,
|
||||
.values = values,
|
||||
.count = 6,
|
||||
.is_2d = true,
|
||||
}, frame);
|
||||
consolidated.acquireRef();
|
||||
errdefer consolidated.releaseRef(frame._page);
|
||||
|
||||
try self.retireAll(frame);
|
||||
try self._items.ensureTotalCapacity(frame.arena, 1);
|
||||
try self.setAttribute(&.{consolidated}, frame);
|
||||
self._items.appendAssumeCapacity(consolidated);
|
||||
self.attach(consolidated);
|
||||
return consolidated;
|
||||
}
|
||||
|
||||
fn requireMutable(self: *const TransformList) !void {
|
||||
if (self._read_only) return error.NoModificationAllowed;
|
||||
}
|
||||
|
||||
fn prepareItem(_: *TransformList, item: *Transform, frame: *Frame) !*Transform {
|
||||
const prepared = if (item.isAttached()) try item.clone(frame) else item;
|
||||
prepared.acquireRef();
|
||||
return prepared;
|
||||
}
|
||||
|
||||
fn attach(self: *TransformList, transform: *Transform) void {
|
||||
transform.attach(.{
|
||||
.owner = self,
|
||||
.mutate = TransformList.mutateTransform,
|
||||
}, self._read_only);
|
||||
}
|
||||
|
||||
fn mutateTransform(context: *anyopaque, transform: *Transform, state: Transform.State) anyerror!void {
|
||||
const self: *TransformList = @ptrCast(@alignCast(context));
|
||||
const frame = self._frame;
|
||||
try self.sync(frame);
|
||||
if (!transform.isAttachedTo(self)) {
|
||||
transform.applyStateRaw(state);
|
||||
return;
|
||||
}
|
||||
const index = for (self._items.items, 0..) |candidate, i| {
|
||||
if (candidate == transform) break i;
|
||||
} else unreachable;
|
||||
try self.setAttributeWithOverride(index, state, frame);
|
||||
transform.applyStateRaw(state);
|
||||
}
|
||||
|
||||
fn sync(self: *TransformList, frame: *Frame) !void {
|
||||
self.releaseRetired(frame._page);
|
||||
|
||||
const raw = self._element.getAttributeSafe(comptime .wrap("transform")) orelse "";
|
||||
if (self._synced and std.mem.eql(u8, self._snapshot.items, raw)) return;
|
||||
|
||||
self._synced = false;
|
||||
var parsed = parse(raw, frame) catch |err| switch (err) {
|
||||
error.SyntaxError => std.ArrayList(*Transform).empty,
|
||||
else => return err,
|
||||
};
|
||||
errdefer for (parsed.items) |transform| transform.releaseRef(frame._page);
|
||||
|
||||
self._snapshot.clearRetainingCapacity();
|
||||
try self._snapshot.appendSlice(frame.arena, raw);
|
||||
try self.retireAll(frame);
|
||||
try self._items.ensureTotalCapacity(frame.arena, parsed.items.len);
|
||||
for (parsed.items) |transform| {
|
||||
self._items.appendAssumeCapacity(transform);
|
||||
self.attach(transform);
|
||||
}
|
||||
parsed.clearRetainingCapacity();
|
||||
self._synced = true;
|
||||
}
|
||||
|
||||
// A retired item must outlive the operation that retired it: removeItem's
|
||||
// return value has no JS wrapper until the bridge wraps it after we return.
|
||||
// By the next operation, anything still reachable holds its own ref.
|
||||
fn releaseRetired(self: *TransformList, page: *Page) void {
|
||||
for (self._retired.items) |transform| {
|
||||
transform.releaseRef(page);
|
||||
}
|
||||
self._retired.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
fn parse(raw: []const u8, frame: *Frame) !std.ArrayList(*Transform) {
|
||||
var parsed: std.ArrayList(*Transform) = .empty;
|
||||
errdefer for (parsed.items) |transform| transform.releaseRef(frame._page);
|
||||
const trimmed = std.mem.trim(u8, raw, " \t\r\n");
|
||||
if (trimmed.len == 0 or std.mem.eql(u8, trimmed, "none")) return parsed;
|
||||
|
||||
var iterator = DOMMatrixReadOnly.TransformFunctionIterator{ .input = trimmed, .allow_comma = true };
|
||||
while (try iterator.next()) |function| {
|
||||
const value = try DOMMatrixReadOnly.parseTransformFunction(function, .svg);
|
||||
const transform = try Transform.fromParsed(value, frame);
|
||||
transform.acquireRef();
|
||||
parsed.append(frame.local_arena, transform) catch |err| {
|
||||
transform.releaseRef(frame._page);
|
||||
return err;
|
||||
};
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
fn retireAll(self: *TransformList, frame: *Frame) !void {
|
||||
self._synced = false;
|
||||
try self._retired.ensureUnusedCapacity(frame.arena, self._items.items.len);
|
||||
for (self._items.items) |transform| {
|
||||
transform.detach(self);
|
||||
self._retired.appendAssumeCapacity(transform);
|
||||
}
|
||||
self._items.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
fn setAttribute(self: *TransformList, items: []const *Transform, frame: *Frame) !void {
|
||||
var serialized: std.Io.Writer.Allocating = .init(frame.local_arena);
|
||||
const writer = &serialized.writer;
|
||||
for (items, 0..) |transform, i| {
|
||||
if (i != 0) try writer.writeByte(' ');
|
||||
try Transform.writeState(transform.getState(), writer);
|
||||
}
|
||||
try self.commitAttribute(serialized.written(), frame);
|
||||
}
|
||||
|
||||
fn setAttributeWithOverride(self: *TransformList, index: usize, state: Transform.State, frame: *Frame) !void {
|
||||
var serialized: std.Io.Writer.Allocating = .init(frame.local_arena);
|
||||
const writer = &serialized.writer;
|
||||
for (self._items.items, 0..) |transform, i| {
|
||||
if (i != 0) try writer.writeByte(' ');
|
||||
try Transform.writeState(if (i == index) state else transform.getState(), writer);
|
||||
}
|
||||
try self.commitAttribute(serialized.written(), frame);
|
||||
}
|
||||
|
||||
fn commitAttribute(self: *TransformList, serialized: []const u8, frame: *Frame) !void {
|
||||
self._synced = false;
|
||||
try self._element.setAttributeSafe(comptime .wrap("transform"), .wrap(serialized), frame);
|
||||
self._snapshot.clearRetainingCapacity();
|
||||
try self._snapshot.appendSlice(frame.arena, serialized);
|
||||
self._synced = true;
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(TransformList);
|
||||
|
||||
pub const Meta = struct {
|
||||
pub const name = "SVGTransformList";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const length = bridge.accessor(TransformList.getLength, null, .{});
|
||||
pub const numberOfItems = bridge.accessor(TransformList.getNumberOfItems, null, .{});
|
||||
pub const clear = bridge.function(TransformList.clear, .{});
|
||||
pub const initialize = bridge.function(TransformList.initialize, .{});
|
||||
pub const getItem = bridge.function(TransformList.getItem, .{});
|
||||
pub const insertItemBefore = bridge.function(TransformList.insertItemBefore, .{});
|
||||
pub const replaceItem = bridge.function(TransformList.replaceItem, .{});
|
||||
pub const removeItem = bridge.function(TransformList.removeItem, .{});
|
||||
pub const appendItem = bridge.function(TransformList.appendItem, .{});
|
||||
pub const consolidate = bridge.function(TransformList.consolidate, .{});
|
||||
};
|
||||
Reference in New Issue
Block a user