From 0253de80deffade8347e0f7bb8be0ab47439439a Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 1 Apr 2025 17:51:33 +0800 Subject: [PATCH 1/4] Add a dumb renderer to get coordinates FlatRenderer positions items on a single row, giving each a height and width of 1. Added getBoundingClientRect to the DOMelement which, when requested for the first time, will place the item in with the renderer. The goal here is to give elements a fixed position and to make it easy to map x,y coordinates onto an element. This should work, at least with puppeteer, since it first requests the boundingClientRect before issuing a click. --- src/browser/browser.zig | 97 ++++++++++++++++++++++++++++++++++++++- src/cdp/cdp.zig | 3 ++ src/cdp/domains/input.zig | 80 ++++++++++++++++++++++++++++++++ src/cdp/domains/page.zig | 40 ++++++++-------- src/cdp/testing.zig | 10 +++- src/dom/element.zig | 48 +++++++++++++++++++ src/main_tests.zig | 6 +++ src/netsurf/netsurf.zig | 18 ++++++++ src/user_context.zig | 4 +- vendor/zig-js-runtime | 2 +- 10 files changed, 285 insertions(+), 23 deletions(-) create mode 100644 src/cdp/domains/input.zig diff --git a/src/browser/browser.zig b/src/browser/browser.zig index 1d549bccb..7bdeff664 100644 --- a/src/browser/browser.zig +++ b/src/browser/browser.zig @@ -298,10 +298,14 @@ pub const Page = struct { // current_script could by fetch module to resolve module's url to fetch. current_script: ?*const Script = null, + renderer: FlatRenderer, + fn init(session: *Session) Page { + const arena = session.browser.page_arena.allocator(); return .{ + .arena = arena, .session = session, - .arena = session.browser.page_arena.allocator(), + .renderer = FlatRenderer.init(arena), }; } @@ -423,6 +427,32 @@ pub const Page = struct { } } + pub const ClickResult = union(enum) { + navigate: std.Uri, + }; + + pub fn click(self: *Page, allocator: Allocator, x: u32, y: u32) !?ClickResult { + const element = self.renderer.getElementAtPosition(x, y) orelse return null; + + const event = try parser.eventCreate(); + defer parser.eventDestroy(event); + + try parser.eventInit(event, "click", .{ .bubbles = true, .cancelable = true }); + if ((try parser.eventDefaultPrevented(event)) == true) { + return null; + } + + const node = parser.elementToNode(element); + const tag = try parser.nodeName(node); + if (std.ascii.eqlIgnoreCase(tag, "a")) { + const href = (try parser.elementGetAttribute(element, "href")) orelse return null; + var buf = try allocator.alloc(u8, 1024); + return .{ .navigate = try std.Uri.resolve_inplace(self.uri, href, &buf) }; + } + + return null; + } + // https://html.spec.whatwg.org/#read-html fn loadHTMLDoc(self: *Page, reader: anytype, charset: []const u8, aux_data: ?[]const u8) !void { const arena = self.arena; @@ -462,6 +492,7 @@ pub const Page = struct { try session.env.setUserContext(.{ .uri = self.uri, .document = html_doc, + .renderer = @ptrCast(&self.renderer), .cookie_jar = @ptrCast(&self.session.cookie_jar), .http_client = @ptrCast(self.session.http_client), }); @@ -753,6 +784,70 @@ pub const Page = struct { }; }; +// provide very poor abstration to the rest of the code. In theory, we can change +// the FlatRendere to a different implementation, and it'll all just work. +pub const Renderer = FlatRenderer; + +// This "renderer" positions elements in a single row in an unspecified order. +// The important thing is that elements have a consistent position/index within +// that row, which can be turned into a rectangle. +const FlatRenderer = struct { + allocator: Allocator, + + // key is a @ptrFromInt of the element + // value is the index position + positions: std.AutoHashMapUnmanaged(u64, u32), + + // given an index, get the element + elements: std.ArrayListUnmanaged(u64), + + const Element = @import("../dom/element.zig").Element; + + // we expect allocator to be an arena + pub fn init(allocator: Allocator) FlatRenderer { + return .{ + .elements = .{}, + .positions = .{}, + .allocator = allocator, + }; + } + + pub fn getRect(self: *FlatRenderer, e: *parser.Element) !Element.DOMRect { + var elements = &self.elements; + const gop = try self.positions.getOrPut(self.allocator, @intFromPtr(e)); + var x: u32 = gop.value_ptr.*; + if (gop.found_existing == false) { + try elements.append(self.allocator, @intFromPtr(e)); + x = @intCast(elements.items.len); + gop.value_ptr.* = x; + } + + return .{ + .x = @floatFromInt(x), + .y = 0.0, + .width = 1.0, + .height = 1.0, + }; + } + + pub fn width(self: *const FlatRenderer) u32 { + return @intCast(self.elements.items.len); + } + + pub fn height(_: *const FlatRenderer) u32 { + return 1; + } + + pub fn getElementAtPosition(self: *const FlatRenderer, x: u32, y: u32) ?*parser.Element { + if (y > 1) { + return null; + } + + const elements = self.elements.items; + return if (x < elements.len) @ptrFromInt(elements[x]) else null; + } +}; + const NoopInspector = struct { pub fn onInspectorResponse(_: *anyopaque, _: u32, _: []const u8) void {} pub fn onInspectorEvent(_: *anyopaque, _: []const u8) void {} diff --git a/src/cdp/cdp.zig b/src/cdp/cdp.zig index 253442bb1..15a81c76a 100644 --- a/src/cdp/cdp.zig +++ b/src/cdp/cdp.zig @@ -37,6 +37,7 @@ pub const CDP = CDPT(struct { const SessionIdGen = Incrementing(u32, "SID"); const TargetIdGen = Incrementing(u32, "TID"); +const LoaderIdGen = Incrementing(u32, "LID"); const BrowserContextIdGen = Incrementing(u32, "BID"); // Generic so that we can inject mocks into it. @@ -54,6 +55,7 @@ pub fn CDPT(comptime TypeProvider: type) type { target_auto_attach: bool = false, target_id_gen: TargetIdGen = .{}, + loader_id_gen: LoaderIdGen = .{}, session_id_gen: SessionIdGen = .{}, browser_context_id_gen: BrowserContextIdGen = .{}, @@ -183,6 +185,7 @@ pub fn CDPT(comptime TypeProvider: type) type { }, 5 => switch (@as(u40, @bitCast(domain[0..5].*))) { asUint("Fetch") => return @import("domains/fetch.zig").processMessage(command), + asUint("Input") => return @import("domains/input.zig").processMessage(command), else => {}, }, 6 => switch (@as(u48, @bitCast(domain[0..6].*))) { diff --git a/src/cdp/domains/input.zig b/src/cdp/domains/input.zig new file mode 100644 index 000000000..44c39eb5d --- /dev/null +++ b/src/cdp/domains/input.zig @@ -0,0 +1,80 @@ +// 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"); + +pub fn processMessage(cmd: anytype) !void { + const action = std.meta.stringToEnum(enum { + dispatchMouseEvent, + }, cmd.input.action) orelse return error.UnknownMethod; + + switch (action) { + .dispatchMouseEvent => return dispatchMouseEvent(cmd), + } +} + +// https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchMouseEvent +fn dispatchMouseEvent(cmd: anytype) !void { + const params = (try cmd.params(struct { + type: []const u8, + x: u32, + y: u32, + })) orelse return error.InvalidParams; + + try cmd.sendResult(null, .{}); + + if (std.ascii.eqlIgnoreCase(params.type, "mousePressed") == false) { + return; + } + + const bc = cmd.browser_context orelse return; + const page = bc.session.currentPage() orelse return; + const click_result = (try page.click(cmd.arena, params.x, params.y)) orelse return; + + switch (click_result) { + .navigate => |uri| try clickNavigate(cmd, uri), + } + // result already sent +} + +fn clickNavigate(cmd: anytype, uri: std.Uri) !void { + const bc = cmd.browser_context.?; + + var url_buf: std.ArrayListUnmanaged(u8) = .{}; + try uri.writeToStream(.{ + .scheme = true, + .authentication = true, + .authority = true, + .port = true, + .path = true, + .query = true, + }, url_buf.writer(cmd.arena)); + const url = url_buf.items; + + try cmd.sendEvent("Page.frameRequestedNavigation", .{ + .url = url, + .frameId = bc.target_id.?, + .reason = "anchorClick", + .disposition = "currentTab", + }, .{ .session_id = bc.session_id.? }); + + bc.session.removePage(); + _ = try bc.session.createPage(null); + + try @import("page.zig").navigateToUrl(cmd, url, false); +} diff --git a/src/cdp/domains/page.zig b/src/cdp/domains/page.zig index d5e179297..b74b8878f 100644 --- a/src/cdp/domains/page.zig +++ b/src/cdp/domains/page.zig @@ -129,6 +129,18 @@ fn createIsolatedWorld(cmd: anytype) !void { } fn navigate(cmd: anytype) !void { + const params = (try cmd.params(struct { + url: []const u8, + // referrer: ?[]const u8 = null, + // transitionType: ?[]const u8 = null, // TODO: enum + // frameId: ?[]const u8 = null, + // referrerPolicy: ?[]const u8 = null, // TODO: enum + })) orelse return error.InvalidParams; + + return navigateToUrl(cmd, params.url, true); +} + +pub fn navigateToUrl(cmd: anytype, url: []const u8, send_result: bool) !void { const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; // didn't create? @@ -140,20 +152,10 @@ fn navigate(cmd: anytype) !void { // if we have a target_id we have to have a page; std.debug.assert(bc.session.page != null); - const params = (try cmd.params(struct { - url: []const u8, - referrer: ?[]const u8 = null, - transitionType: ?[]const u8 = null, // TODO: enum - frameId: ?[]const u8 = null, - referrerPolicy: ?[]const u8 = null, // TODO: enum - })) orelse return error.InvalidParams; - // change state bc.reset(); - bc.url = params.url; - - // TODO: hard coded ID - bc.loader_id = "AF8667A203C5392DBE9AC290044AA4C2"; + bc.url = url; + bc.loader_id = cmd.cdp.loader_id_gen.next(); const LifecycleEvent = struct { frameId: []const u8, @@ -180,10 +182,12 @@ fn navigate(cmd: anytype) !void { } // output - try cmd.sendResult(.{ - .frameId = target_id, - .loaderId = bc.loader_id, - }, .{}); + if (send_result) { + try cmd.sendResult(.{ + .frameId = target_id, + .loaderId = bc.loader_id, + }, .{}); + } // TODO: at this point do we need async the following actions to be async? @@ -199,7 +203,7 @@ fn navigate(cmd: anytype) !void { ); var page = bc.session.currentPage().?; - try page.navigate(params.url, aux_data); + try page.navigate(url, aux_data); // Events @@ -218,7 +222,7 @@ fn navigate(cmd: anytype) !void { .type = "Navigation", .frame = Frame{ .id = target_id, - .url = bc.url, + .url = url, .securityOrigin = bc.security_origin, .secureContextType = bc.secure_context_type, .loaderId = bc.loader_id, diff --git a/src/cdp/testing.zig b/src/cdp/testing.zig index a8b0e7d94..296e33eb4 100644 --- a/src/cdp/testing.zig +++ b/src/cdp/testing.zig @@ -106,11 +106,17 @@ const Page = struct { aux_data: []const u8 = "", doc: ?*parser.Document = null, - pub fn navigate(self: *Page, url: []const u8, aux_data: []const u8) !void { - _ = self; + pub fn navigate(_: *Page, url: []const u8, aux_data: []const u8) !void { _ = url; _ = aux_data; } + + const ClickResult = @import("../browser/browser.zig").Page.ClickResult; + pub fn click(_: *Page, _: Allocator, x: u32, y: u32) !?ClickResult { + _ = x; + _ = y; + return null; + } }; const Client = struct { diff --git a/src/dom/element.zig b/src/dom/element.zig index 030630daf..0453a283a 100644 --- a/src/dom/element.zig +++ b/src/dom/element.zig @@ -33,6 +33,7 @@ const Node = @import("node.zig").Node; const Walker = @import("walker.zig").WalkerDepthFirst; const NodeList = @import("nodelist.zig").NodeList; const HTMLElem = @import("../html/elements.zig"); +const UserContext = @import("../user_context.zig").UserContext; pub const Union = @import("../html/elements.zig").Union; const DOMException = @import("exceptions.zig").DOMException; @@ -43,6 +44,13 @@ pub const Element = struct { pub const prototype = *Node; pub const mem_guarantied = true; + pub const DOMRect = struct { + x: f64, + y: f64, + width: f64, + height: f64, + }; + pub fn toInterface(e: *parser.Element) !Union { return try HTMLElem.toInterface(Union, e); } @@ -339,6 +347,18 @@ pub const Element = struct { return Node.replaceChildren(parser.elementToNode(self), nodes); } + pub fn _getBoundingClientRect(self: *parser.Element, user_context: UserContext) !DOMRect { + return user_context.renderer.getRect(self); + } + + pub fn get_clientWidth(_: *parser.Element, user_context: UserContext) u32 { + return user_context.renderer.width(); + } + + pub fn get_clientHeight(_: *parser.Element, user_context: UserContext) u32 { + return user_context.renderer.height(); + } + pub fn deinit(_: *parser.Element, _: std.mem.Allocator) void {} }; @@ -484,5 +504,33 @@ pub fn testExecFn( var outerHTML = [_]Case{ .{ .src = "document.getElementById('para').outerHTML", .ex = "

And

" }, }; + + var getBoundingClientRect = [_]Case{ + .{ .src = "document.getElementById('para').clientWidth", .ex = "0" }, + .{ .src = "document.getElementById('para').clientHeight", .ex = "1" }, + + .{ .src = "let r1 = document.getElementById('para').getBoundingClientRect()", .ex = "undefined" }, + .{ .src = "r1.x", .ex = "1" }, + .{ .src = "r1.y", .ex = "0" }, + .{ .src = "r1.width", .ex = "1" }, + .{ .src = "r1.height", .ex = "1" }, + + .{ .src = "let r2 = document.getElementById('content').getBoundingClientRect()", .ex = "undefined" }, + .{ .src = "r2.x", .ex = "2" }, + .{ .src = "r2.y", .ex = "0" }, + .{ .src = "r2.width", .ex = "1" }, + .{ .src = "r2.height", .ex = "1" }, + + .{ .src = "let r3 = document.getElementById('para').getBoundingClientRect()", .ex = "undefined" }, + .{ .src = "r3.x", .ex = "1" }, + .{ .src = "r3.y", .ex = "0" }, + .{ .src = "r3.width", .ex = "1" }, + .{ .src = "r3.height", .ex = "1" }, + + .{ .src = "document.getElementById('para').clientWidth", .ex = "2" }, + .{ .src = "document.getElementById('para').clientHeight", .ex = "1" }, + }; + try checkCases(js_env, &getBoundingClientRect); + try checkCases(js_env, &outerHTML); } diff --git a/src/main_tests.zig b/src/main_tests.zig index 65bb4e3fd..85b584c29 100644 --- a/src/main_tests.zig +++ b/src/main_tests.zig @@ -25,6 +25,7 @@ const pretty = @import("pretty"); const parser = @import("netsurf"); const apiweb = @import("apiweb.zig"); +const browser = @import("browser/browser.zig"); const Window = @import("html/window.zig").Window; const xhr = @import("xhr/xhr.zig"); const storage = @import("storage/storage.zig"); @@ -100,9 +101,14 @@ fn testExecFn( var cookie_jar = storage.CookieJar.init(alloc); defer cookie_jar.deinit(); + var renderer = browser.Renderer.init(alloc); + defer renderer.elements.deinit(alloc); + defer renderer.positions.deinit(alloc); + try js_env.setUserContext(.{ .uri = try std.Uri.parse(url), .document = doc, + .renderer = &renderer, .cookie_jar = &cookie_jar, .http_client = &http_client, }); diff --git a/src/netsurf/netsurf.zig b/src/netsurf/netsurf.zig index d57b46193..2471b2b23 100644 --- a/src/netsurf/netsurf.zig +++ b/src/netsurf/netsurf.zig @@ -801,6 +801,24 @@ pub fn eventTargetDispatchEvent(et: *EventTarget, event: *Event) !bool { return res; } +const DispatchOpts = struct { + type: []const u8, + bubbles: bool = true, + cancelable: bool = true, +}; +pub fn elementDispatchEvent(element: *Element, opts: DispatchOpts) !bool { + const event = try eventCreate(); + defer eventDestroy(event); + + try eventInit(event, opts.type, .{ .bubbles = opts.bubbles, .cancelable = opts.cancelable }); + + var res: bool = undefined; + const et: *EventTarget = @ptrCast(element); + const err = eventTargetVtable(et).dispatch_event.?(et, event, &res); + try DOMErr(err); + return res; +} + pub fn eventTargetTBaseFieldName(comptime T: type) ?[]const u8 { std.debug.assert(@inComptime()); switch (@typeInfo(T)) { diff --git a/src/user_context.zig b/src/user_context.zig index e71b29f88..d97089329 100644 --- a/src/user_context.zig +++ b/src/user_context.zig @@ -2,10 +2,12 @@ const std = @import("std"); const parser = @import("netsurf"); const storage = @import("storage/storage.zig"); const Client = @import("http/client.zig").Client; +const Renderer = @import("browser/browser.zig").Renderer; pub const UserContext = struct { - http_client: *Client, uri: std.Uri, + http_client: *Client, document: *parser.DocumentHTML, cookie_jar: *storage.CookieJar, + renderer: *Renderer, }; diff --git a/vendor/zig-js-runtime b/vendor/zig-js-runtime index 64b9b2b0c..304a12e31 160000 --- a/vendor/zig-js-runtime +++ b/vendor/zig-js-runtime @@ -1 +1 @@ -Subproject commit 64b9b2b0c9e7a46a71bd5c1baff513a6916e7363 +Subproject commit 304a12e3174f73fc92ff20b90baca4786f233942 From b76875bf5d65ff9e3caca9d7bccbed295cb8e807 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Mon, 7 Apr 2025 11:50:45 +0800 Subject: [PATCH 2/4] use netsurf's mousevent --- src/browser/browser.zig | 38 ++++++++++++++++++------ src/cdp/domains/input.zig | 32 ++++++++++++++++---- src/netsurf/netsurf.zig | 62 +++++++++++++++++++++++++++++++++++---- vendor/zig-js-runtime | 2 +- 4 files changed, 113 insertions(+), 21 deletions(-) diff --git a/src/browser/browser.zig b/src/browser/browser.zig index 7bdeff664..14d85df14 100644 --- a/src/browser/browser.zig +++ b/src/browser/browser.zig @@ -431,14 +431,34 @@ pub const Page = struct { navigate: std.Uri, }; - pub fn click(self: *Page, allocator: Allocator, x: u32, y: u32) !?ClickResult { - const element = self.renderer.getElementAtPosition(x, y) orelse return null; + pub const MouseEvent = struct { + x: i32, + y: i32, + type: Type, + + const Type = enum { + pressed, + released, + }; + }; - const event = try parser.eventCreate(); - defer parser.eventDestroy(event); + pub fn mouseEvent(self: *Page, allocator: Allocator, me: MouseEvent) !?ClickResult { + if (me.type != .pressed) { + return null; + } - try parser.eventInit(event, "click", .{ .bubbles = true, .cancelable = true }); - if ((try parser.eventDefaultPrevented(event)) == true) { + const element = self.renderer.getElementAtPosition(me.x, me.y) orelse return null; + + const event = try parser.mouseEventCreate(); + defer parser.mouseEventDestroy(event); + + try parser.mouseEventInit(event, "click", .{ + .bubbles = true, + .cancelable = true, + .x = me.x, + .y = me.y, + }); + if ((try parser.mouseEventDefaultPrevented(event)) == true) { return null; } @@ -838,13 +858,13 @@ const FlatRenderer = struct { return 1; } - pub fn getElementAtPosition(self: *const FlatRenderer, x: u32, y: u32) ?*parser.Element { - if (y > 1) { + pub fn getElementAtPosition(self: *const FlatRenderer, x: i32, y: i32) ?*parser.Element { + if (y != 1 or x < 0) { return null; } const elements = self.elements.items; - return if (x < elements.len) @ptrFromInt(elements[x]) else null; + return if (x < elements.len) @ptrFromInt(elements[@intCast(x)]) else null; } }; diff --git a/src/cdp/domains/input.zig b/src/cdp/domains/input.zig index 44c39eb5d..196ed1920 100644 --- a/src/cdp/domains/input.zig +++ b/src/cdp/domains/input.zig @@ -17,6 +17,7 @@ // along with this program. If not, see . const std = @import("std"); +const Page = @import("../../browser/browser.zig").Page; pub fn processMessage(cmd: anytype) !void { const action = std.meta.stringToEnum(enum { @@ -31,20 +32,39 @@ pub fn processMessage(cmd: anytype) !void { // https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchMouseEvent fn dispatchMouseEvent(cmd: anytype) !void { const params = (try cmd.params(struct { - type: []const u8, - x: u32, - y: u32, + x: i32, + y: i32, + type: Type, + + const Type = enum { + mousePressed, + mouseReleased, + mouseMoved, + mouseWheel, + }; })) orelse return error.InvalidParams; try cmd.sendResult(null, .{}); - if (std.ascii.eqlIgnoreCase(params.type, "mousePressed") == false) { - return; + // quickly ignore types we know we don't handle + switch (params.type) { + .mouseMoved, .mouseWheel => return, + else => {}, } const bc = cmd.browser_context orelse return; const page = bc.session.currentPage() orelse return; - const click_result = (try page.click(cmd.arena, params.x, params.y)) orelse return; + + const mouse_event = Page.MouseEvent{ + .x = params.x, + .y = params.y, + .type = switch (params.type) { + .mousePressed => .pressed, + .mouseReleased => .released, + else => unreachable, + }, + }; + const click_result = (try page.mouseEvent(cmd.arena, mouse_event)) orelse return; switch (click_result) { .navigate => |uri| try clickNavigate(cmd, uri), diff --git a/src/netsurf/netsurf.zig b/src/netsurf/netsurf.zig index 2471b2b23..109c547ff 100644 --- a/src/netsurf/netsurf.zig +++ b/src/netsurf/netsurf.zig @@ -24,6 +24,7 @@ const c = @cImport({ @cInclude("dom/bindings/hubbub/parser.h"); @cInclude("events/event_target.h"); @cInclude("events/event.h"); + @cInclude("events/mouse_event.h"); }); const mimalloc = @import("mimalloc"); @@ -809,14 +810,10 @@ const DispatchOpts = struct { pub fn elementDispatchEvent(element: *Element, opts: DispatchOpts) !bool { const event = try eventCreate(); defer eventDestroy(event); - try eventInit(event, opts.type, .{ .bubbles = opts.bubbles, .cancelable = opts.cancelable }); - var res: bool = undefined; const et: *EventTarget = @ptrCast(element); - const err = eventTargetVtable(et).dispatch_event.?(et, event, &res); - try DOMErr(err); - return res; + return eventTargetDispatchEvent(et, event); } pub fn eventTargetTBaseFieldName(comptime T: type) ?[]const u8 { @@ -878,6 +875,61 @@ pub const EventTargetTBase = extern struct { } }; +// MouseEvent + +pub const MouseEvent = c.dom_mouse_event; + +pub fn mouseEventCreate() !*MouseEvent { + var evt: ?*MouseEvent = undefined; + const err = c._dom_mouse_event_create(&evt); + try DOMErr(err); + return evt.?; +} + +pub fn mouseEventDestroy(evt: *MouseEvent) void { + c._dom_mouse_event_destroy(evt); +} + +const MouseEventOpts = struct { + x: i32, + y: i32, + bubbles: bool = false, + cancelable: bool = false, + ctrl: bool = false, + alt: bool = false, + shift: bool = false, + meta: bool = false, + button: u16 = 0, + click_count: u16 = 1, +}; + +pub fn mouseEventInit(evt: *MouseEvent, typ: []const u8, opts: MouseEventOpts) !void { + const s = try strFromData(typ); + const err = c._dom_mouse_event_init( + evt, + s, + opts.bubbles, + opts.cancelable, + null, // dom_abstract_view* ? + opts.click_count, // details + opts.x, // screen_x + opts.y, // screen_y + opts.x, // client_x + opts.y, // client_y + opts.ctrl, + opts.alt, + opts.shift, + opts.meta, + opts.button, + null, // related target + ); + try DOMErr(err); +} + +pub fn mouseEventDefaultPrevented(evt: *MouseEvent) !bool { + return eventDefaultPrevented(@ptrCast(evt)); +} + // NodeType pub const NodeType = enum(u4) { diff --git a/vendor/zig-js-runtime b/vendor/zig-js-runtime index 304a12e31..6b48960a0 160000 --- a/vendor/zig-js-runtime +++ b/vendor/zig-js-runtime @@ -1 +1 @@ -Subproject commit 304a12e3174f73fc92ff20b90baca4786f233942 +Subproject commit 6b48960a0664b74016c777da5e3f9129e5f041f7 From f38a0d2d67842830f1a4693923d215b7437031f9 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Mon, 7 Apr 2025 13:51:50 +0800 Subject: [PATCH 3/4] Remove BrowserContext URL Add BrowserContext.getURL which gets the URL from the session.page. --- src/cdp/cdp.zig | 8 +++++--- src/cdp/domains/page.zig | 5 ++--- src/cdp/domains/target.zig | 5 ++--- src/cdp/testing.zig | 7 ++++--- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/cdp/cdp.zig b/src/cdp/cdp.zig index 15a81c76a..45d4c63ce 100644 --- a/src/cdp/cdp.zig +++ b/src/cdp/cdp.zig @@ -284,8 +284,6 @@ pub fn BrowserContext(comptime CDP_T: type) type { // we should reject it. session_id: ?[]const u8, - // State - url: []const u8, loader_id: []const u8, security_origin: []const u8, page_life_cycle_events: bool, @@ -306,7 +304,6 @@ pub fn BrowserContext(comptime CDP_T: type) type { .cdp = cdp, .target_id = null, .session_id = null, - .url = URL_BASE, .security_origin = URL_BASE, .secure_context_type = "Secure", // TODO = enum .loader_id = LOADER_ID, @@ -336,6 +333,11 @@ pub fn BrowserContext(comptime CDP_T: type) type { }; } + pub fn getURL(self: *const Self) ?[]const u8 { + const page = self.session.currentPage() orelse return null; + return page.rawuri; + } + pub fn onInspectorResponse(ctx: *anyopaque, _: u32, msg: []const u8) void { if (std.log.defaultLogEnabled(.debug)) { // msg should be {"id":,... diff --git a/src/cdp/domains/page.zig b/src/cdp/domains/page.zig index b74b8878f..02134cd27 100644 --- a/src/cdp/domains/page.zig +++ b/src/cdp/domains/page.zig @@ -61,10 +61,10 @@ fn getFrameTree(cmd: anytype) !void { return cmd.sendResult(.{ .frameTree = .{ .frame = Frame{ - .url = bc.url, .id = target_id, .loaderId = bc.loader_id, .securityOrigin = bc.security_origin, + .url = bc.getURL() orelse "about:blank", .secureContextType = bc.secure_context_type, }, }, @@ -154,7 +154,6 @@ pub fn navigateToUrl(cmd: anytype, url: []const u8, send_result: bool) !void { // change state bc.reset(); - bc.url = url; bc.loader_id = cmd.cdp.loader_id_gen.next(); const LifecycleEvent = struct { @@ -285,7 +284,7 @@ test "cdp.page: getFrameTree" { .frame = .{ .id = "TID-3", .loaderId = bc.loader_id, - .url = bc.url, + .url = "about:blank", .domainAndRegistry = "", .securityOrigin = bc.security_origin, .mimeType = "text/html", diff --git a/src/cdp/domains/target.zig b/src/cdp/domains/target.zig index e94284c99..6c4fd4421 100644 --- a/src/cdp/domains/target.zig +++ b/src/cdp/domains/target.zig @@ -132,7 +132,6 @@ fn createTarget(cmd: anytype) !void { _ = try bc.session.createPage(aux_data); // change CDP state - bc.url = "about:blank"; bc.security_origin = "://"; bc.secure_context_type = "InsecureScheme"; bc.loader_id = LOADER_ID; @@ -142,11 +141,11 @@ fn createTarget(cmd: anytype) !void { // has been enabled? try cmd.sendEvent("Target.targetCreated", .{ .targetInfo = TargetInfo{ - .url = bc.url, + .attached = false, .targetId = target_id, .title = "about:blank", .browserContextId = bc.id, - .attached = false, + .url = "about:blank", }, }, .{}); diff --git a/src/cdp/testing.zig b/src/cdp/testing.zig index 296e33eb4..c9ce2ebb3 100644 --- a/src/cdp/testing.zig +++ b/src/cdp/testing.zig @@ -85,6 +85,7 @@ const Session = struct { return error.MockBrowserPageAlreadyExists; } self.page = .{ + .rawuri = "", .session = self, .aux_data = try self.arena.dupe(u8, aux_data orelse ""), }; @@ -103,6 +104,7 @@ const Session = struct { const Page = struct { session: *Session, + rawuri: []const u8, aux_data: []const u8 = "", doc: ?*parser.Document = null, @@ -111,10 +113,9 @@ const Page = struct { _ = aux_data; } + const MouseEvent = @import("../browser/browser.zig").Page.MouseEvent; const ClickResult = @import("../browser/browser.zig").Page.ClickResult; - pub fn click(_: *Page, _: Allocator, x: u32, y: u32) !?ClickResult { - _ = x; - _ = y; + pub fn mouseEvent(_: *Page, _: Allocator, _: MouseEvent) !?ClickResult { return null; } }; From 0fbf48ab4d2c2c3514298774c61461844395983e Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Mon, 7 Apr 2025 16:36:06 +0800 Subject: [PATCH 4/4] actually dispatch click --- src/browser/browser.zig | 3 ++- src/netsurf/netsurf.zig | 15 +++------------ 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/src/browser/browser.zig b/src/browser/browser.zig index 14d85df14..743aaf6cf 100644 --- a/src/browser/browser.zig +++ b/src/browser/browser.zig @@ -451,13 +451,14 @@ pub const Page = struct { const event = try parser.mouseEventCreate(); defer parser.mouseEventDestroy(event); - try parser.mouseEventInit(event, "click", .{ .bubbles = true, .cancelable = true, .x = me.x, .y = me.y, }); + _ = try parser.elementDispatchEvent(element, @ptrCast(event)); + if ((try parser.mouseEventDefaultPrevented(event)) == true) { return null; } diff --git a/src/netsurf/netsurf.zig b/src/netsurf/netsurf.zig index 109c547ff..92c0bbea3 100644 --- a/src/netsurf/netsurf.zig +++ b/src/netsurf/netsurf.zig @@ -802,18 +802,9 @@ pub fn eventTargetDispatchEvent(et: *EventTarget, event: *Event) !bool { return res; } -const DispatchOpts = struct { - type: []const u8, - bubbles: bool = true, - cancelable: bool = true, -}; -pub fn elementDispatchEvent(element: *Element, opts: DispatchOpts) !bool { - const event = try eventCreate(); - defer eventDestroy(event); - try eventInit(event, opts.type, .{ .bubbles = opts.bubbles, .cancelable = opts.cancelable }); - - const et: *EventTarget = @ptrCast(element); - return eventTargetDispatchEvent(et, event); +pub fn elementDispatchEvent(element: *Element, event: *Event) !bool { + const et: *EventTarget = toEventTarget(Element, element); + return eventTargetDispatchEvent(et, @ptrCast(event)); } pub fn eventTargetTBaseFieldName(comptime T: type) ?[]const u8 {