From 988f4997230ce330c2a08d711606497a6113fd2c Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Tue, 20 Jan 2026 19:11:36 +0300 Subject: [PATCH 1/3] `EventManager`: add `hasListener` Not sure if this should be in `EventTarget` or `EventManager`, here goes nothing. `Image`: dispatch `load` event when `src` set add load event test remove `hasListener` let `Scheduler` dispatch `load` event Simulates async nature. update test free `args` when done implement `load` event dispatch for `` tags This dispatches `load` events tied to `EventManager` but not the `onload` for some reason... `"load"` event must be dispatched even if `onload` not set Resolves the bug that cause event listeners added through `EventTarget` not executing if `onload` not set. add `onload` getter/setter for `Image` prefer `attributeChange` to run side-effects This should give more consistent results than using `setSrc`. add inline `` test `Image`: prefer `inline_lookup` for `onload` remove incorrect URL check + prefer 0ms in `Scheduler` change after rebase --- src/browser/tests/element/html/image.html | 59 +++++++++++++++++++ src/browser/webapi/element/html/Image.zig | 71 +++++++++++++++++++++++ 2 files changed, 130 insertions(+) diff --git a/src/browser/tests/element/html/image.html b/src/browser/tests/element/html/image.html index b9cb8153..b834aebf 100644 --- a/src/browser/tests/element/html/image.html +++ b/src/browser/tests/element/html/image.html @@ -97,3 +97,62 @@ testing.expectEqual('lazy', img.getAttribute('loading')); } + + + + + + diff --git a/src/browser/webapi/element/html/Image.zig b/src/browser/webapi/element/html/Image.zig index 41482ec9..4a675e6b 100644 --- a/src/browser/webapi/element/html/Image.zig +++ b/src/browser/webapi/element/html/Image.zig @@ -5,6 +5,8 @@ const URL = @import("../../../URL.zig"); const Node = @import("../../Node.zig"); const Element = @import("../../Element.zig"); const HtmlElement = @import("../Html.zig"); +const Event = @import("../../Event.zig"); +const log = @import("../../../../log.zig"); const Image = @This(); _proto: *HtmlElement, @@ -47,6 +49,41 @@ pub fn getSrc(self: *const Image, page: *Page) ![]const u8 { pub fn setSrc(self: *Image, value: []const u8, page: *Page) !void { try self.asElement().setAttributeSafe(comptime .wrap("src"), .wrap(value), page); + + const event_target = self.asNode().asEventTarget(); + + // Have to do this since `Scheduler` only allow passing a single arg. + const SetSrcCallback = struct { + page: *Page, + event_target: *@import("../../EventTarget.zig"), + }; + const args = try page._factory.create(SetSrcCallback{ + .page = page, + .event_target = event_target, + }); + errdefer page._factory.destroy(args); + + // We don't actually fetch the media, here we fake the load call. + try page.scheduler.add( + args, + struct { + fn wrap(raw: *anyopaque) anyerror!?u32 { + const _args: *SetSrcCallback = @ptrCast(@alignCast(raw)); + const _page = _args.page; + defer _page._factory.destroy(_args); + // Dispatch. + const event = try Event.initTrusted("load", .{}, _page); + try _page._event_manager.dispatch(_args.event_target, event); + + return null; + } + }.wrap, + 25, + .{ + .low_priority = false, + .name = "Image.setSrc", + }, + ); } pub fn getAlt(self: *const Image) []const u8 { @@ -115,6 +152,40 @@ pub const JsApi = struct { pub const loading = bridge.accessor(Image.getLoading, Image.setLoading, .{}); }; +/// Argument passed to `dispatchLoadEvent`. +const CallbackParams = struct { page: *Page, element: *Element }; + +/// Callback passed to `Scheduler` to execute load listeners. +fn dispatchLoadEvent(raw: *anyopaque) !?u32 { + const _args: *CallbackParams = @ptrCast(@alignCast(raw)); + const _page = _args.page; + defer _page._factory.destroy(_args); + + const _element = _args.element; + const _img = _element.as(Image); + const event_target = _element.asEventTarget(); + const event = try Event.initTrusted("load", .{}, _page); + + // If onload provided, dispatch with it. + if (_img.getOnLoad(_page)) |_on_load| { + var ls: js.Local.Scope = undefined; + _page.js.localScope(&ls); + defer ls.deinit(); + + try _page._event_manager.dispatchWithFunction( + event_target, + event, + _on_load.local(&ls.local), + .{ .context = "Image.onload" }, + ); + return null; + } + + // Dispatch to addEventListener listeners. + try _page._event_manager.dispatch(event_target, event); + return null; +} + const testing = @import("../../../../testing.zig"); test "WebApi: HTML.Image" { try testing.htmlRunner("element/html/image.html", .{}); From 8ecbd8e71c94f67e916d76c4dbfeb52bd6cc8da7 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Wed, 4 Feb 2026 23:17:25 +0300 Subject: [PATCH 2/3] add `Page._to_load` and implement load even dispatching for `Image` --- src/browser/Page.zig | 33 +++++++++- src/browser/webapi/element/html/Image.zig | 78 ++++------------------- 2 files changed, 42 insertions(+), 69 deletions(-) diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 8e86c47a..8dd10fde 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -41,8 +41,10 @@ const Parser = @import("parser/Parser.zig"); const URL = @import("URL.zig"); const Node = @import("webapi/Node.zig"); const Event = @import("webapi/Event.zig"); +const EventTarget = @import("webapi/EventTarget.zig"); const CData = @import("webapi/CData.zig"); const Element = @import("webapi/Element.zig"); +const HtmlElement = @import("webapi/element/Html.zig"); const Window = @import("webapi/Window.zig"); const Location = @import("webapi/Location.zig"); const Document = @import("webapi/Document.zig"); @@ -124,6 +126,10 @@ _element_assigned_slots: Element.AssignedSlotLookup = .{}, /// ``` _element_attr_listeners: GlobalEventHandlersLookup = .{}, +/// `load` events that'll be fired before window's `load` event. +/// A call to `documentIsComplete` (which calls `_documentIsComplete`) resets it. +_to_load: std.ArrayList(*Element) = .{}, + _script_manager: ScriptManager, // List of active MutationObservers @@ -344,6 +350,8 @@ fn reset(self: *Page, comptime initializing: bool) !void { self._element_attr_listeners = .{}; + self._to_load = .{}; + self._notified_network_idle = .init; self._notified_network_almost_idle = .init; @@ -699,15 +707,34 @@ pub fn documentIsComplete(self: *Page) void { fn _documentIsComplete(self: *Page) !void { self.document._ready_state = .complete; - // dispatch window.load event const event = try Event.initTrusted("load", .{}, self); - // this event is weird, it's dispatched directly on the window, but - // with the document as the target var ls: JS.Local.Scope = undefined; self.js.localScope(&ls); defer ls.deinit(); + // Dispatch `_to_load` events before window.load. + for (self._to_load.items) |element| { + const maybe_inline_listener = self.getAttrListener(element, .onload); + + try self._event_manager.dispatchWithFunction( + element.asEventTarget(), + event, + ls.toLocal(maybe_inline_listener), + .{ .context = "Page dispatch load events" }, + ); + + if (comptime IS_DEBUG) { + log.debug(.page, "load event for element", .{ .element = element }); + } + } + + // `_to_load` can be cleaned here. + self._to_load.clearAndFree(self.arena); + + // Dispatch window.load event. + // This event is weird, it's dispatched directly on the window, but + // with the document as the target. event._target = self.document.asEventTarget(); try self._event_manager.dispatchWithFunction( self.window.asEventTarget(), diff --git a/src/browser/webapi/element/html/Image.zig b/src/browser/webapi/element/html/Image.zig index 4a675e6b..10a37ce7 100644 --- a/src/browser/webapi/element/html/Image.zig +++ b/src/browser/webapi/element/html/Image.zig @@ -8,6 +8,8 @@ const HtmlElement = @import("../Html.zig"); const Event = @import("../../Event.zig"); const log = @import("../../../../log.zig"); +const IS_DEBUG = @import("builtin").mode == .Debug; + const Image = @This(); _proto: *HtmlElement, @@ -49,41 +51,6 @@ pub fn getSrc(self: *const Image, page: *Page) ![]const u8 { pub fn setSrc(self: *Image, value: []const u8, page: *Page) !void { try self.asElement().setAttributeSafe(comptime .wrap("src"), .wrap(value), page); - - const event_target = self.asNode().asEventTarget(); - - // Have to do this since `Scheduler` only allow passing a single arg. - const SetSrcCallback = struct { - page: *Page, - event_target: *@import("../../EventTarget.zig"), - }; - const args = try page._factory.create(SetSrcCallback{ - .page = page, - .event_target = event_target, - }); - errdefer page._factory.destroy(args); - - // We don't actually fetch the media, here we fake the load call. - try page.scheduler.add( - args, - struct { - fn wrap(raw: *anyopaque) anyerror!?u32 { - const _args: *SetSrcCallback = @ptrCast(@alignCast(raw)); - const _page = _args.page; - defer _page._factory.destroy(_args); - // Dispatch. - const event = try Event.initTrusted("load", .{}, _page); - try _page._event_manager.dispatch(_args.event_target, event); - - return null; - } - }.wrap, - 25, - .{ - .low_priority = false, - .name = "Image.setSrc", - }, - ); } pub fn getAlt(self: *const Image) []const u8 { @@ -152,39 +119,18 @@ pub const JsApi = struct { pub const loading = bridge.accessor(Image.getLoading, Image.setLoading, .{}); }; -/// Argument passed to `dispatchLoadEvent`. -const CallbackParams = struct { page: *Page, element: *Element }; +pub const Build = struct { + pub fn created(node: *Node, page: *Page) !void { + const self = node.as(Image); + const image = self.asElement(); + // Exit if src not set. + // TODO: We might want to check if src point to valid image. + _ = image.getAttributeSafe(comptime .wrap("src")) orelse return; -/// Callback passed to `Scheduler` to execute load listeners. -fn dispatchLoadEvent(raw: *anyopaque) !?u32 { - const _args: *CallbackParams = @ptrCast(@alignCast(raw)); - const _page = _args.page; - defer _page._factory.destroy(_args); - - const _element = _args.element; - const _img = _element.as(Image); - const event_target = _element.asEventTarget(); - const event = try Event.initTrusted("load", .{}, _page); - - // If onload provided, dispatch with it. - if (_img.getOnLoad(_page)) |_on_load| { - var ls: js.Local.Scope = undefined; - _page.js.localScope(&ls); - defer ls.deinit(); - - try _page._event_manager.dispatchWithFunction( - event_target, - event, - _on_load.local(&ls.local), - .{ .context = "Image.onload" }, - ); - return null; + // Push to `_to_load` to dispatch load event just before window load event. + return page._to_load.append(page.arena, image); } - - // Dispatch to addEventListener listeners. - try _page._event_manager.dispatch(event_target, event); - return null; -} +}; const testing = @import("../../../../testing.zig"); test "WebApi: HTML.Image" { From 3afbb6fcc2b86d2fc42a8d8e4c61957be20ab2e5 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Wed, 4 Feb 2026 23:17:47 +0300 Subject: [PATCH 3/3] `load` event dispatching for `Style` --- src/browser/webapi/element/html/Style.zig | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/browser/webapi/element/html/Style.zig b/src/browser/webapi/element/html/Style.zig index b9372bd3..483d07f7 100644 --- a/src/browser/webapi/element/html/Style.zig +++ b/src/browser/webapi/element/html/Style.zig @@ -96,6 +96,15 @@ pub const JsApi = struct { pub const sheet = bridge.accessor(Style.getSheet, null, .{}); }; +pub const Build = struct { + pub fn created(node: *Node, page: *Page) !void { + const self = node.as(Style); + const style = self.asElement(); + // Push to `_to_load` to dispatch load event just before window load event. + return page._to_load.append(page.arena, style); + } +}; + const testing = @import("../../../../testing.zig"); test "WebApi: Style" { try testing.htmlRunner("element/html/style.html", .{});