Skip to content

Commit bd54395

Browse files
Merge pull request #588 from lightpanda-io/custom_events
Add CustomEvent api
2 parents 74eaee5 + 89ac27b commit bd54395

File tree

4 files changed

+135
-10
lines changed

4 files changed

+135
-10
lines changed

src/browser/events/custom_event.zig

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2+
//
3+
// Francis Bouvier <[email protected]>
4+
// Pierre Tachoire <[email protected]>
5+
//
6+
// This program is free software: you can redistribute it and/or modify
7+
// it under the terms of the GNU Affero General Public License as
8+
// published by the Free Software Foundation, either version 3 of the
9+
// License, or (at your option) any later version.
10+
//
11+
// This program is distributed in the hope that it will be useful,
12+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
// GNU Affero General Public License for more details.
15+
//
16+
// You should have received a copy of the GNU Affero General Public License
17+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
19+
const std = @import("std");
20+
21+
const parser = @import("../netsurf.zig");
22+
const Event = @import("event.zig").Event;
23+
const JsObject = @import("../env.zig").JsObject;
24+
25+
// https://dom.spec.whatwg.org/#interface-customevent
26+
pub const CustomEvent = struct {
27+
pub const prototype = *Event;
28+
29+
proto: parser.Event,
30+
detail: ?JsObject,
31+
32+
const CustomEventInit = struct {
33+
bubbles: bool = false,
34+
cancelable: bool = false,
35+
composed: bool = false,
36+
detail: ?JsObject = null,
37+
};
38+
39+
pub fn constructor(event_type: []const u8, opts_: ?CustomEventInit) !CustomEvent {
40+
const opts = opts_ orelse CustomEventInit{};
41+
42+
const event = try parser.eventCreate();
43+
defer parser.eventDestroy(event);
44+
try parser.eventInit(event, event_type, .{
45+
.bubbles = opts.bubbles,
46+
.cancelable = opts.cancelable,
47+
.composed = opts.composed,
48+
});
49+
50+
return .{
51+
.proto = event.*,
52+
.detail = if (opts.detail) |d| try d.persist() else null,
53+
};
54+
}
55+
56+
pub fn get_detail(self: *CustomEvent) ?JsObject {
57+
return self.detail;
58+
}
59+
};
60+
61+
const testing = @import("../../testing.zig");
62+
test "Browser.CustomEvent" {
63+
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
64+
defer runner.deinit();
65+
66+
try runner.testCases(&.{
67+
.{ "let capture = null", "undefined"},
68+
.{ "const el = document.createElement('div');", "undefined"},
69+
.{ "el.addEventListener('c1', (e) => { capture = 'c1-' + new String(e.detail)})", "undefined"},
70+
.{ "el.addEventListener('c2', (e) => { capture = 'c2-' + new String(e.detail.over)})", "undefined"},
71+
72+
.{ "el.dispatchEvent(new CustomEvent('c1'));", "true"},
73+
.{ "capture", "c1-null"},
74+
75+
.{ "el.dispatchEvent(new CustomEvent('c1', {detail: '123'}));", "true"},
76+
.{ "capture", "c1-123"},
77+
78+
.{ "el.dispatchEvent(new CustomEvent('c2', {detail: {over: 9000}}));", "true"},
79+
.{ "capture", "c2-9000"},
80+
}, .{});
81+
}

src/browser/events/event.zig

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,15 @@ const DOMException = @import("../dom/exceptions.zig").DOMException;
2727
const EventTarget = @import("../dom/event_target.zig").EventTarget;
2828
const EventTargetUnion = @import("../dom/event_target.zig").Union;
2929

30+
const CustomEvent = @import("custom_event.zig").CustomEvent;
3031
const ProgressEvent = @import("../xhr/progress_event.zig").ProgressEvent;
3132

3233
const log = std.log.scoped(.events);
3334

3435
// Event interfaces
3536
pub const Interfaces = .{
3637
Event,
38+
CustomEvent,
3739
ProgressEvent,
3840
};
3941

