mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-29 15:13:28 +00:00
Merge pull request #588 from lightpanda-io/custom_events
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / puppeteer-perf (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
wpt / web platform tests (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / puppeteer-perf (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
wpt / web platform tests (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
Add CustomEvent api
This commit is contained in:
81
src/browser/events/custom_event.zig
Normal file
81
src/browser/events/custom_event.zig
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
// Copyright (C) 2023-2024 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/>.
|
||||||
|
|
||||||
|
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"},
|
||||||
|
}, .{});
|
||||||
|
}
|
||||||
@@ -27,6 +27,7 @@ const DOMException = @import("../dom/exceptions.zig").DOMException;
|
|||||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||||
const EventTargetUnion = @import("../dom/event_target.zig").Union;
|
const EventTargetUnion = @import("../dom/event_target.zig").Union;
|
||||||
|
|
||||||
|
const CustomEvent = @import("custom_event.zig").CustomEvent;
|
||||||
const ProgressEvent = @import("../xhr/progress_event.zig").ProgressEvent;
|
const ProgressEvent = @import("../xhr/progress_event.zig").ProgressEvent;
|
||||||
|
|
||||||
const log = std.log.scoped(.events);
|
const log = std.log.scoped(.events);
|
||||||
@@ -34,6 +35,7 @@ const log = std.log.scoped(.events);
|
|||||||
// Event interfaces
|
// Event interfaces
|
||||||
pub const Interfaces = .{
|
pub const Interfaces = .{
|
||||||
Event,
|
Event,
|
||||||
|
CustomEvent,
|
||||||
ProgressEvent,
|
ProgressEvent,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -56,13 +58,14 @@ pub const Event = struct {
|
|||||||
pub fn toInterface(evt: *parser.Event) !Union {
|
pub fn toInterface(evt: *parser.Event) !Union {
|
||||||
return switch (try parser.eventGetInternalType(evt)) {
|
return switch (try parser.eventGetInternalType(evt)) {
|
||||||
.event => .{ .Event = evt },
|
.event => .{ .Event = evt },
|
||||||
|
.custom_event => .{ .CustomEvent = @as(*CustomEvent, @ptrCast(evt)).* },
|
||||||
.progress_event => .{ .ProgressEvent = @as(*ProgressEvent, @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();
|
const event = try parser.eventCreate();
|
||||||
try parser.eventInit(event, eventType, opts orelse EventInit{});
|
try parser.eventInit(event, event_type, opts orelse EventInit{});
|
||||||
return event;
|
return event;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -519,6 +519,7 @@ pub fn eventSetInternalType(evt: *Event, internal_type: EventType) !void {
|
|||||||
pub const EventType = enum(u8) {
|
pub const EventType = enum(u8) {
|
||||||
event = 0,
|
event = 0,
|
||||||
progress_event = 1,
|
progress_event = 1,
|
||||||
|
custom_event = 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const MutationEvent = c.dom_mutation_event;
|
pub const MutationEvent = c.dom_mutation_event;
|
||||||
|
|||||||
@@ -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.
|
// every PeristentObjet we've created during the lifetime of the scope.
|
||||||
// More importantly, it serves as an identity map - for a given Zig
|
// More importantly, it serves as an identity map - for a given Zig
|
||||||
// instance, we map it to the same PersistentObject.
|
// instance, we map it to the same PersistentObject.
|
||||||
|
// The key is the @intFromPtr of the Zig value
|
||||||
identity_map: std.AutoHashMapUnmanaged(usize, PersistentObject) = .{},
|
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
|
// 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
|
// this function to get the source. This is always a reference to the
|
||||||
// Page's fetchModuleSource, but we use a function pointer
|
// 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()
|
// no init, started with executor.startScope()
|
||||||
|
|
||||||
fn deinit(self: *Scope) void {
|
fn deinit(self: *Scope) void {
|
||||||
var it = self.identity_map.valueIterator();
|
{
|
||||||
while (it.next()) |p| {
|
var it = self.identity_map.valueIterator();
|
||||||
p.deinit();
|
while (it.next()) |p| {
|
||||||
|
p.deinit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
var it = self.js_object_map.valueIterator();
|
||||||
|
while (it.next()) |p| {
|
||||||
|
p.deinit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (self.callbacks.items) |*cb| {
|
for (self.callbacks.items) |*cb| {
|
||||||
cb.deinit();
|
cb.deinit();
|
||||||
}
|
}
|
||||||
@@ -871,13 +890,13 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
|
|||||||
scope: *Scope,
|
scope: *Scope,
|
||||||
js_obj: v8.Object,
|
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:
|
// 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
|
// When we're trying to call this function, we can't just do
|
||||||
// if (params[i].type.? == Object)
|
// if (params[i].type.? == JsObject)
|
||||||
// Because there is _no_ object, there's only an Env.Object, where
|
// Because there is _no_ JsObject, there's only an Env.JsObject, where
|
||||||
// Env is a generic.
|
// Env is a generic.
|
||||||
// We could probably figure out a way to do this, but simply checking
|
// We could probably figure out a way to do this, but simply checking
|
||||||
// for this declaration is _a lot_ easier.
|
// 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 {
|
pub fn format(self: JsObject, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
|
||||||
return writer.writeAll(try self.toString());
|
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
|
// 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();
|
return value.func.toValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (T == JsObject) {
|
||||||
|
// we're returning a v8.Object
|
||||||
|
return value.js_obj.toValue();
|
||||||
|
}
|
||||||
|
|
||||||
if (s.is_tuple) {
|
if (s.is_tuple) {
|
||||||
// return the tuple struct as an array
|
// return the tuple struct as an array
|
||||||
var js_arr = v8.Array.init(isolate, @intCast(s.fields.len));
|
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),
|
.error_union => return zigValueToJs(templates, isolate, context, value catch |err| return err),
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
@compileLog(@typeInfo(T));
|
|
||||||
@compileError("A function returns an unsupported type: " ++ @typeName(T));
|
@compileError("A function returns an unsupported type: " ++ @typeName(T));
|
||||||
}
|
}
|
||||||
// Reverses the mapZigInstanceToJs, making sure that our TaggedAnyOpaque
|
// Reverses the mapZigInstanceToJs, making sure that our TaggedAnyOpaque
|
||||||
|
|||||||
Reference in New Issue
Block a user