diff --git a/src/browser/browser.zig b/src/browser/browser.zig index 679fdc52..8133140d 100644 --- a/src/browser/browser.zig +++ b/src/browser/browser.zig @@ -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, diff --git a/src/browser/dom/mutation_observer.zig b/src/browser/dom/mutation_observer.zig index 0324399e..ae050fe5 100644 --- a/src/browser/dom/mutation_observer.zig +++ b/src/browser/dom/mutation_observer.zig @@ -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; diff --git a/src/browser/events/event.zig b/src/browser/events/event.zig index 517b852d..ff89af70 100644 --- a/src/browser/events/event.zig +++ b/src/browser/events/event.zig @@ -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}); diff --git a/src/browser/html/elements.zig b/src/browser/html/elements.zig index 0cee0cca..ff7ad400 100644 --- a/src/browser/html/elements.zig +++ b/src/browser/html/elements.zig @@ -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) diff --git a/src/browser/netsurf.zig b/src/browser/netsurf.zig index eef41155..f08a6b73 100644 --- a/src/browser/netsurf.zig +++ b/src/browser/netsurf.zig @@ -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) { - return lst; + 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); + const JS = struct { + data: JSEventHandlerData, + func: JSEventHandlerFunc, + }; - // 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 Zig = struct { + ctx: *anyopaque, + func: ZigEventHandlerFunc, + }; -// 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; -}; - -// 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, - }; - 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)); - } - - // 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 fn handle(self: *EventHandlerData, event: ?*Event) void { + switch (self.*) { + .js => |*js| js.func(event, &js.data), + .zig => |zig| zig.func(zig.ctx, event.?), + } } }; +pub const JSEventHandlerData = struct { + cbk: Callback, + ctx: ?*anyopaque = null, + // deinitFunc implements the data deinitialization. + deinitFunc: ?DeinitFunc = null, + + pub const DeinitFunc = *const fn (data: ?*anyopaque, alloc: std.mem.Allocator) void; +}; + +const JSEventHandlerFunc = *const fn (event: ?*Event, data: *JSEventHandlerData) void; +const ZigEventHandlerFunc = *const fn (ctx: *anyopaque, event: *Event) void; + pub fn eventTargetAddEventListener( et: *EventTarget, alloc: std.mem.Allocator, typ: []const u8, - 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,16 +794,21 @@ 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; + } - const err = eventTargetVtable(et).remove_event_listener.?( - et, - null, - lst, - false, - ); - try DOMErr(err); + ehd.deinit(alloc); + const err = eventTargetVtable(et).remove_event_listener.?( + et, + null, + lst, + false, + ); + try DOMErr(err); + } } if (next == null) { diff --git a/src/events/event.zig b/src/events/event.zig new file mode 100644 index 00000000..e37d0512 --- /dev/null +++ b/src/events/event.zig @@ -0,0 +1,263 @@ +// Copyright (C) 2023-2024 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// 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 . + +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;