Dedupe PointLIst and TransformList

Add an ugly mixin. Ensures the two implementations are the same.
This commit is contained in:
Karl Seguin
2026-07-29 15:56:00 +08:00
parent 93d2305041
commit 36cbf1747d
3 changed files with 302 additions and 343 deletions

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../../js/js.zig");
const Frame = @import("../../Frame.zig");
@@ -24,6 +25,7 @@ const Page = @import("../../Page.zig");
const DOMPoint = @import("../DOMPoint.zig");
const DOMPointReadOnly = @import("../DOMPointReadOnly.zig");
const Element = @import("../Element.zig");
const reflected_list = @import("reflected_list.zig");
const PointList = @This();
@@ -35,6 +37,16 @@ _snapshot: std.ArrayList(u8) = .empty,
_items: std.ArrayList(*DOMPoint) = .empty,
_retired: std.ArrayList(*DOMPoint) = .empty,
const M = reflected_list.Mixin(PointList, DOMPoint, .{
.attrName = attrName,
.parse = parse,
.writeItem = writeItem,
.prepareItem = prepareItem,
.attach = attach,
.detachItem = detachItem,
.releaseItem = releaseItem,
});
pub const Kind = enum { base, animated };
pub const Key = struct {
@@ -61,114 +73,19 @@ pub fn getOrCreate(element: *Element, kind: Kind, frame: *Frame) !*PointList {
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 const deinit = M.deinit;
pub const getLength = M.getLength;
pub const getNumberOfItems = M.getNumberOfItems;
pub const clear = M.clear;
pub const initialize = M.initialize;
pub const getItem = M.getItem;
pub const insertItemBefore = M.insertItemBefore;
pub const replaceItem = M.replaceItem;
pub const removeItem = M.removeItem;
pub const appendItem = M.appendItem;
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 attrName(_: *const PointList) lp.String {
return .wrap("points");
}
fn prepareItem(_: *PointList, item: *DOMPoint, frame: *Frame) !*DOMPoint {
@@ -189,6 +106,14 @@ fn attach(self: *PointList, point: *DOMPoint) void {
}, self._read_only);
}
fn detachItem(point: *DOMPoint, owner: *PointList) void {
point._proto.detach(owner);
}
fn releaseItem(point: *DOMPoint, page: *Page) void {
point._proto.releaseRef(page);
}
fn mutatePoint(
context: *anyopaque,
point: *DOMPointReadOnly,
@@ -197,7 +122,7 @@ fn mutatePoint(
) anyerror!void {
const self: *PointList = @ptrCast(@alignCast(context));
const frame = self._frame;
try self.sync(frame);
try M.sync(self, 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.
@@ -220,43 +145,6 @@ fn mutatePoint(
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;
@@ -276,24 +164,8 @@ fn parse(raw: []const u8, frame: *Frame) !std.ArrayList(*DOMPoint) {
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 writeItem(point: *DOMPoint, writer: *std.Io.Writer) !void {
try writer.print("{d} {d}", .{ point._proto._x, point._proto._y });
}
fn setAttributeWithOverride(
@@ -311,15 +183,7 @@ fn setAttributeWithOverride(
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;
try M.commitAttribute(self, serialized.written(), frame);
}
const NumberScanner = struct {

View File

@@ -25,6 +25,7 @@ const Page = @import("../../Page.zig");
const DOMMatrixReadOnly = @import("../DOMMatrixReadOnly.zig");
const Element = @import("../Element.zig");
const Transform = @import("Transform.zig");
const reflected_list = @import("reflected_list.zig");
const TransformList = @This();
@@ -37,6 +38,16 @@ _snapshot: std.ArrayList(u8) = .empty,
_items: std.ArrayList(*Transform) = .empty,
_retired: std.ArrayList(*Transform) = .empty,
const M = reflected_list.Mixin(TransformList, Transform, .{
.attrName = attrName,
.parse = parse,
.writeItem = writeItem,
.prepareItem = prepareItem,
.attach = attach,
.detachItem = detachItem,
.releaseItem = releaseItem,
});
pub fn createForAttribute(element: *Element, attr_name: lp.String, read_only: bool, frame: *Frame) !*TransformList {
return frame._factory.create(TransformList{
._frame = frame,
@@ -46,111 +57,20 @@ pub fn createForAttribute(element: *Element, attr_name: lp.String, read_only: bo
});
}
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 const deinit = M.deinit;
pub const getLength = M.getLength;
pub const getNumberOfItems = M.getNumberOfItems;
pub const clear = M.clear;
pub const initialize = M.initialize;
pub const getItem = M.getItem;
pub const insertItemBefore = M.insertItemBefore;
pub const replaceItem = M.replaceItem;
pub const removeItem = M.removeItem;
pub const appendItem = M.appendItem;
pub fn consolidate(self: *TransformList, frame: *Frame) !?*Transform {
try self.requireMutable();
try self.sync(frame);
try M.requireMutable(self);
try M.sync(self, frame);
if (self._items.items.len == 0) return null;
var matrix = DOMMatrixReadOnly.identity();
@@ -173,16 +93,16 @@ pub fn consolidate(self: *TransformList, frame: *Frame) !?*Transform {
consolidated.acquireRef();
errdefer consolidated.releaseRef(frame._page);
try self.retireAll(frame);
try M.retireAll(self, frame);
try self._items.ensureTotalCapacity(frame.arena, 1);
try self.setAttribute(&.{consolidated}, frame);
try M.setAttribute(self, &.{consolidated}, frame);
self._items.appendAssumeCapacity(consolidated);
self.attach(consolidated);
attach(self, consolidated);
return consolidated;
}
fn requireMutable(self: *const TransformList) !void {
if (self._read_only) return error.NoModificationAllowed;
fn attrName(self: *const TransformList) lp.String {
return self._attr_name;
}
fn prepareItem(_: *TransformList, item: *Transform, frame: *Frame) !*Transform {
@@ -198,10 +118,18 @@ fn attach(self: *TransformList, transform: *Transform) void {
}, self._read_only);
}
fn detachItem(transform: *Transform, owner: *TransformList) void {
transform.detach(owner);
}
fn releaseItem(transform: *Transform, page: *Page) void {
transform.releaseRef(page);
}
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);
try M.sync(self, frame);
if (!transform.isAttachedTo(self)) {
transform.applyStateRaw(state);
return;
@@ -213,41 +141,6 @@ fn mutateTransform(context: *anyopaque, transform: *Transform, state: Transform.
transform.applyStateRaw(state);
}
fn sync(self: *TransformList, frame: *Frame) !void {
self.releaseRetired(frame._page);
const raw = self._element.getAttributeSafe(self._attr_name) orelse "";
if (self._synced and std.mem.eql(u8, self._snapshot.items, raw)) return;
self._synced = false;
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);
@@ -267,24 +160,8 @@ fn parse(raw: []const u8, frame: *Frame) !std.ArrayList(*Transform) {
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 writeItem(transform: *Transform, writer: *std.Io.Writer) !void {
try Transform.writeState(transform.getState(), writer);
}
fn setAttributeWithOverride(self: *TransformList, index: usize, state: Transform.State, frame: *Frame) !void {
@@ -294,15 +171,7 @@ fn setAttributeWithOverride(self: *TransformList, index: usize, state: Transform
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(self._attr_name, .wrap(serialized), frame);
self._snapshot.clearRetainingCapacity();
try self._snapshot.appendSlice(frame.arena, serialized);
self._synced = true;
try M.commitAttribute(self, serialized.written(), frame);
}
pub const JsApi = struct {

View File

@@ -0,0 +1,226 @@
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// 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.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//! Shared machinery for the attribute-reflecting SVG list interfaces whose
//! items are live, ref-counted objects (SVGPointList, SVGTransformList):
//! attribute-snapshot sync, the retire/release item lifetime protocol, and
//! the DOM list operations. The concrete list supplies the item specifics
//! via `hooks` and re-exports the operations as decl aliases.
//!
//! StringList reflects an attribute too but is deliberately not built on
//! this: its items are plain slices (no ref counting, no attach/retire) and
//! its commit path is list-authoritative rather than reparse-based.
//!
//! `List` must carry the `_frame`, `_element`, `_read_only`, `_synced`,
//! `_snapshot`, `_items` and `_retired` fields. `hooks` must provide:
//! attrName(*const List) String
//! parse([]const u8, *Frame) !std.ArrayList(*Item) (SyntaxError -> empty list)
//! writeItem(*Item, *std.Io.Writer) !void
//! prepareItem(*List, *Item, *Frame) !*Item
//! attach(*List, *Item) void
//! detachItem(*Item, *List) void
//! releaseItem(*Item, *Page) void
//!
//! The operations keep a strict commit order: reserve list capacity, write
//! the attribute, then apply the infallible list update — so a failed
//! setAttribute never leaves the mirror out of step with the attribute.
const std = @import("std");
const Frame = @import("../../Frame.zig");
const Page = @import("../../Page.zig");
pub fn Mixin(comptime List: type, comptime Item: type, comptime hooks: anytype) type {
return struct {
pub fn deinit(self: *List, page: *Page) void {
for (self._items.items) |item| {
hooks.detachItem(item, self);
hooks.releaseItem(item, page);
}
self._items.clearRetainingCapacity();
releaseRetired(self, page);
}
pub fn getLength(self: *List, frame: *Frame) !u32 {
try sync(self, frame);
return @intCast(self._items.items.len);
}
pub fn getNumberOfItems(self: *List, frame: *Frame) !u32 {
return getLength(self, frame);
}
pub fn clear(self: *List, frame: *Frame) !void {
try requireMutable(self);
try sync(self, frame);
try retireAll(self, frame);
try setAttribute(self, &.{}, frame);
}
pub fn initialize(self: *List, item: *Item, frame: *Frame) !*Item {
try requireMutable(self);
try sync(self, frame);
const prepared = try hooks.prepareItem(self, item, frame);
errdefer hooks.releaseItem(prepared, frame._page);
try retireAll(self, frame);
try self._items.ensureTotalCapacity(frame.arena, 1);
try setAttribute(self, &.{prepared}, frame);
self._items.appendAssumeCapacity(prepared);
hooks.attach(self, prepared);
return prepared;
}
pub fn getItem(self: *List, index: u32, frame: *Frame) !*Item {
try sync(self, frame);
if (index >= self._items.items.len) return error.IndexSizeError;
return self._items.items[index];
}
pub fn insertItemBefore(self: *List, item: *Item, index: u32, frame: *Frame) !*Item {
try requireMutable(self);
try sync(self, frame);
const prepared = try hooks.prepareItem(self, item, frame);
errdefer hooks.releaseItem(prepared, frame._page);
const at = @min(@as(usize, index), self._items.items.len);
const next = try frame.local_arena.alloc(*Item, 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 setAttribute(self, next, frame);
self._items.insertAssumeCapacity(at, prepared);
hooks.attach(self, prepared);
return prepared;
}
pub fn replaceItem(self: *List, item: *Item, index: u32, frame: *Frame) !*Item {
try requireMutable(self);
try sync(self, frame);
if (index >= self._items.items.len) return error.IndexSizeError;
const prepared = try hooks.prepareItem(self, item, frame);
errdefer hooks.releaseItem(prepared, frame._page);
const next = try frame.local_arena.dupe(*Item, self._items.items);
next[index] = prepared;
try self._retired.ensureUnusedCapacity(frame.arena, 1);
try setAttribute(self, next, frame);
const replaced = self._items.items[index];
hooks.detachItem(replaced, self);
self._retired.appendAssumeCapacity(replaced);
self._items.items[index] = prepared;
hooks.attach(self, prepared);
return prepared;
}
pub fn removeItem(self: *List, index: u32, frame: *Frame) !*Item {
try requireMutable(self);
try sync(self, frame);
if (index >= self._items.items.len) return error.IndexSizeError;
const next = try frame.local_arena.alloc(*Item, 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 setAttribute(self, next, frame);
const removed = self._items.orderedRemove(index);
hooks.detachItem(removed, self);
self._retired.appendAssumeCapacity(removed);
return removed;
}
pub fn appendItem(self: *List, item: *Item, frame: *Frame) !*Item {
return insertItemBefore(self, item, std.math.maxInt(u32), frame);
}
pub fn requireMutable(self: *const List) !void {
if (self._read_only) return error.NoModificationAllowed;
}
pub fn sync(self: *List, frame: *Frame) !void {
releaseRetired(self, frame._page);
const raw = self._element.getAttributeSafe(hooks.attrName(self)) orelse "";
if (self._synced and std.mem.eql(u8, self._snapshot.items, raw)) {
return;
}
self._synced = false;
var parsed = hooks.parse(raw, frame) catch |err| switch (err) {
error.SyntaxError => std.ArrayList(*Item).empty,
else => return err,
};
errdefer for (parsed.items) |item| hooks.releaseItem(item, frame._page);
self._snapshot.clearRetainingCapacity();
try self._snapshot.appendSlice(frame.arena, raw);
try retireAll(self, frame);
try self._items.ensureTotalCapacity(frame.arena, parsed.items.len);
for (parsed.items) |item| {
self._items.appendAssumeCapacity(item);
hooks.attach(self, item);
}
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.
pub fn releaseRetired(self: *List, page: *Page) void {
for (self._retired.items) |item| {
hooks.releaseItem(item, page);
}
self._retired.clearRetainingCapacity();
}
pub fn retireAll(self: *List, frame: *Frame) !void {
self._synced = false;
try self._retired.ensureUnusedCapacity(frame.arena, self._items.items.len);
for (self._items.items) |item| {
hooks.detachItem(item, self);
self._retired.appendAssumeCapacity(item);
}
self._items.clearRetainingCapacity();
}
pub fn setAttribute(self: *List, items: []const *Item, frame: *Frame) !void {
var serialized: std.Io.Writer.Allocating = .init(frame.local_arena);
const writer = &serialized.writer;
for (items, 0..) |item, i| {
if (i != 0) try writer.writeByte(' ');
try hooks.writeItem(item, writer);
}
try commitAttribute(self, serialized.written(), frame);
}
pub fn commitAttribute(self: *List, serialized: []const u8, frame: *Frame) !void {
self._synced = false;
try self._element.setAttributeSafe(hooks.attrName(self), .wrap(serialized), frame);
self._snapshot.clearRetainingCapacity();
try self._snapshot.appendSlice(frame.arena, serialized);
self._synced = true;
}
};
}