From 482d0951a0824f234cd9c533b6acf1b0d18a7cf7 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Mon, 31 Aug 2026 12:37:37 +0800 Subject: [PATCH] render: clientHeight/clientWidth returns the viewport for the body Meant to improve https://github.com/lightpanda-io/browser/issues/3251 There's a script on taobao and tmall which tries to tile the visible porition or the root element. Well, the root element's fake height is 100,000,000 so the page stalls. This changes is so that the body's clientHeight/Width returns the viewport dimensions. --- src/browser/tests/element/position.html | 17 +++++++---- src/browser/webapi/Element.zig | 40 ++++++++++++++++++++----- 2 files changed, 44 insertions(+), 13 deletions(-) diff --git a/src/browser/tests/element/position.html b/src/browser/tests/element/position.html index 6147cb0b4..291bf60f3 100644 --- a/src/browser/tests/element/position.html +++ b/src/browser/tests/element/position.html @@ -100,10 +100,13 @@ testing.expectTrue(zeroWidth.scrollWidth > 0); // The root containers keep their synthetic size rather than summing - // children, so horizontal page-overflow checks stay stable. + // children, so horizontal page-overflow checks stay stable. In standards + // mode the root element's clientWidth is the viewport (CSSOM View), not + // its box; offsetWidth and scrollWidth still report the box. testing.expectEqual(document.body.clientWidth, document.body.scrollWidth); + testing.expectEqual(window.innerWidth, document.documentElement.clientWidth); testing.expectEqual( - document.documentElement.clientWidth, + document.documentElement.offsetWidth, document.documentElement.scrollWidth, ); } @@ -335,14 +338,16 @@ testing.expectTrue(panel.scrollHeight > 0); // The root containers keep their synthetic size rather than summing - // children. This is what keeps infinite-scroll triggers reading - // `scrollTop + clientHeight >= scrollHeight` on body/documentElement - // behaving as they do today. + // children. The root element's clientHeight is the viewport (CSSOM View): + // jstracker tiles clientWidth/10 cells over clientHeight and calls + // elementFromPoint per cell, which the 100M px box turned into ~5M walks. testing.expectEqual(document.body.clientHeight, document.body.scrollHeight); + testing.expectEqual(window.innerHeight, document.documentElement.clientHeight); testing.expectEqual( - document.documentElement.clientHeight, + document.documentElement.offsetHeight, document.documentElement.scrollHeight, ); + testing.expectTrue(document.documentElement.scrollHeight > document.documentElement.clientHeight); } diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 70b046bca..50a1ad790 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -1398,17 +1398,35 @@ pub fn getElementAxis(self: *Element, frame: *Frame, comptime axis: Axis) Axis.S // width / height treshold is reached. If the size isn't explicit, we fallback // to the content size. pub fn getClientWidth(self: *Element, frame: *Frame) f64 { - if (!self.checkVisibilityCached(null, frame, .materialize)) { - return 0.0; - } - return self.boxAxis(frame, .width); + return self.clientAxis(frame, .width); } pub fn getClientHeight(self: *Element, frame: *Frame) f64 { + return self.clientAxis(frame, .height); +} + +fn clientAxis(self: *Element, frame: *Frame, comptime axis: Axis) f64 { if (!self.checkVisibilityCached(null, frame, .materialize)) { return 0.0; } - return self.boxAxis(frame, .height); + return self.viewportAxis(frame, axis) orelse self.boxAxis(frame, axis); +} + +fn viewportAxis(self: *Element, frame: *Frame, comptime axis: Axis) ?f64 { + const tag = self.getTag(); + if (tag != .html and tag != .body) { + return null; + } + const doc = self.asNode().ownerDocument(frame) orelse frame.document; + if ((tag == .body) != doc.isQuirksMode()) { + return null; + } + // In quicks mode, the root element (the body) reports the viewport for + // clientWidth and clientHeight rather than its own MASSIVE box. This + // fixes jstracker's uiContourMap which attempts to tile the clientHeight + // of the body. (https://github.com/lightpanda-io/browser/issues/3251) + const viewport = frame._page.getViewport(); + return @floatFromInt(if (axis == .width) viewport.width else viewport.height); } // Caller must have made sure self is visible. @@ -1581,12 +1599,20 @@ fn contentAxis(self: *Element, frame: *Frame, comptime axis: Axis) f64 { return total; } +// Unlike clientHeight, the root's offsetHeight is its box (the document +// extent), so it stays on the synthetic root default. pub fn getOffsetHeight(self: *Element, frame: *Frame) f64 { - return self.getClientHeight(frame); + if (!self.checkVisibilityCached(null, frame, .materialize)) { + return 0.0; + } + return self.boxAxis(frame, .height); } pub fn getOffsetWidth(self: *Element, frame: *Frame) f64 { - return self.getClientWidth(frame); + if (!self.checkVisibilityCached(null, frame, .materialize)) { + return 0.0; + } + return self.boxAxis(frame, .width); } pub fn getOffsetTop(self: *Element, frame: *Frame) f64 {