From 9fd889bc2cf08a3308f196664b48466085d6af40 Mon Sep 17 00:00:00 2001 From: sjorsdonkers <72333389+sjorsdonkers@users.noreply.github.com> Date: Wed, 14 May 2025 12:15:12 +0200 Subject: [PATCH 1/8] elementsFromPoint --- src/browser/html/document.zig | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/browser/html/document.zig b/src/browser/html/document.zig index c7f95e195..213bf9bac 100644 --- a/src/browser/html/document.zig +++ b/src/browser/html/document.zig @@ -226,6 +226,23 @@ pub const HTMLDocument = struct { return ""; } + pub fn _elementFromPoint(_: *parser.DocumentHTML, x: f32, y: f32, state: *SessionState) !?*parser.Element { + const ix: i32 = @intFromFloat(@floor(x)); + const iy: i32 = @intFromFloat(@floor(y)); + return state.renderer.getElementAtPosition(ix, iy) orelse return null; + } + + pub fn _elementsFromPoint(_: *parser.DocumentHTML, x: f32, y: f32, state: *SessionState) ![]*parser.Element { // empty array or optional array? + const ix: i32 = @intFromFloat(@floor(x)); + const iy: i32 = @intFromFloat(@floor(y)); + const element = state.renderer.getElementAtPosition(ix, iy) orelse return &.{}; // Or should we return the window element instead of empty -> parser.documentGetDocumentElement(self); + // We need to return either 0 or 1 item, so we cannot fix the size to [1]*parser.Element + // Converting the pointer to a slice []parser.Element is not supported by our framework. + // So instead we just need to allocate the pointer to create a slice of 1. + const heap_ptr = try state.arena.create(@TypeOf(element)); + return heap_ptr[0..1]; + } + pub fn documentIsLoaded(html_doc: *parser.DocumentHTML, state: *SessionState) !void { const self = try state.getNodeWrapper(HTMLDocument, @ptrCast(html_doc)); self.ready_state = .interactive; @@ -295,6 +312,16 @@ test "Browser.HTML.Document" { .{ "document.cookie", "name=Oeschger; favorite_food=tripe" }, }, .{}); + try runner.testCases(&.{ + .{ "document.elementFromPoint(0.5, 0.5)", "null" }, + .{ "document.elementsFromPoint(0.5, 0.5)", "" }, + .{ "document.createElement('div').getClientRects()", "[object Object]" }, + .{ "document.elementFromPoint(0.5, 0.5)", "[object HTMLDivElement]" }, + .{ "let elems = document.elementsFromPoint(0.5, 0.5)", "undefined" }, + .{ "elems.length", "1" }, + .{ "elems[0]", "[object Element]" }, // TBD why is this not: HTMLDivElement? + }, .{}); + try runner.testCases(&.{ .{ "!document.all", "true" }, .{ "!!document.all", "false" }, From acf0e204ad2e2a37b4982a84b9d36632ee7afd06 Mon Sep 17 00:00:00 2001 From: sjorsdonkers <72333389+sjorsdonkers@users.noreply.github.com> Date: Wed, 14 May 2025 17:41:46 +0200 Subject: [PATCH 2/8] fix unset heap_ptr --- src/browser/html/document.zig | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/browser/html/document.zig b/src/browser/html/document.zig index 213bf9bac..cf4e34fb4 100644 --- a/src/browser/html/document.zig +++ b/src/browser/html/document.zig @@ -229,17 +229,22 @@ pub const HTMLDocument = struct { pub fn _elementFromPoint(_: *parser.DocumentHTML, x: f32, y: f32, state: *SessionState) !?*parser.Element { const ix: i32 = @intFromFloat(@floor(x)); const iy: i32 = @intFromFloat(@floor(y)); - return state.renderer.getElementAtPosition(ix, iy) orelse return null; + const element = state.renderer.getElementAtPosition(ix, iy) orelse return null; + // TODO if pointer-events set to none the underlying element should be returned (parser.documentGetDocumentElement(self.document);?) + return element; } - pub fn _elementsFromPoint(_: *parser.DocumentHTML, x: f32, y: f32, state: *SessionState) ![]*parser.Element { // empty array or optional array? + pub fn _elementsFromPoint(_: *parser.DocumentHTML, x: f32, y: f32, state: *SessionState) ![]*parser.Element { const ix: i32 = @intFromFloat(@floor(x)); const iy: i32 = @intFromFloat(@floor(y)); - const element = state.renderer.getElementAtPosition(ix, iy) orelse return &.{}; // Or should we return the window element instead of empty -> parser.documentGetDocumentElement(self); + const element = state.renderer.getElementAtPosition(ix, iy) orelse return &.{}; + // TODO if pointer-events set to none the underlying element should be returned (parser.documentGetDocumentElement(self.document);?) + // We need to return either 0 or 1 item, so we cannot fix the size to [1]*parser.Element // Converting the pointer to a slice []parser.Element is not supported by our framework. // So instead we just need to allocate the pointer to create a slice of 1. - const heap_ptr = try state.arena.create(@TypeOf(element)); + const heap_ptr = try state.call_arena.create(@TypeOf(element)); + heap_ptr.* = element; return heap_ptr[0..1]; } @@ -315,11 +320,11 @@ test "Browser.HTML.Document" { try runner.testCases(&.{ .{ "document.elementFromPoint(0.5, 0.5)", "null" }, .{ "document.elementsFromPoint(0.5, 0.5)", "" }, - .{ "document.createElement('div').getClientRects()", "[object Object]" }, + .{ "document.createElement('div').getClientRects()", null }, .{ "document.elementFromPoint(0.5, 0.5)", "[object HTMLDivElement]" }, - .{ "let elems = document.elementsFromPoint(0.5, 0.5)", "undefined" }, + .{ "let elems = document.elementsFromPoint(0.5, 0.5)", null }, .{ "elems.length", "1" }, - .{ "elems[0]", "[object Element]" }, // TBD why is this not: HTMLDivElement? + .{ "elems[0]", "[object HTMLDivElement]" }, }, .{}); try runner.testCases(&.{ From 71c7ede7737b762354f2ed02bfac42802cdd49e7 Mon Sep 17 00:00:00 2001 From: sjorsdonkers <72333389+sjorsdonkers@users.noreply.github.com> Date: Thu, 15 May 2025 10:51:13 +0200 Subject: [PATCH 3/8] confirm data is retained in elementFromPoint --- src/browser/html/document.zig | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/browser/html/document.zig b/src/browser/html/document.zig index cf4e34fb4..f2265bfb9 100644 --- a/src/browser/html/document.zig +++ b/src/browser/html/document.zig @@ -327,6 +327,17 @@ test "Browser.HTML.Document" { .{ "elems[0]", "[object HTMLDivElement]" }, }, .{}); + try runner.testCases(&.{ + .{ "let a = document.createElement('a')", null }, + .{ "a.href = \"https://lightpanda.io\"", null }, + .{ "a.getClientRects()", null }, // Note this will be placed after the div of previous test + .{ "let a_again = document.elementFromPoint(1.5, 0.5)", null }, + .{ "a_again", "[object HTMLAnchorElement]" }, + .{ "a_again.href", "https://lightpanda.io" }, + .{ "let a_agains = document.elementsFromPoint(1.5, 0.5)", null }, + .{ "a_agains[0].href", "https://lightpanda.io" }, + }, .{}); + try runner.testCases(&.{ .{ "!document.all", "true" }, .{ "!!document.all", "false" }, From 3064b2f3e0762acfdb23929bf3295a0d1229d3d9 Mon Sep 17 00:00:00 2001 From: sjorsdonkers <72333389+sjorsdonkers@users.noreply.github.com> Date: Thu, 15 May 2025 10:59:45 +0200 Subject: [PATCH 4/8] make elementFromPoint more robust against future changes --- src/browser/html/document.zig | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/browser/html/document.zig b/src/browser/html/document.zig index f2265bfb9..4516e779c 100644 --- a/src/browser/html/document.zig +++ b/src/browser/html/document.zig @@ -22,6 +22,8 @@ const parser = @import("../netsurf.zig"); const SessionState = @import("../env.zig").SessionState; const Window = @import("window.zig").Window; +const Element = @import("../dom/element.zig").Element; +const ElementUnion = @import("../dom/element.zig").Union; const Document = @import("../dom/document.zig").Document; const NodeList = @import("../dom/nodelist.zig").NodeList; const Location = @import("location.zig").Location; @@ -226,15 +228,16 @@ pub const HTMLDocument = struct { return ""; } - pub fn _elementFromPoint(_: *parser.DocumentHTML, x: f32, y: f32, state: *SessionState) !?*parser.Element { + // This returns an ElementUnion instead of a *Parser.Element in case the element somehow hasn't passed through the js runtime yet. + pub fn _elementFromPoint(_: *parser.DocumentHTML, x: f32, y: f32, state: *SessionState) !?ElementUnion { const ix: i32 = @intFromFloat(@floor(x)); const iy: i32 = @intFromFloat(@floor(y)); const element = state.renderer.getElementAtPosition(ix, iy) orelse return null; // TODO if pointer-events set to none the underlying element should be returned (parser.documentGetDocumentElement(self.document);?) - return element; + return try Element.toInterface(element); } - pub fn _elementsFromPoint(_: *parser.DocumentHTML, x: f32, y: f32, state: *SessionState) ![]*parser.Element { + pub fn _elementsFromPoint(_: *parser.DocumentHTML, x: f32, y: f32, state: *SessionState) ![]ElementUnion { const ix: i32 = @intFromFloat(@floor(x)); const iy: i32 = @intFromFloat(@floor(y)); const element = state.renderer.getElementAtPosition(ix, iy) orelse return &.{}; @@ -243,8 +246,8 @@ pub const HTMLDocument = struct { // We need to return either 0 or 1 item, so we cannot fix the size to [1]*parser.Element // Converting the pointer to a slice []parser.Element is not supported by our framework. // So instead we just need to allocate the pointer to create a slice of 1. - const heap_ptr = try state.call_arena.create(@TypeOf(element)); - heap_ptr.* = element; + const heap_ptr = try state.call_arena.create(ElementUnion); + heap_ptr.* = try Element.toInterface(element); return heap_ptr[0..1]; } From 14fbc163139f870baae48f9cd5c45e52aac01813 Mon Sep 17 00:00:00 2001 From: sjorsdonkers <72333389+sjorsdonkers@users.noreply.github.com> Date: Fri, 16 May 2025 11:10:54 +0200 Subject: [PATCH 5/8] handle detached elements --- src/browser/dom/element.zig | 42 ++++++++++++++++++++--- src/browser/dom/intersection_observer.zig | 13 ++++--- src/browser/dom/node.zig | 18 ++++++---- src/browser/html/document.zig | 21 +++++++++--- src/browser/html/window.zig | 16 +++++++-- src/browser/netsurf.zig | 11 ++++++ src/browser/renderer.zig | 2 ++ src/cdp/domains/dom.zig | 8 ++--- 8 files changed, 104 insertions(+), 27 deletions(-) diff --git a/src/browser/dom/element.zig b/src/browser/dom/element.zig index 187fd50f1..c0d7993de 100644 --- a/src/browser/dom/element.zig +++ b/src/browser/dom/element.zig @@ -365,14 +365,28 @@ pub const Element = struct { return Node.replaceChildren(parser.elementToNode(self), nodes); } + // A DOMRect object providing information about the size of an element and its position relative to the viewport. + // Returns a 0 DOMRect object if the element is eventually detached from the main window pub fn _getBoundingClientRect(self: *parser.Element, state: *SessionState) !DOMRect { + // Since we are lazy rendering we need to do this check. We could store the renderer in a viewport such that it could cache these, but it would require tracking changes. + const root = try parser.nodeGetRootNode(parser.elementToNode(self)); + if (root != parser.documentToNode(parser.documentHTMLToDocument(state.document.?))) { + return DOMRect{ .x = 0, .y = 0, .width = 0, .height = 0 }; + } return state.renderer.getRect(self); } - // returns a collection of DOMRect objects that indicate the bounding rectangles for each CSS border box in a client. - // We do not render so just always return the element's rect. - pub fn _getClientRects(self: *parser.Element, state: *SessionState) ![1]DOMRect { - return [_]DOMRect{try state.renderer.getRect(self)}; + // Returns a collection of DOMRect objects that indicate the bounding rectangles for each CSS border box in a client. + // We do not render so it only always return the element's bounding rect. + // Returns an empty array if the element is eventually detached from the main window + pub fn _getClientRects(self: *parser.Element, state: *SessionState) ![]DOMRect { + const root = try parser.nodeGetRootNode(parser.elementToNode(self)); + if (root != parser.documentToNode(parser.documentHTMLToDocument(state.document.?))) { + return &.{}; + } + const heap_ptr = try state.arena.create(DOMRect); + heap_ptr.* = try state.renderer.getRect(self); + return heap_ptr[0..1]; } pub fn get_clientWidth(_: *parser.Element, state: *SessionState) u32 { @@ -568,6 +582,26 @@ test "Browser.DOM.Element" { .{ "document.getElementById('para').clientWidth", "2" }, .{ "document.getElementById('para').clientHeight", "1" }, + + .{ "let r4 = document.createElement('div').getBoundingClientRect()", null }, + .{ "r4.x", "0" }, + .{ "r4.y", "0" }, + .{ "r4.width", "0" }, + .{ "r4.height", "0" }, + + // Test setup causes WrongDocument or HierarchyRequest error unlike in chrome/firefox + // .{ // An element of another document, even if created from the main document, is not rendered. + // \\ let div5 = document.createElement('div'); + // \\ const newDoc = document.implementation.createHTMLDocument("New Document"); + // \\ newDoc.body.appendChild(div5); + // \\ let r5 = div5.getBoundingClientRect(); + // , + // null, + // }, + // .{ "r5.x", "0" }, + // .{ "r5.y", "0" }, + // .{ "r5.width", "0" }, + // .{ "r5.height", "0" }, }, .{}); try runner.testCases(&.{ diff --git a/src/browser/dom/intersection_observer.zig b/src/browser/dom/intersection_observer.zig index b9aad9715..35ad8bbc8 100644 --- a/src/browser/dom/intersection_observer.zig +++ b/src/browser/dom/intersection_observer.zig @@ -121,7 +121,7 @@ pub const IntersectionObserverEntry = struct { // Returns the bounds rectangle of the target element as a DOMRectReadOnly. The bounds are computed as described in the documentation for Element.getBoundingClientRect(). pub fn get_boundingClientRect(self: *const IntersectionObserverEntry) !Element.DOMRect { - return self.state.renderer.getRect(self.target); + return Element._getBoundingClientRect(self.target, self.state); } // Returns the ratio of the intersectionRect to the boundingClientRect. @@ -131,7 +131,7 @@ pub const IntersectionObserverEntry = struct { // Returns a DOMRectReadOnly representing the target's visible area. pub fn get_intersectionRect(self: *const IntersectionObserverEntry) !Element.DOMRect { - return self.state.renderer.getRect(self.target); + return Element._getBoundingClientRect(self.target, self.state); } // A Boolean value which is true if the target element intersects with the intersection observer's root. If this is true, then, the IntersectionObserverEntry describes a transition into a state of intersection; if it's false, then you know the transition is from intersecting to not-intersecting. @@ -158,7 +158,7 @@ pub const IntersectionObserverEntry = struct { else => return error.InvalidState, } - return try self.state.renderer.getRect(element); + return Element._getBoundingClientRect(element, self.state); } // The Element whose intersection with the root changed. @@ -244,7 +244,9 @@ test "Browser.DOM.IntersectionObserver" { // Entry try runner.testCases(&.{ .{ "let entry;", "undefined" }, - .{ "new IntersectionObserver(entries => { entry = entries[0]; }).observe(document.createElement('div'));", "undefined" }, + .{ "let div1 = document.createElement('div')", null }, + .{ "document.body.appendChild(div1);", null }, + .{ "new IntersectionObserver(entries => { entry = entries[0]; }).observe(div1);", null }, .{ "entry.boundingClientRect.x;", "0" }, .{ "entry.intersectionRatio;", "1" }, .{ "entry.intersectionRect.x;", "0" }, @@ -261,7 +263,8 @@ test "Browser.DOM.IntersectionObserver" { // Options try runner.testCases(&.{ - .{ "const new_root = document.createElement('span');", "undefined" }, + .{ "const new_root = document.createElement('span');", null }, + .{ "document.body.appendChild(new_root);", null }, .{ "let new_entry;", "undefined" }, .{ \\ const new_observer = new IntersectionObserver( diff --git a/src/browser/dom/node.zig b/src/browser/dom/node.zig index 7f2747e14..14bfe136f 100644 --- a/src/browser/dom/node.zig +++ b/src/browser/dom/node.zig @@ -41,6 +41,8 @@ const Walker = @import("walker.zig").WalkerDepthFirst; const HTML = @import("../html/html.zig"); const HTMLElem = @import("../html/elements.zig"); +const log = std.log.scoped(.node); + // Node interfaces pub const Interfaces = .{ Attr, @@ -262,13 +264,15 @@ pub const Node = struct { return try parser.nodeContains(self, other); } - pub fn _getRootNode(self: *parser.Node) !?HTMLElem.Union { - // TODO return this’s shadow-including root if options["composed"] is true - const res = try parser.nodeOwnerDocument(self); - if (res == null) { - return null; - } - return try HTMLElem.toInterface(HTMLElem.Union, @as(*parser.Element, @ptrCast(res.?))); + // Returns itself or ancestor object inheriting from Node. + // - An Element inside a standard web page will return an HTMLDocument object representing the entire page (or