From 853965e7a92ccadc4c089c5ddadb853b03789280 Mon Sep 17 00:00:00 2001 From: sjorsdonkers <72333389+sjorsdonkers@users.noreply.github.com> Date: Tue, 13 May 2025 17:19:20 +0200 Subject: [PATCH 1/4] scollifneeded and contentQuads wip --- src/browser/dom/element.zig | 11 +++++ src/cdp/domains/dom.zig | 87 ++++++++++++++++++++++++++++++++++++- 2 files changed, 97 insertions(+), 1 deletion(-) diff --git a/src/browser/dom/element.zig b/src/browser/dom/element.zig index f48451a8..9e8eb04f 100644 --- a/src/browser/dom/element.zig +++ b/src/browser/dom/element.zig @@ -389,6 +389,11 @@ pub const Element = struct { const s = try cssParse(state.call_arena, selectors, .{}); return s.match(CssNodeWrap{ .node = parser.elementToNode(self) }); } + + pub fn _scrollIntoViewIfNeeded(self: *parser.Element, center_if_needed: ?bool) void { + _ = self; + _ = center_if_needed; + } }; // Tests @@ -575,6 +580,12 @@ test "Browser.DOM.Element" { .{ "el.matches('.notok')", "false" }, }, .{}); + try runner.testCases(&.{ + .{ "const el3 = document.createElement('div');", "undefined" }, + .{ "el3.scrollIntoViewIfNeeded();", "undefined" }, + .{ "el3.scrollIntoViewIfNeeded(false);", "undefined" }, + }, .{}); + // before try runner.testCases(&.{ .{ "const before_container = document.createElement('div');", "undefined" }, diff --git a/src/cdp/domains/dom.zig b/src/cdp/domains/dom.zig index 6613e373..d7c948e5 100644 --- a/src/cdp/domains/dom.zig +++ b/src/cdp/domains/dom.zig @@ -31,6 +31,8 @@ pub fn processMessage(cmd: anytype) !void { discardSearchResults, resolveNode, describeNode, + scrollIntoViewIfNeeded, + getContentQuads, }, cmd.input.action) orelse return error.UnknownMethod; switch (action) { @@ -41,6 +43,8 @@ pub fn processMessage(cmd: anytype) !void { .discardSearchResults => return discardSearchResults(cmd), .resolveNode => return resolveNode(cmd), .describeNode => return describeNode(cmd), + .scrollIntoViewIfNeeded => return scrollIntoViewIfNeeded(cmd), + .getContentQuads => return getContentQuads(cmd), } } @@ -239,7 +243,8 @@ fn describeNode(cmd: anytype) !void { const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; - if (params.nodeId != null) { + const input_node_id = params.nodeId orelse params.backendNodeId; + if (input_node_id != null) { const node = bc.node_registry.lookup_by_id.get(params.nodeId.?) orelse return error.NodeNotFound; return cmd.sendResult(.{ .node = bc.nodeWriter(node, .{}) }, .{}); } @@ -252,6 +257,86 @@ fn describeNode(cmd: anytype) !void { return error.MissingParams; } +// Note Element.DOMRect exists, but there is no need to couple them at this time. +const Rect = struct { + x: f64, + y: f64, + width: f64, + height: f64, +}; + +// An array of quad vertices, x immediately followed by y for each point, points clock-wise. +// Note Y points downward +// We are assuming the start/endpoint is not repeated. +const Quad = [8]f64; + +fn scrollIntoViewIfNeeded(cmd: anytype) !void { + const params = (try cmd.params(struct { + nodeId: ?Node.Id = null, + backendNodeId: ?u32 = null, + objectId: ?[]const u8 = null, + rect: ?Rect = null, + })) orelse return error.InvalidParams; + + var set_count: u2 = 0; // only 1 of nodeId backendNodeId objectId may be set + if (params.nodeId != null) set_count += 1; + if (params.backendNodeId != null) set_count += 1; + if (params.objectId != null) set_count += 1; + if (set_count != 1) return error.InvalidParams; + + // Since element.scrollIntoViewIfNeeded is a no-op we do not bother retrieving the node. + // This however also means we also do not error in case the node is not found. + + return cmd.sendResult(null, .{}); +} + +fn getContentQuads(cmd: anytype) !void { + const params = (try cmd.params(struct { + nodeId: ?Node.Id = null, + backendNodeId: ?u32 = null, + objectId: ?[]const u8 = null, + })) orelse return error.InvalidParams; + + var set_count: u2 = 0; // only 1 of nodeId backendNodeId objectId may be set + if (params.nodeId != null) set_count += 1; + if (params.backendNodeId != null) set_count += 1; + if (params.objectId != null) set_count += 1; + if (set_count != 1) return error.InvalidParams; + + const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; + + const node = blk: { + const input_node_id = params.nodeId orelse params.backendNodeId; + if (input_node_id != null) { + const node = bc.node_registry.lookup_by_id.get(params.nodeId.?) orelse return error.NodeNotFound; + break :blk node; + } + if (params.objectId != null) { + const parser_node = try bc.inspector.getNodePtr(cmd.arena, params.objectId.?); + const node = try bc.node_registry.register(@ptrCast(parser_node)); + break :blk node; + } + unreachable; + }; + + if (try parser.nodeType(node._node) != .element) return error.NodeISNotAnElement; + + const element = parser.nodeToElement(node._node); + const rect = try bc.session.page.?.state.renderer.getRect(element); + const quad = Quad{ + rect.x, + rect.y, + rect.x + rect.width, + rect.y, + rect.x + rect.width, + rect.y + rect.height, + rect.x, + rect.y + rect.height, + }; + + return cmd.sendResult(.{ .quads = &.{quad} }, .{}); +} + const testing = @import("../testing.zig"); test "cdp.dom: getSearchResults unknown search id" { From b92a85f0a9f11af118401c4cdbbba1b2982e1c1c Mon Sep 17 00:00:00 2001 From: sjorsdonkers <72333389+sjorsdonkers@users.noreply.github.com> Date: Wed, 14 May 2025 11:49:41 +0200 Subject: [PATCH 2/4] Cleanup and inner dimensions --- src/browser/dom/element.zig | 3 +- src/browser/html/window.zig | 21 ++++++++ src/cdp/domains/dom.zig | 102 +++++++++++++++--------------------- 3 files changed, 63 insertions(+), 63 deletions(-) diff --git a/src/browser/dom/element.zig b/src/browser/dom/element.zig index 9e8eb04f..187fd50f 100644 --- a/src/browser/dom/element.zig +++ b/src/browser/dom/element.zig @@ -390,8 +390,7 @@ pub const Element = struct { return s.match(CssNodeWrap{ .node = parser.elementToNode(self) }); } - pub fn _scrollIntoViewIfNeeded(self: *parser.Element, center_if_needed: ?bool) void { - _ = self; + pub fn _scrollIntoViewIfNeeded(_: *parser.Element, center_if_needed: ?bool) void { _ = center_if_needed; } }; diff --git a/src/browser/html/window.zig b/src/browser/html/window.zig index a594da03..09ef2003 100644 --- a/src/browser/html/window.zig +++ b/src/browser/html/window.zig @@ -120,6 +120,18 @@ pub const Window = struct { return &self.history; } + // The interior height of the window in pixels, including the height of the horizontal scroll bar, if present. + pub fn get_innerHeight(_: *Window, state: *SessionState) u32 { + // We do not have scrollbars or padding so this is the same as Element.clientHeight + return state.renderer.height(); + } + + // The interior width of the window in pixels. That includes the width of the vertical scroll bar, if one is present. + pub fn get_innerWidth(_: *Window, state: *SessionState) u32 { + // We do not have scrollbars or padding so this is the same as Element.clientWidth + return state.renderer.width(); + } + pub fn get_name(self: *Window) []const u8 { return self.target; } @@ -291,4 +303,13 @@ test "Browser.HTML.Window" { .{ "let request_id = requestAnimationFrame(timestamp => {});", "undefined" }, .{ "cancelAnimationFrame(request_id);", "undefined" }, }, .{}); + + try runner.testCases(&.{ + .{ "innerHeight", "1" }, + .{ "innerWidth", "1" }, // Width is 1 even if there are no elements + .{ "document.createElement('div').getClientRects()", "[object Object]" }, + .{ "document.createElement('div').getClientRects()", "[object Object]" }, + .{ "innerHeight", "1" }, + .{ "innerWidth", "2" }, + }, .{}); } diff --git a/src/cdp/domains/dom.zig b/src/cdp/domains/dom.zig index d7c948e5..7fe243ba 100644 --- a/src/cdp/domains/dom.zig +++ b/src/cdp/domains/dom.zig @@ -17,10 +17,12 @@ // along with this program. If not, see . const std = @import("std"); +const Allocator = std.mem.Allocator; const Node = @import("../Node.zig"); const css = @import("../../browser/dom/css.zig"); const parser = @import("../../browser/netsurf.zig"); const dom_node = @import("../../browser/dom/node.zig"); +const DOMRect = @import("../../browser/dom/element.zig").Element.DOMRect; pub fn processMessage(cmd: anytype) !void { const action = std.meta.stringToEnum(enum { @@ -237,52 +239,43 @@ fn describeNode(cmd: anytype) !void { depth: u32 = 1, pierce: bool = false, })) orelse return error.InvalidParams; - if (params.backendNodeId != null or params.depth != 1 or params.pierce) { - return error.NotYetImplementedParams; - } + if (params.depth != 1 or params.pierce) return error.NotYetImplementedParams; const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; - const input_node_id = params.nodeId orelse params.backendNodeId; - if (input_node_id != null) { - const node = bc.node_registry.lookup_by_id.get(params.nodeId.?) orelse return error.NodeNotFound; - return cmd.sendResult(.{ .node = bc.nodeWriter(node, .{}) }, .{}); - } - if (params.objectId != null) { - // Retrieve the object from which ever context it is in. - const parser_node = try bc.inspector.getNodePtr(cmd.arena, params.objectId.?); - const node = try bc.node_registry.register(@ptrCast(parser_node)); - return cmd.sendResult(.{ .node = bc.nodeWriter(node, .{}) }, .{}); - } - return error.MissingParams; -} + const node = try getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId); -// Note Element.DOMRect exists, but there is no need to couple them at this time. -const Rect = struct { - x: f64, - y: f64, - width: f64, - height: f64, -}; + return cmd.sendResult(.{ .node = bc.nodeWriter(node, .{}) }, .{}); +} // An array of quad vertices, x immediately followed by y for each point, points clock-wise. // Note Y points downward // We are assuming the start/endpoint is not repeated. const Quad = [8]f64; +fn rectToQuad(rect: DOMRect) Quad { + return Quad{ + rect.x, + rect.y, + rect.x + rect.width, + rect.y, + rect.x + rect.width, + rect.y + rect.height, + rect.x, + rect.y + rect.height, + }; +} + fn scrollIntoViewIfNeeded(cmd: anytype) !void { - const params = (try cmd.params(struct { + _ = (try cmd.params(struct { nodeId: ?Node.Id = null, backendNodeId: ?u32 = null, objectId: ?[]const u8 = null, - rect: ?Rect = null, + rect: ?DOMRect = null, })) orelse return error.InvalidParams; - var set_count: u2 = 0; // only 1 of nodeId backendNodeId objectId may be set - if (params.nodeId != null) set_count += 1; - if (params.backendNodeId != null) set_count += 1; - if (params.objectId != null) set_count += 1; - if (set_count != 1) return error.InvalidParams; + // Only 1 of nodeId, backendNodeId, objectId may be set, but we don't want to error unnecessarily + // TBD what do other browsers do in this user error sceneario? // Since element.scrollIntoViewIfNeeded is a no-op we do not bother retrieving the node. // This however also means we also do not error in case the node is not found. @@ -290,49 +283,36 @@ fn scrollIntoViewIfNeeded(cmd: anytype) !void { return cmd.sendResult(null, .{}); } +fn getNode(arena: Allocator, browser_context: anytype, node_id: ?Node.Id, backend_node_id: ?Node.Id, object_id: ?[]const u8) !*Node { + const input_node_id = node_id orelse backend_node_id; + if (input_node_id) |input_node_id_| { + return browser_context.node_registry.lookup_by_id.get(input_node_id_) orelse return error.NodeNotFound; + } + if (object_id) |object_id_| { + // Retrieve the object from which ever context it is in. + const parser_node = try browser_context.inspector.getNodePtr(arena, object_id_); + return try browser_context.node_registry.register(@ptrCast(parser_node)); + } + return error.MissingParams; +} + fn getContentQuads(cmd: anytype) !void { const params = (try cmd.params(struct { nodeId: ?Node.Id = null, - backendNodeId: ?u32 = null, + backendNodeId: ?Node.Id = null, objectId: ?[]const u8 = null, })) orelse return error.InvalidParams; - var set_count: u2 = 0; // only 1 of nodeId backendNodeId objectId may be set - if (params.nodeId != null) set_count += 1; - if (params.backendNodeId != null) set_count += 1; - if (params.objectId != null) set_count += 1; - if (set_count != 1) return error.InvalidParams; - const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; - const node = blk: { - const input_node_id = params.nodeId orelse params.backendNodeId; - if (input_node_id != null) { - const node = bc.node_registry.lookup_by_id.get(params.nodeId.?) orelse return error.NodeNotFound; - break :blk node; - } - if (params.objectId != null) { - const parser_node = try bc.inspector.getNodePtr(cmd.arena, params.objectId.?); - const node = try bc.node_registry.register(@ptrCast(parser_node)); - break :blk node; - } - unreachable; - }; + const node = try getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId); - if (try parser.nodeType(node._node) != .element) return error.NodeISNotAnElement; + if (try parser.nodeType(node._node) != .element) return error.NodeIsNotAnElement; + // TBD should the funcion work on nodes that are not elements, but may have geometry like Window? const element = parser.nodeToElement(node._node); const rect = try bc.session.page.?.state.renderer.getRect(element); - const quad = Quad{ - rect.x, - rect.y, - rect.x + rect.width, - rect.y, - rect.x + rect.width, - rect.y + rect.height, - rect.x, - rect.y + rect.height, - }; + const quad = rectToQuad(rect); return cmd.sendResult(.{ .quads = &.{quad} }, .{}); } From f74647ccfca5e48d3b4abf660222f71f41cc11c0 Mon Sep 17 00:00:00 2001 From: sjorsdonkers <72333389+sjorsdonkers@users.noreply.github.com> Date: Wed, 14 May 2025 17:13:38 +0200 Subject: [PATCH 3/4] Allign error detection --- src/cdp/domains/dom.zig | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/cdp/domains/dom.zig b/src/cdp/domains/dom.zig index 7fe243ba..49a4863c 100644 --- a/src/cdp/domains/dom.zig +++ b/src/cdp/domains/dom.zig @@ -267,18 +267,25 @@ fn rectToQuad(rect: DOMRect) Quad { } fn scrollIntoViewIfNeeded(cmd: anytype) !void { - _ = (try cmd.params(struct { + const params = (try cmd.params(struct { nodeId: ?Node.Id = null, backendNodeId: ?u32 = null, objectId: ?[]const u8 = null, rect: ?DOMRect = null, })) orelse return error.InvalidParams; + // Only 1 of nodeId, backendNodeId, objectId may be set, but chrome just takes the first non-null - // Only 1 of nodeId, backendNodeId, objectId may be set, but we don't want to error unnecessarily - // TBD what do other browsers do in this user error sceneario? + // We retrieve the node to at least check if it exists and is valid. + const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; + const node = try getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId); - // Since element.scrollIntoViewIfNeeded is a no-op we do not bother retrieving the node. - // This however also means we also do not error in case the node is not found. + const node_type = parser.nodeType(node._node) catch return error.InvalidNode; + switch (node_type) { + .element => {}, + .document => {}, + .text => {}, + else => return error.NodeDoesNotHaveGeometry, + } return cmd.sendResult(null, .{}); } @@ -296,6 +303,8 @@ fn getNode(arena: Allocator, browser_context: anytype, node_id: ?Node.Id, backen return error.MissingParams; } +// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getContentQuads +// Related to: https://drafts.csswg.org/cssom-view/#the-geometryutils-interface fn getContentQuads(cmd: anytype) !void { const params = (try cmd.params(struct { nodeId: ?Node.Id = null, @@ -307,8 +316,15 @@ fn getContentQuads(cmd: anytype) !void { const node = try getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId); + // TODO likely if the following CSS properties are set the quads should be empty + // visibility: hidden + // display: none + if (try parser.nodeType(node._node) != .element) return error.NodeIsNotAnElement; - // TBD should the funcion work on nodes that are not elements, but may have geometry like Window? + // TODO implement for document or text + // Most likely document would require some hierachgy in the renderer. It is left unimplemented till we have a good example. + // Text may be tricky, multiple quads in case of multiple lines? empty quads of text = ""? + // Elements like SVGElement may have multiple quads. const element = parser.nodeToElement(node._node); const rect = try bc.session.page.?.state.renderer.getRect(element); From 48de14ade3a11b4f2625811fb1d50180c16f9ec6 Mon Sep 17 00:00:00 2001 From: sjorsdonkers <72333389+sjorsdonkers@users.noreply.github.com> Date: Wed, 14 May 2025 17:17:58 +0200 Subject: [PATCH 4/4] null JS tests where we are not checking the output --- src/browser/html/window.zig | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/browser/html/window.zig b/src/browser/html/window.zig index 09ef2003..73eab497 100644 --- a/src/browser/html/window.zig +++ b/src/browser/html/window.zig @@ -293,22 +293,22 @@ test "Browser.HTML.Window" { \\ } \\ } , - "undefined", + null, }, - .{ "let id = requestAnimationFrame(step);", "undefined" }, + .{ "requestAnimationFrame(step);", null }, // returned id is checked in the next test }, .{}); // cancelAnimationFrame should be able to cancel a request with the given id try runner.testCases(&.{ - .{ "let request_id = requestAnimationFrame(timestamp => {});", "undefined" }, + .{ "let request_id = requestAnimationFrame(timestamp => {});", null }, .{ "cancelAnimationFrame(request_id);", "undefined" }, }, .{}); try runner.testCases(&.{ .{ "innerHeight", "1" }, .{ "innerWidth", "1" }, // Width is 1 even if there are no elements - .{ "document.createElement('div').getClientRects()", "[object Object]" }, - .{ "document.createElement('div').getClientRects()", "[object Object]" }, + .{ "document.createElement('div').getClientRects()", null }, + .{ "document.createElement('div').getClientRects()", null }, .{ "innerHeight", "1" }, .{ "innerWidth", "2" }, }, .{});