From 38fb5b101ec5a13e08fab1bade7b3312f28d14cc Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 11 Dec 2025 19:49:51 +0800 Subject: [PATCH] add Document.elementFromPoint and elementsFromPoint --- .../tests/document/element_from_point.html | 233 ++++++++++++++++++ src/browser/tests/legacy/html/document.html | 18 +- src/browser/tests/legacy/html/image.html | 4 +- src/browser/tests/legacy/html/link.html | 2 +- src/browser/tests/window/navigator.html | 51 +--- src/browser/webapi/Document.zig | 44 ++++ src/browser/webapi/Navigator.zig | 13 +- .../collections/HTMLOptionsCollection.zig | 6 - src/browser/webapi/element/Html.zig | 32 +-- src/browser/webapi/selector/List.zig | 6 - 10 files changed, 311 insertions(+), 98 deletions(-) create mode 100644 src/browser/tests/document/element_from_point.html diff --git a/src/browser/tests/document/element_from_point.html b/src/browser/tests/document/element_from_point.html new file mode 100644 index 00000000..d3ea7da9 --- /dev/null +++ b/src/browser/tests/document/element_from_point.html @@ -0,0 +1,233 @@ + + + + +
Div 1
+
Div 2
+ +
+
Child
+
+ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/browser/tests/legacy/html/document.html b/src/browser/tests/legacy/html/document.html index 003ee820..7abb0b03 100644 --- a/src/browser/tests/legacy/html/document.html +++ b/src/browser/tests/legacy/html/document.html @@ -52,13 +52,6 @@ let div1 = document.createElement('div'); document.body.appendChild(div1); div1.getClientRects(); // clal this to position it - testing.expectEqual('[object HTMLDivElement]', document.elementFromPoint(2.5, 2.5).toString()); - - let elems = document.elementsFromPoint(2.5, 2.5); - testing.expectEqual(3, elems.length); - testing.expectEqual('[object HTMLDivElement]', elems[0].toString()); - testing.expectEqual('[object HTMLBodyElement]', elems[1].toString()); - testing.expectEqual('[object HTMLHtmlElement]', elems[2].toString()); let a = document.createElement('a'); a.href = "https://lightpanda.io"; @@ -66,20 +59,11 @@ // Note this will be placed after the div of previous test a.getClientRects(); - let a_again = document.elementFromPoint(7.5, 0.5); - testing.expectEqual('[object HTMLAnchorElement]', a_again.toString()); - testing.expectEqual('https://lightpanda.io', a_again.href); - - let a_agains = document.elementsFromPoint(7.5, 0.5); - testing.expectEqual('https://lightpanda.io', a_agains[0].href); - - testing.expectEqual(true, !document.all); testing.expectEqual(false, !!document.all); - testing.expectEqual('[object HTMLScriptElement]', document.all(5).toString()); + testing.expectEqual('[object HTMLScriptElement]', document.all(6).toString()); testing.expectEqual('[object HTMLDivElement]', document.all('content').toString()); - testing.expectEqual(document, document.defaultView.document ); testing.expectEqual('loading', document.readyState); diff --git a/src/browser/tests/legacy/html/image.html b/src/browser/tests/legacy/html/image.html index 1e3f6aff..053b2cfa 100644 --- a/src/browser/tests/legacy/html/image.html +++ b/src/browser/tests/legacy/html/image.html @@ -26,7 +26,7 @@ let lyric = new Image testing.expectEqual('', lyric.src); lyric.src = 'okay'; - testing.expectEqual('okay', lyric.src); + testing.expectEqual('http://localhost:9589/html/okay', lyric.src); lyric.src = 15; - testing.expectEqual('15', lyric.src); + testing.expectEqual('http://localhost:9589/html/15', lyric.src); diff --git a/src/browser/tests/legacy/html/link.html b/src/browser/tests/legacy/html/link.html index 15da6461..95869052 100644 --- a/src/browser/tests/legacy/html/link.html +++ b/src/browser/tests/legacy/html/link.html @@ -9,7 +9,7 @@ testing.expectEqual('_blank', link.target); link.target = ''; - testing.expectEqual('foo', link.href); + testing.expectEqual('http://localhost:9589/html/foo', link.href); link.href = 'https://lightpanda.io/'; testing.expectEqual('https://lightpanda.io/', link.href); diff --git a/src/browser/tests/window/navigator.html b/src/browser/tests/window/navigator.html index e00f429b..11ad9ade 100644 --- a/src/browser/tests/window/navigator.html +++ b/src/browser/tests/window/navigator.html @@ -4,67 +4,26 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig index a665849f..87019a54 100644 --- a/src/browser/webapi/Document.zig +++ b/src/browser/webapi/Document.zig @@ -345,6 +345,48 @@ pub fn prepend(self: *Document, nodes: []const Node.NodeOrText, page: *Page) !vo } } +pub fn elementFromPoint(self: *Document, x: f64, y: f64, page: *Page) !?*Element { + // Traverse document in depth-first order to find the topmost (last in document order) + // element that contains the point (x, y) + var topmost: ?*Element = null; + + const root = self.asNode(); + var stack: std.ArrayList(*Node) = .empty; + try stack.append(page.call_arena, root); + + while (stack.items.len > 0) { + const node = stack.pop() orelse break; + if (node.is(Element)) |element| { + if (try element.checkVisibility(page)) { + const rect = try element.getBoundingClientRect(page); + if (x >= rect._left and x <= rect._right and y >= rect._top and y <= rect._bottom) { + topmost = element; + } + } + } + + // Add children to stack in reverse order so we process them in document order + var child = node.lastChild(); + while (child) |c| { + try stack.append(page.call_arena, c); + child = c.previousSibling(); + } + } + + return topmost; +} + +pub fn elementsFromPoint(self: *Document, x: f64, y: f64, page: *Page) ![]const *Element { + // Get topmost element + var current: ?*Element = (try self.elementFromPoint(x, y, page)) orelse return &.{}; + var result: std.ArrayList(*Element) = .empty; + while (current) |el| { + try result.append(page.call_arena, el); + current = el.parentElement(); + } + return result.items; +} + const ReadyState = enum { loading, interactive, @@ -404,6 +446,8 @@ pub const JsApi = struct { pub const importNode = bridge.function(Document.importNode, .{ .dom_exception = true }); pub const append = bridge.function(Document.append, .{}); pub const prepend = bridge.function(Document.prepend, .{}); + pub const elementFromPoint = bridge.function(Document.elementFromPoint, .{}); + pub const elementsFromPoint = bridge.function(Document.elementsFromPoint, .{}); pub const defaultView = bridge.accessor(struct { fn defaultView(_: *const Document, page: *Page) *@import("Window.zig") { diff --git a/src/browser/webapi/Navigator.zig b/src/browser/webapi/Navigator.zig index 23efd49f..b38d1093 100644 --- a/src/browser/webapi/Navigator.zig +++ b/src/browser/webapi/Navigator.zig @@ -25,15 +25,19 @@ _pad: bool = false, pub const init: Navigator = .{}; pub fn getUserAgent(_: *const Navigator) []const u8 { - return "Mozilla/5.0 (compatible; LiteFetch/0.1)"; + return "Lightpanda/1.0"; } pub fn getAppName(_: *const Navigator) []const u8 { - return "LiteFetch"; + return "Netscape"; +} + +pub fn getAppCodeName(_: *const Navigator) []const u8 { + return "Netscape"; } pub fn getAppVersion(_: *const Navigator) []const u8 { - return "0.1"; + return "1.0"; } pub fn getPlatform(_: *const Navigator) []const u8 { @@ -73,7 +77,7 @@ pub fn getMaxTouchPoints(_: *const Navigator) u32 { /// Returns the vendor name pub fn getVendor(_: *const Navigator) []const u8 { - return "LiteFetch"; + return ""; } /// Returns the product name (typically "Gecko" for compatibility) @@ -104,6 +108,7 @@ pub const JsApi = struct { // Read-only properties pub const userAgent = bridge.accessor(Navigator.getUserAgent, null, .{}); pub const appName = bridge.accessor(Navigator.getAppName, null, .{}); + pub const appCodeName = bridge.accessor(Navigator.getAppCodeName, null, .{}); pub const appVersion = bridge.accessor(Navigator.getAppVersion, null, .{}); pub const platform = bridge.accessor(Navigator.getPlatform, null, .{}); pub const language = bridge.accessor(Navigator.getLanguage, null, .{}); diff --git a/src/browser/webapi/collections/HTMLOptionsCollection.zig b/src/browser/webapi/collections/HTMLOptionsCollection.zig index 6a0cadc9..4474cb76 100644 --- a/src/browser/webapi/collections/HTMLOptionsCollection.zig +++ b/src/browser/webapi/collections/HTMLOptionsCollection.zig @@ -30,11 +30,6 @@ const HTMLOptionsCollection = @This(); _proto: *HTMLCollection, _select: *@import("../element/html/Select.zig"), -pub fn deinit(self: *HTMLOptionsCollection) void { - const page = Page.current; - page._factory.destroy(self); -} - // Forward length to HTMLCollection pub fn length(self: *HTMLOptionsCollection, page: *Page) u32 { return self._proto.length(page); @@ -102,7 +97,6 @@ pub const JsApi = struct { pub const name = "HTMLOptionsCollection"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; - pub const finalizer = HTMLOptionsCollection.deinit; pub const manage = false; }; diff --git a/src/browser/webapi/element/Html.zig b/src/browser/webapi/element/Html.zig index 341fbc94..e0220504 100644 --- a/src/browser/webapi/element/Html.zig +++ b/src/browser/webapi/element/Html.zig @@ -120,11 +120,11 @@ pub fn is(self: *HtmlElement, comptime T: type) ?*T { pub fn className(self: *const HtmlElement) []const u8 { return switch (self._type) { - .anchor => "[object HtmlAnchorElement]", - .div => "[object HtmlDivElement]", - .embed => "[object HtmlEmbedElement]", + .anchor => "[object HTMLAnchorElement]", + .div => "[object HTMLDivElement]", + .embed => "[object HTMLEmbedElement]", .form => "[object HTMLFormElement]", - .p => "[object HtmlParagraphElement]", + .p => "[object HTMLParagraphElement]", .custom => "[object CUSTOM-TODO]", .data => "[object HTMLDataElement]", .dialog => "[object HTMLDialogElement]", @@ -137,22 +137,22 @@ pub fn className(self: *const HtmlElement) []const u8 { .ul => "[object HTMLULElement]", .ol => "[object HTMLOLElement]", .generic => "[object HTMLElement]", - .script => "[object HtmlScriptElement]", + .script => "[object HTMLScriptElement]", .select => "[object HTMLSelectElement]", .slot => "[object HTMLSlotElement]", .template => "[object HTMLTemplateElement]", .option => "[object HTMLOptionElement]", - .text_area => "[object HtmlTextAreaElement]", - .input => "[object HtmlInputElement]", - .link => "[object HtmlLinkElement]", - .meta => "[object HtmlMetaElement]", - .hr => "[object HtmlHRElement]", - .style => "[object HtmlSyleElement]", - .title => "[object HtmlTitleElement]", - .body => "[object HtmlBodyElement]", - .html => "[object HtmlHtmlElement]", - .head => "[object HtmlHeadElement]", - .unknown => "[object HtmlUnknownElement]", + .text_area => "[object HTMLTextAreaElement]", + .input => "[object HTMLInputElement]", + .link => "[object HTMLLinkElement]", + .meta => "[object HTMLMetaElement]", + .hr => "[object HTMLHRElement]", + .style => "[object HTMLSyleElement]", + .title => "[object HTMLTitleElement]", + .body => "[object HTMLBodyElement]", + .html => "[object HTMLHtmlElement]", + .head => "[object HTMLHeadElement]", + .unknown => "[object HTMLUnknownElement]", }; } diff --git a/src/browser/webapi/selector/List.zig b/src/browser/webapi/selector/List.zig index 06d2045e..5b1eb632 100644 --- a/src/browser/webapi/selector/List.zig +++ b/src/browser/webapi/selector/List.zig @@ -77,12 +77,6 @@ pub fn initOne(root: *Node, selector: Selector.Selector, page: *Page) ?*Node { return null; } -pub fn deinit(self: *List) void { - const page = Page.current; - page._mem.releaseArena(self._arena); - page._factory.destroy(self); -} - const OptimizeResult = struct { root: *Node, exclude_root: bool,