diff --git a/src/browser/browser.zig b/src/browser/browser.zig index ca37d30f..5ceb0bbc 100644 --- a/src/browser/browser.zig +++ b/src/browser/browser.zig @@ -341,12 +341,15 @@ pub const Page = struct { renderer: FlatRenderer, + window_clicked_event_node: parser.EventNode, + fn init(session: *Session) Page { const arena = session.browser.page_arena.allocator(); return .{ .arena = arena, .session = session, .renderer = FlatRenderer.init(arena), + .window_clicked_event_node = .{ .func = windowClicked }, }; } @@ -481,12 +484,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, ); @@ -766,8 +767,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}); }; diff --git a/src/browser/dom/event_target.zig b/src/browser/dom/event_target.zig index 959a35ca..ffee9993 100644 --- a/src/browser/dom/event_target.zig +++ b/src/browser/dom/event_target.zig @@ -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, ); diff --git a/src/browser/dom/mutation_observer.zig b/src/browser/dom/mutation_observer.zig index 8aac8f3b..3a9a7c35 100644 --- a/src/browser/dom/mutation_observer.zig +++ b/src/browser/dom/mutation_observer.zig @@ -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: { diff --git a/src/browser/events/event.zig b/src/browser/events/event.zig index ff89af70..70298cc2 100644 --- a/src/browser/events/event.zig +++ b/src/browser/events/event.zig @@ -17,6 +17,7 @@ // along with this program. If not, see . 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" { diff --git a/src/browser/netsurf.zig b/src/browser/netsurf.zig index df5ab147..9629def7 100644 --- a/src/browser/netsurf.zig +++ b/src/browser/netsurf.zig @@ -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, diff --git a/src/browser/xhr/event_target.zig b/src/browser/xhr/event_target.zig index 02feb929..1f11bdbc 100644 --- a/src/browser/xhr/event_target.zig +++ b/src/browser/xhr/event_target.zig @@ -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; } diff --git a/src/runtime/js.zig b/src/runtime/js.zig index b861aa91..4a20ef35 100644 --- a/src/runtime/js.zig +++ b/src/runtime/js.zig @@ -1200,7 +1200,7 @@ pub fn Env(comptime S: type, comptime types: anytype) type { pub const Callback = struct { id: usize, executor: *Executor, - _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 @@ -1215,8 +1215,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.executor.valueToExistingObject(value); + pub fn withThis(self: *const Callback, value: anytype) !Callback { + return .{ + .id = self.id, + .func = self.func, + .executor = self.executor, + .this = try self.executor.valueToExistingObject(value), + }; } pub fn call(self: *const Callback, args: anytype) !void { @@ -1264,7 +1269,7 @@ pub fn Env(comptime S: type, comptime types: anytype) type { } fn getThis(self: *const Callback) v8.Object { - return self._this orelse self.executor.context.getGlobal(); + return self.this orelse self.executor.context.getGlobal(); } // debug/helper to print the source of the JS callback