From 7b8ea3916448c9f983a0c76ed504e6aec2bfb6ae Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Mon, 13 Jul 2026 12:03:52 +0800 Subject: [PATCH] webapi: Improve DOMRect We had a basic DORect implementation (which probably covered more cases). This commit extends the implementation to be more complete: - Add DOMRectReadOnly (which DOMRect inherits from) - Add StructuredClone support to DOMRect and DOMRectReadOnly - Make DOMRect and DOMRectReadOnly usable from Worker DOMRect can be on the hot path with IntersectionObserver, so we maintain a value-based data object (DOMRect.Data) for internal use (things returned to js always have to be heap allocated (and always were, we're just more explicit about this part now)). --- src/browser/Factory.zig | 18 ++ src/browser/js/Value.zig | 2 + src/browser/js/bridge.zig | 3 + src/browser/tests/domrect.html | 155 ++++++++++++++++++ src/browser/webapi/DOMRect.zig | 98 ++++++----- src/browser/webapi/DOMRectReadOnly.zig | 172 ++++++++++++++++++++ src/browser/webapi/Element.zig | 42 ++--- src/browser/webapi/IntersectionObserver.zig | 34 ++-- src/browser/webapi/Range.zig | 8 +- src/browser/webapi/element/svg/Svg.zig | 2 +- src/cdp/domains/dom.zig | 28 ++-- 11 files changed, 462 insertions(+), 100 deletions(-) create mode 100644 src/browser/tests/domrect.html create mode 100644 src/browser/webapi/DOMRectReadOnly.zig diff --git a/src/browser/Factory.zig b/src/browser/Factory.zig index 9e1ee0685..00626ed12 100644 --- a/src/browser/Factory.zig +++ b/src/browser/Factory.zig @@ -35,6 +35,8 @@ const EventTarget = @import("webapi/EventTarget.zig"); const XMLHttpRequestEventTarget = @import("webapi/net/XMLHttpRequestEventTarget.zig"); const Blob = @import("webapi/Blob.zig"); const AbstractRange = @import("webapi/AbstractRange.zig"); +const DOMRect = @import("webapi/DOMRect.zig"); +const DOMRectReadOnly = @import("webapi/DOMRectReadOnly.zig"); const log = lp.log; const String = lp.String; @@ -292,6 +294,22 @@ pub fn abstractRange(_: *const Factory, arena: Allocator, child: anytype, frame: return chain.get(1); } +pub fn domRect(self: *Factory, rect: DOMRectReadOnly.Data) !*DOMRect { + const chain = try PrototypeChain(&.{ DOMRectReadOnly, DOMRect }).allocate(self._slab.allocator()); + + const base = chain.get(0); + base.* = .{ + ._type = .{ .mutable = chain.get(1) }, + ._x = rect.x, + ._y = rect.y, + ._width = rect.width, + ._height = rect.height, + }; + chain.setLeaf(1, DOMRect{ ._proto = base }); + + return chain.get(1); +} + pub fn node(self: *Factory, child: anytype) !*@TypeOf(child) { const allocator = self._slab.allocator(); return try AutoPrototypeChain( diff --git a/src/browser/js/Value.zig b/src/browser/js/Value.zig index 08bb1b8b4..95dc641dc 100644 --- a/src/browser/js/Value.zig +++ b/src/browser/js/Value.zig @@ -425,6 +425,8 @@ const cloneable_types = .{ @import("../webapi/ImageData.zig"), @import("../webapi/DOMPointReadOnly.zig"), @import("../webapi/DOMPoint.zig"), + @import("../webapi/DOMRectReadOnly.zig"), + @import("../webapi/DOMRect.zig"), }; // Passed to a type's structuredSerialize hook to write its payload into the diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index b4c0f4168..03bb0bbaf 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -907,6 +907,7 @@ pub const PageJsApis = flattenTypes(&.{ @import("../webapi/DOMImplementation.zig"), @import("../webapi/DOMTreeWalker.zig"), @import("../webapi/DOMNodeIterator.zig"), + @import("../webapi/DOMRectReadOnly.zig"), @import("../webapi/DOMRect.zig"), @import("../webapi/DOMMatrixReadOnly.zig"), @import("../webapi/DOMMatrix.zig"), @@ -1128,6 +1129,8 @@ pub const WorkerJsApis = flattenTypes(&.{ @import("../webapi/event/PromiseRejectionEvent.zig"), @import("../webapi/event/CloseEvent.zig"), @import("../webapi/DOMException.zig"), + @import("../webapi/DOMRectReadOnly.zig"), + @import("../webapi/DOMRect.zig"), @import("../webapi/DOMMatrixReadOnly.zig"), @import("../webapi/DOMMatrix.zig"), @import("../webapi/DOMPointReadOnly.zig"), diff --git a/src/browser/tests/domrect.html b/src/browser/tests/domrect.html new file mode 100644 index 000000000..e7af22990 --- /dev/null +++ b/src/browser/tests/domrect.html @@ -0,0 +1,155 @@ + + + DOMRect Test + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/browser/webapi/DOMRect.zig b/src/browser/webapi/DOMRect.zig index e3b2502c6..00eee6b1c 100644 --- a/src/browser/webapi/DOMRect.zig +++ b/src/browser/webapi/DOMRect.zig @@ -16,55 +16,73 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +const js = @import("../js/js.zig"); +const Page = @import("../Page.zig"); +const Factory = @import("../Factory.zig"); + +const RO = @import("DOMRectReadOnly.zig"); + +pub const Data = RO.Data; + const DOMRect = @This(); -const js = @import("../js/js.zig"); -const Frame = @import("../Frame.zig"); +_proto: *RO, -_x: f64, -_y: f64, -_width: f64, -_height: f64, - -pub fn init(x: f64, y: f64, width: f64, height: f64, frame: *Frame) !*DOMRect { - return frame._factory.create(DOMRect{ - ._x = x, - ._y = y, - ._width = width, - ._height = height, - }); +pub fn init(x_: ?f64, y_: ?f64, width_: ?f64, height_: ?f64, exec: *const js.Execution) !*DOMRect { + return create(.{ + .x = x_ orelse 0, + .y = y_ orelse 0, + .width = width_ orelse 0, + .height = height_ orelse 0, + }, exec._factory); } +pub fn create(rect: Data, factory: *Factory) !*DOMRect { + return factory.domRect(rect); +} + +pub fn fromRect(other_: ?Data, exec: *const js.Execution) !*DOMRect { + return create(other_ orelse .{}, exec._factory); +} + +pub fn structuredSerialize(self: *const DOMRect, writer: *js.StructuredWriter) !void { + try self._proto.structuredSerialize(writer); +} + +pub fn structuredDeserialize(reader: *js.StructuredReader, page: *Page) !*DOMRect { + return page.factory.domRect(try RO.readData(reader)); +} + +// DOMRect redeclares x/y/width/height as writable, so DOMRect.prototype needs its +// own read-write accessors distinct from the read-only ones on +// DOMRectReadOnly.prototype. The setters are the point; each accessor bundles a +// getter too, so we pair them with DOMRect-typed getters that read through +// `_proto`. top/right/bottom/left stay read-only and are inherited from the base. + pub fn getX(self: *const DOMRect) f64 { - return self._x; + return self._proto._x; } - pub fn getY(self: *const DOMRect) f64 { - return self._y; + return self._proto._y; } - pub fn getWidth(self: *const DOMRect) f64 { - return self._width; + return self._proto._width; } - pub fn getHeight(self: *const DOMRect) f64 { - return self._height; + return self._proto._height; } -pub fn getTop(self: *const DOMRect) f64 { - return @min(self._y, self._y + self._height); +pub fn setX(self: *DOMRect, v: f64) void { + self._proto._x = v; } - -pub fn getRight(self: *const DOMRect) f64 { - return @max(self._x, self._x + self._width); +pub fn setY(self: *DOMRect, v: f64) void { + self._proto._y = v; } - -pub fn getBottom(self: *const DOMRect) f64 { - return @max(self._y, self._y + self._height); +pub fn setWidth(self: *DOMRect, v: f64) void { + self._proto._width = v; } - -pub fn getLeft(self: *const DOMRect) f64 { - return @min(self._x, self._x + self._width); +pub fn setHeight(self: *DOMRect, v: f64) void { + self._proto._height = v; } pub const JsApi = struct { @@ -77,12 +95,12 @@ pub const JsApi = struct { }; pub const constructor = bridge.constructor(DOMRect.init, .{}); - pub const x = bridge.accessor(DOMRect.getX, null, .{}); - pub const y = bridge.accessor(DOMRect.getY, null, .{}); - pub const width = bridge.accessor(DOMRect.getWidth, null, .{}); - pub const height = bridge.accessor(DOMRect.getHeight, null, .{}); - pub const top = bridge.accessor(DOMRect.getTop, null, .{}); - pub const right = bridge.accessor(DOMRect.getRight, null, .{}); - pub const bottom = bridge.accessor(DOMRect.getBottom, null, .{}); - pub const left = bridge.accessor(DOMRect.getLeft, null, .{}); + pub const fromRect = bridge.function(DOMRect.fromRect, .{ .static = true }); + + // Writable components (the read-only top/right/bottom/left are inherited + // from DOMRectReadOnly.prototype). + pub const x = bridge.accessor(DOMRect.getX, DOMRect.setX, .{}); + pub const y = bridge.accessor(DOMRect.getY, DOMRect.setY, .{}); + pub const width = bridge.accessor(DOMRect.getWidth, DOMRect.setWidth, .{}); + pub const height = bridge.accessor(DOMRect.getHeight, DOMRect.setHeight, .{}); }; diff --git a/src/browser/webapi/DOMRectReadOnly.zig b/src/browser/webapi/DOMRectReadOnly.zig new file mode 100644 index 000000000..49ee7e54e --- /dev/null +++ b/src/browser/webapi/DOMRectReadOnly.zig @@ -0,0 +1,172 @@ +// Copyright (C) 2023-2026 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 js = @import("../js/js.zig"); +const Page = @import("../Page.zig"); +const Factory = @import("../Factory.zig"); + +const DOMRect = @import("DOMRect.zig"); + +const DOMRectReadOnly = @This(); + +pub const _prototype_root = true; + +_type: Type, + +_x: f64, +_y: f64, +_width: f64, +_height: f64, + +pub const Type = union(enum) { + generic, + mutable: *DOMRect, +}; + +pub const Data = struct { + x: f64 = 0, + y: f64 = 0, + width: f64 = 0, + height: f64 = 0, +}; + +pub fn init(x_: ?f64, y_: ?f64, width_: ?f64, height_: ?f64, exec: *const js.Execution) !*DOMRectReadOnly { + return createBare(.{ + .x = x_ orelse 0, + .y = y_ orelse 0, + .width = width_ orelse 0, + .height = height_ orelse 0, + }, exec._factory); +} + +pub fn createBare(rect: Data, factory: *Factory) !*DOMRectReadOnly { + return factory.create(DOMRectReadOnly{ + ._type = .generic, + ._x = rect.x, + ._y = rect.y, + ._width = rect.width, + ._height = rect.height, + }); +} + +pub fn fromRect(other_: ?Data, exec: *const js.Execution) !*DOMRectReadOnly { + return createBare(other_ orelse .{}, exec._factory); +} + +pub fn structuredSerialize(self: *const DOMRectReadOnly, writer: *js.StructuredWriter) !void { + writer.writeUint64(@bitCast(self._x)); + writer.writeUint64(@bitCast(self._y)); + writer.writeUint64(@bitCast(self._width)); + writer.writeUint64(@bitCast(self._height)); +} + +pub fn readData(reader: *js.StructuredReader) !Data { + return .{ + .x = @bitCast(try reader.readUint64()), + .y = @bitCast(try reader.readUint64()), + .width = @bitCast(try reader.readUint64()), + .height = @bitCast(try reader.readUint64()), + }; +} + +pub fn structuredDeserialize(reader: *js.StructuredReader, page: *Page) !*DOMRectReadOnly { + return createBare(try readData(reader), &page.factory); +} + +pub fn getX(self: *const DOMRectReadOnly) f64 { + return self._x; +} + +pub fn getY(self: *const DOMRectReadOnly) f64 { + return self._y; +} + +pub fn getWidth(self: *const DOMRectReadOnly) f64 { + return self._width; +} + +pub fn getHeight(self: *const DOMRectReadOnly) f64 { + return self._height; +} + +pub fn getTop(self: *const DOMRectReadOnly) f64 { + return @min(self._y, self._y + self._height); +} + +pub fn getRight(self: *const DOMRectReadOnly) f64 { + return @max(self._x, self._x + self._width); +} + +pub fn getBottom(self: *const DOMRectReadOnly) f64 { + return @max(self._y, self._y + self._height); +} + +pub fn getLeft(self: *const DOMRectReadOnly) f64 { + return @min(self._x, self._x + self._width); +} + +pub fn toJSON(self: *const DOMRectReadOnly) struct { + x: f64, + y: f64, + width: f64, + height: f64, + top: f64, + right: f64, + bottom: f64, + left: f64, +} { + return .{ + .x = self._x, + .y = self._y, + .width = self._width, + .height = self._height, + .top = self.getTop(), + .right = self.getRight(), + .bottom = self.getBottom(), + .left = self.getLeft(), + }; +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(DOMRectReadOnly); + + pub const Meta = struct { + pub const name = "DOMRectReadOnly"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const constructor = bridge.constructor(DOMRectReadOnly.init, .{}); + pub const fromRect = bridge.function(DOMRectReadOnly.fromRect, .{ .static = true }); + + pub const x = bridge.accessor(DOMRectReadOnly.getX, null, .{}); + pub const y = bridge.accessor(DOMRectReadOnly.getY, null, .{}); + pub const width = bridge.accessor(DOMRectReadOnly.getWidth, null, .{}); + pub const height = bridge.accessor(DOMRectReadOnly.getHeight, null, .{}); + pub const top = bridge.accessor(DOMRectReadOnly.getTop, null, .{}); + pub const right = bridge.accessor(DOMRectReadOnly.getRight, null, .{}); + pub const bottom = bridge.accessor(DOMRectReadOnly.getBottom, null, .{}); + pub const left = bridge.accessor(DOMRectReadOnly.getLeft, null, .{}); + + pub const toJSON = bridge.function(DOMRectReadOnly.toJSON, .{}); +}; + +const testing = @import("../../testing.zig"); +test "WebApi: DOMRect" { + try testing.htmlRunner("domrect.html", .{}); +} diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index bfb758281..7aa341e7c 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -1223,22 +1223,22 @@ pub fn getClientHeight(self: *Element, frame: *Frame) f64 { return dims.height; } -pub fn getBoundingClientRect(self: *Element, frame: *Frame) DOMRect { - if (!self.checkVisibilityCached(null, frame)) { - return .{ - ._x = 0.0, - ._y = 0.0, - ._width = 0.0, - ._height = 0.0, - }; - } - - return self.getBoundingClientRectForVisible(frame); +pub fn getBoundingClientRect(self: *Element, frame: *Frame) !*DOMRect { + return DOMRect.create(self.boundingClientRectValues(frame), frame._factory); } -// Some cases need a the BoundingClientRect but have already done the -// visibility check. -pub fn getBoundingClientRectForVisible(self: *Element, frame: *Frame) DOMRect { +// Plain rect values, no JS-backed allocation: the internal fast path shared by +// getBoundingClientRect, getClientRects, and IntersectionObserver. A DOMRect is +// only materialized at the JS boundary. +pub fn boundingClientRectValues(self: *Element, frame: *Frame) DOMRect.Data { + if (!self.checkVisibilityCached(null, frame)) { + return .{}; + } + return self.boundingClientRectValuesForVisible(frame); +} + +// Some cases need the bounding rect but have already done the visibility check. +pub fn boundingClientRectValuesForVisible(self: *Element, frame: *Frame) DOMRect.Data { const y = calculateDocumentPosition(self.asNode()); const dims = self.getElementDimensions(frame); @@ -1246,19 +1246,19 @@ pub fn getBoundingClientRectForVisible(self: *Element, frame: *Frame) DOMRect { const x = calculateSiblingPosition(self.asNode()); return .{ - ._x = x, - ._y = y, - ._width = dims.width, - ._height = dims.height, + .x = x, + .y = y, + .width = dims.width, + .height = dims.height, }; } -pub fn getClientRects(self: *Element, frame: *Frame) ![]DOMRect { +pub fn getClientRects(self: *Element, frame: *Frame) ![]*DOMRect { if (!self.checkVisibilityCached(null, frame)) { return &.{}; } - const rects = try frame.local_arena.alloc(DOMRect, 1); - rects[0] = self.getBoundingClientRectForVisible(frame); + const rects = try frame.local_arena.alloc(*DOMRect, 1); + rects[0] = try DOMRect.create(self.boundingClientRectValuesForVisible(frame), frame._factory); return rects; } diff --git a/src/browser/webapi/IntersectionObserver.zig b/src/browser/webapi/IntersectionObserver.zig index 8a70a7436..c6899c739 100644 --- a/src/browser/webapi/IntersectionObserver.zig +++ b/src/browser/webapi/IntersectionObserver.zig @@ -50,13 +50,9 @@ _pending_entries: std.ArrayList(*IntersectionObserverEntry) = .{}, // tracked targets that aren't reported yet _tracked: std.AutoHashMapUnmanaged(*Element, void) = .{}, -// Shared zero DOMRect to avoid repeated allocations for non-intersecting elements -var zero_rect: DOMRect = .{ - ._x = 0.0, - ._y = 0.0, - ._width = 0.0, - ._height = 0.0, -}; +// Shared zero rect (plain values) for non-intersecting elements. Materialized +// into a DOMRect only if it ends up on a delivered entry. +const zero_rect: DOMRect.Data = .{}; pub const ObserverInit = struct { root: ?*Node = null, @@ -202,18 +198,16 @@ fn calculateIntersection( has_parent: bool, frame: *Frame, ) !IntersectionData { - const target_rect = target.getBoundingClientRect(frame); + const target_rect = target.boundingClientRectValues(frame); // Use root element's rect or the faux-layout viewport. const root_rect = if (self._root) |root| - root.getBoundingClientRect(frame) + root.boundingClientRectValues(frame) else blk: { const viewport = frame._page.getViewport(); - break :blk DOMRect{ - ._x = 0.0, - ._y = 0.0, - ._width = @floatFromInt(viewport.width), - ._height = @floatFromInt(viewport.height), + break :blk DOMRect.Data{ + .width = @floatFromInt(viewport.width), + .height = @floatFromInt(viewport.height), }; }; @@ -238,9 +232,9 @@ fn calculateIntersection( const IntersectionData = struct { is_intersecting: bool, intersection_ratio: f64, - intersection_rect: DOMRect, - bounding_client_rect: DOMRect, - root_bounds: DOMRect, + intersection_rect: DOMRect.Data, + bounding_client_rect: DOMRect.Data, + root_bounds: DOMRect.Data, }; fn meetsThreshold(self: *IntersectionObserver, ratio: f64) bool { @@ -280,9 +274,9 @@ fn checkIntersection(self: *IntersectionObserver, target: *Element, frame: *Fram ._target = target, ._time = frame.window._performance.now(), ._is_intersecting = is_now_intersecting, - ._root_bounds = try frame._factory.create(data.root_bounds), - ._intersection_rect = try frame._factory.create(data.intersection_rect), - ._bounding_client_rect = try frame._factory.create(data.bounding_client_rect), + ._root_bounds = try DOMRect.create(data.root_bounds, frame._factory), + ._intersection_rect = try DOMRect.create(data.intersection_rect, frame._factory), + ._bounding_client_rect = try DOMRect.create(data.bounding_client_rect, frame._factory), ._intersection_ratio = data.intersection_ratio, }; try self._pending_entries.append(self._arena, entry); diff --git a/src/browser/webapi/Range.zig b/src/browser/webapi/Range.zig index 29a6d31f8..d0f23c649 100644 --- a/src/browser/webapi/Range.zig +++ b/src/browser/webapi/Range.zig @@ -664,17 +664,17 @@ fn nextAfterSubtree(node: *Node, root: *Node) ?*Node { return null; } -pub fn getBoundingClientRect(self: *const Range, frame: *Frame) DOMRect { +pub fn getBoundingClientRect(self: *const Range, frame: *Frame) !*DOMRect { if (self._proto.getCollapsed()) { - return .{ ._x = 0, ._y = 0, ._width = 0, ._height = 0 }; + return DOMRect.create(.{}, frame._factory); } const element = self.getContainerElement() orelse { - return .{ ._x = 0, ._y = 0, ._width = 0, ._height = 0 }; + return DOMRect.create(.{}, frame._factory); }; return element.getBoundingClientRect(frame); } -pub fn getClientRects(self: *const Range, frame: *Frame) ![]DOMRect { +pub fn getClientRects(self: *const Range, frame: *Frame) ![]*DOMRect { if (self._proto.getCollapsed()) { return &.{}; } diff --git a/src/browser/webapi/element/svg/Svg.zig b/src/browser/webapi/element/svg/Svg.zig index 920b25c7f..11b809b43 100644 --- a/src/browser/webapi/element/svg/Svg.zig +++ b/src/browser/webapi/element/svg/Svg.zig @@ -85,7 +85,7 @@ pub const JsApi = struct { pub const createSVGRect = bridge.function(_createSVGRect, .{}); fn _createSVGRect(_: *Svg, frame: *Frame) !*DOMRect { - return DOMRect.init(0, 0, 0, 0, frame); + return DOMRect.create(.{}, frame._factory); } pub const createSVGNumber = bridge.function(_createSVGNumber, .{}); diff --git a/src/cdp/domains/dom.zig b/src/cdp/domains/dom.zig index 37235ba20..6db73357f 100644 --- a/src/cdp/domains/dom.zig +++ b/src/cdp/domains/dom.zig @@ -429,16 +429,16 @@ const BoxModel = struct { // shapeOutside: ?ShapeOutsideInfo, }; -fn rectToQuad(rect: DOMNode.Element.DOMRect) Quad { +fn rectToQuad(rect: DOMNode.Element.DOMRect.Data) Quad { return Quad{ - rect._x, - rect._y, - rect._x + rect._width, - rect._y, - rect._x + rect._width, - rect._y + rect._height, - rect._x, - rect._y + rect._height, + rect.x, + rect.y, + rect.x + rect.width, + rect.y, + rect.x + rect.width, + rect.y + rect.height, + rect.x, + rect.y + rect.height, }; } @@ -447,7 +447,7 @@ fn scrollIntoViewIfNeeded(cmd: *CDP.Command) !void { nodeId: ?Node.Id = null, backendNodeId: ?u32 = null, objectId: ?[]const u8 = null, - rect: ?DOMNode.Element.DOMRect = null, + rect: ?DOMNode.Element.DOMRect.Data = null, })) orelse return error.InvalidParams; // Only 1 of nodeId, backendNodeId, objectId may be set, but chrome just takes the first non-null @@ -507,7 +507,7 @@ fn getContentQuads(cmd: *CDP.Command) !void { // Text may be tricky, multiple quads in case of multiple lines? empty quads of text = ""? // Elements like SVGElement may have multiple quads. - const quad = rectToQuad(element.getBoundingClientRect(frame)); + const quad = rectToQuad(element.boundingClientRectValues(frame)); return cmd.sendResult(.{ .quads = &.{quad} }, .{}); } @@ -526,7 +526,7 @@ fn getBoxModel(cmd: *CDP.Command) !void { // TODO implement for document or text const element = node.dom.is(DOMNode.Element) orelse return error.NodeIsNotAnElement; - const rect = element.getBoundingClientRect(frame); + const rect = element.boundingClientRectValues(frame); const quad = rectToQuad(rect); const zero = [_]f64{0.0} ** 8; @@ -535,8 +535,8 @@ fn getBoxModel(cmd: *CDP.Command) !void { .padding = zero, .border = zero, .margin = zero, - .width = @intFromFloat(rect._width), - .height = @intFromFloat(rect._height), + .width = @intFromFloat(rect.width), + .height = @intFromFloat(rect.height), } }, .{}); }