diff --git a/src/browser/Renderer.zig b/src/browser/Renderer.zig deleted file mode 100644 index 9a11dd32..00000000 --- a/src/browser/Renderer.zig +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright (C) 2023-2025 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 parser = @import("netsurf.zig"); - -const Allocator = std.mem.Allocator; - -const Renderer = @This(); - -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) Renderer { - return .{ - .elements = .{}, - .positions = .{}, - .allocator = allocator, - }; -} - -// The DOMRect is always relative to the viewport, not the document the element belongs to. -// Element that are not part of the main document, either detached or in a shadow DOM should not call this function. -pub fn getRect(self: *Renderer, 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) { - x = @intCast(elements.items.len); - try elements.append(self.allocator, @intFromPtr(e)); - gop.value_ptr.* = x; - } - - const _x: f64 = @floatFromInt(x); - const y: f64 = 0.0; - const w: f64 = 1.0; - const h: f64 = 1.0; - - return .{ - .x = _x, - .y = y, - .width = w, - .height = h, - .left = _x, - .top = y, - .right = _x + w, - .bottom = y + h, - }; -} - -pub fn boundingRect(self: *const Renderer) Element.DOMRect { - const x: f64 = 0.0; - const y: f64 = 0.0; - const w: f64 = @floatFromInt(self.width()); - const h: f64 = @floatFromInt(self.width()); - - return .{ - .x = x, - .y = y, - .width = w, - .height = h, - .left = x, - .top = y, - .right = x + w, - .bottom = y + h, - }; -} - -pub fn width(self: *const Renderer) u32 { - return @max(@as(u32, @intCast(self.elements.items.len)), 1); // At least 1 pixel even if empty -} - -pub fn height(_: *const Renderer) u32 { - return 1; -} - -pub fn getElementAtPosition(self: *const Renderer, x: i32, y: i32) ?*parser.Element { - if (y != 0 or x < 0) { - return null; - } - - const elements = self.elements.items; - return if (x < elements.len) @ptrFromInt(elements[@intCast(x)]) else null; -} diff --git a/src/browser/js/Inspector.zig b/src/browser/js/Inspector.zig index 04a8c5c8..d53820d8 100644 --- a/src/browser/js/Inspector.zig +++ b/src/browser/js/Inspector.zig @@ -123,14 +123,24 @@ pub fn getRemoteObject( } // Gets a value by object ID regardless of which context it is in. -pub fn getNodePtr(self: *const Inspector, allocator: Allocator, object_id: []const u8) !?*anyopaque { +// Our TaggedAnyOpaque stores the "resolved" ptr value (the most specific _type, +// e.g. we store the ptr to the Div not the EventTarget). But, this is asking for +// the pointer to the Node, so we need to use the same resolution mechanism which +// is used when we're calling a function to turn the Div into a Node, which is +// what Context.typeTaggedAnyOpaque does. +pub fn getNodePtr(self: *const Inspector, allocator: Allocator, object_id: []const u8) !*anyopaque { const unwrapped = try self.session.unwrapObject(allocator, object_id); // The values context and groupId are not used here - const toa = getTaggedAnyOpaque(unwrapped.value) orelse return null; - if (toa.subtype == null or toa.subtype != .node) { + const js_val = unwrapped.value; + if (js_val.isObject() == false) { + std.debug.print("XX-0\n", .{}); return error.ObjectIdIsNotANode; } - return toa.value; + const Node = @import("../webapi/Node.zig"); + return Context.typeTaggedAnyOpaque(*Node, js_val.castTo(v8.Object)) catch { + std.debug.print("XX-1\n", .{}); + return error.ObjectIdIsNotANode; + }; } const NoopInspector = struct { diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 8022cea8..f38d21a0 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -755,9 +755,53 @@ pub fn checkVisibility(self: *Element, page: *Page) !bool { return true; } +fn getElementDimensions(self: *Element, page: *Page) !struct { width: f64, height: f64 } { + const style = try self.getStyle(page); + const decl = style.asCSSStyleDeclaration(); + var width = CSS.parseDimension(decl.getPropertyValue("width", page)) orelse 5.0; + var height = CSS.parseDimension(decl.getPropertyValue("height", page)) orelse 5.0; + + if (width == 5.0 or height == 5.0) { + const tag = self.getTag(); + + // Root containers get large default size to contain descendant positions. + // With calculateDocumentPosition using 10x multipliers per level, deep trees + // can position elements at y=millions, so we need a large container height. + // 100M pixels is plausible for very long documents. + if (tag == .html or tag == .body) { + if (width == 5.0) width = 1920.0; + if (height == 5.0) height = 100_000_000.0; + } else if (tag == .img or tag == .iframe) { + if (self.getAttributeSafe("width")) |w| { + width = std.fmt.parseFloat(f64, w) catch width; + } + if (self.getAttributeSafe("height")) |h| { + height = std.fmt.parseFloat(f64, h) catch height; + } + } + } + + return .{ .width = width, .height = height }; +} + +pub fn getClientWidth(self: *Element, page: *Page) !f64 { + if (!try self.checkVisibility(page)) { + return 0.0; + } + const dims = try self.getElementDimensions(page); + return dims.width; +} + +pub fn getClientHeight(self: *Element, page: *Page) !f64 { + if (!try self.checkVisibility(page)) { + return 0.0; + } + const dims = try self.getElementDimensions(page); + return dims.height; +} + pub fn getBoundingClientRect(self: *Element, page: *Page) !*DOMRect { - const is_visible = try self.checkVisibility(page); - if (!is_visible) { + if (!try self.checkVisibility(page)) { return page._factory.create(DOMRect{ ._x = 0.0, ._y = 0.0, @@ -771,38 +815,19 @@ pub fn getBoundingClientRect(self: *Element, page: *Page) !*DOMRect { } const y = calculateDocumentPosition(self.asNode()); - - var width: f64 = 1.0; - var height: f64 = 1.0; - - const style = try self.getStyle(page); - const decl = style.asCSSStyleDeclaration(); - width = CSS.parseDimension(decl.getPropertyValue("width", page)) orelse 1.0; - height = CSS.parseDimension(decl.getPropertyValue("height", page)) orelse 1.0; - - if (width == 1.0 or height == 1.0) { - const tag = self.getTag(); - if (tag == .img or tag == .iframe) { - if (self.getAttributeSafe("width")) |w| { - width = std.fmt.parseFloat(f64, w) catch width; - } - if (self.getAttributeSafe("height")) |h| { - height = std.fmt.parseFloat(f64, h) catch height; - } - } - } + const dims = try self.getElementDimensions(page); const x: f64 = 0.0; const top = y; const left = x; - const right = x + width; - const bottom = y + height; + const right = x + dims.width; + const bottom = y + dims.height; return page._factory.create(DOMRect{ ._x = x, ._y = y, - ._width = width, - ._height = height, + ._width = dims.width, + ._height = dims.height, ._top = top, ._right = right, ._bottom = bottom, @@ -810,11 +835,19 @@ pub fn getBoundingClientRect(self: *Element, page: *Page) !*DOMRect { }); } +pub fn getClientRects(self: *Element, page: *Page) ![]DOMRect { + if (!try self.checkVisibility(page)) { + return &.{}; + } + const ptr = try self.getBoundingClientRect(page); + return ptr[0..1]; +} + // Calculates a pseudo-position in the document using an efficient heuristic. // // Instead of walking the entire DOM tree (which would be O(total_nodes)), this // function walks UP the tree counting previous siblings at each level. Each level -// uses exponential weighting (1000x per depth level) to preserve document order. +// uses exponential weighting (10x per depth level) to preserve document order. // // This gives O(depth * avg_siblings) complexity while maintaining relative positioning // that's useful for scraping and understanding element flow in the document. @@ -825,15 +858,16 @@ pub fn getBoundingClientRect(self: *Element, page: *Page) !*DOMRect { // → position 0 (0 siblings at level 2) // → position 1 (1 sibling at level 2) // -//
→ position 1000 (1 sibling at level 1, weighted by 1000) -//

