Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions src/browser/events/custom_event.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <[email protected]>
// Pierre Tachoire <[email protected]>
//
// 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/>.

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"},
}, .{});
}
7 changes: 5 additions & 2 deletions src/browser/events/event.zig
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,15 @@ 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);

// Event interfaces
pub const Interfaces = .{
Event,
CustomEvent,
ProgressEvent,
};

Expand All @@ -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;
}

Expand Down
1 change: 1 addition & 0 deletions src/browser/netsurf.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
56 changes: 48 additions & 8 deletions src/runtime/js.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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();
}
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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
Expand Down
Loading