Add zig listener support to netsurf event handler

Add _click handler to HTMLElement

Register zig click listener on document.

Largely waiting on https://github.com/lightpanda-io/browser/pull/501/files to
finalize the placeholders.
This commit is contained in:
Karl Seguin
2025-04-08 18:42:11 +08:00
parent 581a79f3fc
commit 072bc514f4
6 changed files with 427 additions and 90 deletions

View File

@@ -481,6 +481,16 @@ pub const Page = struct {
// save a document's pointer in the page.
self.doc = doc;
const document_element = (try parser.documentGetDocumentElement(doc)) orelse return error.DocumentElementError;
try parser.eventTargetAddZigListener(
parser.toEventTarget(parser.Element, document_element),
arena,
"click",
windowClicked,
self,
false,
);
// TODO set document.readyState to interactive
// https://html.spec.whatwg.org/#reporting-document-loading-status
@@ -728,6 +738,29 @@ pub const Page = struct {
return request;
}
fn windowClicked(ctx: *anyopaque, event: *parser.Event) void {
const self: *Page = @alignCast(@ptrCast(ctx));
self._windowClicked(event) catch |err| {
log.err("window click handler: {}", .{err});
};
}
fn _windowClicked(self: *Page, event: *parser.Event) !void {
_ = self;
const target = (try parser.eventTarget(event)) orelse return;
const node = parser.eventTargetToNode(target);
if (try parser.nodeType(node) != .element) {
return;
}
const element: *parser.ElementHTML = @ptrCast(node);
const tag_name = try parser.elementHTMLGetTagType(element);
// TODO https://github.com/lightpanda-io/browser/pull/501
_ = tag_name;
}
const Script = struct {
element: *parser.Element,
kind: Kind,

View File

@@ -104,7 +104,7 @@ pub const MutationObserver = struct {
arena,
"DOMNodeInserted",
EventHandler,
.{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc },
.{ .cbk = self.cbk, .ctx = o, .deinitFunc = deinitFunc },
false,
);
try parser.eventTargetAddEventListener(
@@ -112,7 +112,7 @@ pub const MutationObserver = struct {
arena,
"DOMNodeRemoved",
EventHandler,
.{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc },
.{ .cbk = self.cbk, .ctx = o, .deinitFunc = deinitFunc },
false,
);
}
@@ -122,7 +122,7 @@ pub const MutationObserver = struct {
arena,
"DOMAttrModified",
EventHandler,
.{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc },
.{ .cbk = self.cbk, .ctx = o, .deinitFunc = deinitFunc },
false,
);
}
@@ -132,7 +132,7 @@ pub const MutationObserver = struct {
arena,
"DOMCharacterDataModified",
EventHandler,
.{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc },
.{ .cbk = self.cbk, .ctx = o, .deinitFunc = deinitFunc },
false,
);
}
@@ -142,7 +142,7 @@ pub const MutationObserver = struct {
arena,
"DOMSubtreeModified",
EventHandler,
.{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc },
.{ .cbk = self.cbk, .ctx = o, .deinitFunc = deinitFunc },
false,
);
}
@@ -261,7 +261,7 @@ const EventHandler = struct {
return false;
}
fn handle(evt: ?*parser.Event, data: parser.EventHandlerData) void {
fn handle(evt: ?*parser.Event, data: *const parser.JSEventHandlerData) void {
if (evt == null) return;
var mrs: MutationRecords = .{};
@@ -277,7 +277,7 @@ const EventHandler = struct {
const node = parser.eventTargetToNode(et);
// retrieve the observer from the data.
const o: *MutationObserver.Observer = @ptrCast(@alignCast(data.data));
const o: *MutationObserver.Observer = @ptrCast(@alignCast(data.ctx));
if (!apply(o, node)) return;

View File

@@ -136,7 +136,7 @@ pub const Event = struct {
};
pub const EventHandler = struct {
fn handle(event: ?*parser.Event, data: parser.EventHandlerData) void {
fn handle(event: ?*parser.Event, data: *const parser.JSEventHandlerData) void {
var result: Callback.Result = undefined;
data.cbk.tryCall(.{if (event) |evt| Event.toInterface(evt) catch unreachable else null}, &result) catch {
log.err("event handler error: {s}", .{result.exception});

View File

@@ -130,6 +130,24 @@ pub const HTMLElement = struct {
// attach the text node.
_ = try parser.nodeAppendChild(n, @as(*parser.Node, @ptrCast(t)));
}
pub fn _click(e: *parser.ElementHTML) !void {
_ = e;
// TODO needs: https://github.com/lightpanda-io/browser/pull/501
// TODO: when the above is merged, should we get the element coordinates?
// const event = try parser.mouseEventCreate();
// defer parser.mouseEventDestroy(event);
// try parser.mouseEventInit(event, "click", .{
// .bubbles = true,
// .cancelable = true,
//
// // get the coordinates?
// .x = 0,
// .y = 0,
// });
// _ = try parser.elementDispatchEvent(@ptrCast(e), @ptrCast(event));
}
};
// Deprecated HTMLElements in Chrome (2023/03/15)

View File

@@ -616,10 +616,12 @@ pub fn eventTargetHasListener(
// and capture property,
// let's check if the callback handler is the same
defer c.dom_event_listener_unref(listener);
const ehd = EventHandlerDataInternal.fromListener(listener);
if (ehd) |d| {
if (cbk_id == d.data.cbk.id) {
if (EventHandlerData.fromListener(listener)) |ehd| {
switch (ehd.*) {
.js => |js| if (cbk_id == js.data.cbk.id) {
return lst;
},
.zig => {},
}
}
}
@@ -636,100 +638,115 @@ pub fn eventTargetHasListener(
return null;
}
// EventHandlerFunc is a zig function called when the event is dispatched to a
// listener.
// The EventHandlerFunc is responsible to call the callback included into the
// EventHandlerData.
pub const EventHandlerFunc = *const fn (event: ?*Event, data: EventHandlerData) void;
// The *anyopque that get stored in the libdom listener, which we'll retrieve
// when then event is dispatched so that we can execute the JS or Zig callback.
const EventHandlerData = union(enum) {
js: JS,
zig: Zig,
// EventHandler implements the function exposed in C and called by libdom.
// It retrieves the EventHandlerInternalData and call the EventHandlerFunc with
// the EventHandlerData in parameter.
const EventHandler = struct {
fn handle(event: ?*Event, data: ?*anyopaque) callconv(.C) void {
if (data) |d| {
const ehd = EventHandlerDataInternal.get(d);
ehd.handler(event, ehd.data);
// NOTE: we can not call func.deinit here
// b/c the handler can be called several times
// either on this dispatch event or in anoter one
}
}
}.handle;
// EventHandlerData contains a JS callback and the data associated to the
// handler.
// If given, deinitFunc is called with the data pointer to allow the creator to
// clean memory.
// The callback is deinit by EventHandlerDataInternal. It must NOT be deinit
// into deinitFunc.
pub const EventHandlerData = struct {
cbk: Callback,
data: ?*anyopaque = null,
// deinitFunc implements the data deinitialization.
deinitFunc: ?DeinitFunc = null,
pub const DeinitFunc = *const fn (data: ?*anyopaque, allocator: std.mem.Allocator) void;
const JS = struct {
data: JSEventHandlerData,
func: JSEventHandlerFunc,
};
// EventHandlerDataInternal groups the EventHandlerFunc and the EventHandlerData.
const EventHandlerDataInternal = struct {
data: EventHandlerData,
handler: EventHandlerFunc,
fn init(alloc: std.mem.Allocator, handler: EventHandlerFunc, data: EventHandlerData) !*EventHandlerDataInternal {
const ptr = try alloc.create(EventHandlerDataInternal);
ptr.* = .{
.data = data,
.handler = handler,
const Zig = struct {
ctx: *anyopaque,
func: ZigEventHandlerFunc,
};
return ptr;
// retrieve a EventHandlerDataInternal from a listener.
fn fromListener(lst: *EventListener) ?*EventHandlerData {
const ctx = eventListenerGetData(lst) orelse return null;
const ehd: *EventHandlerData = @alignCast(@ptrCast(ctx));
return ehd;
}
fn deinit(self: *EventHandlerDataInternal, alloc: std.mem.Allocator) void {
if (self.data.deinitFunc) |d| {
d(self.data.data, alloc);
pub fn deinit(self: *EventHandlerData, alloc: std.mem.Allocator) void {
switch (self.*) {
.js => |*js| {
const js_data = &js.data;
if (js_data.deinitFunc) |df| {
df(js_data.ctx, alloc);
}
},
.zig => {},
}
alloc.destroy(self);
}
fn get(data: *anyopaque) *EventHandlerDataInternal {
const ptr: *align(@alignOf(*EventHandlerDataInternal)) anyopaque = @alignCast(data);
return @as(*EventHandlerDataInternal, @ptrCast(ptr));
pub fn handle(self: *EventHandlerData, event: ?*Event) void {
switch (self.*) {
.js => |*js| js.func(event, &js.data),
.zig => |zig| zig.func(zig.ctx, event.?),
}
// retrieve a EventHandlerDataInternal from a listener.
fn fromListener(lst: *EventListener) ?*EventHandlerDataInternal {
const data = eventListenerGetData(lst);
// free cbk allocation made on eventTargetAddEventListener
if (data == null) return null;
return get(data.?);
}
};
pub const JSEventHandlerData = struct {
cbk: Callback,
ctx: ?*anyopaque = null,
// deinitFunc implements the data deinitialization.
deinitFunc: ?DeinitFunc = null,
pub const DeinitFunc = *const fn (data: ?*anyopaque, alloc: std.mem.Allocator) void;
};
const JSEventHandlerFunc = *const fn (event: ?*Event, data: *JSEventHandlerData) void;
const ZigEventHandlerFunc = *const fn (ctx: *anyopaque, event: *Event) void;
pub fn eventTargetAddEventListener(
et: *EventTarget,
alloc: std.mem.Allocator,
typ: []const u8,
handlerFunc: EventHandlerFunc,
data: EventHandlerData,
func: JSEventHandlerFunc,
data: JSEventHandlerData,
capture: bool,
) !void {
// this allocation will be removed either on
// eventTargetRemoveEventListener or eventTargetRemoveAllEventListeners
const ehd = try EventHandlerDataInternal.init(alloc, handlerFunc, data);
const ehd = try alloc.create(EventHandlerData);
errdefer alloc.destroy(ehd);
ehd.* = .{ .js = .{ .data = data, .func = func } };
errdefer ehd.deinit(alloc);
// When a function is used as an event handler, its this parameter is bound
// to the DOM element on which the listener is placed.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this#this_in_dom_event_handlers
try ehd.data.cbk.setThis(et);
try ehd.js.data.cbk.setThis(et);
return addEventTargetListener(et, typ, ehd, capture);
}
pub fn eventTargetAddZigListener(
et: *EventTarget,
alloc: std.mem.Allocator,
typ: []const u8,
func: ZigEventHandlerFunc,
ctx: *anyopaque,
capture: bool,
) !void {
const ehd = try alloc.create(EventHandlerData);
errdefer alloc.destroy(ehd);
ehd.* = .{ .zig = .{ .ctx = ctx, .func = func } };
return addEventTargetListener(et, typ, ehd, capture);
}
fn addEventTargetListener(et: *EventTarget, typ: []const u8, data: *anyopaque, capture: bool) !void {
// event_handler implements the function exposed in C and called by libdom.
// It retrieves the EventHandler and calls the appropriate (JS or Zig)
// handler function with the corresponding data.
const event_handler = struct {
fn handle(event: ?*Event, ptr_: ?*anyopaque) callconv(.C) void {
const ptr = ptr_ orelse return;
@as(*EventHandlerData, @alignCast(@ptrCast(ptr))).handle(event);
// NOTE: we can not call func.deinit here
// b/c the handler can be called several times
// either on this dispatch event or in anoter one
}
}.handle;
const ctx = @as(*anyopaque, @ptrCast(ehd));
var listener: ?*EventListener = undefined;
const errLst = c.dom_event_listener_create(EventHandler, ctx, &listener);
const errLst = c.dom_event_listener_create(event_handler, data, &listener);
try DOMErr(errLst);
defer c.dom_event_listener_unref(listener);
@@ -746,8 +763,9 @@ pub fn eventTargetRemoveEventListener(
capture: bool,
) !void {
// free data allocation made on eventTargetAddEventListener
const ehd = EventHandlerDataInternal.fromListener(lst);
if (ehd) |d| d.deinit(alloc);
if (EventHandlerData.fromListener(lst)) |ehd| {
ehd.deinit(alloc);
}
const s = try strFromData(typ);
const err = eventTargetVtable(et).remove_event_listener.?(et, s, lst, capture);
@@ -776,9 +794,13 @@ pub fn eventTargetRemoveAllEventListeners(
if (lst) |listener| {
defer c.dom_event_listener_unref(listener);
const ehd = EventHandlerDataInternal.fromListener(listener);
if (ehd) |d| d.deinit(alloc);
if (EventHandlerData.fromListener(listener)) |ehd| {
if (ehd.* == .zig) {
// we don't remove Zig listeners
continue;
}
ehd.deinit(alloc);
const err = eventTargetVtable(et).remove_event_listener.?(
et,
null,
@@ -787,6 +809,7 @@ pub fn eventTargetRemoveAllEventListeners(
);
try DOMErr(err);
}
}
if (next == null) {
// no more listeners, end of the iteration

263
src/events/event.zig Normal file
View File

@@ -0,0 +1,263 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const generate = @import("../generate.zig");
const jsruntime = @import("jsruntime");
const Callback = jsruntime.Callback;
const CallbackResult = jsruntime.CallbackResult;
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
const parser = @import("netsurf");
const DOMException = @import("../dom/exceptions.zig").DOMException;
const EventTarget = @import("../dom/event_target.zig").EventTarget;
const EventTargetUnion = @import("../dom/event_target.zig").Union;
const ProgressEvent = @import("../xhr/progress_event.zig").ProgressEvent;
const log = std.log.scoped(.events);
// Event interfaces
pub const Interfaces = .{
Event,
ProgressEvent,
};
pub const Union = generate.Union(Interfaces);
// https://dom.spec.whatwg.org/#event
pub const Event = struct {
pub const Self = parser.Event;
pub const Exception = DOMException;
pub const mem_guarantied = true;
pub const EventInit = parser.EventInit;
// JS
// --
pub const _CAPTURING_PHASE = 1;
pub const _AT_TARGET = 2;
pub const _BUBBLING_PHASE = 3;
pub fn toInterface(evt: *parser.Event) !Union {
return switch (try parser.eventGetInternalType(evt)) {
.event => .{ .Event = evt },
.progress_event => .{ .ProgressEvent = @as(*ProgressEvent, @ptrCast(evt)).* },
};
}
pub fn constructor(eventType: []const u8, opts: ?EventInit) !*parser.Event {
const event = try parser.eventCreate();
try parser.eventInit(event, eventType, opts orelse EventInit{});
return event;
}
// Getters
pub fn get_type(self: *parser.Event) ![]const u8 {
return try parser.eventType(self);
}
pub fn get_target(self: *parser.Event) !?EventTargetUnion {
const et = try parser.eventTarget(self);
if (et == null) return null;
return try EventTarget.toInterface(et.?);
}
pub fn get_currentTarget(self: *parser.Event) !?EventTargetUnion {
const et = try parser.eventCurrentTarget(self);
if (et == null) return null;
return try EventTarget.toInterface(et.?);
}
pub fn get_eventPhase(self: *parser.Event) !u8 {
return try parser.eventPhase(self);
}
pub fn get_bubbles(self: *parser.Event) !bool {
return try parser.eventBubbles(self);
}
pub fn get_cancelable(self: *parser.Event) !bool {
return try parser.eventCancelable(self);
}
pub fn get_defaultPrevented(self: *parser.Event) !bool {
return try parser.eventDefaultPrevented(self);
}
pub fn get_isTrusted(self: *parser.Event) !bool {
return try parser.eventIsTrusted(self);
}
pub fn get_timestamp(self: *parser.Event) !u32 {
return try parser.eventTimestamp(self);
}
// Methods
pub fn _initEvent(
self: *parser.Event,
eventType: []const u8,
bubbles: ?bool,
cancelable: ?bool,
) !void {
const opts = EventInit{
.bubbles = bubbles orelse false,
.cancelable = cancelable orelse false,
};
return try parser.eventInit(self, eventType, opts);
}
pub fn _stopPropagation(self: *parser.Event) !void {
return try parser.eventStopPropagation(self);
}
pub fn _stopImmediatePropagation(self: *parser.Event) !void {
return try parser.eventStopImmediatePropagation(self);
}
pub fn _preventDefault(self: *parser.Event) !void {
return try parser.eventPreventDefault(self);
}
};
pub fn testExecFn(
_: std.mem.Allocator,
js_env: *jsruntime.Env,
) anyerror!void {
var common = [_]Case{
.{ .src = "let content = document.getElementById('content')", .ex = "undefined" },
.{ .src = "let para = document.getElementById('para')", .ex = "undefined" },
.{ .src = "var nb = 0; var evt", .ex = "undefined" },
};
try checkCases(js_env, &common);
var basic = [_]Case{
.{ .src =
\\content.addEventListener('target',
\\function(e) {
\\evt = e; nb = nb + 1;
\\e.preventDefault();
\\})
, .ex = "undefined" },
.{ .src = "content.dispatchEvent(new Event('target', {bubbles: true, cancelable: true}))", .ex = "false" },
.{ .src = "nb", .ex = "1" },
.{ .src = "evt.target === content", .ex = "true" },
.{ .src = "evt.bubbles", .ex = "true" },
.{ .src = "evt.cancelable", .ex = "true" },
.{ .src = "evt.defaultPrevented", .ex = "true" },
.{ .src = "evt.isTrusted", .ex = "true" },
.{ .src = "evt.timestamp > 1704063600", .ex = "true" }, // 2024/01/01 00:00
// event.type, event.currentTarget, event.phase checked in EventTarget
};
try checkCases(js_env, &basic);
var stop = [_]Case{
.{ .src = "nb = 0", .ex = "0" },
.{ .src =
\\content.addEventListener('stop',
\\function(e) {
\\e.stopPropagation();
\\nb = nb + 1;
\\}, true)
, .ex = "undefined" },
// the following event listener will not be invoked
.{ .src =
\\para.addEventListener('stop',
\\function(e) {
\\nb = nb + 1;
\\})
, .ex = "undefined" },
.{ .src = "para.dispatchEvent(new Event('stop'))", .ex = "true" },
.{ .src = "nb", .ex = "1" }, // will be 2 if event was not stopped at content event listener
};
try checkCases(js_env, &stop);
var stop_immediate = [_]Case{
.{ .src = "nb = 0", .ex = "0" },
.{ .src =
\\content.addEventListener('immediate',
\\function(e) {
\\e.stopImmediatePropagation();
\\nb = nb + 1;
\\})
, .ex = "undefined" },
// the following event listener will not be invoked
.{ .src =
\\content.addEventListener('immediate',
\\function(e) {
\\nb = nb + 1;
\\})
, .ex = "undefined" },
.{ .src = "content.dispatchEvent(new Event('immediate'))", .ex = "true" },
.{ .src = "nb", .ex = "1" }, // will be 2 if event was not stopped at first content event listener
};
try checkCases(js_env, &stop_immediate);
var legacy = [_]Case{
.{ .src = "nb = 0", .ex = "0" },
.{ .src =
\\content.addEventListener('legacy',
\\function(e) {
\\evt = e; nb = nb + 1;
\\})
, .ex = "undefined" },
.{ .src = "let evtLegacy = document.createEvent('Event')", .ex = "undefined" },
.{ .src = "evtLegacy.initEvent('legacy')", .ex = "undefined" },
.{ .src = "content.dispatchEvent(evtLegacy)", .ex = "true" },
.{ .src = "nb", .ex = "1" },
};
try checkCases(js_env, &legacy);
var remove = [_]Case{
.{ .src = "var nb = 0; var evt = null; function cbk(event) { nb ++; evt=event; }", .ex = "undefined" },
.{ .src = "document.addEventListener('count', cbk)", .ex = "undefined" },
.{ .src = "document.removeEventListener('count', cbk)", .ex = "undefined" },
.{ .src = "document.dispatchEvent(new Event('count'))", .ex = "true" },
.{ .src = "nb", .ex = "0" },
};
try checkCases(js_env, &remove);
}
pub const EventHandler = struct {
fn handle(event: ?*parser.Event, data: *const parser.JSEventHandlerData) void {
// TODO get the allocator by another way?
var res = CallbackResult.init(data.cbk.nat_ctx.alloc);
defer res.deinit();
if (event) |evt| {
data.cbk.trycall(.{
Event.toInterface(evt) catch unreachable,
}, &res) catch |e| log.err("event handler error: {any}", .{e});
} else {
data.cbk.trycall(.{event}, &res) catch |e| log.err("event handler error (null event): {any}", .{e});
}
// in case of function error, we log the result and the trace.
if (!res.success) {
log.info("event handler error try catch: {s}", .{res.result orelse "unknown"});
log.debug("{s}", .{res.stack orelse "no stack trace"});
}
}
}.handle;