→ position 1000 (0 siblings at level 2, parent has 1000) +//
→ position 10 (1 sibling at level 1, weighted by 10) +//

→ position 10 (0 siblings at level 2, parent has 10) //
// // // Trade-offs: // - Much faster than full tree-walking for deep/large DOMs // - Positions reflect document order and parent-child relationships -// - Not pixel-accurate, but sufficient for 1x1 layout heuristics +// - Keeps positions within reasonable bounds (10-level deep tree → ~10M pixels) +// - Not pixel-accurate, but sufficient for layout heuristics fn calculateDocumentPosition(node: *Node) f64 { var position: f64 = 0.0; var multiplier: f64 = 1.0; @@ -849,7 +883,7 @@ fn calculateDocumentPosition(node: *Node) f64 { } position += count * multiplier; - multiplier *= 1000.0; + multiplier *= 10.0; current = parent; } @@ -1145,6 +1179,9 @@ pub const JsApi = struct { pub const getAnimations = bridge.function(Element.getAnimations, .{}); pub const animate = bridge.function(Element.animate, .{}); pub const checkVisibility = bridge.function(Element.checkVisibility, .{}); + pub const clientWidth = bridge.accessor(Element.getClientWidth, null, .{}); + pub const clientHeight = bridge.accessor(Element.getClientHeight, null, .{}); + pub const getClientRects = bridge.function(Element.getClientRects, .{}); pub const getBoundingClientRect = bridge.function(Element.getBoundingClientRect, .{}); pub const getElementsByTagName = bridge.function(Element.getElementsByTagName, .{}); pub const getElementsByClassName = bridge.function(Element.getElementsByClassName, .{}); diff --git a/src/browser/webapi/IntersectionObserver.zig b/src/browser/webapi/IntersectionObserver.zig index c6940899..4666e526 100644 --- a/src/browser/webapi/IntersectionObserver.zig +++ b/src/browser/webapi/IntersectionObserver.zig @@ -142,7 +142,7 @@ fn calculateIntersection( ) !IntersectionData { const target_rect = try target.getBoundingClientRect(page); - // Use root element's rect or viewport (simplified: assume infinite viewport) + // Use root element's rect or viewport (simplified: assume 1920x1080) const root_rect = if (self._root) |root| try root.getBoundingClientRect(page) else @@ -158,46 +158,19 @@ fn calculateIntersection( ._left = 0.0, }); - // Calculate intersection rectangle - const left = @max(target_rect._left, root_rect._left); - const top = @max(target_rect._top, root_rect._top); - const right = @min(target_rect._right, root_rect._right); - const bottom = @min(target_rect._bottom, root_rect._bottom); + // For a headless browser without real layout, we treat all elements as fully visible. + // This avoids fingerprinting issues (massive viewports) and matches the behavior + // scripts expect when querying element visibility. + const is_intersecting = true; + const intersection_ratio: f64 = 1.0; - const is_intersecting = left < right and top < bottom; - - var intersection_rect: ?*DOMRect = null; - var intersection_ratio: f64 = 0.0; - - if (is_intersecting) { - const width = right - left; - const height = bottom - top; - const intersection_area = width * height; - const target_area = target_rect._width * target_rect._height; - - if (target_area > 0) { - intersection_ratio = intersection_area / target_area; - } - - intersection_rect = try page._factory.create(DOMRect{ - ._x = left, - ._y = top, - ._width = width, - ._height = height, - ._top = top, - ._right = right, - ._bottom = bottom, - ._left = left, - }); - } else { - // No intersection - reuse shared zero rect to avoid allocation - intersection_rect = &zero_rect; - } + // Intersection rect is the same as the target rect (fully visible) + const intersection_rect = target_rect; return .{ .is_intersecting = is_intersecting, .intersection_ratio = intersection_ratio, - .intersection_rect = intersection_rect.?, + .intersection_rect = intersection_rect, .bounding_client_rect = target_rect, .root_bounds = root_rect, }; diff --git a/src/cdp/domains/dom.zig b/src/cdp/domains/dom.zig index 791f9458..7fa429fb 100644 --- a/src/cdp/domains/dom.zig +++ b/src/cdp/domains/dom.zig @@ -429,12 +429,13 @@ fn getBoxModel(cmd: anytype) !void { const rect = try element.getBoundingClientRect(page); const quad = rectToQuad(rect); + const zero = [_]f64{0.0} ** 8; return cmd.sendResult(.{ .model = BoxModel{ .content = quad, - .padding = quad, - .border = quad, - .margin = quad, + .padding = zero, + .border = zero, + .margin = zero, .width = @intFromFloat(rect._width), .height = @intFromFloat(rect._height), } }, .{}); @@ -649,11 +650,11 @@ test "cdp.dom: getBoxModel" { .params = .{ .nodeId = 6 }, }); try ctx.expectSentResult(.{ .model = BoxModel{ - .content = Quad{ 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0 }, - .padding = Quad{ 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0 }, - .border = Quad{ 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0 }, - .margin = Quad{ 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0 }, - .width = 1, - .height = 1, + .content = Quad{ 0.0, 0.0, 5.0, 0.0, 5.0, 5.0, 0.0, 5.0 }, + .padding = Quad{ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }, + .border = Quad{ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }, + .margin = Quad{ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }, + .width = 5, + .height = 5, } }, .{ .id = 5 }); }