From 89ac27ba97d58fa7c079e0f539a8a40dd39f46f9 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 1 May 2025 18:28:40 +0800 Subject: [PATCH] Add CustomEvent api --- src/browser/events/custom_event.zig | 81 +++++++++++++++++++++++++++++ src/browser/events/event.zig | 7 ++- src/browser/netsurf.zig | 1 + src/runtime/js.zig | 56 +++++++++++++++++--- 4 files changed, 135 insertions(+), 10 deletions(-) create mode 100644 src/browser/events/custom_event.zig diff --git a/src/browser/events/custom_event.zig b/src/browser/events/custom_event.zig new file mode 100644 index 00000000..98318f9e --- /dev/null +++ b/src/browser/events/custom_event.zig @@ -0,0 +1,81 @@ +// Copyright (C) 2023-2024 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// 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 . + +const std = @import("std"); + +const parser = @import("../netsurf.zig"); +const Event = @import("event.zig").Event; +const JsObject = @import("../env.zig").JsObject; + +// https://dom.spec.whatwg.org/#interface-customevent +pub const CustomEvent = struct { + pub const prototype = *Event; + + proto: parser.Event, + detail: ?JsObject, + + const CustomEventInit = struct { + bubbles: bool = false, + cancelable: bool = false, + composed: bool = false, + detail: ?JsObject = null, + }; + + pub fn constructor(event_type: []const u8, opts_: ?CustomEventInit) !CustomEvent { + const opts = opts_ orelse CustomEventInit{}; + + const event = try parser.eventCreate(); + defer parser.eventDestroy(event); + try parser.eventInit(event, event_type, .{ + .bubbles = opts.bubbles, + .cancelable = opts.cancelable, + .composed = opts.composed, + }); + + return .{ + .proto = event.*, + .detail = if (opts.detail) |d| try d.persist() else null, + }; + } + + pub fn get_detail(self: *CustomEvent) ?JsObject { + return self.detail; + } +}; + +const testing = @import("../../testing.zig"); +test "Browser.CustomEvent" { + var runner = try testing.jsRunner(testing.tracking_allocator, .{}); + defer runner.deinit(); + + try runner.testCases(&.{ + .{ "let capture = null", "undefined"}, + .{ "const el = document.createElement('div');", "undefined"}, + .{ "el.addEventListener('c1', (e) => { capture = 'c1-' + new String(e.detail)})", "undefined"}, + .{ "el.addEventListener('c2', (e) => { capture = 'c2-' + new String(e.detail.over)})", "undefined"}, + + .{ "el.dispatchEvent(new CustomEvent('c1'));", "true"}, + .{ "capture", "c1-null"}, + + .{ "el.dispatchEvent(new CustomEvent('c1', {detail: '123'}));", "true"}, + .{ "capture", "c1-123"}, + + .{ "el.dispatchEvent(new CustomEvent('c2', {detail: {over: 9000}}));", "true"}, + .{ "capture", "c2-9000"}, + }, .{}); +} diff --git a/src/browser/events/event.zig b/src/browser/events/event.zig index 70298cc2..1d1a504d 100644 --- a/src/browser/events/event.zig +++ b/src/browser/events/event.zig @@ -27,6 +27,7 @@ const DOMException = @import("../dom/exceptions.zig").DOMException; const EventTarget = @import("../dom/event_target.zig").EventTarget; const EventTargetUnion = @import("../dom/event_target.zig").Union; +const CustomEvent = @import("custom_event.zig").CustomEvent; const ProgressEvent = @import("../xhr/progress_event.zig").ProgressEvent; const log = std.log.scoped(.events); @@ -34,6 +35,7 @@ const log = std.log.scoped(.events); // Event interfaces pub const Interfaces = .{ Event, + CustomEvent, ProgressEvent, }; @@ -56,13 +58,14 @@ pub const Event = struct { pub fn toInterface(evt: *parser.Event) !Union { return switch (try parser.eventGetInternalType(evt)) { .event => .{ .Event = evt }, + .custom_event => .{ .CustomEvent = @as(*CustomEvent, @ptrCast(evt)).* }, .progress_event => .{ .ProgressEvent = @as(*ProgressEvent, @ptrCast(evt)).* }, }; } - pub fn constructor(eventType: []const u8, opts: ?EventInit) !*parser.Event { + pub fn constructor(event_type: []const u8, opts: ?EventInit) !*parser.Event { const event = try parser.eventCreate(); - try parser.eventInit(event, eventType, opts orelse EventInit{}); + try parser.eventInit(event, event_type, opts orelse EventInit{}); return event; } diff --git a/src/browser/netsurf.zig b/src/browser/netsurf.zig index 9629def7..eb28fd0e 100644 --- a/src/browser/netsurf.zig +++ b/src/browser/netsurf.zig @@ -519,6 +519,7 @@ pub fn eventSetInternalType(evt: *Event, internal_type: EventType) !void { pub const EventType = enum(u8) { event = 0, progress_event = 1, + custom_event = 2, }; pub const MutationEvent = c.dom_mutation_event; diff --git a/src/runtime/js.zig b/src/runtime/js.zig index fe2c5b86..f501eac6 100644 --- a/src/runtime/js.zig +++ b/src/runtime/js.zig @@ -516,8 +516,17 @@ pub fn Env(comptime S: type, comptime types: anytype) type { // every PeristentObjet we've created during the lifetime of the scope. // More importantly, it serves as an identity map - for a given Zig // instance, we map it to the same PersistentObject. + // The key is the @intFromPtr of the Zig value identity_map: std.AutoHashMapUnmanaged(usize, PersistentObject) = .{}, + // Similar to the identity map, but used much less frequently. Some + // web APIs have to manage opaque values. Ideally, they use an + // JsObject, but the JsObject has no lifetime guarantee beyond the + // current call. They can call .persist() on their JsObject to get + // a `*PersistentObject()`. We need to track these to free them. + // The key is the @intFromPtr of the v8.Object.handle. + js_object_map: std.AutoHashMapUnmanaged(usize, PersistentObject) = .{}, + // When we need to load a resource (i.e. an external script), we call // this function to get the source. This is always a reference to the // Page's fetchModuleSource, but we use a function pointer @@ -535,10 +544,20 @@ pub fn Env(comptime S: type, comptime types: anytype) type { // no init, started with executor.startScope() fn deinit(self: *Scope) void { - var it = self.identity_map.valueIterator(); - while (it.next()) |p| { - p.deinit(); + { + var it = self.identity_map.valueIterator(); + while (it.next()) |p| { + p.deinit(); + } } + + { + var it = self.js_object_map.valueIterator(); + while (it.next()) |p| { + p.deinit(); + } + } + for (self.callbacks.items) |*cb| { cb.deinit(); } @@ -871,13 +890,13 @@ pub fn Env(comptime S: type, comptime types: anytype) type { scope: *Scope, js_obj: v8.Object, - // If a Zig struct wants the Object parameter, it'll declare a + // If a Zig struct wants the JsObject parameter, it'll declare a // function like: - // fn _length(self: *const NodeList, js_obj: Env.Object) usize + // fn _length(self: *const NodeList, js_obj: Env.JsObject) usize // // When we're trying to call this function, we can't just do - // if (params[i].type.? == Object) - // Because there is _no_ object, there's only an Env.Object, where + // if (params[i].type.? == JsObject) + // Because there is _no_ JsObject, there's only an Env.JsObject, where // Env is a generic. // We could probably figure out a way to do this, but simply checking // for this declaration is _a lot_ easier. @@ -915,6 +934,22 @@ pub fn Env(comptime S: type, comptime types: anytype) type { pub fn format(self: JsObject, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { return writer.writeAll(try self.toString()); } + + pub fn persist(self: JsObject) !JsObject { + var scope = self.scope; + const js_obj = self.js_obj; + const handle = js_obj.handle; + + const gop = try scope.js_object_map.getOrPut(scope.scope_arena, @intFromPtr(handle)); + if (gop.found_existing == false) { + gop.value_ptr.* = PersistentObject.init(scope.isolate, js_obj); + } + + return .{ + .scope = scope, + .js_obj = gop.value_ptr.castToObject(), + }; + } }; // This only exists so that we know whether a function wants the opaque @@ -1448,6 +1483,11 @@ pub fn Env(comptime S: type, comptime types: anytype) type { return value.func.toValue(); } + if (T == JsObject) { + // we're returning a v8.Object + return value.js_obj.toValue(); + } + if (s.is_tuple) { // return the tuple struct as an array var js_arr = v8.Array.init(isolate, @intCast(s.fields.len)); @@ -1495,7 +1535,7 @@ pub fn Env(comptime S: type, comptime types: anytype) type { .error_union => return zigValueToJs(templates, isolate, context, value catch |err| return err), else => {}, } - @compileLog(@typeInfo(T)); + @compileError("A function returns an unsupported type: " ++ @typeName(T)); } // Reverses the mapZigInstanceToJs, making sure that our TaggedAnyOpaque