mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-12-15 15:58: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.
|
// 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);
|
const unwrapped = try self.session.unwrapObject(allocator, object_id);
|
||||||
// The values context and groupId are not used here
|
// The values context and groupId are not used here
|
||||||
const toa = getTaggedAnyOpaque(unwrapped.value) orelse return null;
|
const js_val = unwrapped.value;
|
||||||
if (toa.subtype == null or toa.subtype != .node) {
|
if (js_val.isObject() == false) {
|
||||||
|
std.debug.print("XX-0\n", .{});
|
||||||
return error.ObjectIdIsNotANode;
|
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 {
|
const NoopInspector = struct {
|
||||||
|
|||||||
@@ -755,9 +755,53 @@ pub fn checkVisibility(self: *Element, page: *Page) !bool {
|
|||||||
return true;
|
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 {
|
pub fn getBoundingClientRect(self: *Element, page: *Page) !*DOMRect {
|
||||||
const is_visible = try self.checkVisibility(page);
|
if (!try self.checkVisibility(page)) {
|
||||||
if (!is_visible) {
|
|
||||||
return page._factory.create(DOMRect{
|
return page._factory.create(DOMRect{
|
||||||
._x = 0.0,
|
._x = 0.0,
|
||||||
._y = 0.0,
|
._y = 0.0,
|
||||||
@@ -771,38 +815,19 @@ pub fn getBoundingClientRect(self: *Element, page: *Page) !*DOMRect {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const y = calculateDocumentPosition(self.asNode());
|
const y = calculateDocumentPosition(self.asNode());
|
||||||
|
const dims = try self.getElementDimensions(page);
|
||||||
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 x: f64 = 0.0;
|
const x: f64 = 0.0;
|
||||||
const top = y;
|
const top = y;
|
||||||
const left = x;
|
const left = x;
|
||||||
const right = x + width;
|
const right = x + dims.width;
|
||||||
const bottom = y + height;
|
const bottom = y + dims.height;
|
||||||
|
|
||||||
return page._factory.create(DOMRect{
|
return page._factory.create(DOMRect{
|
||||||
._x = x,
|
._x = x,
|
||||||
._y = y,
|
._y = y,
|
||||||
._width = width,
|
._width = dims.width,
|
||||||
._height = height,
|
._height = dims.height,
|
||||||
._top = top,
|
._top = top,
|
||||||
._right = right,
|
._right = right,
|
||||||
._bottom = bottom,
|
._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.
|
// 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
|
// 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
|
// 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
|
// This gives O(depth * avg_siblings) complexity while maintaining relative positioning
|
||||||
// that's useful for scraping and understanding element flow in the document.
|
// 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 0 (0 siblings at level 2)
|
||||||
// <span></span> → position 1 (1 sibling at level 2)
|
// <span></span> → position 1 (1 sibling at level 2)
|
||||||
// </div>
|
// </div>
|
||||||
// <div> → position 1000 (1 sibling at level 1, weighted by 1000)
|
// <div> → position 10 (1 sibling at level 1, weighted by 10)
|
||||||
// <p></p> → position 1000 (0 siblings at level 2, parent has 1000)
|
// <p></p> → position 10 (0 siblings at level 2, parent has 10)
|
||||||
// </div>
|
// </div>
|
||||||
// </body>
|
// </body>
|
||||||
//
|
//
|
||||||
// Trade-offs:
|
// Trade-offs:
|
||||||
// - Much faster than full tree-walking for deep/large DOMs
|
// - Much faster than full tree-walking for deep/large DOMs
|
||||||
// - Positions reflect document order and parent-child relationships
|
// - 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 {
|
fn calculateDocumentPosition(node: *Node) f64 {
|
||||||
var position: f64 = 0.0;
|
var position: f64 = 0.0;
|
||||||
var multiplier: f64 = 1.0;
|
var multiplier: f64 = 1.0;
|
||||||
@@ -849,7 +883,7 @@ fn calculateDocumentPosition(node: *Node) f64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
position += count * multiplier;
|
position += count * multiplier;
|
||||||
multiplier *= 1000.0;
|
multiplier *= 10.0;
|
||||||
current = parent;
|
current = parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1145,6 +1179,9 @@ pub const JsApi = struct {
|
|||||||
pub const getAnimations = bridge.function(Element.getAnimations, .{});
|
pub const getAnimations = bridge.function(Element.getAnimations, .{});
|
||||||
pub const animate = bridge.function(Element.animate, .{});
|
pub const animate = bridge.function(Element.animate, .{});
|
||||||
pub const checkVisibility = bridge.function(Element.checkVisibility, .{});
|
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 getBoundingClientRect = bridge.function(Element.getBoundingClientRect, .{});
|
||||||
pub const getElementsByTagName = bridge.function(Element.getElementsByTagName, .{});
|
pub const getElementsByTagName = bridge.function(Element.getElementsByTagName, .{});
|
||||||
pub const getElementsByClassName = bridge.function(Element.getElementsByClassName, .{});
|
pub const getElementsByClassName = bridge.function(Element.getElementsByClassName, .{});
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ fn calculateIntersection(
|
|||||||
) !IntersectionData {
|
) !IntersectionData {
|
||||||
const target_rect = try target.getBoundingClientRect(page);
|
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|
|
const root_rect = if (self._root) |root|
|
||||||
try root.getBoundingClientRect(page)
|
try root.getBoundingClientRect(page)
|
||||||
else
|
else
|
||||||
@@ -158,46 +158,19 @@ fn calculateIntersection(
|
|||||||
._left = 0.0,
|
._left = 0.0,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Calculate intersection rectangle
|
// For a headless browser without real layout, we treat all elements as fully visible.
|
||||||
const left = @max(target_rect._left, root_rect._left);
|
// This avoids fingerprinting issues (massive viewports) and matches the behavior
|
||||||
const top = @max(target_rect._top, root_rect._top);
|
// scripts expect when querying element visibility.
|
||||||
const right = @min(target_rect._right, root_rect._right);
|
const is_intersecting = true;
|
||||||
const bottom = @min(target_rect._bottom, root_rect._bottom);
|
const intersection_ratio: f64 = 1.0;
|
||||||
|
|
||||||
const is_intersecting = left < right and top < bottom;
|
// Intersection rect is the same as the target rect (fully visible)
|
||||||
|
const intersection_rect = target_rect;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.is_intersecting = is_intersecting,
|
.is_intersecting = is_intersecting,
|
||||||
.intersection_ratio = intersection_ratio,
|
.intersection_ratio = intersection_ratio,
|
||||||
.intersection_rect = intersection_rect.?,
|
.intersection_rect = intersection_rect,
|
||||||
.bounding_client_rect = target_rect,
|
.bounding_client_rect = target_rect,
|
||||||
.root_bounds = root_rect,
|
.root_bounds = root_rect,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -429,12 +429,13 @@ fn getBoxModel(cmd: anytype) !void {
|
|||||||
|
|
||||||
const rect = try element.getBoundingClientRect(page);
|
const rect = try element.getBoundingClientRect(page);
|
||||||
const quad = rectToQuad(rect);
|
const quad = rectToQuad(rect);
|
||||||
|
const zero = [_]f64{0.0} ** 8;
|
||||||
|
|
||||||
return cmd.sendResult(.{ .model = BoxModel{
|
return cmd.sendResult(.{ .model = BoxModel{
|
||||||
.content = quad,
|
.content = quad,
|
||||||
.padding = quad,
|
.padding = zero,
|
||||||
.border = quad,
|
.border = zero,
|
||||||
.margin = quad,
|
.margin = zero,
|
||||||
.width = @intFromFloat(rect._width),
|
.width = @intFromFloat(rect._width),
|
||||||
.height = @intFromFloat(rect._height),
|
.height = @intFromFloat(rect._height),
|
||||||
} }, .{});
|
} }, .{});
|
||||||
@@ -649,11 +650,11 @@ test "cdp.dom: getBoxModel" {
|
|||||||
.params = .{ .nodeId = 6 },
|
.params = .{ .nodeId = 6 },
|
||||||
});
|
});
|
||||||
try ctx.expectSentResult(.{ .model = BoxModel{
|
try ctx.expectSentResult(.{ .model = BoxModel{
|
||||||
.content = Quad{ 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0 },
|
.content = Quad{ 0.0, 0.0, 5.0, 0.0, 5.0, 5.0, 0.0, 5.0 },
|
||||||
.padding = Quad{ 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.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, 1.0, 0.0, 1.0, 1.0, 0.0, 1.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, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0 },
|
.margin = Quad{ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 },
|
||||||
.width = 1,
|
.width = 5,
|
||||||
.height = 1,
|
.height = 5,
|
||||||
} }, .{ .id = 5 });
|
} }, .{ .id = 5 });
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user