From 9a0aadd62a086226c9aba80bcdedbb92dcb7be8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Arrufat?= Date: Thu, 24 Sep 2026 09:50:12 +0200 Subject: [PATCH] Move the document extent onto Document A document without a frame isn't rendered, so its extent is zero. The extent cache moves from Frame to Document with it, and the body lookup reuses HTMLDocument.getBody. --- src/browser/Frame.zig | 8 ---- src/browser/webapi/Document.zig | 62 +++++++++++++++++++++++++++ src/browser/webapi/Element.zig | 75 +++------------------------------ src/browser/webapi/Window.zig | 2 +- 4 files changed, 70 insertions(+), 77 deletions(-) diff --git a/src/browser/Frame.zig b/src/browser/Frame.zig index f953a3147..7298a2150 100644 --- a/src/browser/Frame.zig +++ b/src/browser/Frame.zig @@ -154,14 +154,6 @@ _queued_events: *std.ArrayList(QueuedEvent) = undefined, _focus_fixup_pending: bool = false, _style_manager: StyleManager, - -// Element.documentExtent cache, keyed on style version and viewport. -_document_extent: ?struct { - version: usize, - viewport_width: u32, - viewport_height: u32, - extent: Element.DocumentExtent, -} = null, _script_manager: ScriptManager, _http_owner: HttpClient.Owner, diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig index c98868d8c..667cd4697 100644 --- a/src/browser/webapi/Document.zig +++ b/src/browser/webapi/Document.zig @@ -82,6 +82,13 @@ _script_created_parser: ?Parser.Streaming = null, _close_requested: bool = false, _adopted_style_sheets: ?js.Object.Global = null, _selection: Selection = .{ ._rc = .init(1) }, +// extent() cache, keyed on style version and viewport. +_extent: ?struct { + version: usize, + viewport_width: u32, + viewport_height: u32, + extent: Extent, +} = null, // Ordered stack of currently-showing popovers _open_popovers: std.ArrayList(*Element) = .empty, @@ -495,6 +502,61 @@ pub fn getDocumentElement(self: *Document) ?*Element { return null; } +pub const Extent = struct { width: f64, height: f64 }; + +/// The document's size. Height: enough for every synthetic position (5px per +/// node) and body's stacked children. Width: body's widest child. An inline +/// size on body stretches both. A document without a frame isn't rendered, +/// so it has no size. +pub fn extent(self: *Document) Extent { + const frame = self._frame orelse return .{ .width = 0, .height = 0 }; + const version = frame.page.style_version; + const viewport = frame.page.getViewport(); + if (self._extent) |cached| { + if (cached.version == version and cached.viewport_width == viewport.width and cached.viewport_height == viewport.height) { + return cached.extent; + } + } + + var size: Extent = .{ .width = 0, .height = Element.countSubtreeNodes(self.asNode()) * 5.0 }; + if (self.is(HTMLDocument)) |html_doc| { + if (html_doc.getBody()) |html_body| { + const body = html_body.asElement(); + const style_manager = &frame._style_manager; + size.height = @max(size.height, body.contentAxis(frame, .height), style_manager.inlineSize(body, .height) orelse 0); + size.width = style_manager.inlineSize(body, .width) orelse 0; + var child = body.asNode().firstChild(); + while (child) |node| : (child = node.nextSibling()) { + const el = node.is(Element) orelse continue; + if (!style_manager.hasDisplayNone(el)) { + size.width = @max(size.width, el.getElementAxis(frame, .width).value); + } + } + } + } + + // Whole pixels, like scroll offsets + size = .{ .width = @ceil(size.width), .height = @ceil(size.height) }; + self._extent = .{ + .version = version, + .viewport_width = viewport.width, + .viewport_height = viewport.height, + .extent = size, + }; + return size; +} + +/// What the viewport scrolls over: the document, at least viewport-sized. +pub fn scrollSize(self: *Document) Extent { + const frame = self._frame orelse return .{ .width = 0, .height = 0 }; + const size = self.extent(); + const viewport = frame.page.getViewport(); + return .{ + .width = @max(size.width, @as(f64, @floatFromInt(viewport.width))), + .height = @max(size.height, @as(f64, @floatFromInt(viewport.height))), + }; +} + fn getSelection(self: *Document) *Selection { return &self._selection; } diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 5a62980b4..e5e76a9c6 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -1500,11 +1500,11 @@ pub fn getElementAxis(self: *Element, frame: *Frame, comptime axis: Axis) Axis.S } } - // Root containers span the document, see documentExtent. + // Root containers span the document, see Document.extent. if (root) { return .{ .value = switch (axis) { .width => 1920.0, - .height => if (self.ownerFrame(frame)) |owner| documentExtent(owner).height else 0.0, + .height => if (self.asNode().ownerDocument(frame)) |doc| doc.extent().height else 0.0, } }; } @@ -1539,11 +1539,11 @@ fn clientAxis(self: *Element, frame: *Frame, comptime axis: Axis) f64 { return self.viewportAxis(frame, axis) orelse self.boxAxis(frame, axis); } -/// documentScrollSize, when self is the root scroller (see viewportAxis). +/// Document.scrollSize, when self is the root scroller (see viewportAxis). fn rootScrollSize(self: *Element, frame: *Frame, comptime axis: Axis) ?f64 { _ = self.viewportAxis(frame, axis) orelse return null; - const owner = self.ownerFrame(frame) orelse return null; - return @field(documentScrollSize(owner), @tagName(axis)); + const doc = self.asNode().ownerDocument(frame) orelse return null; + return @field(doc.scrollSize(), @tagName(axis)); } fn viewportAxis(self: *Element, frame: *Frame, comptime axis: Axis) ?f64 { @@ -1751,7 +1751,7 @@ fn scrollExtent(self: *Element, frame: *Frame, comptime axis: Axis) ?f64 { // // Text children add height only under an explicit width to wrap at. // Otherwise almost every element with text would report overflow. -fn contentAxis(self: *Element, frame: *Frame, comptime axis: Axis) f64 { +pub fn contentAxis(self: *Element, frame: *Frame, comptime axis: Axis) f64 { var total: f64 = 0; const owner = self.ownerFrame(frame) orelse return 0; const style_manager = &owner._style_manager; @@ -1915,69 +1915,8 @@ fn calculateDocumentPosition(node: *Node) f64 { return position * 5.0; // 5px per node } -pub const DocumentExtent = struct { width: f64, height: f64 }; - -/// The document's size. Height: enough for every synthetic position (5px per -/// node) and body's stacked children. Width: body's widest child. An inline -/// size on body stretches both. -pub fn documentExtent(frame: *Frame) DocumentExtent { - const version = frame.page.style_version; - const viewport = frame.page.getViewport(); - if (frame._document_extent) |cached| { - if (cached.version == version and cached.viewport_width == viewport.width and cached.viewport_height == viewport.height) { - return cached.extent; - } - } - - const doc = frame.document; - var extent: DocumentExtent = .{ .width = 0, .height = countSubtreeNodes(doc.asNode()) * 5.0 }; - if (findBody(doc)) |body| { - const style_manager = &frame._style_manager; - extent.height = @max(extent.height, body.contentAxis(frame, .height), style_manager.inlineSize(body, .height) orelse 0); - extent.width = style_manager.inlineSize(body, .width) orelse 0; - var child = body.asNode().firstChild(); - while (child) |node| : (child = node.nextSibling()) { - const el = node.is(Element) orelse continue; - if (!style_manager.hasDisplayNone(el)) { - extent.width = @max(extent.width, el.getElementAxis(frame, .width).value); - } - } - } - - // Whole pixels, like scroll offsets - extent = .{ .width = @ceil(extent.width), .height = @ceil(extent.height) }; - frame._document_extent = .{ - .version = version, - .viewport_width = viewport.width, - .viewport_height = viewport.height, - .extent = extent, - }; - return extent; -} - -/// What the viewport scrolls over: the document, at least viewport-sized. -pub fn documentScrollSize(frame: *Frame) DocumentExtent { - const extent = documentExtent(frame); - const viewport = frame.page.getViewport(); - return .{ - .width = @max(extent.width, @as(f64, @floatFromInt(viewport.width))), - .height = @max(extent.height, @as(f64, @floatFromInt(viewport.height))), - }; -} - -fn findBody(doc: *Node.Document) ?*Element { - const root = doc.getDocumentElement() orelse return null; - var child = root.asNode().firstChild(); - while (child) |node| : (child = node.nextSibling()) { - if (node.is(Html.Body)) |body| { - return body.asElement(); - } - } - return null; -} - // Counts total nodes in a subtree (node + all descendants) -fn countSubtreeNodes(node: *Node) f64 { +pub fn countSubtreeNodes(node: *Node) f64 { var count: f64 = 1.0; // Count this node var child = node.firstChild(); diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index a44afbd9f..38216d106 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -951,7 +951,7 @@ fn getDevicePixelRatio(_: *const Window, frame: *Frame) f32 { pub fn scrollTo(self: *Window, opts: Element.ScrollToOpts, y: ?i32, frame: *Frame) !void { const o = opts.offsets(y); - const size = Element.documentScrollSize(self._frame); + const size = self._frame.document.scrollSize(); const max_x = scrollLimit(size.width, self.getInnerWidth(self._frame)); const max_y = scrollLimit(size.height, self.getInnerHeight(self._frame)); const new_x: u32 = if (o.left) |left| @min(@as(u32, @intCast(@max(0, left))), max_x) else self._scroll_pos.x;