From c3ba83ff93d7d0d4134e8b23b690cc2391d1ad85 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Tue, 27 Jan 2026 09:39:08 +0100 Subject: [PATCH 01/19] use less aggressive v8 GC Isolate.lowMemoryNotification runs an aggrissive GC. Using Isolate.memoryPressureNotification allow a more granular control of GC. --- src/browser/Browser.zig | 2 +- src/browser/Page.zig | 9 ++++----- src/browser/js/Env.zig | 18 ++++++++++++++++++ src/browser/js/Isolate.zig | 10 ++++++++++ 4 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/browser/Browser.zig b/src/browser/Browser.zig index 70b04429..1d514a12 100644 --- a/src/browser/Browser.zig +++ b/src/browser/Browser.zig @@ -100,7 +100,7 @@ pub fn closeSession(self: *Browser) void { session.deinit(); self.session = null; _ = self.session_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 }); - self.env.lowMemoryNotification(); + self.env.memoryPressureNotification(.critical); } } diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 0cdd177f..1c8cd701 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -253,7 +253,7 @@ fn reset(self: *Page, comptime initializing: bool) !void { } if (comptime initializing == false) { - // Removins the context triggers the linked inspector. + // Removing the context triggers the linked inspector. // It seems to append a collect task to the message loop. self._session.executor.removeContext(); @@ -262,10 +262,9 @@ fn reset(self: *Page, comptime initializing: bool) !void { // will run after the GC and we will use memory after free. self._session.browser.runMessageLoop(); - // We force a garbage collection with lowMemoryNotification between - // page navigations to keep v8 memory usage as low as possible. - // Calling immediately after a runMessageLoop ensure - self._session.browser.env.lowMemoryNotification(); + // We force a garbage collection between page navigations to keep v8 + // memory usage as low as possible. + self._session.browser.env.memoryPressureNotification(.moderate); self._script_manager.shutdown = true; self._session.browser.http_client.abort(); diff --git a/src/browser/js/Env.zig b/src/browser/js/Env.zig index 78fde66a..69f7cd20 100644 --- a/src/browser/js/Env.zig +++ b/src/browser/js/Env.zig @@ -24,6 +24,7 @@ const log = @import("../../log.zig"); const bridge = @import("bridge.zig"); const Context = @import("Context.zig"); +const Isolate = @import("Isolate.zig"); const Platform = @import("Platform.zig"); const Snapshot = @import("Snapshot.zig"); const Inspector = @import("Inspector.zig"); @@ -193,6 +194,8 @@ pub fn newExecutionWorld(self: *Env) !ExecutionWorld { // a Context, it's managed by the garbage collector. We use the // `lowMemoryNotification` call on the isolate to encourage v8 to free // any contexts which have been freed. +// This GC is very aggressive. Use memoryPressureNotification for less +// aggressive GC passes. pub fn lowMemoryNotification(self: *Env) void { var handle_scope: js.HandleScope = undefined; handle_scope.init(self.isolate); @@ -200,6 +203,21 @@ pub fn lowMemoryNotification(self: *Env) void { self.isolate.lowMemoryNotification(); } +// V8 doesn't immediately free memory associated with +// a Context, it's managed by the garbage collector. We use the +// `memoryPressureNotification` call on the isolate to encourage v8 to free +// any contexts which have been freed. +// The level indicates the aggressivity of the GC required: +// moderate speeds up incremental GC +// critical runs one full GC +// For a more aggressive GC, use lowMemoryNotification. +pub fn memoryPressureNotification(self: *Env, level: Isolate.MemoryPressureLevel) void { + var handle_scope: js.HandleScope = undefined; + handle_scope.init(self.isolate); + defer handle_scope.deinit(); + self.isolate.memoryPressureNotification(level); +} + pub fn dumpMemoryStats(self: *Env) void { const stats = self.isolate.getHeapStatistics(); std.debug.print( diff --git a/src/browser/js/Isolate.zig b/src/browser/js/Isolate.zig index fdede915..74974cc0 100644 --- a/src/browser/js/Isolate.zig +++ b/src/browser/js/Isolate.zig @@ -57,6 +57,16 @@ pub fn lowMemoryNotification(self: Isolate) void { v8.v8__Isolate__LowMemoryNotification(self.handle); } +pub const MemoryPressureLevel = enum(u32) { + none = v8.kNone, + moderate = v8.kModerate, + critical = v8.kCritical, +}; + +pub fn memoryPressureNotification(self: Isolate, level: MemoryPressureLevel) void { + v8.v8__Isolate__MemoryPressureNotification(self.handle, @intFromEnum(level)); +} + pub fn notifyContextDisposed(self: Isolate) void { _ = v8.v8__Isolate__ContextDisposedNotification(self.handle); } From fc5496e570dde06360b098f1b952a08fdfc2e16c Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Tue, 27 Jan 2026 18:39:29 +0100 Subject: [PATCH 02/19] always log try/catch error on call function We force log of detailled error caught during function call. --- src/browser/js/Function.zig | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/src/browser/js/Function.zig b/src/browser/js/Function.zig index 8fecccfe..458bbb40 100644 --- a/src/browser/js/Function.zig +++ b/src/browser/js/Function.zig @@ -20,6 +20,8 @@ const std = @import("std"); const js = @import("js.zig"); const v8 = js.v8; +const log = @import("../../log.zig"); + const Allocator = std.mem.Allocator; const Function = @This(); @@ -69,25 +71,41 @@ pub fn newInstance(self: *const Function, caught: *js.TryCatch.Caught) !js.Objec pub fn call(self: *const Function, comptime T: type, args: anytype) !T { var caught: js.TryCatch.Caught = undefined; - return self._tryCallWithThis(T, self.getThis(), args, &caught, false); + return self._tryCallWithThis(T, self.getThis(), args, &caught) catch |err| { + log.warn(.js, "call caught", .{ + .err = err, + .exception = caught.exception, + .line = caught.line orelse 0, + .stack = caught.stack orelse "???", + }); + return err; + }; } pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype) !T { var caught: js.TryCatch.Caught = undefined; - return self._tryCallWithThis(T, this, args, &caught, false); + return self._tryCallWithThis(T, this, args, &caught) catch |err| { + log.warn(.js, "callWithThis caught", .{ + .err = err, + .exception = caught.exception, + .line = caught.line orelse 0, + .stack = caught.stack orelse "???", + }); + return err; + }; } pub fn tryCall(self: *const Function, comptime T: type, args: anytype, caught: *js.TryCatch.Caught) !T { caught.* = .{}; - return self._tryCallWithThis(T, self.getThis(), args, caught, true); + return self._tryCallWithThis(T, self.getThis(), args, caught); } pub fn tryCallWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype, caught: *js.TryCatch.Caught) !T { caught.* = .{}; - return self._tryCallWithThis(T, this, args, caught, true); + return self._tryCallWithThis(T, this, args, caught); } -pub fn _tryCallWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype, caught: *js.TryCatch.Caught, comptime need_caught: bool) !T { +pub fn _tryCallWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype, caught: *js.TryCatch.Caught) !T { const local = self.local; // When we're calling a function from within JavaScript itself, this isn't @@ -140,11 +158,7 @@ pub fn _tryCallWithThis(self: *const Function, comptime T: type, this: anytype, defer try_catch.deinit(); const handle = v8.v8__Function__Call(self.handle, local.handle, js_this.handle, @as(c_int, @intCast(js_args.len)), c_args) orelse { - if (comptime need_caught) { - // relatively expensive, so if the caller knows caught won't be needed, - // we can leave it uninitialized. - caught.* = try_catch.caughtOrError(local.call_arena, error.JSExecCallback); - } + caught.* = try_catch.caughtOrError(local.call_arena, error.JSExecCallback); return error.JSExecCallback; }; From 89174ba0b60127092c48f4a9181449cd1a15fd26 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Tue, 27 Jan 2026 01:27:18 +0300 Subject: [PATCH 03/19] `EventManager`: introduce `inline_lookup` Idea with this is to have a key-to-function for known event listeners. We pack pointer to event target with listener type to generate key and set function as value. By doing this, we save bytes for optionally and rarely set functions in elements. --- src/browser/EventManager.zig | 150 +++++++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) diff --git a/src/browser/EventManager.zig b/src/browser/EventManager.zig index fb6d3674..3e0d4d56 100644 --- a/src/browser/EventManager.zig +++ b/src/browser/EventManager.zig @@ -22,6 +22,7 @@ const builtin = @import("builtin"); const log = @import("../log.zig"); const String = @import("../string.zig").String; +const lp = @import("lightpanda"); const js = @import("js/js.zig"); const Page = @import("Page.zig"); @@ -42,11 +43,26 @@ list_pool: std.heap.MemoryPool(std.DoublyLinkedList), lookup: std.AutoHashMapUnmanaged(usize, *std.DoublyLinkedList), dispatch_depth: usize, deferred_removals: std.ArrayList(struct { list: *std.DoublyLinkedList, listener: *Listener }), +/// Use this when a listener provided like this: +/// +/// ```html +/// +/// ``` +/// +/// Or: +/// +/// ```js +/// img.onload = () => { ... }; +/// ``` +inline_lookup: std.AutoHashMapUnmanaged(usize, js.Function.Global), pub fn init(page: *Page) EventManager { + lp.assert(@alignOf(EventTarget) == 8, "EventManager.init", .{ .event_target_alignment = @alignOf(EventTarget) }); + return .{ .page = page, .lookup = .{}, + .inline_lookup = .{}, .arena = page.arena, .list_pool = std.heap.MemoryPool(std.DoublyLinkedList).init(page.arena), .listener_pool = std.heap.MemoryPool(Listener).init(page.arena), @@ -67,6 +83,32 @@ pub const Callback = union(enum) { object: js.Object, }; +/// Sets an inline event listener (`onload`, `onclick`, `onwheel` etc.); +/// overrides the listener if there's already one. +pub fn setInlineListener( + self: *EventManager, + event_target: *EventTarget, + event_type: Listener.Type, + listener_callback: js.Function.Global, +) !void { + if (comptime IS_DEBUG) { + log.debug(.event, "EventManager.setInlineListener", .{ .event_target = event_target, .event_type = event_type }); + } + + const key = createLookupKey(event_target, event_type); + const gop = try self.inline_lookup.getOrPut(self.arena, key); + gop.value_ptr.* = listener_callback; +} + +/// Returns the inline event listener by the `EventTarget` and event type. +pub fn getInlineListener( + self: *const EventManager, + event_target: *EventTarget, + event_type: Listener.Type, +) ?js.Function.Global { + return self.inline_lookup.get(createLookupKey(event_target, event_type)); +} + 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, .target = target }); @@ -442,6 +484,16 @@ fn findListener(list: *const std.DoublyLinkedList, typ: []const u8, callback: Ca return null; } +/// Creates a lookup key to use with `inline_lookup`. +inline fn createLookupKey(event_target: *EventTarget, event_type: Listener.Type) usize { + return @intFromPtr(event_target) >> 3 | (@as(u64, @intFromEnum(event_type)) << 57); +} + +/// Returns listener type from `inline_lookup` key. +inline fn getListenerType(key: usize) Listener.Type { + return @enumFromInt(key >> 57); +} + const Listener = struct { typ: String, once: bool, @@ -451,6 +503,104 @@ const Listener = struct { signal: ?*@import("webapi/AbortSignal.zig") = null, node: std.DoublyLinkedList.Node, removed: bool = false, + + const Type = enum(u7) { + abort, + animationcancel, + animationend, + animationiteration, + animationstart, + auxclick, + beforeinput, + beforematch, + beforetoggle, + blur, + cancel, + canplay, + canplaythrough, + change, + click, + close, + command, + contentvisibilityautostatechange, + contextlost, + contextmenu, + contextrestored, + copy, + cuechange, + cut, + dblclick, + drag, + dragend, + dragenter, + dragexit, + dragleave, + dragover, + dragstart, + drop, + durationchange, + emptied, + ended, + @"error", + focus, + formdata, + fullscreenchange, + fullscreenerror, + gotpointercapture, + input, + invalid, + keydown, + keypress, + keyup, + load, + loadeddata, + loadedmetadata, + loadstart, + lostpointercapture, + mousedown, + mousemove, + mouseout, + mouseover, + mouseup, + paste, + pause, + play, + playing, + pointercancel, + pointerdown, + pointerenter, + pointerleave, + pointermove, + pointerout, + pointerover, + pointerrawupdate, + pointerup, + progress, + ratechange, + reset, + resize, + scroll, + scrollend, + securitypolicyviolation, + seeked, + seeking, + select, + selectionchange, + selectstart, + slotchange, + stalled, + submit, + @"suspend", + timeupdate, + toggle, + transitioncancel, + transitionend, + transitionrun, + transitionstart, + volumechange, + waiting, + wheel, + }; }; const Function = union(enum) { From 6ad1a11593f442261fef8e52a410fa460e4eef64 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Tue, 27 Jan 2026 01:55:54 +0300 Subject: [PATCH 04/19] catch pointer overflows in `createLookupKey` Its better to have this; if this is incorrect, its better to get notified. --- src/browser/EventManager.zig | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/browser/EventManager.zig b/src/browser/EventManager.zig index 3e0d4d56..6ec65a9d 100644 --- a/src/browser/EventManager.zig +++ b/src/browser/EventManager.zig @@ -485,8 +485,10 @@ fn findListener(list: *const std.DoublyLinkedList, typ: []const u8, callback: Ca } /// Creates a lookup key to use with `inline_lookup`. -inline fn createLookupKey(event_target: *EventTarget, event_type: Listener.Type) usize { - return @intFromPtr(event_target) >> 3 | (@as(u64, @intFromEnum(event_type)) << 57); +fn createLookupKey(event_target: *EventTarget, event_type: Listener.Type) usize { + const ptr = @intFromPtr(event_target) >> 3; + lp.assert(ptr < (1 << 57), "createLookupKey: pointer overflow", .{ .ptr = ptr }); + return ptr | (@as(u64, @intFromEnum(event_type)) << 57); } /// Returns listener type from `inline_lookup` key. From fd1e77df8facda519bbd23907b9d31a5f79ce7af Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Wed, 28 Jan 2026 01:31:43 +0300 Subject: [PATCH 05/19] parse event listeners provided as attributes --- src/browser/Page.zig | 232 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 232 insertions(+) diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 830b29b1..ae580737 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -2173,6 +2173,238 @@ fn populateElementAttributes(self: *Page, element: *Element, list: anytype) !voi } var attributes = try element.createAttributeList(self); while (list.next()) |attr| { + // Event handlers can be provided like attributes; here we check if there's such. + const name = attr.name.local; + lp.assert(name.len != 0, "populateElementAttributes: 0-length attr name", .{ .attr = attr }); + // Idea here is to make this check as cheap as possible. + const has_on_prefix = @as(u16, @bitCast([2]u8{ name.ptr[0], name.ptr[1 % name.len] })) == asUint("on"); + // We may have found an event handler. + if (has_on_prefix) { + // Must be usable as function. + const func = try self.js.stringToPersistedFunction(attr.value.slice()); + const target = element.asEventTarget(); + const event_manager = &self._event_manager; + + // Longest known listener kind is 32 bytes long. + const remaining: u6 = @truncate(name.len -| 2); + const unsafe = name.ptr + 2; + const Vec16x8 = @Vector(16, u8); + const Vec32x8 = @Vector(32, u8); + + switch (remaining) { + 3 => if (@as(u24, @bitCast(unsafe[0..3].*)) == asUint("cut")) { + try event_manager.setInlineListener(target, .cut, func); + }, + 4 => switch (@as(u32, @bitCast(unsafe[0..4].*))) { + asUint("blur") => try event_manager.setInlineListener(target, .blur, func), + asUint("copy") => try event_manager.setInlineListener(target, .copy, func), + asUint("drag") => try event_manager.setInlineListener(target, .drag, func), + asUint("drop") => try event_manager.setInlineListener(target, .drop, func), + asUint("load") => try event_manager.setInlineListener(target, .load, func), + asUint("play") => try event_manager.setInlineListener(target, .play, func), + else => {}, + }, + 5 => switch (@as(u40, @bitCast(unsafe[0..5].*))) { + asUint("abort") => try event_manager.setInlineListener(target, .abort, func), + asUint("click") => try event_manager.setInlineListener(target, .click, func), + asUint("close") => try event_manager.setInlineListener(target, .close, func), + asUint("ended") => try event_manager.setInlineListener(target, .ended, func), + asUint("error") => try event_manager.setInlineListener(target, .@"error", func), + asUint("focus") => try event_manager.setInlineListener(target, .focus, func), + asUint("input") => try event_manager.setInlineListener(target, .input, func), + asUint("keyup") => try event_manager.setInlineListener(target, .keyup, func), + asUint("paste") => try event_manager.setInlineListener(target, .paste, func), + asUint("pause") => try event_manager.setInlineListener(target, .pause, func), + asUint("reset") => try event_manager.setInlineListener(target, .reset, func), + asUint("wheel") => try event_manager.setInlineListener(target, .wheel, func), + else => {}, + }, + 6 => switch (@as(u48, @bitCast(unsafe[0..6].*))) { + asUint("cancel") => try event_manager.setInlineListener(target, .cancel, func), + asUint("change") => try event_manager.setInlineListener(target, .change, func), + asUint("resize") => try event_manager.setInlineListener(target, .resize, func), + asUint("scroll") => try event_manager.setInlineListener(target, .scroll, func), + asUint("seeked") => try event_manager.setInlineListener(target, .seeked, func), + asUint("select") => try event_manager.setInlineListener(target, .select, func), + asUint("submit") => try event_manager.setInlineListener(target, .submit, func), + asUint("toggle") => try event_manager.setInlineListener(target, .toggle, func), + else => {}, + }, + 7 => switch (@as(u56, @bitCast(unsafe[0..7].*))) { + asUint("canplay") => try event_manager.setInlineListener(target, .canplay, func), + asUint("command") => try event_manager.setInlineListener(target, .command, func), + asUint("dragend") => try event_manager.setInlineListener(target, .dragend, func), + asUint("emptied") => try event_manager.setInlineListener(target, .emptied, func), + asUint("invalid") => try event_manager.setInlineListener(target, .invalid, func), + asUint("keydown") => try event_manager.setInlineListener(target, .keydown, func), + asUint("mouseup") => try event_manager.setInlineListener(target, .mouseup, func), + asUint("playing") => try event_manager.setInlineListener(target, .playing, func), + asUint("seeking") => try event_manager.setInlineListener(target, .seeking, func), + asUint("stalled") => try event_manager.setInlineListener(target, .stalled, func), + asUint("suspend") => try event_manager.setInlineListener(target, .@"suspend", func), + asUint("waiting") => try event_manager.setInlineListener(target, .waiting, func), + else => {}, + }, + 8 => switch (@as(u64, @bitCast(unsafe[0..8].*))) { + asUint("auxclick") => try event_manager.setInlineListener(target, .auxclick, func), + asUint("dblclick") => try event_manager.setInlineListener(target, .dblclick, func), + asUint("dragexit") => try event_manager.setInlineListener(target, .dragexit, func), + asUint("dragover") => try event_manager.setInlineListener(target, .dragover, func), + asUint("formdata") => try event_manager.setInlineListener(target, .formdata, func), + asUint("keypress") => try event_manager.setInlineListener(target, .keypress, func), + asUint("mouseout") => try event_manager.setInlineListener(target, .mouseout, func), + asUint("progress") => try event_manager.setInlineListener(target, .progress, func), + else => {}, + }, + // Won't fit to 64-bit integer; we do 2 checks. + 9 => switch (@as(u64, @bitCast(unsafe[0..8].*))) { + asUint("cuechang") => if (unsafe[8] == 'e') try event_manager.setInlineListener(target, .cuechange, func), + asUint("dragente") => if (unsafe[8] == 'r') try event_manager.setInlineListener(target, .dragenter, func), + asUint("dragleav") => if (unsafe[8] == 'e') try event_manager.setInlineListener(target, .dragleave, func), + asUint("dragstar") => if (unsafe[8] == 't') try event_manager.setInlineListener(target, .dragstart, func), + asUint("loadstar") => if (unsafe[8] == 't') try event_manager.setInlineListener(target, .loadstart, func), + asUint("mousedow") => if (unsafe[8] == 'n') try event_manager.setInlineListener(target, .mousedown, func), + asUint("mousemov") => if (unsafe[8] == 'e') try event_manager.setInlineListener(target, .mousemove, func), + asUint("mouseove") => if (unsafe[8] == 'r') try event_manager.setInlineListener(target, .mouseover, func), + asUint("pointeru") => if (unsafe[8] == 'p') try event_manager.setInlineListener(target, .pointerup, func), + asUint("scrollen") => if (unsafe[8] == 'd') try event_manager.setInlineListener(target, .scrollend, func), + else => {}, + }, + 10 => switch (@as(u64, @bitCast(unsafe[0..8].*))) { + asUint("loadedda") => if (asUint("ta") == @as(u16, @bitCast(unsafe[8..10].*))) + try event_manager.setInlineListener(target, .loadeddata, func), + asUint("pointero") => if (asUint("ut") == @as(u16, @bitCast(unsafe[8..10].*))) + try event_manager.setInlineListener(target, .pointerout, func), + asUint("ratechan") => if (asUint("ge") == @as(u16, @bitCast(unsafe[8..10].*))) + try event_manager.setInlineListener(target, .ratechange, func), + asUint("slotchan") => if (asUint("ge") == @as(u16, @bitCast(unsafe[8..10].*))) + try event_manager.setInlineListener(target, .slotchange, func), + asUint("timeupda") => if (asUint("te") == @as(u16, @bitCast(unsafe[8..10].*))) + try event_manager.setInlineListener(target, .timeupdate, func), + else => {}, + }, + 11 => switch (@as(u64, @bitCast(unsafe[0..8].*))) { + asUint("beforein") => if (asUint("put") == @as(u24, @bitCast(unsafe[8..11].*))) + try event_manager.setInlineListener(target, .beforeinput, func), + asUint("beforema") => if (asUint("tch") == @as(u24, @bitCast(unsafe[8..11].*))) + try event_manager.setInlineListener(target, .beforematch, func), + asUint("contextl") => if (asUint("ost") == @as(u24, @bitCast(unsafe[8..11].*))) + try event_manager.setInlineListener(target, .contextlost, func), + asUint("contextm") => if (asUint("enu") == @as(u24, @bitCast(unsafe[8..11].*))) + try event_manager.setInlineListener(target, .contextmenu, func), + asUint("pointerd") => if (asUint("own") == @as(u24, @bitCast(unsafe[8..11].*))) + try event_manager.setInlineListener(target, .pointerdown, func), + asUint("pointerm") => if (asUint("ove") == @as(u24, @bitCast(unsafe[8..11].*))) + try event_manager.setInlineListener(target, .pointermove, func), + asUint("pointero") => if (asUint("ver") == @as(u24, @bitCast(unsafe[8..11].*))) + try event_manager.setInlineListener(target, .pointerover, func), + asUint("selectst") => if (asUint("art") == @as(u24, @bitCast(unsafe[8..11].*))) + try event_manager.setInlineListener(target, .selectstart, func), + else => {}, + }, + 12 => switch (@as(u64, @bitCast(unsafe[0..8].*))) { + asUint("animatio") => if (asUint("nend") == @as(u32, @bitCast(unsafe[8..12].*))) + try event_manager.setInlineListener(target, .animationend, func), + asUint("beforeto") => if (asUint("ggle") == @as(u32, @bitCast(unsafe[8..12].*))) + try event_manager.setInlineListener(target, .beforetoggle, func), + asUint("pointere") => if (asUint("nter") == @as(u32, @bitCast(unsafe[8..12].*))) + try event_manager.setInlineListener(target, .pointerenter, func), + asUint("pointerl") => if (asUint("eave") == @as(u32, @bitCast(unsafe[8..12].*))) + try event_manager.setInlineListener(target, .pointerleave, func), + asUint("volumech") => if (asUint("ange") == @as(u32, @bitCast(unsafe[8..12].*))) + try event_manager.setInlineListener(target, .volumechange, func), + else => {}, + }, + 13 => switch (@as(u64, @bitCast(unsafe[0..8].*))) { + asUint("pointerc") => if (asUint("ancel") == @as(u40, @bitCast(unsafe[8..13].*))) + try event_manager.setInlineListener(target, .pointercancel, func), + asUint("transiti") => switch (@as(u40, @bitCast(unsafe[8..13].*))) { + asUint("onend") => try event_manager.setInlineListener(target, .transitionend, func), + asUint("onrun") => try event_manager.setInlineListener(target, .transitionrun, func), + else => {}, + }, + else => {}, + }, + 14 => switch (@as(u64, @bitCast(unsafe[0..8].*))) { + asUint("animatio") => if (asUint("nstart") == @as(u48, @bitCast(unsafe[8..14].*))) + try event_manager.setInlineListener(target, .animationstart, func), + asUint("canplayt") => if (asUint("hrough") == @as(u48, @bitCast(unsafe[8..14].*))) + try event_manager.setInlineListener(target, .canplaythrough, func), + asUint("duration") => if (asUint("change") == @as(u48, @bitCast(unsafe[8..14].*))) + try event_manager.setInlineListener(target, .durationchange, func), + asUint("loadedme") => if (asUint("tadata") == @as(u48, @bitCast(unsafe[8..14].*))) + try event_manager.setInlineListener(target, .loadedmetadata, func), + else => {}, + }, + 15 => switch (@as(u64, @bitCast(unsafe[0..8].*))) { + asUint("animatio") => if (asUint("ncancel") == @as(u56, @bitCast(unsafe[8..15].*))) + try event_manager.setInlineListener(target, .animationcancel, func), + asUint("contextr") => if (asUint("estored") == @as(u56, @bitCast(unsafe[8..15].*))) + try event_manager.setInlineListener(target, .contextrestored, func), + asUint("fullscre") => if (asUint("enerror") == @as(u56, @bitCast(unsafe[8..15].*))) + try event_manager.setInlineListener(target, .fullscreenerror, func), + asUint("selectio") => if (asUint("nchange") == @as(u56, @bitCast(unsafe[8..15].*))) + try event_manager.setInlineListener(target, .selectionchange, func), + asUint("transiti") => if (asUint("onstart") == @as(u56, @bitCast(unsafe[8..15].*))) + try event_manager.setInlineListener(target, .transitionstart, func), + else => {}, + }, + // Can't switch on vector types. + 16 => { + const as_vector: Vec16x8 = unsafe[0..16].*; + + if (@reduce(.And, as_vector == @as(Vec16x8, "fullscreenchange".*))) { + try event_manager.setInlineListener(target, .fullscreenchange, func); + } else if (@reduce(.And, as_vector == @as(Vec16x8, "pointerrawupdate".*))) { + try event_manager.setInlineListener(target, .pointerrawupdate, func); + } else if (@reduce(.And, as_vector == @as(Vec16x8, "transitioncancel".*))) { + try event_manager.setInlineListener(target, .transitioncancel, func); + } + }, + 17 => { + const as_vector: Vec16x8 = unsafe[0..16].*; + + const dirty = @reduce(.And, as_vector == @as(Vec16x8, "gotpointercaptur".*)) and + unsafe[16] == 'e'; + if (dirty) { + try event_manager.setInlineListener(target, .gotpointercapture, func); + } + }, + 18 => { + const as_vector: Vec16x8 = unsafe[0..16].*; + + const is_animationiteration = @reduce(.And, as_vector == @as(Vec16x8, "animationiterati".*)) and + asUint("on") == @as(u16, @bitCast(unsafe[16..18].*)); + if (is_animationiteration) { + try event_manager.setInlineListener(target, .animationiteration, func); + } else { + const is_lostpointercapture = @reduce(.And, as_vector == @as(Vec16x8, "lostpointercaptu".*)) and + asUint("re") == @as(u16, @bitCast(unsafe[16..18].*)); + if (is_lostpointercapture) { + try event_manager.setInlineListener(target, .lostpointercapture, func); + } + } + }, + 23 => { + const as_vector: Vec16x8 = unsafe[0..16].*; + + const dirty = @reduce(.And, as_vector == @as(Vec16x8, "securitypolicyvi".*)) and + asUint("olation") == @as(u56, @bitCast(unsafe[16..23].*)); + if (dirty) { + try event_manager.setInlineListener(target, .securitypolicyviolation, func); + } + }, + 32 => { + const as_vector: Vec32x8 = unsafe[0..32].*; + + if (@reduce(.And, as_vector == @as(Vec32x8, "contentvisibilityautostatechange".*))) { + try event_manager.setInlineListener(target, .contentvisibilityautostatechange, func); + } + }, + else => {}, + } + } + try attributes.putNew(attr.name.local.slice(), attr.value.slice(), self); } } From 560f028bdac7a2236f0f186fd17dedbc7a21fa10 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Wed, 28 Jan 2026 01:33:17 +0300 Subject: [PATCH 06/19] remove unused `getListenerType` --- src/browser/EventManager.zig | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/browser/EventManager.zig b/src/browser/EventManager.zig index 6ec65a9d..90cc888c 100644 --- a/src/browser/EventManager.zig +++ b/src/browser/EventManager.zig @@ -491,11 +491,6 @@ fn createLookupKey(event_target: *EventTarget, event_type: Listener.Type) usize return ptr | (@as(u64, @intFromEnum(event_type)) << 57); } -/// Returns listener type from `inline_lookup` key. -inline fn getListenerType(key: usize) Listener.Type { - return @enumFromInt(key >> 57); -} - const Listener = struct { typ: String, once: bool, From 066069baada6f370d8c1e364efc43b989edced71 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Wed, 28 Jan 2026 07:33:04 +0800 Subject: [PATCH 07/19] Add defensiveness around Parser.appendCallback We're seeing an assertion in Page.appendNew fail because the node has a parent. According to html5ever, this shouldn't be possible (appendNew is only called from the Parser). BUT, it's possible we're mutating the node in a way that we shouldn't...maybe there's JavaScript executing as we're parsing which is mutating the node. In release, this will be more defensive. In debug, this still crashes. It's possible this is valid (like I said, maybe there's JS interleaved which is mutating the node), but if so, I'd like to know the exact scenario that produces this case. --- src/browser/URL.zig | 2 +- src/browser/parser/Parser.zig | 12 ++++++++++++ src/cdp/Node.zig | 8 ++++---- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/browser/URL.zig b/src/browser/URL.zig index c5177d95..356d2d55 100644 --- a/src/browser/URL.zig +++ b/src/browser/URL.zig @@ -95,7 +95,7 @@ pub fn resolve(allocator: Allocator, base: [:0]const u8, path: anytype, comptime in_i += 2; continue; } - if (out[in_i + 1] == '.' and out[in_i + 2] == '/') { // always safe, because we added two whitespaces + if (out[in_i + 1] == '.' and out[in_i + 2] == '/') { // always safe, because we added two whitespaces // /../ if (out_i > path_marker) { // go back before the / diff --git a/src/browser/parser/Parser.zig b/src/browser/parser/Parser.zig index d2d952f4..49eb362b 100644 --- a/src/browser/parser/Parser.zig +++ b/src/browser/parser/Parser.zig @@ -24,6 +24,7 @@ const Page = @import("../Page.zig"); const Node = @import("../webapi/Node.zig"); const Element = @import("../webapi/Element.zig"); const Allocator = std.mem.Allocator; +const IS_DEBUG = @import("builtin").mode == .Debug; pub const ParsedNode = struct { node: *Node, @@ -373,6 +374,17 @@ fn _appendCallback(self: *Parser, parent: *Node, node_or_text: h5e.NodeOrText) ! switch (node_or_text.toUnion()) { .node => |cpn| { const child = getNode(cpn); + if (child._parent) |previous_parent| { + // html5ever says this can't happen, but we might be screwing up + // the node on our side. We shouldn't be, but we're seeing this + // in the wild, and I'm not sure why. In debug, let's crash so + // we can try to figure it out. In release, let's disconnect + // the child first. + if (comptime IS_DEBUG) { + unreachable; + } + self._page.removeNode(previous_parent, child, .{ .will_be_reconnected = parent.isConnected() }); + } try self.page.appendNew(parent, .{ .node = child }); }, .text => |txt| try self.page.appendNew(parent, .{ .text = txt }), diff --git a/src/cdp/Node.zig b/src/cdp/Node.zig index d11dd8bd..2997a438 100644 --- a/src/cdp/Node.zig +++ b/src/cdp/Node.zig @@ -356,7 +356,7 @@ test "cdp Node: Registry register" { } { - const dom_node = (try doc.querySelector(.wrap ("p"), page)).?.asNode(); + const dom_node = (try doc.querySelector(.wrap("p"), page)).?.asNode(); const node = try registry.register(dom_node); const n1b = registry.lookup_by_id.get(2).?; const n1c = registry.lookup_by_node.get(node.dom).?; @@ -400,18 +400,18 @@ test "cdp Node: search list" { defer page._session.removePage(); var doc = page.window._document; - const s1 = try search_list.create((try doc.querySelectorAll(.wrap ("a"), page))._nodes); + const s1 = try search_list.create((try doc.querySelectorAll(.wrap("a"), page))._nodes); try testing.expectEqual("1", s1.name); try testing.expectEqualSlices(u32, &.{ 1, 2 }, s1.node_ids); try testing.expectEqual(2, registry.lookup_by_id.count()); try testing.expectEqual(2, registry.lookup_by_node.count()); - const s2 = try search_list.create((try doc.querySelectorAll(.wrap ("#a1"), page))._nodes); + const s2 = try search_list.create((try doc.querySelectorAll(.wrap("#a1"), page))._nodes); try testing.expectEqual("2", s2.name); try testing.expectEqualSlices(u32, &.{1}, s2.node_ids); - const s3 = try search_list.create((try doc.querySelectorAll(.wrap ("#a2"), page))._nodes); + const s3 = try search_list.create((try doc.querySelectorAll(.wrap("#a2"), page))._nodes); try testing.expectEqual("3", s3.name); try testing.expectEqualSlices(u32, &.{2}, s3.node_ids); From 30ed58ff0759ec945e8ffbecd1150e2b35206903 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Wed, 28 Jan 2026 08:06:22 +0800 Subject: [PATCH 08/19] fix build --- src/browser/parser/Parser.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/parser/Parser.zig b/src/browser/parser/Parser.zig index 49eb362b..2cb2acaa 100644 --- a/src/browser/parser/Parser.zig +++ b/src/browser/parser/Parser.zig @@ -383,7 +383,7 @@ fn _appendCallback(self: *Parser, parent: *Node, node_or_text: h5e.NodeOrText) ! if (comptime IS_DEBUG) { unreachable; } - self._page.removeNode(previous_parent, child, .{ .will_be_reconnected = parent.isConnected() }); + self.page.removeNode(previous_parent, child, .{ .will_be_reconnected = parent.isConnected() }); } try self.page.appendNew(parent, .{ .node = child }); }, From e51e6aa2b080b34377eba3c5d4affbc8a6aedecd Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Wed, 28 Jan 2026 14:44:05 +0800 Subject: [PATCH 09/19] Use ArenaPool when parsing HTML and for TextDecoder (with finalizer) Slowly more page.arena -> ArenaPool wherever possible. In some cases, an arena from the arenapool will be preferred over the call_arena also. --- src/browser/Page.zig | 11 +++++++++-- src/browser/webapi/DOMParser.zig | 7 +++++-- src/browser/webapi/Document.zig | 7 +++++-- src/browser/webapi/element/Html.zig | 5 ++++- src/browser/webapi/encoding/TextDecoder.zig | 21 +++++++++++++++++---- 5 files changed, 40 insertions(+), 11 deletions(-) diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 830b29b1..90eae0f4 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -745,7 +745,10 @@ fn pageDoneCallback(ctx: *anyopaque) !void { switch (self._parse_state) { .html => |buf| { - var parser = Parser.init(self.arena, self.document.asNode(), self); + const parse_arena = try self.getArena(.{ .debug = "Page.parse" }); + defer self.releaseArena(parse_arena); + + var parser = Parser.init(parse_arena, self.document.asNode(), self); parser.parse(buf.items); self._script_manager.staticScriptsDone(); if (self._script_manager.isDone()) { @@ -757,7 +760,11 @@ fn pageDoneCallback(ctx: *anyopaque) !void { }, .text => |*buf| { try buf.appendSlice(self.arena, ""); - var parser = Parser.init(self.arena, self.document.asNode(), self); + + const parse_arena = try self.getArena(.{ .debug = "Page.parse" }); + defer self.releaseArena(parse_arena); + + var parser = Parser.init(parse_arena, self.document.asNode(), self); parser.parse(buf.items); self.documentIsComplete(); }, diff --git a/src/browser/webapi/DOMParser.zig b/src/browser/webapi/DOMParser.zig index c79f9956..30f8bf33 100644 --- a/src/browser/webapi/DOMParser.zig +++ b/src/browser/webapi/DOMParser.zig @@ -48,6 +48,9 @@ pub fn parseFromString( @"image/svg+xml", }, mime_type) orelse return error.NotSupported; + const arena = try page.getArena(.{ .debug = "DOMParser.parseFromString" }); + defer page.releaseArena(arena); + return switch (target_mime) { .@"text/html" => { // Create a new HTMLDocument @@ -61,7 +64,7 @@ pub fn parseFromString( } // Parse HTML into the document - var parser = Parser.init(page.arena, doc.asNode(), page); + var parser = Parser.init(arena, doc.asNode(), page); parser.parse(normalized); if (parser.err) |pe| { @@ -78,7 +81,7 @@ pub fn parseFromString( // Parse XML into XMLDocument. const doc_node = doc.asNode(); - var parser = Parser.init(page.arena, doc_node, page); + var parser = Parser.init(arena, doc_node, page); parser.parseXML(html); if (parser.err) |pe| { diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig index e5c5f623..17b2639b 100644 --- a/src/browser/webapi/Document.zig +++ b/src/browser/webapi/Document.zig @@ -648,7 +648,10 @@ pub fn write(self: *Document, text: []const []const u8, page: *Page) !void { page._parse_mode = .document_write; defer page._parse_mode = previous_parse_mode; - var parser = Parser.init(page.call_arena, fragment_node, page); + const arena = try page.getArena(.{ .debug = "Document.write" }); + defer page.releaseArena(arena); + + var parser = Parser.init(arena, fragment_node, page); parser.parseFragment(html); // Extract children from wrapper HTML element (html5ever wraps fragments) @@ -661,7 +664,7 @@ pub fn write(self: *Document, text: []const []const u8, page: *Page) !void { var it = if (first.is(Element.Html.Html) == null) fragment_node.childrenIterator() else first.childrenIterator(); while (it.next()) |child| { - try children_to_insert.append(page.call_arena, child); + try children_to_insert.append(arena, child); } if (children_to_insert.items.len == 0) { diff --git a/src/browser/webapi/element/Html.zig b/src/browser/webapi/element/Html.zig index 9eb41edd..c96c25cb 100644 --- a/src/browser/webapi/element/Html.zig +++ b/src/browser/webapi/element/Html.zig @@ -281,8 +281,11 @@ pub fn insertAdjacentHTML( }); const doc_node = doc.asNode(); + const arena = try page.getArena(.{ .debug = "HTML.insertAdjacentHTML" }); + defer page.releaseArena(arena); + const Parser = @import("../../parser/Parser.zig"); - var parser = Parser.init(page.call_arena, doc_node, page); + var parser = Parser.init(arena, doc_node, page); parser.parse(html); // Check if there's parsing error. diff --git a/src/browser/webapi/encoding/TextDecoder.zig b/src/browser/webapi/encoding/TextDecoder.zig index 3148868b..ad4bf0f6 100644 --- a/src/browser/webapi/encoding/TextDecoder.zig +++ b/src/browser/webapi/encoding/TextDecoder.zig @@ -25,8 +25,9 @@ const Allocator = std.mem.Allocator; const TextDecoder = @This(); _fatal: bool, -_ignore_bom: bool, +_page: *Page, _arena: Allocator, +_ignore_bom: bool, _stream: std.ArrayListUnmanaged(u8), const Label = enum { @@ -45,13 +46,23 @@ pub fn init(label_: ?[]const u8, opts_: ?InitOpts, page: *Page) !*TextDecoder { _ = std.meta.stringToEnum(Label, label) orelse return error.RangeError; } + const arena = try page.getArena(.{ .debug = "TextDecoder" }); + errdefer page.releaseArena(arena); + const opts = opts_ orelse InitOpts{}; - return page._factory.create(TextDecoder{ - ._arena = page.arena, + const self = try arena.create(TextDecoder); + self.* = .{ + ._page = page, + ._arena = arena, ._stream = .empty, ._fatal = opts.fatal, ._ignore_bom = opts.ignoreBOM, - }); + }; + return self; +} + +pub fn deinit(self: *TextDecoder, _: bool) void { + self._page.releaseArena(self._arena); } pub fn getEncoding(_: *const TextDecoder) []const u8 { @@ -103,6 +114,8 @@ pub const JsApi = struct { pub const name = "TextDecoder"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; + pub const weak = true; + pub const finalizer = bridge.finalizer(TextDecoder.deinit); }; pub const constructor = bridge.constructor(TextDecoder.init, .{}); From 7eb026cc0d7ed23cda3c24cc7512932a98520a29 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Wed, 28 Jan 2026 10:38:04 +0100 Subject: [PATCH 10/19] update zig-v8 deps --- .github/actions/install/action.yml | 2 +- Dockerfile | 2 +- build.zig.zon | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/actions/install/action.yml b/.github/actions/install/action.yml index 9e4b8e38..a4567fae 100644 --- a/.github/actions/install/action.yml +++ b/.github/actions/install/action.yml @@ -13,7 +13,7 @@ inputs: zig-v8: description: 'zig v8 version to install' required: false - default: 'v0.2.5' + default: 'v0.2.6' v8: description: 'v8 version to install' required: false diff --git a/Dockerfile b/Dockerfile index 5ee73356..d2b9bf57 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ FROM debian:stable-slim ARG MINISIG=0.12 ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U ARG V8=14.0.365.4 -ARG ZIG_V8=v0.2.5 +ARG ZIG_V8=v0.2.6 ARG TARGETPLATFORM RUN apt-get update -yq && \ diff --git a/build.zig.zon b/build.zig.zon index 92506e1f..f32868b8 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -6,8 +6,8 @@ .minimum_zig_version = "0.15.2", .dependencies = .{ .v8 = .{ - .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/v0.2.5.tar.gz", - .hash = "v8-0.0.0-xddH641NBAC3MqKV44YCkwvnUenhQyGlgJI8OScx0tlP", + .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/v0.2.6.tar.gz", + .hash = "v8-0.0.0-xddH60NRBAAWmpZq9nWdfFAEqVJ9zqJnvr1Nl9m2AbcY", }, //.v8 = .{ .path = "../zig-v8-fork" }, .@"boringssl-zig" = .{ From 68fbc0bde3163a2d2fe9418e4dab00098c183f4b Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Tue, 27 Jan 2026 17:01:55 +0100 Subject: [PATCH 11/19] use inspector.resetContextGroup during cdp deinit Ensure the inspector is correctly reset from context before deinit it. It fixes the contextCollected crash in a better way. --- src/browser/Page.zig | 7 ------- src/browser/js/Inspector.zig | 12 ++++++++++++ src/cdp/cdp.zig | 5 +++++ 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 71ca5db2..31803987 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -253,15 +253,8 @@ fn reset(self: *Page, comptime initializing: bool) !void { } if (comptime initializing == false) { - // Removing the context triggers the linked inspector. - // It seems to append a collect task to the message loop. self._session.executor.removeContext(); - // We force running the message loop after removing the context b/c we - // will force a GC run just after. If we remove this part, the task - // will run after the GC and we will use memory after free. - self._session.browser.runMessageLoop(); - // We force a garbage collection between page navigations to keep v8 // memory usage as low as possible. self._session.browser.env.memoryPressureNotification(.moderate); diff --git a/src/browser/js/Inspector.zig b/src/browser/js/Inspector.zig index 1d35f3f5..13ad79ed 100644 --- a/src/browser/js/Inspector.zig +++ b/src/browser/js/Inspector.zig @@ -151,6 +151,18 @@ pub fn contextCreated( } } +pub fn contextDestroyed(self: *Inspector, local: *const js.Local) void { + v8.v8_inspector__Inspector__ContextDestroyed(self.handle, local.handle); +} + +pub fn resetContextGroup(self: *const Inspector) void { + var hs: v8.HandleScope = undefined; + v8.v8__HandleScope__CONSTRUCT(&hs, self.isolate); + defer v8.v8__HandleScope__DESTRUCT(&hs); + + v8.v8_inspector__Inspector__ResetContextGroup(self.handle, CONTEXT_GROUP_ID); +} + // Retrieves the RemoteObject for a given value. // The value is loaded through the ExecutionWorld's mapZigInstanceToJs function, // just like a method return value. Therefore, if we've mapped this diff --git a/src/cdp/cdp.zig b/src/cdp/cdp.zig index 3f0af4a8..9942decf 100644 --- a/src/cdp/cdp.zig +++ b/src/cdp/cdp.zig @@ -424,6 +424,11 @@ pub fn BrowserContext(comptime CDP_T: type) type { // in progress before deinit. self.cdp.browser.env.runMicrotasks(); + // resetContextGroup detach the inspector from all contexts. + // It append async tasks, so we make sure we run the message loop + // before deinit it. + self.inspector.resetContextGroup(); + self.session.browser.runMessageLoop(); self.inspector.deinit(); // abort all intercepted requests before closing the sesion/page From ae298fc2e6241d7443d609276fa0cb5a5915e64e Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Wed, 28 Jan 2026 11:27:05 +0100 Subject: [PATCH 12/19] use caught formatter and init caught into _tryCallWithThis --- src/browser/js/Function.zig | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/src/browser/js/Function.zig b/src/browser/js/Function.zig index 458bbb40..f30508a6 100644 --- a/src/browser/js/Function.zig +++ b/src/browser/js/Function.zig @@ -72,12 +72,7 @@ pub fn newInstance(self: *const Function, caught: *js.TryCatch.Caught) !js.Objec pub fn call(self: *const Function, comptime T: type, args: anytype) !T { var caught: js.TryCatch.Caught = undefined; return self._tryCallWithThis(T, self.getThis(), args, &caught) catch |err| { - log.warn(.js, "call caught", .{ - .err = err, - .exception = caught.exception, - .line = caught.line orelse 0, - .stack = caught.stack orelse "???", - }); + log.warn(.js, "call caught", .{ .err = err, .caught = caught }); return err; }; } @@ -85,27 +80,21 @@ pub fn call(self: *const Function, comptime T: type, args: anytype) !T { pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype) !T { var caught: js.TryCatch.Caught = undefined; return self._tryCallWithThis(T, this, args, &caught) catch |err| { - log.warn(.js, "callWithThis caught", .{ - .err = err, - .exception = caught.exception, - .line = caught.line orelse 0, - .stack = caught.stack orelse "???", - }); + log.warn(.js, "callWithThis caught", .{ .err = err, .caught = caught }); return err; }; } pub fn tryCall(self: *const Function, comptime T: type, args: anytype, caught: *js.TryCatch.Caught) !T { - caught.* = .{}; return self._tryCallWithThis(T, self.getThis(), args, caught); } pub fn tryCallWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype, caught: *js.TryCatch.Caught) !T { - caught.* = .{}; return self._tryCallWithThis(T, this, args, caught); } pub fn _tryCallWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype, caught: *js.TryCatch.Caught) !T { + caught.* = .{}; const local = self.local; // When we're calling a function from within JavaScript itself, this isn't From 76a53bedbe095a3c20110fa0ab01476997871ad4 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Wed, 28 Jan 2026 17:26:56 +0300 Subject: [PATCH 13/19] split inline event listener logic to `Page.zig` and `Element.zig` --- src/browser/EventManager.zig | 146 -------------------- src/browser/Page.zig | 238 +++++++++++++++++++-------------- src/browser/webapi/Element.zig | 122 +++++++++++++++++ 3 files changed, 263 insertions(+), 243 deletions(-) diff --git a/src/browser/EventManager.zig b/src/browser/EventManager.zig index 90cc888c..c21792c4 100644 --- a/src/browser/EventManager.zig +++ b/src/browser/EventManager.zig @@ -43,26 +43,11 @@ list_pool: std.heap.MemoryPool(std.DoublyLinkedList), lookup: std.AutoHashMapUnmanaged(usize, *std.DoublyLinkedList), dispatch_depth: usize, deferred_removals: std.ArrayList(struct { list: *std.DoublyLinkedList, listener: *Listener }), -/// Use this when a listener provided like this: -/// -/// ```html -/// -/// ``` -/// -/// Or: -/// -/// ```js -/// img.onload = () => { ... }; -/// ``` -inline_lookup: std.AutoHashMapUnmanaged(usize, js.Function.Global), pub fn init(page: *Page) EventManager { - lp.assert(@alignOf(EventTarget) == 8, "EventManager.init", .{ .event_target_alignment = @alignOf(EventTarget) }); - return .{ .page = page, .lookup = .{}, - .inline_lookup = .{}, .arena = page.arena, .list_pool = std.heap.MemoryPool(std.DoublyLinkedList).init(page.arena), .listener_pool = std.heap.MemoryPool(Listener).init(page.arena), @@ -83,32 +68,6 @@ pub const Callback = union(enum) { object: js.Object, }; -/// Sets an inline event listener (`onload`, `onclick`, `onwheel` etc.); -/// overrides the listener if there's already one. -pub fn setInlineListener( - self: *EventManager, - event_target: *EventTarget, - event_type: Listener.Type, - listener_callback: js.Function.Global, -) !void { - if (comptime IS_DEBUG) { - log.debug(.event, "EventManager.setInlineListener", .{ .event_target = event_target, .event_type = event_type }); - } - - const key = createLookupKey(event_target, event_type); - const gop = try self.inline_lookup.getOrPut(self.arena, key); - gop.value_ptr.* = listener_callback; -} - -/// Returns the inline event listener by the `EventTarget` and event type. -pub fn getInlineListener( - self: *const EventManager, - event_target: *EventTarget, - event_type: Listener.Type, -) ?js.Function.Global { - return self.inline_lookup.get(createLookupKey(event_target, event_type)); -} - 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, .target = target }); @@ -484,13 +443,6 @@ fn findListener(list: *const std.DoublyLinkedList, typ: []const u8, callback: Ca return null; } -/// Creates a lookup key to use with `inline_lookup`. -fn createLookupKey(event_target: *EventTarget, event_type: Listener.Type) usize { - const ptr = @intFromPtr(event_target) >> 3; - lp.assert(ptr < (1 << 57), "createLookupKey: pointer overflow", .{ .ptr = ptr }); - return ptr | (@as(u64, @intFromEnum(event_type)) << 57); -} - const Listener = struct { typ: String, once: bool, @@ -500,104 +452,6 @@ const Listener = struct { signal: ?*@import("webapi/AbortSignal.zig") = null, node: std.DoublyLinkedList.Node, removed: bool = false, - - const Type = enum(u7) { - abort, - animationcancel, - animationend, - animationiteration, - animationstart, - auxclick, - beforeinput, - beforematch, - beforetoggle, - blur, - cancel, - canplay, - canplaythrough, - change, - click, - close, - command, - contentvisibilityautostatechange, - contextlost, - contextmenu, - contextrestored, - copy, - cuechange, - cut, - dblclick, - drag, - dragend, - dragenter, - dragexit, - dragleave, - dragover, - dragstart, - drop, - durationchange, - emptied, - ended, - @"error", - focus, - formdata, - fullscreenchange, - fullscreenerror, - gotpointercapture, - input, - invalid, - keydown, - keypress, - keyup, - load, - loadeddata, - loadedmetadata, - loadstart, - lostpointercapture, - mousedown, - mousemove, - mouseout, - mouseover, - mouseup, - paste, - pause, - play, - playing, - pointercancel, - pointerdown, - pointerenter, - pointerleave, - pointermove, - pointerout, - pointerover, - pointerrawupdate, - pointerup, - progress, - ratechange, - reset, - resize, - scroll, - scrollend, - securitypolicyviolation, - seeked, - seeking, - select, - selectionchange, - selectstart, - slotchange, - stalled, - submit, - @"suspend", - timeupdate, - toggle, - transitioncancel, - transitionend, - transitionrun, - transitionstart, - volumechange, - waiting, - wheel, - }; }; const Function = union(enum) { diff --git a/src/browser/Page.zig b/src/browser/Page.zig index ae580737..5e4f1113 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -102,6 +102,20 @@ _element_shadow_roots: Element.ShadowRootLookup = .{}, _node_owner_documents: Node.OwnerDocumentLookup = .{}, _element_assigned_slots: Element.AssignedSlotLookup = .{}, +/// Lazily-created inline event listeners (or listeners provided as attributes). +/// Avoids bloating all elements with extra function fields for rare usage. +/// +/// Use this when a listener provided like these: +/// +/// ```html +/// +/// ``` +/// +/// ```js +/// img.onload = () => { ... }; +/// ``` +_element_attr_listeners: Element.AttrListenerLookup = .{}, + _script_manager: ScriptManager, // List of active MutationObservers @@ -316,6 +330,9 @@ fn reset(self: *Page, comptime initializing: bool) !void { self._element_shadow_roots = .{}; self._node_owner_documents = .{}; self._element_assigned_slots = .{}; + + self._element_attr_listeners = .{}; + self._notified_network_idle = .init; self._notified_network_almost_idle = .init; @@ -1134,6 +1151,35 @@ pub fn getElementByIdFromNode(self: *Page, node: *Node, id: []const u8) ?*Elemen return null; } +/// Sets an inline event listener (`onload`, `onclick`, `onwheel` etc.); +/// overrides the listener if there's already one. +pub fn setAttrListener( + self: *Page, + element: *Element, + listener_type: Element.KnownListener, + listener_callback: JS.Function.Global, +) !void { + if (comptime IS_DEBUG) { + log.debug(.event, "Page.setAttrListener", .{ + .element = element, + .listener_type = listener_type, + }); + } + + const key = element.calcAttrListenerKey(listener_type); + const gop = try self._element_attr_listeners.getOrPut(self.arena, key); + gop.value_ptr.* = listener_callback; +} + +/// Returns the inline event listener by an element and listener type. +pub fn getAttrListener( + self: *const Page, + element: *Element, + listener_type: Element.KnownListener, +) ?JS.Function.Global { + return self._element_attr_listeners.get(element.calcAttrListenerKey(listener_type)); +} + pub fn registerPerformanceObserver(self: *Page, observer: *PerformanceObserver) !void { return self._performance_observers.append(self.arena, observer); } @@ -2182,8 +2228,6 @@ fn populateElementAttributes(self: *Page, element: *Element, list: anytype) !voi if (has_on_prefix) { // Must be usable as function. const func = try self.js.stringToPersistedFunction(attr.value.slice()); - const target = element.asEventTarget(); - const event_manager = &self._event_manager; // Longest known listener kind is 32 bytes long. const remaining: u6 = @truncate(name.len -| 2); @@ -2193,160 +2237,160 @@ fn populateElementAttributes(self: *Page, element: *Element, list: anytype) !voi switch (remaining) { 3 => if (@as(u24, @bitCast(unsafe[0..3].*)) == asUint("cut")) { - try event_manager.setInlineListener(target, .cut, func); + try self.setAttrListener(element, .cut, func); }, 4 => switch (@as(u32, @bitCast(unsafe[0..4].*))) { - asUint("blur") => try event_manager.setInlineListener(target, .blur, func), - asUint("copy") => try event_manager.setInlineListener(target, .copy, func), - asUint("drag") => try event_manager.setInlineListener(target, .drag, func), - asUint("drop") => try event_manager.setInlineListener(target, .drop, func), - asUint("load") => try event_manager.setInlineListener(target, .load, func), - asUint("play") => try event_manager.setInlineListener(target, .play, func), + asUint("blur") => try self.setAttrListener(element, .blur, func), + asUint("copy") => try self.setAttrListener(element, .copy, func), + asUint("drag") => try self.setAttrListener(element, .drag, func), + asUint("drop") => try self.setAttrListener(element, .drop, func), + asUint("load") => try self.setAttrListener(element, .load, func), + asUint("play") => try self.setAttrListener(element, .play, func), else => {}, }, 5 => switch (@as(u40, @bitCast(unsafe[0..5].*))) { - asUint("abort") => try event_manager.setInlineListener(target, .abort, func), - asUint("click") => try event_manager.setInlineListener(target, .click, func), - asUint("close") => try event_manager.setInlineListener(target, .close, func), - asUint("ended") => try event_manager.setInlineListener(target, .ended, func), - asUint("error") => try event_manager.setInlineListener(target, .@"error", func), - asUint("focus") => try event_manager.setInlineListener(target, .focus, func), - asUint("input") => try event_manager.setInlineListener(target, .input, func), - asUint("keyup") => try event_manager.setInlineListener(target, .keyup, func), - asUint("paste") => try event_manager.setInlineListener(target, .paste, func), - asUint("pause") => try event_manager.setInlineListener(target, .pause, func), - asUint("reset") => try event_manager.setInlineListener(target, .reset, func), - asUint("wheel") => try event_manager.setInlineListener(target, .wheel, func), + asUint("abort") => try self.setAttrListener(element, .abort, func), + asUint("click") => try self.setAttrListener(element, .click, func), + asUint("close") => try self.setAttrListener(element, .close, func), + asUint("ended") => try self.setAttrListener(element, .ended, func), + asUint("error") => try self.setAttrListener(element, .@"error", func), + asUint("focus") => try self.setAttrListener(element, .focus, func), + asUint("input") => try self.setAttrListener(element, .input, func), + asUint("keyup") => try self.setAttrListener(element, .keyup, func), + asUint("paste") => try self.setAttrListener(element, .paste, func), + asUint("pause") => try self.setAttrListener(element, .pause, func), + asUint("reset") => try self.setAttrListener(element, .reset, func), + asUint("wheel") => try self.setAttrListener(element, .wheel, func), else => {}, }, 6 => switch (@as(u48, @bitCast(unsafe[0..6].*))) { - asUint("cancel") => try event_manager.setInlineListener(target, .cancel, func), - asUint("change") => try event_manager.setInlineListener(target, .change, func), - asUint("resize") => try event_manager.setInlineListener(target, .resize, func), - asUint("scroll") => try event_manager.setInlineListener(target, .scroll, func), - asUint("seeked") => try event_manager.setInlineListener(target, .seeked, func), - asUint("select") => try event_manager.setInlineListener(target, .select, func), - asUint("submit") => try event_manager.setInlineListener(target, .submit, func), - asUint("toggle") => try event_manager.setInlineListener(target, .toggle, func), + asUint("cancel") => try self.setAttrListener(element, .cancel, func), + asUint("change") => try self.setAttrListener(element, .change, func), + asUint("resize") => try self.setAttrListener(element, .resize, func), + asUint("scroll") => try self.setAttrListener(element, .scroll, func), + asUint("seeked") => try self.setAttrListener(element, .seeked, func), + asUint("select") => try self.setAttrListener(element, .select, func), + asUint("submit") => try self.setAttrListener(element, .submit, func), + asUint("toggle") => try self.setAttrListener(element, .toggle, func), else => {}, }, 7 => switch (@as(u56, @bitCast(unsafe[0..7].*))) { - asUint("canplay") => try event_manager.setInlineListener(target, .canplay, func), - asUint("command") => try event_manager.setInlineListener(target, .command, func), - asUint("dragend") => try event_manager.setInlineListener(target, .dragend, func), - asUint("emptied") => try event_manager.setInlineListener(target, .emptied, func), - asUint("invalid") => try event_manager.setInlineListener(target, .invalid, func), - asUint("keydown") => try event_manager.setInlineListener(target, .keydown, func), - asUint("mouseup") => try event_manager.setInlineListener(target, .mouseup, func), - asUint("playing") => try event_manager.setInlineListener(target, .playing, func), - asUint("seeking") => try event_manager.setInlineListener(target, .seeking, func), - asUint("stalled") => try event_manager.setInlineListener(target, .stalled, func), - asUint("suspend") => try event_manager.setInlineListener(target, .@"suspend", func), - asUint("waiting") => try event_manager.setInlineListener(target, .waiting, func), + asUint("canplay") => try self.setAttrListener(element, .canplay, func), + asUint("command") => try self.setAttrListener(element, .command, func), + asUint("dragend") => try self.setAttrListener(element, .dragend, func), + asUint("emptied") => try self.setAttrListener(element, .emptied, func), + asUint("invalid") => try self.setAttrListener(element, .invalid, func), + asUint("keydown") => try self.setAttrListener(element, .keydown, func), + asUint("mouseup") => try self.setAttrListener(element, .mouseup, func), + asUint("playing") => try self.setAttrListener(element, .playing, func), + asUint("seeking") => try self.setAttrListener(element, .seeking, func), + asUint("stalled") => try self.setAttrListener(element, .stalled, func), + asUint("suspend") => try self.setAttrListener(element, .@"suspend", func), + asUint("waiting") => try self.setAttrListener(element, .waiting, func), else => {}, }, 8 => switch (@as(u64, @bitCast(unsafe[0..8].*))) { - asUint("auxclick") => try event_manager.setInlineListener(target, .auxclick, func), - asUint("dblclick") => try event_manager.setInlineListener(target, .dblclick, func), - asUint("dragexit") => try event_manager.setInlineListener(target, .dragexit, func), - asUint("dragover") => try event_manager.setInlineListener(target, .dragover, func), - asUint("formdata") => try event_manager.setInlineListener(target, .formdata, func), - asUint("keypress") => try event_manager.setInlineListener(target, .keypress, func), - asUint("mouseout") => try event_manager.setInlineListener(target, .mouseout, func), - asUint("progress") => try event_manager.setInlineListener(target, .progress, func), + asUint("auxclick") => try self.setAttrListener(element, .auxclick, func), + asUint("dblclick") => try self.setAttrListener(element, .dblclick, func), + asUint("dragexit") => try self.setAttrListener(element, .dragexit, func), + asUint("dragover") => try self.setAttrListener(element, .dragover, func), + asUint("formdata") => try self.setAttrListener(element, .formdata, func), + asUint("keypress") => try self.setAttrListener(element, .keypress, func), + asUint("mouseout") => try self.setAttrListener(element, .mouseout, func), + asUint("progress") => try self.setAttrListener(element, .progress, func), else => {}, }, // Won't fit to 64-bit integer; we do 2 checks. 9 => switch (@as(u64, @bitCast(unsafe[0..8].*))) { - asUint("cuechang") => if (unsafe[8] == 'e') try event_manager.setInlineListener(target, .cuechange, func), - asUint("dragente") => if (unsafe[8] == 'r') try event_manager.setInlineListener(target, .dragenter, func), - asUint("dragleav") => if (unsafe[8] == 'e') try event_manager.setInlineListener(target, .dragleave, func), - asUint("dragstar") => if (unsafe[8] == 't') try event_manager.setInlineListener(target, .dragstart, func), - asUint("loadstar") => if (unsafe[8] == 't') try event_manager.setInlineListener(target, .loadstart, func), - asUint("mousedow") => if (unsafe[8] == 'n') try event_manager.setInlineListener(target, .mousedown, func), - asUint("mousemov") => if (unsafe[8] == 'e') try event_manager.setInlineListener(target, .mousemove, func), - asUint("mouseove") => if (unsafe[8] == 'r') try event_manager.setInlineListener(target, .mouseover, func), - asUint("pointeru") => if (unsafe[8] == 'p') try event_manager.setInlineListener(target, .pointerup, func), - asUint("scrollen") => if (unsafe[8] == 'd') try event_manager.setInlineListener(target, .scrollend, func), + asUint("cuechang") => if (unsafe[8] == 'e') try self.setAttrListener(element, .cuechange, func), + asUint("dragente") => if (unsafe[8] == 'r') try self.setAttrListener(element, .dragenter, func), + asUint("dragleav") => if (unsafe[8] == 'e') try self.setAttrListener(element, .dragleave, func), + asUint("dragstar") => if (unsafe[8] == 't') try self.setAttrListener(element, .dragstart, func), + asUint("loadstar") => if (unsafe[8] == 't') try self.setAttrListener(element, .loadstart, func), + asUint("mousedow") => if (unsafe[8] == 'n') try self.setAttrListener(element, .mousedown, func), + asUint("mousemov") => if (unsafe[8] == 'e') try self.setAttrListener(element, .mousemove, func), + asUint("mouseove") => if (unsafe[8] == 'r') try self.setAttrListener(element, .mouseover, func), + asUint("pointeru") => if (unsafe[8] == 'p') try self.setAttrListener(element, .pointerup, func), + asUint("scrollen") => if (unsafe[8] == 'd') try self.setAttrListener(element, .scrollend, func), else => {}, }, 10 => switch (@as(u64, @bitCast(unsafe[0..8].*))) { asUint("loadedda") => if (asUint("ta") == @as(u16, @bitCast(unsafe[8..10].*))) - try event_manager.setInlineListener(target, .loadeddata, func), + try self.setAttrListener(element, .loadeddata, func), asUint("pointero") => if (asUint("ut") == @as(u16, @bitCast(unsafe[8..10].*))) - try event_manager.setInlineListener(target, .pointerout, func), + try self.setAttrListener(element, .pointerout, func), asUint("ratechan") => if (asUint("ge") == @as(u16, @bitCast(unsafe[8..10].*))) - try event_manager.setInlineListener(target, .ratechange, func), + try self.setAttrListener(element, .ratechange, func), asUint("slotchan") => if (asUint("ge") == @as(u16, @bitCast(unsafe[8..10].*))) - try event_manager.setInlineListener(target, .slotchange, func), + try self.setAttrListener(element, .slotchange, func), asUint("timeupda") => if (asUint("te") == @as(u16, @bitCast(unsafe[8..10].*))) - try event_manager.setInlineListener(target, .timeupdate, func), + try self.setAttrListener(element, .timeupdate, func), else => {}, }, 11 => switch (@as(u64, @bitCast(unsafe[0..8].*))) { asUint("beforein") => if (asUint("put") == @as(u24, @bitCast(unsafe[8..11].*))) - try event_manager.setInlineListener(target, .beforeinput, func), + try self.setAttrListener(element, .beforeinput, func), asUint("beforema") => if (asUint("tch") == @as(u24, @bitCast(unsafe[8..11].*))) - try event_manager.setInlineListener(target, .beforematch, func), + try self.setAttrListener(element, .beforematch, func), asUint("contextl") => if (asUint("ost") == @as(u24, @bitCast(unsafe[8..11].*))) - try event_manager.setInlineListener(target, .contextlost, func), + try self.setAttrListener(element, .contextlost, func), asUint("contextm") => if (asUint("enu") == @as(u24, @bitCast(unsafe[8..11].*))) - try event_manager.setInlineListener(target, .contextmenu, func), + try self.setAttrListener(element, .contextmenu, func), asUint("pointerd") => if (asUint("own") == @as(u24, @bitCast(unsafe[8..11].*))) - try event_manager.setInlineListener(target, .pointerdown, func), + try self.setAttrListener(element, .pointerdown, func), asUint("pointerm") => if (asUint("ove") == @as(u24, @bitCast(unsafe[8..11].*))) - try event_manager.setInlineListener(target, .pointermove, func), + try self.setAttrListener(element, .pointermove, func), asUint("pointero") => if (asUint("ver") == @as(u24, @bitCast(unsafe[8..11].*))) - try event_manager.setInlineListener(target, .pointerover, func), + try self.setAttrListener(element, .pointerover, func), asUint("selectst") => if (asUint("art") == @as(u24, @bitCast(unsafe[8..11].*))) - try event_manager.setInlineListener(target, .selectstart, func), + try self.setAttrListener(element, .selectstart, func), else => {}, }, 12 => switch (@as(u64, @bitCast(unsafe[0..8].*))) { asUint("animatio") => if (asUint("nend") == @as(u32, @bitCast(unsafe[8..12].*))) - try event_manager.setInlineListener(target, .animationend, func), + try self.setAttrListener(element, .animationend, func), asUint("beforeto") => if (asUint("ggle") == @as(u32, @bitCast(unsafe[8..12].*))) - try event_manager.setInlineListener(target, .beforetoggle, func), + try self.setAttrListener(element, .beforetoggle, func), asUint("pointere") => if (asUint("nter") == @as(u32, @bitCast(unsafe[8..12].*))) - try event_manager.setInlineListener(target, .pointerenter, func), + try self.setAttrListener(element, .pointerenter, func), asUint("pointerl") => if (asUint("eave") == @as(u32, @bitCast(unsafe[8..12].*))) - try event_manager.setInlineListener(target, .pointerleave, func), + try self.setAttrListener(element, .pointerleave, func), asUint("volumech") => if (asUint("ange") == @as(u32, @bitCast(unsafe[8..12].*))) - try event_manager.setInlineListener(target, .volumechange, func), + try self.setAttrListener(element, .volumechange, func), else => {}, }, 13 => switch (@as(u64, @bitCast(unsafe[0..8].*))) { asUint("pointerc") => if (asUint("ancel") == @as(u40, @bitCast(unsafe[8..13].*))) - try event_manager.setInlineListener(target, .pointercancel, func), + try self.setAttrListener(element, .pointercancel, func), asUint("transiti") => switch (@as(u40, @bitCast(unsafe[8..13].*))) { - asUint("onend") => try event_manager.setInlineListener(target, .transitionend, func), - asUint("onrun") => try event_manager.setInlineListener(target, .transitionrun, func), + asUint("onend") => try self.setAttrListener(element, .transitionend, func), + asUint("onrun") => try self.setAttrListener(element, .transitionrun, func), else => {}, }, else => {}, }, 14 => switch (@as(u64, @bitCast(unsafe[0..8].*))) { asUint("animatio") => if (asUint("nstart") == @as(u48, @bitCast(unsafe[8..14].*))) - try event_manager.setInlineListener(target, .animationstart, func), + try self.setAttrListener(element, .animationstart, func), asUint("canplayt") => if (asUint("hrough") == @as(u48, @bitCast(unsafe[8..14].*))) - try event_manager.setInlineListener(target, .canplaythrough, func), + try self.setAttrListener(element, .canplaythrough, func), asUint("duration") => if (asUint("change") == @as(u48, @bitCast(unsafe[8..14].*))) - try event_manager.setInlineListener(target, .durationchange, func), + try self.setAttrListener(element, .durationchange, func), asUint("loadedme") => if (asUint("tadata") == @as(u48, @bitCast(unsafe[8..14].*))) - try event_manager.setInlineListener(target, .loadedmetadata, func), + try self.setAttrListener(element, .loadedmetadata, func), else => {}, }, 15 => switch (@as(u64, @bitCast(unsafe[0..8].*))) { asUint("animatio") => if (asUint("ncancel") == @as(u56, @bitCast(unsafe[8..15].*))) - try event_manager.setInlineListener(target, .animationcancel, func), + try self.setAttrListener(element, .animationcancel, func), asUint("contextr") => if (asUint("estored") == @as(u56, @bitCast(unsafe[8..15].*))) - try event_manager.setInlineListener(target, .contextrestored, func), + try self.setAttrListener(element, .contextrestored, func), asUint("fullscre") => if (asUint("enerror") == @as(u56, @bitCast(unsafe[8..15].*))) - try event_manager.setInlineListener(target, .fullscreenerror, func), + try self.setAttrListener(element, .fullscreenerror, func), asUint("selectio") => if (asUint("nchange") == @as(u56, @bitCast(unsafe[8..15].*))) - try event_manager.setInlineListener(target, .selectionchange, func), + try self.setAttrListener(element, .selectionchange, func), asUint("transiti") => if (asUint("onstart") == @as(u56, @bitCast(unsafe[8..15].*))) - try event_manager.setInlineListener(target, .transitionstart, func), + try self.setAttrListener(element, .transitionstart, func), else => {}, }, // Can't switch on vector types. @@ -2354,11 +2398,11 @@ fn populateElementAttributes(self: *Page, element: *Element, list: anytype) !voi const as_vector: Vec16x8 = unsafe[0..16].*; if (@reduce(.And, as_vector == @as(Vec16x8, "fullscreenchange".*))) { - try event_manager.setInlineListener(target, .fullscreenchange, func); + try self.setAttrListener(element, .fullscreenchange, func); } else if (@reduce(.And, as_vector == @as(Vec16x8, "pointerrawupdate".*))) { - try event_manager.setInlineListener(target, .pointerrawupdate, func); + try self.setAttrListener(element, .pointerrawupdate, func); } else if (@reduce(.And, as_vector == @as(Vec16x8, "transitioncancel".*))) { - try event_manager.setInlineListener(target, .transitioncancel, func); + try self.setAttrListener(element, .transitioncancel, func); } }, 17 => { @@ -2367,7 +2411,7 @@ fn populateElementAttributes(self: *Page, element: *Element, list: anytype) !voi const dirty = @reduce(.And, as_vector == @as(Vec16x8, "gotpointercaptur".*)) and unsafe[16] == 'e'; if (dirty) { - try event_manager.setInlineListener(target, .gotpointercapture, func); + try self.setAttrListener(element, .gotpointercapture, func); } }, 18 => { @@ -2376,12 +2420,12 @@ fn populateElementAttributes(self: *Page, element: *Element, list: anytype) !voi const is_animationiteration = @reduce(.And, as_vector == @as(Vec16x8, "animationiterati".*)) and asUint("on") == @as(u16, @bitCast(unsafe[16..18].*)); if (is_animationiteration) { - try event_manager.setInlineListener(target, .animationiteration, func); + try self.setAttrListener(element, .animationiteration, func); } else { const is_lostpointercapture = @reduce(.And, as_vector == @as(Vec16x8, "lostpointercaptu".*)) and asUint("re") == @as(u16, @bitCast(unsafe[16..18].*)); if (is_lostpointercapture) { - try event_manager.setInlineListener(target, .lostpointercapture, func); + try self.setAttrListener(element, .lostpointercapture, func); } } }, @@ -2391,14 +2435,14 @@ fn populateElementAttributes(self: *Page, element: *Element, list: anytype) !voi const dirty = @reduce(.And, as_vector == @as(Vec16x8, "securitypolicyvi".*)) and asUint("olation") == @as(u56, @bitCast(unsafe[16..23].*)); if (dirty) { - try event_manager.setInlineListener(target, .securitypolicyviolation, func); + try self.setAttrListener(element, .securitypolicyviolation, func); } }, 32 => { const as_vector: Vec32x8 = unsafe[0..32].*; if (@reduce(.And, as_vector == @as(Vec32x8, "contentvisibilityautostatechange".*))) { - try event_manager.setInlineListener(target, .contentvisibilityautostatechange, func); + try self.setAttrListener(element, .contentvisibilityautostatechange, func); } }, else => {}, diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index f983b514..512f5821 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -48,6 +48,128 @@ pub const ClassListLookup = std.AutoHashMapUnmanaged(*Element, *collections.DOMT pub const RelListLookup = std.AutoHashMapUnmanaged(*Element, *collections.DOMTokenList); pub const ShadowRootLookup = std.AutoHashMapUnmanaged(*Element, *ShadowRoot); pub const AssignedSlotLookup = std.AutoHashMapUnmanaged(*Element, *Html.Slot); +/// Better to discriminate it since not directly a pointer int. +/// +/// See `calcAttrListenerKey` to obtain one. +pub const AttrListenerKey = u64; +/// Use `getAttrListenerKey` to create a key. +pub const AttrListenerLookup = std.AutoHashMapUnmanaged(AttrListenerKey, js.Function.Global); + +/// Enum of known event listeners; increasing the size of it (u7) +/// can cause `AttrListenerKey` to behave incorrectly. +pub const KnownListener = enum(u7) { + abort, + animationcancel, + animationend, + animationiteration, + animationstart, + auxclick, + beforeinput, + beforematch, + beforetoggle, + blur, + cancel, + canplay, + canplaythrough, + change, + click, + close, + command, + contentvisibilityautostatechange, + contextlost, + contextmenu, + contextrestored, + copy, + cuechange, + cut, + dblclick, + drag, + dragend, + dragenter, + dragexit, + dragleave, + dragover, + dragstart, + drop, + durationchange, + emptied, + ended, + @"error", + focus, + formdata, + fullscreenchange, + fullscreenerror, + gotpointercapture, + input, + invalid, + keydown, + keypress, + keyup, + load, + loadeddata, + loadedmetadata, + loadstart, + lostpointercapture, + mousedown, + mousemove, + mouseout, + mouseover, + mouseup, + paste, + pause, + play, + playing, + pointercancel, + pointerdown, + pointerenter, + pointerleave, + pointermove, + pointerout, + pointerover, + pointerrawupdate, + pointerup, + progress, + ratechange, + reset, + resize, + scroll, + scrollend, + securitypolicyviolation, + seeked, + seeking, + select, + selectionchange, + selectstart, + slotchange, + stalled, + submit, + @"suspend", + timeupdate, + toggle, + transitioncancel, + transitionend, + transitionrun, + transitionstart, + volumechange, + waiting, + wheel, +}; + +/// Calculates a lookup key (`AttrListenerKey`) to use with `AttrListenerLookup` for an element. +/// NEVER use generated key to retrieve a pointer back. Portability is not guaranteed. +pub fn calcAttrListenerKey(self: *Element, event_type: KnownListener) AttrListenerKey { + // We can use `Element` for the key too; `EventTarget` is strict about + // its size and alignment, though. + const target = self.asEventTarget(); + // Check if we have 3 bits available from alignment of 8. + lp.assert(@alignOf(@TypeOf(target)) == 8, "createLookupKey: incorrect alignment", .{ + .event_target_alignment = @alignOf(@TypeOf(target)), + }); + + const ptr = @intFromPtr(target) >> 3; + lp.assert(ptr < (1 << 57), "createLookupKey: pointer overflow", .{ .ptr = ptr }); + return ptr | (@as(u64, @intFromEnum(event_type)) << 57); +} pub const Namespace = enum(u8) { html, From 9f5c2e4ca761c34ca4be3b4ab42f8d43ca367033 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Wed, 28 Jan 2026 17:27:22 +0300 Subject: [PATCH 14/19] add getter/setter functions for attribute event listeners Spec say these belong to `HTMLElement`. --- src/browser/webapi/element/Html.zig | 856 ++++++++++++++++++++++++++++ 1 file changed, 856 insertions(+) diff --git a/src/browser/webapi/element/Html.zig b/src/browser/webapi/element/Html.zig index 9eb41edd..d7831227 100644 --- a/src/browser/webapi/element/Html.zig +++ b/src/browser/webapi/element/Html.zig @@ -330,6 +330,766 @@ pub fn click(self: *HtmlElement, page: *Page) !void { try page._event_manager.dispatch(self.asEventTarget(), event.asEvent()); } +pub fn setOnAbort(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .abort, callback); +} + +pub fn getOnAbort(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .abort); +} + +pub fn setOnAnimationCancel(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .animationcancel, callback); +} + +pub fn getOnAnimationCancel(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .animationcancel); +} + +pub fn setOnAnimationEnd(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .animationend, callback); +} + +pub fn getOnAnimationEnd(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .animationend); +} + +pub fn setOnAnimationIteration(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .animationiteration, callback); +} + +pub fn getOnAnimationIteration(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .animationiteration); +} + +pub fn setOnAnimationStart(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .animationstart, callback); +} + +pub fn getOnAnimationStart(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .animationstart); +} + +pub fn setOnAuxClick(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .auxclick, callback); +} + +pub fn getOnAuxClick(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .auxclick); +} + +pub fn setOnBeforeInput(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .beforeinput, callback); +} + +pub fn getOnBeforeInput(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .beforeinput); +} + +pub fn setOnBeforeMatch(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .beforematch, callback); +} + +pub fn getOnBeforeMatch(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .beforematch); +} + +pub fn setOnBeforeToggle(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .beforetoggle, callback); +} + +pub fn getOnBeforeToggle(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .beforetoggle); +} + +pub fn setOnBlur(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .blur, callback); +} + +pub fn getOnBlur(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .blur); +} + +pub fn setOnCancel(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .cancel, callback); +} + +pub fn getOnCancel(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .cancel); +} + +pub fn setOnCanPlay(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .canplay, callback); +} + +pub fn getOnCanPlay(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .canplay); +} + +pub fn setOnCanPlayThrough(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .canplaythrough, callback); +} + +pub fn getOnCanPlayThrough(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .canplaythrough); +} + +pub fn setOnChange(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .change, callback); +} + +pub fn getOnChange(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .change); +} + +pub fn setOnClick(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .click, callback); +} + +pub fn getOnClick(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .click); +} + +pub fn setOnClose(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .close, callback); +} + +pub fn getOnClose(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .close); +} + +pub fn setOnCommand(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .command, callback); +} + +pub fn getOnCommand(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .command); +} + +pub fn setOnContentVisibilityAutoStateChange(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .contentvisibilityautostatechange, callback); +} + +pub fn getOnContentVisibilityAutoStateChange(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .contentvisibilityautostatechange); +} + +pub fn setOnContextLost(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .contextlost, callback); +} + +pub fn getOnContextLost(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .contextlost); +} + +pub fn setOnContextMenu(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .contextmenu, callback); +} + +pub fn getOnContextMenu(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .contextmenu); +} + +pub fn setOnContextRestored(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .contextrestored, callback); +} + +pub fn getOnContextRestored(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .contextrestored); +} + +pub fn setOnCopy(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .copy, callback); +} + +pub fn getOnCopy(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .copy); +} + +pub fn setOnCueChange(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .cuechange, callback); +} + +pub fn getOnCueChange(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .cuechange); +} + +pub fn setOnCut(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .cut, callback); +} + +pub fn getOnCut(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .cut); +} + +pub fn setOnDblClick(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .dblclick, callback); +} + +pub fn getOnDblClick(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .dblclick); +} + +pub fn setOnDrag(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .drag, callback); +} + +pub fn getOnDrag(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .drag); +} + +pub fn setOnDragEnd(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .dragend, callback); +} + +pub fn getOnDragEnd(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .dragend); +} + +pub fn setOnDragEnter(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .dragenter, callback); +} + +pub fn getOnDragEnter(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .dragenter); +} + +pub fn setOnDragExit(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .dragexit, callback); +} + +pub fn getOnDragExit(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .dragexit); +} + +pub fn setOnDragLeave(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .dragleave, callback); +} + +pub fn getOnDragLeave(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .dragleave); +} + +pub fn setOnDragOver(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .dragover, callback); +} + +pub fn getOnDragOver(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .dragover); +} + +pub fn setOnDragStart(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .dragstart, callback); +} + +pub fn getOnDragStart(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .dragstart); +} + +pub fn setOnDrop(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .drop, callback); +} + +pub fn getOnDrop(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .drop); +} + +pub fn setOnDurationChange(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .durationchange, callback); +} + +pub fn getOnDurationChange(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .durationchange); +} + +pub fn setOnEmptied(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .emptied, callback); +} + +pub fn getOnEmptied(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .emptied); +} + +pub fn setOnEnded(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .ended, callback); +} + +pub fn getOnEnded(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .ended); +} + +pub fn setOnError(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .@"error", callback); +} + +pub fn getOnError(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .@"error"); +} + +pub fn setOnFocus(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .focus, callback); +} + +pub fn getOnFocus(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .focus); +} + +pub fn setOnFormData(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .formdata, callback); +} + +pub fn getOnFormData(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .formdata); +} + +pub fn setOnFullscreenChange(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .fullscreenchange, callback); +} + +pub fn getOnFullscreenChange(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .fullscreenchange); +} + +pub fn setOnFullscreenError(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .fullscreenerror, callback); +} + +pub fn getOnFullscreenError(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .fullscreenerror); +} + +pub fn setOnGotPointerCapture(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .gotpointercapture, callback); +} + +pub fn getOnGotPointerCapture(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .gotpointercapture); +} + +pub fn setOnInput(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .input, callback); +} + +pub fn getOnInput(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .input); +} + +pub fn setOnInvalid(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .invalid, callback); +} + +pub fn getOnInvalid(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .invalid); +} + +pub fn setOnKeyDown(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .keydown, callback); +} + +pub fn getOnKeyDown(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .keydown); +} + +pub fn setOnKeyPress(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .keypress, callback); +} + +pub fn getOnKeyPress(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .keypress); +} + +pub fn setOnKeyUp(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .keyup, callback); +} + +pub fn getOnKeyUp(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .keyup); +} + +pub fn setOnLoad(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .load, callback); +} + +pub fn getOnLoad(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .load); +} + +pub fn setOnLoadedData(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .loadeddata, callback); +} + +pub fn getOnLoadedData(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .loadeddata); +} + +pub fn setOnLoadedMetadata(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .loadedmetadata, callback); +} + +pub fn getOnLoadedMetadata(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .loadedmetadata); +} + +pub fn setOnLoadStart(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .loadstart, callback); +} + +pub fn getOnLoadStart(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .loadstart); +} + +pub fn setOnLostPointerCapture(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .lostpointercapture, callback); +} + +pub fn getOnLostPointerCapture(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .lostpointercapture); +} + +pub fn setOnMouseDown(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .mousedown, callback); +} + +pub fn getOnMouseDown(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .mousedown); +} + +pub fn setOnMouseMove(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .mousemove, callback); +} + +pub fn getOnMouseMove(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .mousemove); +} + +pub fn setOnMouseOut(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .mouseout, callback); +} + +pub fn getOnMouseOut(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .mouseout); +} + +pub fn setOnMouseOver(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .mouseover, callback); +} + +pub fn getOnMouseOver(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .mouseover); +} + +pub fn setOnMouseUp(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .mouseup, callback); +} + +pub fn getOnMouseUp(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .mouseup); +} + +pub fn setOnPaste(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .paste, callback); +} + +pub fn getOnPaste(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .paste); +} + +pub fn setOnPause(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .pause, callback); +} + +pub fn getOnPause(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .pause); +} + +pub fn setOnPlay(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .play, callback); +} + +pub fn getOnPlay(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .play); +} + +pub fn setOnPlaying(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .playing, callback); +} + +pub fn getOnPlaying(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .playing); +} + +pub fn setOnPointerCancel(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .pointercancel, callback); +} + +pub fn getOnPointerCancel(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .pointercancel); +} + +pub fn setOnPointerDown(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .pointerdown, callback); +} + +pub fn getOnPointerDown(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .pointerdown); +} + +pub fn setOnPointerEnter(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .pointerenter, callback); +} + +pub fn getOnPointerEnter(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .pointerenter); +} + +pub fn setOnPointerLeave(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .pointerleave, callback); +} + +pub fn getOnPointerLeave(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .pointerleave); +} + +pub fn setOnPointerMove(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .pointermove, callback); +} + +pub fn getOnPointerMove(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .pointermove); +} + +pub fn setOnPointerOut(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .pointerout, callback); +} + +pub fn getOnPointerOut(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .pointerout); +} + +pub fn setOnPointerOver(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .pointerover, callback); +} + +pub fn getOnPointerOver(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .pointerover); +} + +pub fn setOnPointerRawUpdate(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .pointerrawupdate, callback); +} + +pub fn getOnPointerRawUpdate(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .pointerrawupdate); +} + +pub fn setOnPointerUp(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .pointerup, callback); +} + +pub fn getOnPointerUp(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .pointerup); +} + +pub fn setOnProgress(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .progress, callback); +} + +pub fn getOnProgress(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .progress); +} + +pub fn setOnRateChange(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .ratechange, callback); +} + +pub fn getOnRateChange(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .ratechange); +} + +pub fn setOnReset(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .reset, callback); +} + +pub fn getOnReset(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .reset); +} + +pub fn setOnResize(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .resize, callback); +} + +pub fn getOnResize(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .resize); +} + +pub fn setOnScroll(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .scroll, callback); +} + +pub fn getOnScroll(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .scroll); +} + +pub fn setOnScrollEnd(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .scrollend, callback); +} + +pub fn getOnScrollEnd(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .scrollend); +} + +pub fn setOnSecurityPolicyViolation(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .securitypolicyviolation, callback); +} + +pub fn getOnSecurityPolicyViolation(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .securitypolicyviolation); +} + +pub fn setOnSeeked(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .seeked, callback); +} + +pub fn getOnSeeked(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .seeked); +} + +pub fn setOnSeeking(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .seeking, callback); +} + +pub fn getOnSeeking(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .seeking); +} + +pub fn setOnSelect(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .select, callback); +} + +pub fn getOnSelect(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .select); +} + +pub fn setOnSelectionChange(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .selectionchange, callback); +} + +pub fn getOnSelectionChange(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .selectionchange); +} + +pub fn setOnSelectStart(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .selectstart, callback); +} + +pub fn getOnSelectStart(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .selectstart); +} + +pub fn setOnSlotChange(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .slotchange, callback); +} + +pub fn getOnSlotChange(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .slotchange); +} + +pub fn setOnStalled(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .stalled, callback); +} + +pub fn getOnStalled(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .stalled); +} + +pub fn setOnSubmit(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .submit, callback); +} + +pub fn getOnSubmit(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .submit); +} + +pub fn setOnSuspend(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .@"suspend", callback); +} + +pub fn getOnSuspend(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .@"suspend"); +} + +pub fn setOnTimeUpdate(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .timeupdate, callback); +} + +pub fn getOnTimeUpdate(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .timeupdate); +} + +pub fn setOnToggle(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .toggle, callback); +} + +pub fn getOnToggle(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .toggle); +} + +pub fn setOnTransitionCancel(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .transitioncancel, callback); +} + +pub fn getOnTransitionCancel(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .transitioncancel); +} + +pub fn setOnTransitionEnd(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .transitionend, callback); +} + +pub fn getOnTransitionEnd(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .transitionend); +} + +pub fn setOnTransitionRun(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .transitionrun, callback); +} + +pub fn getOnTransitionRun(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .transitionrun); +} + +pub fn setOnTransitionStart(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .transitionstart, callback); +} + +pub fn getOnTransitionStart(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .transitionstart); +} + +pub fn setOnVolumeChange(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .volumechange, callback); +} + +pub fn getOnVolumeChange(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .volumechange); +} + +pub fn setOnWaiting(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .waiting, callback); +} + +pub fn getOnWaiting(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .waiting); +} + +pub fn setOnWheel(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .wheel, callback); +} + +pub fn getOnWheel(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .wheel); +} + pub const JsApi = struct { pub const bridge = js.Bridge(HtmlElement); @@ -349,6 +1109,102 @@ pub const JsApi = struct { } pub const insertAdjacentHTML = bridge.function(HtmlElement.insertAdjacentHTML, .{ .dom_exception = true }); pub const click = bridge.function(HtmlElement.click, .{}); + + pub const onabort = bridge.accessor(HtmlElement.getOnAbort, HtmlElement.setOnAbort, .{}); + pub const onanimationcancel = bridge.accessor(HtmlElement.getOnAnimationCancel, HtmlElement.setOnAnimationCancel, .{}); + pub const onanimationend = bridge.accessor(HtmlElement.getOnAnimationEnd, HtmlElement.setOnAnimationEnd, .{}); + pub const onanimationiteration = bridge.accessor(HtmlElement.getOnAnimationIteration, HtmlElement.setOnAnimationIteration, .{}); + pub const onanimationstart = bridge.accessor(HtmlElement.getOnAnimationStart, HtmlElement.setOnAnimationStart, .{}); + pub const onauxclick = bridge.accessor(HtmlElement.getOnAuxClick, HtmlElement.setOnAuxClick, .{}); + pub const onbeforeinput = bridge.accessor(HtmlElement.getOnBeforeInput, HtmlElement.setOnBeforeInput, .{}); + pub const onbeforematch = bridge.accessor(HtmlElement.getOnBeforeMatch, HtmlElement.setOnBeforeMatch, .{}); + pub const onbeforetoggle = bridge.accessor(HtmlElement.getOnBeforeToggle, HtmlElement.setOnBeforeToggle, .{}); + pub const onblur = bridge.accessor(HtmlElement.getOnBlur, HtmlElement.setOnBlur, .{}); + pub const oncancel = bridge.accessor(HtmlElement.getOnCancel, HtmlElement.setOnCancel, .{}); + pub const oncanplay = bridge.accessor(HtmlElement.getOnCanPlay, HtmlElement.setOnCanPlay, .{}); + pub const oncanplaythrough = bridge.accessor(HtmlElement.getOnCanPlayThrough, HtmlElement.setOnCanPlayThrough, .{}); + pub const onchange = bridge.accessor(HtmlElement.getOnChange, HtmlElement.setOnChange, .{}); + pub const onclick = bridge.accessor(HtmlElement.getOnClick, HtmlElement.setOnClick, .{}); + pub const onclose = bridge.accessor(HtmlElement.getOnClose, HtmlElement.setOnClose, .{}); + pub const oncommand = bridge.accessor(HtmlElement.getOnCommand, HtmlElement.setOnCommand, .{}); + pub const oncontentvisibilityautostatechange = bridge.accessor(HtmlElement.getOnContentVisibilityAutoStateChange, HtmlElement.setOnContentVisibilityAutoStateChange, .{}); + pub const oncontextlost = bridge.accessor(HtmlElement.getOnContextLost, HtmlElement.setOnContextLost, .{}); + pub const oncontextmenu = bridge.accessor(HtmlElement.getOnContextMenu, HtmlElement.setOnContextMenu, .{}); + pub const oncontextrestored = bridge.accessor(HtmlElement.getOnContextRestored, HtmlElement.setOnContextRestored, .{}); + pub const oncopy = bridge.accessor(HtmlElement.getOnCopy, HtmlElement.setOnCopy, .{}); + pub const oncuechange = bridge.accessor(HtmlElement.getOnCueChange, HtmlElement.setOnCueChange, .{}); + pub const oncut = bridge.accessor(HtmlElement.getOnCut, HtmlElement.setOnCut, .{}); + pub const ondblclick = bridge.accessor(HtmlElement.getOnDblClick, HtmlElement.setOnDblClick, .{}); + pub const ondrag = bridge.accessor(HtmlElement.getOnDrag, HtmlElement.setOnDrag, .{}); + pub const ondragend = bridge.accessor(HtmlElement.getOnDragEnd, HtmlElement.setOnDragEnd, .{}); + pub const ondragenter = bridge.accessor(HtmlElement.getOnDragEnter, HtmlElement.setOnDragEnter, .{}); + pub const ondragexit = bridge.accessor(HtmlElement.getOnDragExit, HtmlElement.setOnDragExit, .{}); + pub const ondragleave = bridge.accessor(HtmlElement.getOnDragLeave, HtmlElement.setOnDragLeave, .{}); + pub const ondragover = bridge.accessor(HtmlElement.getOnDragOver, HtmlElement.setOnDragOver, .{}); + pub const ondragstart = bridge.accessor(HtmlElement.getOnDragStart, HtmlElement.setOnDragStart, .{}); + pub const ondrop = bridge.accessor(HtmlElement.getOnDrop, HtmlElement.setOnDrop, .{}); + pub const ondurationchange = bridge.accessor(HtmlElement.getOnDurationChange, HtmlElement.setOnDurationChange, .{}); + pub const onemptied = bridge.accessor(HtmlElement.getOnEmptied, HtmlElement.setOnEmptied, .{}); + pub const onended = bridge.accessor(HtmlElement.getOnEnded, HtmlElement.setOnEnded, .{}); + pub const onerror = bridge.accessor(HtmlElement.getOnError, HtmlElement.setOnError, .{}); + pub const onfocus = bridge.accessor(HtmlElement.getOnFocus, HtmlElement.setOnFocus, .{}); + pub const onformdata = bridge.accessor(HtmlElement.getOnFormData, HtmlElement.setOnFormData, .{}); + pub const onfullscreenchange = bridge.accessor(HtmlElement.getOnFullscreenChange, HtmlElement.setOnFullscreenChange, .{}); + pub const onfullscreenerror = bridge.accessor(HtmlElement.getOnFullscreenError, HtmlElement.setOnFullscreenError, .{}); + pub const ongotpointercapture = bridge.accessor(HtmlElement.getOnGotPointerCapture, HtmlElement.setOnGotPointerCapture, .{}); + pub const oninput = bridge.accessor(HtmlElement.getOnInput, HtmlElement.setOnInput, .{}); + pub const oninvalid = bridge.accessor(HtmlElement.getOnInvalid, HtmlElement.setOnInvalid, .{}); + pub const onkeydown = bridge.accessor(HtmlElement.getOnKeyDown, HtmlElement.setOnKeyDown, .{}); + pub const onkeypress = bridge.accessor(HtmlElement.getOnKeyPress, HtmlElement.setOnKeyPress, .{}); + pub const onkeyup = bridge.accessor(HtmlElement.getOnKeyUp, HtmlElement.setOnKeyUp, .{}); + pub const onload = bridge.accessor(HtmlElement.getOnLoad, HtmlElement.setOnLoad, .{}); + pub const onloadeddata = bridge.accessor(HtmlElement.getOnLoadedData, HtmlElement.setOnLoadedData, .{}); + pub const onloadedmetadata = bridge.accessor(HtmlElement.getOnLoadedMetadata, HtmlElement.setOnLoadedMetadata, .{}); + pub const onloadstart = bridge.accessor(HtmlElement.getOnLoadStart, HtmlElement.setOnLoadStart, .{}); + pub const onlostpointercapture = bridge.accessor(HtmlElement.getOnLostPointerCapture, HtmlElement.setOnLostPointerCapture, .{}); + pub const onmousedown = bridge.accessor(HtmlElement.getOnMouseDown, HtmlElement.setOnMouseDown, .{}); + pub const onmousemove = bridge.accessor(HtmlElement.getOnMouseMove, HtmlElement.setOnMouseMove, .{}); + pub const onmouseout = bridge.accessor(HtmlElement.getOnMouseOut, HtmlElement.setOnMouseOut, .{}); + pub const onmouseover = bridge.accessor(HtmlElement.getOnMouseOver, HtmlElement.setOnMouseOver, .{}); + pub const onmouseup = bridge.accessor(HtmlElement.getOnMouseUp, HtmlElement.setOnMouseUp, .{}); + pub const onpaste = bridge.accessor(HtmlElement.getOnPaste, HtmlElement.setOnPaste, .{}); + pub const onpause = bridge.accessor(HtmlElement.getOnPause, HtmlElement.setOnPause, .{}); + pub const onplay = bridge.accessor(HtmlElement.getOnPlay, HtmlElement.setOnPlay, .{}); + pub const onplaying = bridge.accessor(HtmlElement.getOnPlaying, HtmlElement.setOnPlaying, .{}); + pub const onpointercancel = bridge.accessor(HtmlElement.getOnPointerCancel, HtmlElement.setOnPointerCancel, .{}); + pub const onpointerdown = bridge.accessor(HtmlElement.getOnPointerDown, HtmlElement.setOnPointerDown, .{}); + pub const onpointerenter = bridge.accessor(HtmlElement.getOnPointerEnter, HtmlElement.setOnPointerEnter, .{}); + pub const onpointerleave = bridge.accessor(HtmlElement.getOnPointerLeave, HtmlElement.setOnPointerLeave, .{}); + pub const onpointermove = bridge.accessor(HtmlElement.getOnPointerMove, HtmlElement.setOnPointerMove, .{}); + pub const onpointerout = bridge.accessor(HtmlElement.getOnPointerOut, HtmlElement.setOnPointerOut, .{}); + pub const onpointerover = bridge.accessor(HtmlElement.getOnPointerOver, HtmlElement.setOnPointerOver, .{}); + pub const onpointerrawupdate = bridge.accessor(HtmlElement.getOnPointerRawUpdate, HtmlElement.setOnPointerRawUpdate, .{}); + pub const onpointerup = bridge.accessor(HtmlElement.getOnPointerUp, HtmlElement.setOnPointerUp, .{}); + pub const onprogress = bridge.accessor(HtmlElement.getOnProgress, HtmlElement.setOnProgress, .{}); + pub const onratechange = bridge.accessor(HtmlElement.getOnRateChange, HtmlElement.setOnRateChange, .{}); + pub const onreset = bridge.accessor(HtmlElement.getOnReset, HtmlElement.setOnReset, .{}); + pub const onresize = bridge.accessor(HtmlElement.getOnResize, HtmlElement.setOnResize, .{}); + pub const onscroll = bridge.accessor(HtmlElement.getOnScroll, HtmlElement.setOnScroll, .{}); + pub const onscrollend = bridge.accessor(HtmlElement.getOnScrollEnd, HtmlElement.setOnScrollEnd, .{}); + pub const onsecuritypolicyviolation = bridge.accessor(HtmlElement.getOnSecurityPolicyViolation, HtmlElement.setOnSecurityPolicyViolation, .{}); + pub const onseeked = bridge.accessor(HtmlElement.getOnSeeked, HtmlElement.setOnSeeked, .{}); + pub const onseeking = bridge.accessor(HtmlElement.getOnSeeking, HtmlElement.setOnSeeking, .{}); + pub const onselect = bridge.accessor(HtmlElement.getOnSelect, HtmlElement.setOnSelect, .{}); + pub const onselectionchange = bridge.accessor(HtmlElement.getOnSelectionChange, HtmlElement.setOnSelectionChange, .{}); + pub const onselectstart = bridge.accessor(HtmlElement.getOnSelectStart, HtmlElement.setOnSelectStart, .{}); + pub const onslotchange = bridge.accessor(HtmlElement.getOnSlotChange, HtmlElement.setOnSlotChange, .{}); + pub const onstalled = bridge.accessor(HtmlElement.getOnStalled, HtmlElement.setOnStalled, .{}); + pub const onsubmit = bridge.accessor(HtmlElement.getOnSubmit, HtmlElement.setOnSubmit, .{}); + pub const onsuspend = bridge.accessor(HtmlElement.getOnSuspend, HtmlElement.setOnSuspend, .{}); + pub const ontimeupdate = bridge.accessor(HtmlElement.getOnTimeUpdate, HtmlElement.setOnTimeUpdate, .{}); + pub const ontoggle = bridge.accessor(HtmlElement.getOnToggle, HtmlElement.setOnToggle, .{}); + pub const ontransitioncancel = bridge.accessor(HtmlElement.getOnTransitionCancel, HtmlElement.setOnTransitionCancel, .{}); + pub const ontransitionend = bridge.accessor(HtmlElement.getOnTransitionEnd, HtmlElement.setOnTransitionEnd, .{}); + pub const ontransitionrun = bridge.accessor(HtmlElement.getOnTransitionRun, HtmlElement.setOnTransitionRun, .{}); + pub const ontransitionstart = bridge.accessor(HtmlElement.getOnTransitionStart, HtmlElement.setOnTransitionStart, .{}); + pub const onvolumechange = bridge.accessor(HtmlElement.getOnVolumeChange, HtmlElement.setOnVolumeChange, .{}); + pub const onwaiting = bridge.accessor(HtmlElement.getOnWaiting, HtmlElement.setOnWaiting, .{}); + pub const onwheel = bridge.accessor(HtmlElement.getOnWheel, HtmlElement.setOnWheel, .{}); }; pub const Build = struct { From 335e781d0c5d197d8bf728fde4ccec8513704698 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Wed, 28 Jan 2026 15:37:58 +0100 Subject: [PATCH 15/19] update required deps for build from sources --- .github/actions/install/action.yml | 2 +- Dockerfile | 2 +- README.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/actions/install/action.yml b/.github/actions/install/action.yml index a4567fae..25ce263b 100644 --- a/.github/actions/install/action.yml +++ b/.github/actions/install/action.yml @@ -32,7 +32,7 @@ runs: shell: bash run: | sudo apt-get update - sudo apt-get install -y wget xz-utils python3 ca-certificates git pkg-config libglib2.0-dev gperf libexpat1-dev cmake clang + sudo apt-get install -y wget xz-utils ca-certificates gcc make git # Zig version used from the `minimum_zig_version` field in build.zig.zon - uses: mlugg/setup-zig@v2 diff --git a/Dockerfile b/Dockerfile index d2b9bf57..dd8f7fac 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,7 @@ ARG TARGETPLATFORM RUN apt-get update -yq && \ apt-get install -yq xz-utils ca-certificates \ - clang make curl git + gcc make curl git # Get Rust RUN curl https://sh.rustup.rs -sSf | sh -s -- --profile minimal -y diff --git a/README.md b/README.md index 478a28ed..b2b695f0 100644 --- a/README.md +++ b/README.md @@ -178,7 +178,7 @@ For **Debian/Ubuntu based Linux**: ``` sudo apt install xz-utils ca-certificates \ - clang make curl git + gcc make curl git ``` You also need to [install Rust](https://rust-lang.org/tools/install/). From 0a68be695d0280d6e797d33c25c53aa5e2ada0bd Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Wed, 28 Jan 2026 17:46:27 +0300 Subject: [PATCH 16/19] add tests --- .../tests/element/html/event_listeners.html | 490 ++++++++++++++++++ src/browser/webapi/element/Html.zig | 5 + 2 files changed, 495 insertions(+) create mode 100644 src/browser/tests/element/html/event_listeners.html diff --git a/src/browser/tests/element/html/event_listeners.html b/src/browser/tests/element/html/event_listeners.html new file mode 100644 index 00000000..9e7f9ebd --- /dev/null +++ b/src/browser/tests/element/html/event_listeners.html @@ -0,0 +1,490 @@ + + + + +
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/browser/webapi/element/Html.zig b/src/browser/webapi/element/Html.zig index d7831227..244e7bed 100644 --- a/src/browser/webapi/element/Html.zig +++ b/src/browser/webapi/element/Html.zig @@ -1235,3 +1235,8 @@ pub const Build = struct { return false; } }; + +const testing = @import("../../../testing.zig"); +test "WebApi: HTML.event_listeners" { + try testing.htmlRunner("element/html/event_listeners.html", .{}); +} From 946f02b7a2c9588915110c70c8c4fbfb05b98df6 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Wed, 28 Jan 2026 12:53:30 +0800 Subject: [PATCH 17/19] Add double-free detection to ArenaPool (in Debug Mode) Double-freeing should eventually cause a segfault (on ArenaPool.deinit, if not sooner), but having an explicit check allows us to log the responsible owner. --- src/browser/Page.zig | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 31803987..e82184a8 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -174,7 +174,10 @@ call_arena: Allocator, arena_pool: *ArenaPool, // In Debug, we use this to see if anything fails to release an arena back to // the pool. -_arena_pool_leak_track: (if (IS_DEBUG) std.AutoHashMapUnmanaged(usize, []const u8) else void), +_arena_pool_leak_track: (if (IS_DEBUG) std.AutoHashMapUnmanaged(usize, struct { + owner: []const u8, + count: usize, +}) else void), window: *Window, document: *Document, @@ -236,7 +239,9 @@ pub fn deinit(self: *Page) void { if (comptime IS_DEBUG) { var it = self._arena_pool_leak_track.valueIterator(); while (it.next()) |value_ptr| { - log.err(.bug, "ArenaPool Leak", .{ .owner = value_ptr.* }); + if (value_ptr.count > 0) { + log.err(.bug, "ArenaPool Leak", .{ .owner = value_ptr.owner }); + } } } @@ -247,7 +252,9 @@ fn reset(self: *Page, comptime initializing: bool) !void { if (comptime IS_DEBUG) { var it = self._arena_pool_leak_track.valueIterator(); while (it.next()) |value_ptr| { - log.err(.bug, "ArenaPool Leak", .{ .owner = value_ptr.* }); + if (value_ptr.count > 0) { + log.err(.bug, "ArenaPool Leak", .{ .owner = value_ptr.owner }); + } } self._arena_pool_leak_track.clearRetainingCapacity(); } @@ -370,14 +377,23 @@ const GetArenaOpts = struct { pub fn getArena(self: *Page, comptime opts: GetArenaOpts) !Allocator { const allocator = try self.arena_pool.acquire(); if (comptime IS_DEBUG) { - try self._arena_pool_leak_track.put(self.arena, @intFromPtr(allocator.ptr), opts.debug); + const gop = try self._arena_pool_leak_track.getOrPut(self.arena, @intFromPtr(allocator.ptr)); + if (gop.found_existing) { + std.debug.assert(gop.value_ptr.count == 0); + } + gop.value_ptr.* = .{ .owner = opts.debug, .count = 1 }; } return allocator; } pub fn releaseArena(self: *Page, allocator: Allocator) void { if (comptime IS_DEBUG) { - _ = self._arena_pool_leak_track.remove(@intFromPtr(allocator.ptr)); + const found = self._arena_pool_leak_track.getPtr(@intFromPtr(allocator.ptr)).?; + if (found.count != 1) { + log.err(.bug, "ArenaPool Double Free", .{ .owner = found.owner, .count = found.count }); + return; + } + found.count = 0; } return self.arena_pool.release(allocator); } From 5d56fea2d3849742cbbde2c4afb06947e5dada03 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Wed, 28 Jan 2026 14:25:37 +0800 Subject: [PATCH 18/19] check for leak after context is removed, as that can cause finalizers to run --- src/browser/Page.zig | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/browser/Page.zig b/src/browser/Page.zig index e82184a8..1853958b 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -249,19 +249,20 @@ pub fn deinit(self: *Page) void { } fn reset(self: *Page, comptime initializing: bool) !void { - if (comptime IS_DEBUG) { - var it = self._arena_pool_leak_track.valueIterator(); - while (it.next()) |value_ptr| { - if (value_ptr.count > 0) { - log.err(.bug, "ArenaPool Leak", .{ .owner = value_ptr.owner }); - } - } - self._arena_pool_leak_track.clearRetainingCapacity(); - } - if (comptime initializing == false) { self._session.executor.removeContext(); + // removing a context can trigger finalizers, so we can only check for + // a leak after the above. + if (comptime IS_DEBUG) { + var it = self._arena_pool_leak_track.valueIterator(); + while (it.next()) |value_ptr| { + log.err(.bug, "ArenaPool Leak", .{ .owner = value_ptr.* }); + } + self._arena_pool_leak_track.clearRetainingCapacity(); + } + + // We force a garbage collection between page navigations to keep v8 // memory usage as low as possible. self._session.browser.env.memoryPressureNotification(.moderate); From dfe5c2440483d0a4429e03c754f29e4d412e8ccf Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 29 Jan 2026 07:04:20 +0800 Subject: [PATCH 19/19] remove unused import and unused export --- src/browser/EventManager.zig | 1 - src/browser/webapi/Element.zig | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/browser/EventManager.zig b/src/browser/EventManager.zig index c21792c4..fb6d3674 100644 --- a/src/browser/EventManager.zig +++ b/src/browser/EventManager.zig @@ -22,7 +22,6 @@ const builtin = @import("builtin"); const log = @import("../log.zig"); const String = @import("../string.zig").String; -const lp = @import("lightpanda"); const js = @import("js/js.zig"); const Page = @import("Page.zig"); diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 512f5821..619d95cd 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -48,10 +48,11 @@ pub const ClassListLookup = std.AutoHashMapUnmanaged(*Element, *collections.DOMT pub const RelListLookup = std.AutoHashMapUnmanaged(*Element, *collections.DOMTokenList); pub const ShadowRootLookup = std.AutoHashMapUnmanaged(*Element, *ShadowRoot); pub const AssignedSlotLookup = std.AutoHashMapUnmanaged(*Element, *Html.Slot); + /// Better to discriminate it since not directly a pointer int. /// /// See `calcAttrListenerKey` to obtain one. -pub const AttrListenerKey = u64; +const AttrListenerKey = u64; /// Use `getAttrListenerKey` to create a key. pub const AttrListenerLookup = std.AutoHashMapUnmanaged(AttrListenerKey, js.Function.Global);