From c9b4067686771b1157e26afa52e2a75f9ac6ad83 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Sat, 13 Dec 2025 17:19:53 +0800 Subject: [PATCH] Event listener can now be an object with a handleEvent function --- src/browser/EventManager.zig | 64 +++++++++++++--- src/browser/js/Object.zig | 4 + src/browser/tests/events.html | 115 +++++++++++++++++++++++++++++ src/browser/webapi/EventTarget.zig | 70 ++++++++++++++---- 4 files changed, 228 insertions(+), 25 deletions(-) diff --git a/src/browser/EventManager.zig b/src/browser/EventManager.zig index d458a3b2..f4bbdd44 100644 --- a/src/browser/EventManager.zig +++ b/src/browser/EventManager.zig @@ -57,7 +57,13 @@ pub const RegisterOptions = struct { passive: bool = false, signal: ?*@import("webapi/AbortSignal.zig") = null, }; -pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, function: js.Function, opts: RegisterOptions) !void { + +pub const Callback = union(enum) { + function: js.Function, + object: js.Object, +}; + +pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, callback: Callback, opts: RegisterOptions) !void { if (comptime IS_DEBUG) { log.debug(.event, "eventManager.register", .{ .type = typ, .capture = opts.capture, .once = opts.once }); } @@ -71,11 +77,15 @@ pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, func const gop = try self.lookup.getOrPut(self.arena, @intFromPtr(target)); if (gop.found_existing) { - // check for duplicate functions already registered + // check for duplicate callbacks already registered var node = gop.value_ptr.first; while (node) |n| { const listener: *Listener = @alignCast(@fieldParentPtr("node", n)); - if (listener.function.eql(function) and listener.capture == opts.capture) { + const is_duplicate = switch (callback) { + .object => |obj| listener.function.eqlObject(obj), + .function => |func| listener.function.eqlFunction(func), + }; + if (is_duplicate and listener.capture == opts.capture) { return; } node = n.next; @@ -84,13 +94,18 @@ pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, func gop.value_ptr.* = .{}; } + const func = switch (callback) { + .function => |f| Function{ .value = f }, + .object => |o| Function{ .object = o }, + }; + const listener = try self.listener_pool.create(); listener.* = .{ .node = .{}, .once = opts.once, .capture = opts.capture, .passive = opts.passive, - .function = .{ .value = function }, + .function = func, .signal = opts.signal, .typ = try String.init(self.arena, typ, .{}), }; @@ -98,9 +113,9 @@ pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, func gop.value_ptr.append(&listener.node); } -pub fn remove(self: *EventManager, target: *EventTarget, typ: []const u8, function: js.Function, use_capture: bool) void { +pub fn remove(self: *EventManager, target: *EventTarget, typ: []const u8, callback: Callback, use_capture: bool) void { const list = self.lookup.getPtr(@intFromPtr(target)) orelse return; - if (findListener(list, typ, function, use_capture)) |listener| { + if (findListener(list, typ, callback, use_capture)) |listener| { self.removeListener(list, listener); } } @@ -119,7 +134,17 @@ pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) !void switch (target._type) { .node => |node| try self.dispatchNode(node, event, &was_handled), - .xhr, .window, .abort_signal, .media_query_list, .message_port, .text_track_cue, .navigation, .screen, .screen_orientation => { + .xhr, + .window, + .abort_signal, + .media_query_list, + .message_port, + .text_track_cue, + .navigation, + .screen, + .screen_orientation, + .generic, + => { const list = self.lookup.getPtr(@intFromPtr(target)) orelse return; try self.dispatchAll(list, target, event, &was_handled); }, @@ -304,6 +329,11 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe const str = try page.call_arena.dupeZ(u8, string.str()); try self.page.js.eval(str, null); }, + .object => |obj| { + if (try obj.getFunction("handleEvent")) |handleEvent| { + try handleEvent.callWithThis(void, obj, .{event}); + } + }, } // Restore original target (only if we changed it) @@ -350,12 +380,16 @@ fn cleanupMarkedListeners(self: *EventManager, list: *std.DoublyLinkedList) void } } -fn findListener(list: *const std.DoublyLinkedList, typ: []const u8, function: js.Function, capture: bool) ?*Listener { +fn findListener(list: *const std.DoublyLinkedList, typ: []const u8, callback: Callback, capture: bool) ?*Listener { var node = list.first; while (node) |n| { node = n.next; const listener: *Listener = @alignCast(@fieldParentPtr("node", n)); - if (!listener.function.eql(function)) { + const matches = switch (callback) { + .object => |obj| listener.function.eqlObject(obj), + .function => |func| listener.function.eqlFunction(func), + }; + if (!matches) { continue; } if (listener.capture != capture) { @@ -383,11 +417,19 @@ const Listener = struct { const Function = union(enum) { value: js.Function, string: String, + object: js.Object, - fn eql(self: Function, func: js.Function) bool { + fn eqlFunction(self: Function, func: js.Function) bool { return switch (self) { - .string => false, .value => |v| return v.id == func.id, + else => false, + }; + } + + fn eqlObject(self: Function, obj: js.Object) bool { + return switch (self) { + .object => |o| return o.getId() == obj.getId(), + else => false, }; } }; diff --git a/src/browser/js/Object.zig b/src/browser/js/Object.zig index 2e77a54a..0e25b963 100644 --- a/src/browser/js/Object.zig +++ b/src/browser/js/Object.zig @@ -32,6 +32,10 @@ const Object = @This(); js_obj: v8.Object, context: *js.Context, +pub fn getId(self: Object) u32 { + return self.js_obj.getIdentityHash(); +} + pub const SetOpts = packed struct(u32) { READ_ONLY: bool = false, DONT_ENUM: bool = false, diff --git a/src/browser/tests/events.html b/src/browser/tests/events.html index a3682ae1..84300217 100644 --- a/src/browser/tests/events.html +++ b/src/browser/tests/events.html @@ -497,3 +497,118 @@ testing.expectEqual('inner1', nested_calls[4]); testing.expectEqual(5, nested_calls.length); + +

+ diff --git a/src/browser/webapi/EventTarget.zig b/src/browser/webapi/EventTarget.zig index b399f7cb..75996fe3 100644 --- a/src/browser/webapi/EventTarget.zig +++ b/src/browser/webapi/EventTarget.zig @@ -20,7 +20,8 @@ const std = @import("std"); const js = @import("../js/js.zig"); const Page = @import("../Page.zig"); -const RegisterOptions = @import("../EventManager.zig").RegisterOptions; +const EventManager = @import("../EventManager.zig"); +const RegisterOptions = EventManager.RegisterOptions; const Event = @import("Event.zig"); @@ -30,6 +31,7 @@ const _prototype_root = true; _type: Type, pub const Type = union(enum) { + generic: void, node: *@import("Node.zig"), window: *@import("Window.zig"), xhr: *@import("net/XMLHttpRequestEventTarget.zig"), @@ -42,6 +44,12 @@ pub const Type = union(enum) { screen_orientation: *@import("Screen.zig").Orientation, }; +pub fn init(page: *Page) !*EventTarget { + return page._factory.create(EventTarget{ + ._type = .generic, + }); +} + pub fn dispatchEvent(self: *EventTarget, event: *Event, page: *Page) !bool { try page._event_manager.dispatch(self, event); return !event._cancelable or !event._prevent_default; @@ -59,9 +67,15 @@ pub const EventListenerCallback = union(enum) { pub fn addEventListener(self: *EventTarget, typ: []const u8, callback_: ?EventListenerCallback, opts_: ?AddEventListenerOptions, page: *Page) !void { const callback = callback_ orelse return; - const actual_callback = switch (callback) { - .function => |func| func, - .object => |obj| (try obj.getFunction("handleEvent")) orelse return, + if (callback == .object) { + if (try callback.object.getFunction("handleEvent") == null) { + return; + } + } + + const em_callback = switch (callback) { + .function => |func| EventManager.Callback{ .function = func }, + .object => |obj| EventManager.Callback{ .object = try obj.persist() }, }; const options = blk: { @@ -71,7 +85,7 @@ pub fn addEventListener(self: *EventTarget, typ: []const u8, callback_: ?EventLi .capture => |capture| RegisterOptions{ .capture = capture }, }; }; - return page._event_manager.register(self, typ, actual_callback, options); + return page._event_manager.register(self, typ, em_callback, options); } const RemoveEventListenerOptions = union(enum) { @@ -79,36 +93,63 @@ const RemoveEventListenerOptions = union(enum) { options: Options, const Options = struct { - useCapture: bool = false, + capture: bool = false, }; }; pub fn removeEventListener(self: *EventTarget, typ: []const u8, callback_: ?EventListenerCallback, opts_: ?RemoveEventListenerOptions, page: *Page) !void { const callback = callback_ orelse return; - const actual_callback = switch (callback) { - .function => |func| func, - .object => |obj| (try obj.getFunction("handleEvent")) orelse return, + // For object callbacks, check if handleEvent exists + if (callback == .object) { + if (try callback.object.getFunction("handleEvent") == null) { + return; + } + } + + const em_callback = switch (callback) { + .function => |func| EventManager.Callback{ .function = func }, + .object => |obj| EventManager.Callback{ .object = try obj.persist() }, }; const use_capture = blk: { const o = opts_ orelse break :blk false; break :blk switch (o) { .capture => |capture| capture, - .options => |opts| opts.useCapture, + .options => |opts| opts.capture, }; }; - return page._event_manager.remove(self, typ, actual_callback, use_capture); + return page._event_manager.remove(self, typ, em_callback, use_capture); } pub fn format(self: *EventTarget, writer: *std.Io.Writer) !void { return switch (self._type) { .node => |n| n.format(writer), - .window => writer.writeAll(""), + .generic => writer.writeAll(""), + .window => writer.writeAll(""), .xhr => writer.writeAll(""), - .abort_signal => writer.writeAll(""), + .abort_signal => writer.writeAll(""), .media_query_list => writer.writeAll(""), .message_port => writer.writeAll(""), .text_track_cue => writer.writeAll(""), + .navigation => writer.writeAll(""), + .screen => writer.writeAll(""), + .screen_orientation => writer.writeAll(""), + }; +} + +pub fn toString(self: *EventTarget) []const u8 { + return switch (self._type) { + .node => |n| return n.className(), + .generic => return "[object EventTarget]", + .window => return "[object Window]", + .xhr => return "[object XMLHttpRequestEventTarget]", + .abort_signal => return "[object AbortSignal]", + .media_query_list => return "[object MediaQueryList]", + .message_port => return "[object MessagePort]", + .text_track_cue => return "[object TextTrackCue]", + .navigation => return "[object Navigation]", + .screen => return "[object Screen]", + .screen_orientation => return "[object ScreenOrientation]", }; } @@ -122,15 +163,16 @@ pub const JsApi = struct { pub var class_id: bridge.ClassId = undefined; }; + pub const constructor = bridge.constructor(EventTarget.init, .{}); pub const dispatchEvent = bridge.function(EventTarget.dispatchEvent, .{}); pub const addEventListener = bridge.function(EventTarget.addEventListener, .{}); pub const removeEventListener = bridge.function(EventTarget.removeEventListener, .{}); + pub const toString = bridge.function(EventTarget.toString, .{}); }; const testing = @import("../../testing.zig"); test "WebApi: EventTarget" { // we create thousands of these per page. Nothing should bloat it. try testing.expectEqual(16, @sizeOf(EventTarget)); - try testing.htmlRunner("events.html", .{}); }