mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-12-14 15:28:57 +00:00
Properly resolve inspector ObjectId back to a DOM Node
Tweak element boundingRect and "renderer" based on what puppeteer needs.
This commit is contained in:
@@ -1,109 +0,0 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
// <span></span> → position 0 (0 siblings at level 2)
|
||||
// <span></span> → position 1 (1 sibling at level 2)
|
||||
// </div>
|
||||
// <div> → position 1000 (1 sibling at level 1, weighted by 1000)
|
||||
// <p></p> → position 1000 (0 siblings at level 2, parent has 1000)
|
||||
// <div> → position 10 (1 sibling at level 1, weighted by 10)
|
||||
// <p></p> → position 10 (0 siblings at level 2, parent has 10)
|
||||
// </div>
|
||||
// </body>
|
||||
//
|
||||
// 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, .{});
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user