Merge pull request #577 from lightpanda-io/unified_intrusive_events

Unify the Zig and JS events using an intrusive node.
This commit is contained in:
Karl Seguin
2025-05-01 09:50:19 +08:00
committed by GitHub
7 changed files with 142 additions and 220 deletions

View File

@@ -229,6 +229,8 @@ pub const Page = struct {
renderer: FlatRenderer,
window_clicked_event_node: parser.EventNode,
scope: *Env.Scope,
// current_script is the script currently evaluated by the page.
@@ -245,6 +247,7 @@ pub const Page = struct {
.url = URL.empty,
.session = session,
.renderer = FlatRenderer.init(arena),
.window_clicked_event_node = .{ .func = windowClicked },
.state = .{
.arena = arena,
.document = null,
@@ -400,12 +403,10 @@ pub const Page = struct {
self.doc = doc;
const document_element = (try parser.documentGetDocumentElement(doc)) orelse return error.DocumentElementError;
try parser.eventTargetAddZigListener(
try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Element, document_element),
arena,
"click",
windowClicked,
self,
&self.window_clicked_event_node,
false,
);
@@ -675,8 +676,8 @@ pub const Page = struct {
_ = try parser.elementDispatchEvent(element, @ptrCast(event));
}
fn windowClicked(ctx: *anyopaque, event: *parser.Event) void {
const self: *Page = @alignCast(@ptrCast(ctx));
fn windowClicked(node: *parser.EventNode, event: *parser.Event) void {
const self: *Page = @fieldParentPtr("window_clicked_event_node", node);
self._windowClicked(event) catch |err| {
log.err("window click handler: {}", .{err});
};

View File

@@ -46,7 +46,7 @@ pub const EventTarget = struct {
pub fn _addEventListener(
self: *parser.EventTarget,
eventType: []const u8,
typ: []const u8,
cbk: Env.Callback,
capture: ?bool,
state: *SessionState,
@@ -56,7 +56,7 @@ pub const EventTarget = struct {
// check if event target has already this listener
const lst = try parser.eventTargetHasListener(
self,
eventType,
typ,
capture orelse false,
cbk.id,
);
@@ -64,29 +64,28 @@ pub const EventTarget = struct {
return;
}
const eh = try EventHandler.init(state.arena, try cbk.withThis(self));
try parser.eventTargetAddEventListener(
self,
state.arena,
eventType,
EventHandler,
.{ .cbk = cbk },
typ,
&eh.node,
capture orelse false,
);
}
pub fn _removeEventListener(
self: *parser.EventTarget,
eventType: []const u8,
typ: []const u8,
cbk: Env.Callback,
capture: ?bool,
state: *SessionState,
// TODO: hanle EventListenerOptions
// see #https://github.com/lightpanda-io/jsruntime-lib/issues/114
) !void {
// check if event target has already this listener
const lst = try parser.eventTargetHasListener(
self,
eventType,
typ,
capture orelse false,
cbk.id,
);
@@ -97,8 +96,7 @@ pub const EventTarget = struct {
// remove listener
try parser.eventTargetRemoveEventListener(
self,
state.arena,
eventType,
typ,
lst.?,
capture orelse false,
);

View File

@@ -59,56 +59,45 @@ pub const MutationObserver = struct {
.node = node,
.options = options,
.mutation_observer = self,
.event_node = .{ .id = self.cbk.id, .func = Observer.handle },
};
const arena = self.arena;
// register node's events
if (options.childList or options.subtree) {
try parser.eventTargetAddZigListener(
try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node),
arena,
"DOMNodeInserted",
Observer.handle,
observer,
&observer.event_node,
false,
);
try parser.eventTargetAddZigListener(
try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node),
arena,
"DOMNodeRemoved",
Observer.handle,
observer,
&observer.event_node,
false,
);
}
if (options.attr()) {
try parser.eventTargetAddZigListener(
try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node),
arena,
"DOMAttrModified",
Observer.handle,
observer,
&observer.event_node,
false,
);
}
if (options.cdata()) {
try parser.eventTargetAddZigListener(
try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node),
arena,
"DOMCharacterDataModified",
Observer.handle,
observer,
&observer.event_node,
false,
);
}
if (options.subtree) {
try parser.eventTargetAddZigListener(
try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node),
arena,
"DOMSubtreeModified",
Observer.handle,
observer,
&observer.event_node,
false,
);
}
@@ -221,6 +210,8 @@ const Observer = struct {
// and batch the mutation records.
mutation_observer: *MutationObserver,
event_node: parser.EventNode,
fn appliesTo(o: *const Observer, target: *parser.Node) bool {
// mutation on any target is always ok.
if (o.options.subtree) {
@@ -250,9 +241,9 @@ const Observer = struct {
return false;
}
fn handle(ctx: *anyopaque, event: *parser.Event) void {
// retrieve the observer from the data.
var self: *Observer = @alignCast(@ptrCast(ctx));
fn handle(en: *parser.EventNode, event: *parser.Event) void {
const self: *Observer = @fieldParentPtr("event_node", en);
var mutation_observer = self.mutation_observer;
const node = blk: {

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const Allocator = std.mem.Allocator;
const parser = @import("../netsurf.zig");
const Callback = @import("../env.zig").Callback;
@@ -136,14 +137,35 @@ pub const Event = struct {
};
pub const EventHandler = struct {
fn handle(event: ?*parser.Event, data: *const parser.JSEventHandlerData) void {
callback: Callback,
node: parser.EventNode,
pub fn init(allocator: Allocator, callback: Callback) !*EventHandler {
const eh = try allocator.create(EventHandler);
eh.* = .{
.callback = callback,
.node = .{
.id = callback.id,
.func = handle,
},
};
return eh;
}
fn handle(node: *parser.EventNode, event: *parser.Event) void {
const ievent = Event.toInterface(event) catch |err| {
log.err("Event.toInterface: {}", .{err});
return;
};
const self: *EventHandler = @fieldParentPtr("node", node);
var result: Callback.Result = undefined;
data.cbk.tryCall(.{if (event) |evt| Event.toInterface(evt) catch unreachable else null}, &result) catch {
self.callback.tryCall(.{ievent}, &result) catch {
log.err("event handler error: {s}", .{result.exception});
log.debug("stack:\n{s}", .{result.stack orelse "???"});
};
}
}.handle;
};
const testing = @import("../../testing.zig");
test "Browser.Event" {

View File

@@ -29,9 +29,6 @@ const c = @cImport({
const mimalloc = @import("mimalloc.zig");
const Callback = @import("env.zig").Callback;
const SessionState = @import("env.zig").SessionState;
// init initializes netsurf lib.
// init starts a mimalloc heap arena for the netsurf session. The caller must
// call deinit() to free the arena memory.
@@ -587,11 +584,61 @@ pub inline fn toEventTarget(comptime T: type, v: *T) *EventTarget {
return @as(*EventTarget, @ptrCast(et_aligned));
}
// The way we implement events is a lot like how Zig implements linked lists.
// A Zig struct contains an `EventNode` field, i.e.:
// node: parser.EventNode,
//
// When eventTargetAddEventListener is called, we pass in `&self.node`.
// This is the pointer that's stored in the netsurf listener and it's the data
// we can get back from the listener. We can call the node's `func` function,
// passing the node itself, and the receiving function will know how to turn
// that node into the our "self", i..e by using @fieldParentPtr.
// https://www.openmymind.net/Zigs-New-LinkedList-API/
pub const EventNode = struct {
// Event id, used for removing. Internal Zig events won't have an id.
// This is normally set to the callback.id for a JavaScript event.
id: ?usize = null,
func: *const fn (node: *EventNode, event: *Event) void,
fn idFromListener(lst: *EventListener) ?usize {
const ctx = eventListenerGetData(lst) orelse return null;
const node: *EventNode = @alignCast(@ptrCast(ctx));
return node.id;
}
};
pub fn eventTargetAddEventListener(
et: *EventTarget,
typ: []const u8,
node: *EventNode,
capture: bool,
) !void {
const event_handler = struct {
fn handle(event_: ?*Event, ptr_: ?*anyopaque) callconv(.C) void {
const ptr = ptr_ orelse return;
const event = event_ orelse return;
const node_: *EventNode = @alignCast(@ptrCast(ptr));
node_.func(node_, event);
}
}.handle;
var listener: ?*EventListener = undefined;
const errLst = c.dom_event_listener_create(event_handler, node, &listener);
try DOMErr(errLst);
defer c.dom_event_listener_unref(listener);
const s = try strFromData(typ);
const err = eventTargetVtable(et).add_event_listener.?(et, s, listener, capture);
try DOMErr(err);
}
pub fn eventTargetHasListener(
et: *EventTarget,
typ: []const u8,
capture: bool,
cbk_id: usize,
id: usize,
) !?*EventListener {
const str = try strFromData(typ);
@@ -616,12 +663,9 @@ pub fn eventTargetHasListener(
// and capture property,
// let's check if the callback handler is the same
defer c.dom_event_listener_unref(listener);
if (EventHandlerData.fromListener(listener)) |ehd| {
switch (ehd.*) {
.js => |js| if (cbk_id == js.data.cbk.id) {
return lst;
},
.zig => {},
if (EventNode.idFromListener(listener)) |node_id| {
if (node_id == id) {
return lst;
}
}
}
@@ -638,144 +682,18 @@ pub fn eventTargetHasListener(
return null;
}
// The *anyopque that get stored in the libdom listener, which we'll retrieve
// when then event is dispatched so that we can execute the JS or Zig callback.
const EventHandlerData = union(enum) {
js: JS,
zig: Zig,
const JS = struct {
data: JSEventHandlerData,
func: JSEventHandlerFunc,
};
const Zig = struct {
ctx: *anyopaque,
func: ZigEventHandlerFunc,
};
// retrieve a EventHandlerDataInternal from a listener.
fn fromListener(lst: *EventListener) ?*EventHandlerData {
const ctx = eventListenerGetData(lst) orelse return null;
const ehd: *EventHandlerData = @alignCast(@ptrCast(ctx));
return ehd;
}
pub fn deinit(self: *EventHandlerData, alloc: std.mem.Allocator) void {
switch (self.*) {
.js => |*js| {
const js_data = &js.data;
if (js_data.deinitFunc) |df| {
df(js_data.ctx, alloc);
}
},
.zig => {},
}
alloc.destroy(self);
}
pub fn handle(self: *EventHandlerData, event: ?*Event) void {
switch (self.*) {
.js => |*js| js.func(event, &js.data),
.zig => |zig| zig.func(zig.ctx, event.?),
}
}
};
pub const JSEventHandlerData = struct {
cbk: Callback,
ctx: ?*anyopaque = null,
// deinitFunc implements the data deinitialization.
deinitFunc: ?DeinitFunc = null,
pub const DeinitFunc = *const fn (data: ?*anyopaque, alloc: std.mem.Allocator) void;
};
const JSEventHandlerFunc = *const fn (event: ?*Event, data: *JSEventHandlerData) void;
const ZigEventHandlerFunc = *const fn (ctx: *anyopaque, event: *Event) void;
pub fn eventTargetAddEventListener(
et: *EventTarget,
alloc: std.mem.Allocator,
typ: []const u8,
func: JSEventHandlerFunc,
data: JSEventHandlerData,
capture: bool,
) !void {
// this allocation will be removed either on
// eventTargetRemoveEventListener or eventTargetRemoveAllEventListeners
const ehd = try alloc.create(EventHandlerData);
errdefer alloc.destroy(ehd);
ehd.* = .{ .js = .{ .data = data, .func = func } };
errdefer ehd.deinit(alloc);
// When a function is used as an event handler, its this parameter is bound
// to the DOM element on which the listener is placed.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this#this_in_dom_event_handlers
try ehd.js.data.cbk.setThis(et);
return addEventTargetListener(et, typ, ehd, capture);
}
pub fn eventTargetAddZigListener(
et: *EventTarget,
alloc: std.mem.Allocator,
typ: []const u8,
func: ZigEventHandlerFunc,
ctx: *anyopaque,
capture: bool,
) !void {
const ehd = try alloc.create(EventHandlerData);
errdefer alloc.destroy(ehd);
ehd.* = .{ .zig = .{ .ctx = ctx, .func = func } };
return addEventTargetListener(et, typ, ehd, capture);
}
fn addEventTargetListener(et: *EventTarget, typ: []const u8, data: *anyopaque, capture: bool) !void {
// event_handler implements the function exposed in C and called by libdom.
// It retrieves the EventHandler and calls the appropriate (JS or Zig)
// handler function with the corresponding data.
const event_handler = struct {
fn handle(event: ?*Event, ptr_: ?*anyopaque) callconv(.C) void {
const ptr = ptr_ orelse return;
@as(*EventHandlerData, @alignCast(@ptrCast(ptr))).handle(event);
// NOTE: we can not call func.deinit here
// b/c the handler can be called several times
// either on this dispatch event or in anoter one
}
}.handle;
var listener: ?*EventListener = undefined;
const errLst = c.dom_event_listener_create(event_handler, data, &listener);
try DOMErr(errLst);
defer c.dom_event_listener_unref(listener);
const s = try strFromData(typ);
const err = eventTargetVtable(et).add_event_listener.?(et, s, listener, capture);
try DOMErr(err);
}
pub fn eventTargetRemoveEventListener(
et: *EventTarget,
alloc: std.mem.Allocator,
typ: []const u8,
lst: *EventListener,
capture: bool,
) !void {
// free data allocation made on eventTargetAddEventListener
if (EventHandlerData.fromListener(lst)) |ehd| {
ehd.deinit(alloc);
}
const s = try strFromData(typ);
const err = eventTargetVtable(et).remove_event_listener.?(et, s, lst, capture);
try DOMErr(err);
}
pub fn eventTargetRemoveAllEventListeners(
et: *EventTarget,
alloc: std.mem.Allocator,
) !void {
pub fn eventTargetRemoveAllEventListeners(et: *EventTarget) !void {
var next: ?*EventListenerEntry = undefined;
var lst: ?*EventListener = undefined;
@@ -792,15 +710,8 @@ pub fn eventTargetRemoveAllEventListeners(
try DOMErr(errIter);
if (lst) |listener| {
defer c.dom_event_listener_unref(listener);
if (EventHandlerData.fromListener(listener)) |ehd| {
if (ehd.* == .zig) {
// we don't remove Zig listeners
continue;
}
ehd.deinit(alloc);
if (EventNode.idFromListener(listener) != null) {
defer c.dom_event_listener_unref(listener);
const err = eventTargetVtable(et).remove_event_listener.?(
et,
null,

View File

@@ -48,25 +48,25 @@ pub const XMLHttpRequestEventTarget = struct {
typ: []const u8,
cbk: Callback,
) !void {
const target = @as(*parser.EventTarget, @ptrCast(self));
const eh = try EventHandler.init(alloc, try cbk.withThis(target));
try parser.eventTargetAddEventListener(
@as(*parser.EventTarget, @ptrCast(self)),
alloc,
target,
typ,
EventHandler,
.{ .cbk = cbk },
&eh.node,
false,
);
}
fn unregister(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, typ: []const u8, cbk: Callback) !void {
fn unregister(self: *XMLHttpRequestEventTarget, typ: []const u8, cbk_id: usize) !void {
const et = @as(*parser.EventTarget, @ptrCast(self));
// check if event target has already this listener
const lst = try parser.eventTargetHasListener(et, typ, false, cbk.id);
const lst = try parser.eventTargetHasListener(et, typ, false, cbk_id);
if (lst == null) {
return;
}
// remove listener
try parser.eventTargetRemoveEventListener(et, alloc, typ, lst.?, false);
try parser.eventTargetRemoveEventListener(et, typ, lst.?, false);
}
pub fn get_onloadstart(self: *XMLHttpRequestEventTarget) ?Callback {
@@ -89,39 +89,33 @@ pub const XMLHttpRequestEventTarget = struct {
}
pub fn set_onloadstart(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void {
const arena = state.arena;
if (self.onloadstart_cbk) |cbk| try self.unregister(arena, "loadstart", cbk);
try self.register(arena, "loadstart", handler);
if (self.onloadstart_cbk) |cbk| try self.unregister("loadstart", cbk.id);
try self.register(state.arena, "loadstart", handler);
self.onloadstart_cbk = handler;
}
pub fn set_onprogress(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void {
const arena = state.arena;
if (self.onprogress_cbk) |cbk| try self.unregister(arena, "progress", cbk);
try self.register(arena, "progress", handler);
if (self.onprogress_cbk) |cbk| try self.unregister("progress", cbk.id);
try self.register(state.arena, "progress", handler);
self.onprogress_cbk = handler;
}
pub fn set_onabort(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void {
const arena = state.arena;
if (self.onabort_cbk) |cbk| try self.unregister(arena, "abort", cbk);
try self.register(arena, "abort", handler);
if (self.onabort_cbk) |cbk| try self.unregister("abort", cbk.id);
try self.register(state.arena, "abort", handler);
self.onabort_cbk = handler;
}
pub fn set_onload(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void {
const arena = state.arena;
if (self.onload_cbk) |cbk| try self.unregister(arena, "load", cbk);
try self.register(arena, "load", handler);
if (self.onload_cbk) |cbk| try self.unregister("load", cbk.id);
try self.register(state.arena, "load", handler);
self.onload_cbk = handler;
}
pub fn set_ontimeout(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void {
const arena = state.arena;
if (self.ontimeout_cbk) |cbk| try self.unregister(arena, "timeout", cbk);
try self.register(arena, "timeout", handler);
if (self.ontimeout_cbk) |cbk| try self.unregister("timeout", cbk.id);
try self.register(state.arena, "timeout", handler);
self.ontimeout_cbk = handler;
}
pub fn set_onloadend(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void {
const arena = state.arena;
if (self.onloadend_cbk) |cbk| try self.unregister(arena, "loadend", cbk);
try self.register(arena, "loadend", handler);
if (self.onloadend_cbk) |cbk| try self.unregister("loadend", cbk.id);
try self.register(state.arena, "loadend", handler);
self.onloadend_cbk = handler;
}

View File

@@ -766,7 +766,7 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
pub const Callback = struct {
id: usize,
scope: *Scope,
_this: ?v8.Object = null,
this: ?v8.Object = null,
func: PersistentFunction,
// We use this when mapping a JS value to a Zig object. We can't
@@ -781,8 +781,13 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
exception: []const u8,
};
pub fn setThis(self: *Callback, value: anytype) !void {
self._this = try self.scope.valueToExistingObject(value);
pub fn withThis(self: *const Callback, value: anytype) !Callback {
return .{
.id = self.id,
.func = self.func,
.scope = self.scope,
.this = try self.scope.valueToExistingObject(value),
};
}
pub fn call(self: *const Callback, args: anytype) !void {
@@ -830,7 +835,7 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
}
fn getThis(self: *const Callback) v8.Object {
return self._this orelse self.scope.context.getGlobal();
return self.this orelse self.scope.context.getGlobal();
}
// debug/helper to print the source of the JS callback