@@ -56,13 +58,14 @@ pub const Event = struct {
5658
pub fn toInterface(evt: *parser.Event) !Union {
5759
return switch (try parser.eventGetInternalType(evt)) {
5860
.event => .{ .Event = evt },
61+
.custom_event => .{ .CustomEvent = @as(*CustomEvent, @ptrCast(evt)).* },
5962
.progress_event => .{ .ProgressEvent = @as(*ProgressEvent, @ptrCast(evt)).* },
6063
};
6164
}
6265

63-
pub fn constructor(eventType: []const u8, opts: ?EventInit) !*parser.Event {
66+
pub fn constructor(event_type: []const u8, opts: ?EventInit) !*parser.Event {
6467
const event = try parser.eventCreate();
65-
try parser.eventInit(event, eventType, opts orelse EventInit{});
68+
try parser.eventInit(event, event_type, opts orelse EventInit{});
6669
return event;
6770
}
6871

src/browser/netsurf.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,7 @@ pub fn eventSetInternalType(evt: *Event, internal_type: EventType) !void {
519519
pub const EventType = enum(u8) {
520520
event = 0,
521521
progress_event = 1,
522+
custom_event = 2,
522523
};
523524

524525
pub const MutationEvent = c.dom_mutation_event;

src/runtime/js.zig

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -516,8 +516,17 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
516516
// every PeristentObjet we've created during the lifetime of the scope.
517517
// More importantly, it serves as an identity map - for a given Zig
518518
// instance, we map it to the same PersistentObject.
519+
// The key is the @intFromPtr of the Zig value
519520
identity_map: std.AutoHashMapUnmanaged(usize, PersistentObject) = .{},
520521

522+
// Similar to the identity map, but used much less frequently. Some
523+
// web APIs have to manage opaque values. Ideally, they use an
524+
// JsObject, but the JsObject has no lifetime guarantee beyond the
525+
// current call. They can call .persist() on their JsObject to get
526+
// a `*PersistentObject()`. We need to track these to free them.
527+
// The key is the @intFromPtr of the v8.Object.handle.
528+
js_object_map: std.AutoHashMapUnmanaged(usize, PersistentObject) = .{},
529+
521530
// When we need to load a resource (i.e. an external script), we call
522531
// this function to get the source. This is always a reference to the
523532
// Page's fetchModuleSource, but we use a function pointer
@@ -535,10 +544,20 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
535544
// no init, started with executor.startScope()
536545

537546
fn deinit(self: *Scope) void {
538-
var it = self.identity_map.valueIterator();
539-
while (it.next()) |p| {
540-
p.deinit();
547+
{
548+
var it = self.identity_map.valueIterator();
549+
while (it.next()) |p| {
550+
p.deinit();
551+
}
552+
}
553+
554+
{
555+
var it = self.js_object_map.valueIterator();
556+
while (it.next()) |p| {
557+
p.deinit();
558+
}
541559
}
560+
542561
for (self.callbacks.items) |*cb| {
543562
cb.deinit();
544563
}
@@ -871,13 +890,13 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
871890
scope: *Scope,
872891
js_obj: v8.Object,
873892

874-
// If a Zig struct wants the Object parameter, it'll declare a
893+
// If a Zig struct wants the JsObject parameter, it'll declare a
875894
// function like:
876-
// fn _length(self: *const NodeList, js_obj: Env.Object) usize
895+
// fn _length(self: *const NodeList, js_obj: Env.JsObject) usize
877896
//
878897
// When we're trying to call this function, we can't just do
879-
// if (params[i].type.? == Object)
880-
// Because there is _no_ object, there's only an Env.Object, where
898+
// if (params[i].type.? == JsObject)
899+
// Because there is _no_ JsObject, there's only an Env.JsObject, where
881900
// Env is a generic.
882901
// We could probably figure out a way to do this, but simply checking
883902
// for this declaration is _a lot_ easier.
@@ -915,6 +934,22 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
915934
pub fn format(self: JsObject, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
916935
return writer.writeAll(try self.toString());
917936
}
937+
938+
pub fn persist(self: JsObject) !JsObject {
939+
var scope = self.scope;
940+
const js_obj = self.js_obj;
941+
const handle = js_obj.handle;
942+
943+
const gop = try scope.js_object_map.getOrPut(scope.scope_arena, @intFromPtr(handle));
944+
if (gop.found_existing == false) {
945+
gop.value_ptr.* = PersistentObject.init(scope.isolate, js_obj);
946+
}
947+
948+
return .{
949+
.scope = scope,
950+
.js_obj = gop.value_ptr.castToObject(),
951+
};
952+
}
918953
};
919954

920955
// 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 {
14481483
return value.func.toValue();
14491484
}
14501485

1486+
if (T == JsObject) {
1487+
// we're returning a v8.Object
1488+
return value.js_obj.toValue();
1489+
}
1490+
14511491
if (s.is_tuple) {
14521492
// return the tuple struct as an array
14531493
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 {
14951535
.error_union => return zigValueToJs(templates, isolate, context, value catch |err| return err),
14961536
else => {},
14971537
}
1498-
@compileLog(@typeInfo(T));
1538+
14991539
@compileError("A function returns an unsupported type: " ++ @typeName(T));
15001540
}
15011541
// Reverses the mapZigInstanceToJs, making sure that our TaggedAnyOpaque

0 commit comments

Comments
 (0)