From 20b7325e1361e55ec1e8062beba8e2455d875846 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Arrufat?= Date: Thu, 24 Sep 2026 09:39:50 +0200 Subject: [PATCH] Resolve width and height from stylesheet rules An element sized by a stylesheet rule had no explicit size, so its scroll offset never clamped. The geometry group now tracks width and height, and getElementAxis reads them from the cascade, which already folds in the inline style. html and body read only their inline size, without materializing the style object. Removes the now unused CSS.parseDimensionViewport. --- src/browser/StyleManager.zig | 82 ++++++++++++++++++++++--- src/browser/tests/element/position.html | 35 +++++++++++ src/browser/webapi/CSS.zig | 13 ---- src/browser/webapi/Element.zig | 41 ++++++++----- 4 files changed, 133 insertions(+), 38 deletions(-) diff --git a/src/browser/StyleManager.zig b/src/browser/StyleManager.zig index 59d441515..0f8f8af9f 100644 --- a/src/browser/StyleManager.zig +++ b/src/browser/StyleManager.zig @@ -20,6 +20,7 @@ const std = @import("std"); const lp = @import("lightpanda"); const Frame = @import("Frame.zig"); +const Viewport = @import("Viewport.zig"); const units = @import("css/units.zig"); const CssParser = @import("css/Parser.zig"); @@ -40,7 +41,7 @@ const String = lp.String; const Allocator = std.mem.Allocator; // Tracks the CSS properties the renderless layout acts on (display, visibility, -// opacity, pointer-events, overflow) from +
content
+
content
+
content
+
+ + diff --git a/src/browser/webapi/CSS.zig b/src/browser/webapi/CSS.zig index 213064d7b..136b8a276 100644 --- a/src/browser/webapi/CSS.zig +++ b/src/browser/webapi/CSS.zig @@ -19,7 +19,6 @@ const std = @import("std"); const js = @import("../js/js.zig"); const Frame = @import("../Frame.zig"); -const units = @import("../css/units.zig"); const Tokenizer = @import("../css/Tokenizer.zig"); const CSS = @This(); @@ -27,18 +26,6 @@ _pad: bool = false, pub const init: CSS = .{}; -// parseDimension plus viewport-relative units, which the faux layout -// resolves against the page viewport. -pub fn parseDimensionViewport(value: []const u8, frame: *Frame) ?f64 { - const parsed = units.parse(value) catch return null; - return switch (parsed.unit) { - .none, .px => parsed.value, - .vh => parsed.value * @as(f64, @floatFromInt(frame.page.getViewport().height)) / 100.0, - .vw => parsed.value * @as(f64, @floatFromInt(frame.page.getViewport().width)) / 100.0, - else => null, - }; -} - // Extract the X value from a transfrom. This could come from a translate, // translatex, translate3d or matrix function. pub fn parseTranslateX(value: []const u8) f64 { diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 4283c67ba..ff9d2e801 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -1486,27 +1486,34 @@ pub const Axis = enum { }; pub fn getElementAxis(self: *Element, frame: *Frame, comptime axis: Axis) Axis.State { - if (self.inlineStyle(frame)) |style| { - const decl = style.asCSSStyleDeclaration(); - if (CSS.parseDimensionViewport(decl.getPropertyValue(@tagName(axis), frame), frame)) |v| { + const tag = self.getTag(); + const root = tag == .html or tag == .body; + + if (self.ownerFrame(frame)) |owner| { + const style_manager = &owner._style_manager; + // Roots take only an inline size: a sheet's `height: 100vh` on body + // would shrink the box every synthetic position must fit in. + const size = if (root) style_manager.inlineSize(self, axis) else style_manager.declaredSize(self, axis); + if (size) |v| { return .{ .value = v, .explicit = true }; } } - switch (self.getTag()) { - // Root containers get large default size to contain descendant positions. - // With calculateDocumentPosition using linear depth scaling (100px per level), - // even very deep trees (100 levels) stay within 10,000px. - // 100M pixels is plausible for very long documents. - .html, .body => return .{ .value = if (axis == .width) 1920.0 else 100_000_000.0 }, - .img, .iframe => { - if (self.getAttributeSafe(comptime .wrap(@tagName(axis)))) |attr| { - if (std.fmt.parseFloat(f64, attr)) |parsed| { - return .{ .value = parsed, .explicit = true }; - } else |_| {} - } - }, - else => {}, + // Root containers get large default size to contain descendant positions. + // With calculateDocumentPosition using linear depth scaling (100px per level), + // even very deep trees (100 levels) stay within 10,000px. + // 100M pixels is plausible for very long documents. + if (root) { + return .{ .value = if (axis == .width) 1920.0 else 100_000_000.0 }; + } + + // Presentational attributes lose to CSS sizes. + if (tag == .img or tag == .iframe) { + if (self.getAttributeSafe(comptime .wrap(@tagName(axis)))) |attr| { + if (std.fmt.parseFloat(f64, attr)) |parsed| { + return .{ .value = parsed, .explicit = true }; + } else |_| {} + } } return .{ .value = 5.0 };