mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-10-09 21:01:58 -04:00
Match Chrome's root scroll size and clamp x too
Only the root scroller's scrollWidth and scrollHeight are floored at the viewport; html and body keep the document's own height, as in Chrome. The document's width is body's widest child, and scrollX clamps against it.
This commit is contained in:
1 parent
f5d9f3e737
commit
8fb49fe507
6 files changed
+96
-43
No files matched your search
@@ -155,9 +155,14 @@ _focus_fixup_pending: bool = false,
|
||||
|
||||
_style_manager: StyleManager,
|
||||
|
||||
// Element.documentHeight, valid while the style version and viewport height
|
||||
// it was computed for hold.
|
||||
_document_height: ?struct { version: usize, viewport: u32, value: f64 } = null,
|
||||
// Element.documentExtent, valid while the style version and viewport it was
|
||||
// computed for hold.
|
||||
_document_extent: ?struct {
|
||||
version: usize,
|
||||
viewport_width: u32,
|
||||
viewport_height: u32,
|
||||
extent: Element.DocumentExtent,
|
||||
} = null,
|
||||
_script_manager: ScriptManager,
|
||||
|
||||
_http_owner: HttpClient.Owner,
|
||||
|
||||
@@ -343,11 +343,11 @@
|
||||
// per cell, which a document-sized box turned into ~5M walks.
|
||||
testing.expectEqual(document.body.clientHeight, document.body.scrollHeight);
|
||||
testing.expectEqual(window.innerHeight, document.documentElement.clientHeight);
|
||||
// What the viewport scrolls over: the document, or the viewport itself
|
||||
testing.expectEqual(
|
||||
document.documentElement.offsetHeight,
|
||||
Math.max(document.documentElement.offsetHeight, document.documentElement.clientHeight),
|
||||
document.documentElement.scrollHeight,
|
||||
);
|
||||
testing.expectTrue(document.documentElement.scrollHeight >= document.documentElement.clientHeight);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -71,6 +71,6 @@
|
||||
document.getElementById('rad').addEventListener('change', function() { window.radChanged = true; });
|
||||
</script>
|
||||
<!-- Room to scroll the window: the document can't scroll past its bottom -->
|
||||
<div style="height: 10000px"></div>
|
||||
<div style="height: 10000px; width: 10000px"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -2,7 +2,7 @@
|
||||
<script src="testing.js"></script>
|
||||
|
||||
<!-- Room to scroll: the document can't scroll past its bottom -->
|
||||
<div style="height: 10000px"></div>
|
||||
<div style="height: 10000px; width: 10000px"></div>
|
||||
|
||||
<script id=scrollBy_exists>
|
||||
testing.expectEqual('function', typeof window.scrollBy);
|
||||
@@ -56,9 +56,11 @@
|
||||
window.scrollBy(0, 100);
|
||||
testing.expectEqual(max, window.scrollY);
|
||||
|
||||
// The document has no honest width, so x isn't bounded
|
||||
window.scrollTo(99999, 0);
|
||||
testing.expectEqual(99999, window.scrollX);
|
||||
// x clamps the same way, against the widest content
|
||||
const maxX = document.documentElement.scrollWidth - window.innerWidth;
|
||||
testing.expectTrue(maxX > 0);
|
||||
window.scrollTo(99999999, 0);
|
||||
testing.expectEqual(maxX, window.scrollX);
|
||||
|
||||
// Content added at the bottom extends the limit
|
||||
const more = document.createElement('div');
|
||||
|
||||
@@ -1500,11 +1500,11 @@ pub fn getElementAxis(self: *Element, frame: *Frame, comptime axis: Axis) Axis.S
|
||||
}
|
||||
}
|
||||
|
||||
// Root containers span the document, see documentHeight.
|
||||
// Root containers span the document, see documentExtent.
|
||||
if (root) {
|
||||
return .{ .value = switch (axis) {
|
||||
.width => 1920.0,
|
||||
.height => if (self.ownerFrame(frame)) |owner| documentHeight(owner) else 0.0,
|
||||
.height => if (self.ownerFrame(frame)) |owner| documentExtent(owner).height else 0.0,
|
||||
} };
|
||||
}
|
||||
|
||||
@@ -1539,6 +1539,13 @@ 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).
|
||||
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));
|
||||
}
|
||||
|
||||
fn viewportAxis(self: *Element, frame: *Frame, comptime axis: Axis) ?f64 {
|
||||
const tag = self.getTag();
|
||||
if (tag != .html and tag != .body) {
|
||||
@@ -1679,9 +1686,10 @@ pub fn getScrollHeight(self: *Element, frame: *Frame) f64 {
|
||||
const height = self.getElementAxis(frame, .height).value;
|
||||
|
||||
const tag = self.getTag();
|
||||
// The root containers already span the document.
|
||||
// The root containers already span the document. The one scrolling the
|
||||
// viewport reports what the viewport scrolls over.
|
||||
if (tag == .html or tag == .body) {
|
||||
return height;
|
||||
return self.rootScrollSize(frame, .height) orelse height;
|
||||
}
|
||||
|
||||
return @max(height, self.contentAxis(frame, .height));
|
||||
@@ -1695,11 +1703,12 @@ pub fn getScrollWidth(self: *Element, frame: *Frame) f64 {
|
||||
const width = self.getElementAxis(frame, .width).value;
|
||||
|
||||
const tag = self.getTag();
|
||||
// The root containers span the document (see getElementAxis), and
|
||||
// their children don't lie side by side on one row: stacking them
|
||||
// would inflate a value sites read to detect page overflow.
|
||||
// The root containers keep their box: their children don't lie side by
|
||||
// side on one row, and stacking them would inflate a value sites read to
|
||||
// detect page overflow. The one scrolling the viewport reports what the
|
||||
// viewport scrolls over.
|
||||
if (tag == .html or tag == .body) {
|
||||
return width;
|
||||
return self.rootScrollSize(frame, .width) orelse width;
|
||||
}
|
||||
|
||||
return @max(width, self.contentAxis(frame, .width));
|
||||
@@ -1912,35 +1921,68 @@ fn calculateDocumentPosition(node: *Node) f64 {
|
||||
return position * 5.0; // 5px per node
|
||||
}
|
||||
|
||||
/// The document's height, which html and body span and the viewport scrolls
|
||||
/// over. Tall enough for every synthetic position (calculateDocumentPosition's
|
||||
/// 5px per node), for body's children stacked on top of each other, and never
|
||||
/// shorter than the viewport, like the root's scrollHeight.
|
||||
pub fn documentHeight(frame: *Frame) f64 {
|
||||
pub const DocumentExtent = struct { width: f64, height: f64 };
|
||||
|
||||
/// The document's extent. Its height, which html and body span, fits every
|
||||
/// synthetic position (calculateDocumentPosition's 5px per node) and body's
|
||||
/// children stacked on top of each other. Its width is body's widest child:
|
||||
/// block children don't lie side by side on one row. An inline size on body
|
||||
/// stretches both.
|
||||
pub fn documentExtent(frame: *Frame) DocumentExtent {
|
||||
const version = frame.page.style_version;
|
||||
const viewport = frame.page.getViewport().height;
|
||||
if (frame._document_height) |cached| {
|
||||
if (cached.version == version and cached.viewport == viewport) {
|
||||
return cached.value;
|
||||
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 height = @max(@as(f64, @floatFromInt(viewport)), countSubtreeNodes(doc.asNode()) * 5.0);
|
||||
if (doc.getDocumentElement()) |root| {
|
||||
var child = root.asNode().firstChild();
|
||||
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()) {
|
||||
if (node.is(Html.Body)) |body| {
|
||||
height = @max(height, body.asElement().contentAxis(frame, .height));
|
||||
break;
|
||||
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 the scroll offsets clamped against it
|
||||
height = @ceil(height);
|
||||
frame._document_height = .{ .version = version, .viewport = viewport, .value = height };
|
||||
return height;
|
||||
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, but never less than the
|
||||
/// viewport itself, like the root scroller's scrollWidth and scrollHeight.
|
||||
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)
|
||||
|
||||
@@ -951,13 +951,12 @@ 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 new_x: u32 = if (o.left) |left| @intCast(@max(0, left)) else self._scroll_pos.x;
|
||||
var new_y: u32 = if (o.top) |top| @intCast(@max(0, top)) else self._scroll_pos.y;
|
||||
|
||||
// The document can't scroll past its bottom. It has no honest width
|
||||
// (children don't lie side by side), so x stays unbounded.
|
||||
const max_y = Element.documentHeight(self._frame) - @as(f64, @floatFromInt(self.getInnerHeight(self._frame)));
|
||||
new_y = @min(new_y, @as(u32, @intFromFloat(@max(0, @min(max_y, std.math.maxInt(u32))))));
|
||||
// The viewport can't scroll past the document's edges
|
||||
const size = Element.documentScrollSize(self._frame);
|
||||
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;
|
||||
const new_y: u32 = if (o.top) |top| @min(@as(u32, @intCast(@max(0, top))), max_y) else self._scroll_pos.y;
|
||||
|
||||
if (new_x == self._scroll_pos.x and new_y == self._scroll_pos.y) {
|
||||
return;
|
||||
@@ -1019,6 +1018,11 @@ pub fn scrollTo(self: *Window, opts: Element.ScrollToOpts, y: ?i32, frame: *Fram
|
||||
);
|
||||
}
|
||||
|
||||
fn scrollLimit(size: f64, visible: u32) u32 {
|
||||
const limit = size - @as(f64, @floatFromInt(visible));
|
||||
return @intFromFloat(std.math.clamp(limit, 0, std.math.maxInt(u32)));
|
||||
}
|
||||
|
||||
pub fn scrollBy(self: *Window, opts: Element.ScrollToOpts, y: ?i32, frame: *Frame) !void {
|
||||
const o = opts.offsets(y);
|
||||
// The viewport has no honest extent, so a stored offset can sit above
|
||||
|
||||
Reference in new issue
Block a user