mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-08-01 02:06:17 -04:00
Merge pull request #3048 from lightpanda-io/scroll-with-content
derive scrollWidth/scrollHeight from element content
This commit is contained in:
@@ -24,12 +24,206 @@
|
||||
{
|
||||
const test1 = $('#test1');
|
||||
|
||||
// In dummy layout, scroll dimensions equal client dimensions (no overflow)
|
||||
// Both scroll dimensions are approximated from the content, so neither falls
|
||||
// below the element's own box but either may exceed it.
|
||||
testing.expectTrue(test1.scrollWidth >= test1.clientWidth);
|
||||
testing.expectTrue(test1.scrollHeight >= test1.clientHeight);
|
||||
|
||||
// test1 holds only a text node, and text is not measured on either axis, so
|
||||
// both still match the element's own box.
|
||||
testing.expectEqual(test1.clientWidth, test1.scrollWidth);
|
||||
testing.expectEqual(test1.clientHeight, test1.scrollHeight);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="scrollWidthFromContent">
|
||||
{
|
||||
// An empty element has no content to overflow its box.
|
||||
const empty = document.createElement('div');
|
||||
document.body.appendChild(empty);
|
||||
testing.expectEqual(empty.clientWidth, empty.scrollWidth);
|
||||
|
||||
// Each added child strictly increases scrollWidth, and enough of them push
|
||||
// it past the element's own box.
|
||||
const box = document.createElement('div');
|
||||
document.body.appendChild(box);
|
||||
box.appendChild(document.createElement('span'));
|
||||
const oneChild = box.scrollWidth;
|
||||
testing.expectTrue(oneChild >= box.clientWidth);
|
||||
|
||||
box.appendChild(document.createElement('span'));
|
||||
testing.expectTrue(box.scrollWidth > oneChild);
|
||||
testing.expectTrue(box.scrollWidth > box.clientWidth);
|
||||
|
||||
// Text contributes nothing, however long. Estimating a run from its length
|
||||
// would need a per-character advance that tracks font-size, and would report
|
||||
// overflow for practically every element containing text.
|
||||
const short = document.createElement('div');
|
||||
const long = document.createElement('div');
|
||||
short.textContent = 'ab';
|
||||
long.textContent = 'abcdefghijklmnop';
|
||||
document.body.appendChild(short);
|
||||
document.body.appendChild(long);
|
||||
testing.expectEqual(short.clientWidth, short.scrollWidth);
|
||||
testing.expectEqual(long.clientWidth, long.scrollWidth);
|
||||
testing.expectEqual(short.scrollWidth, long.scrollWidth);
|
||||
|
||||
// Comments are not rendered, so they contribute nothing.
|
||||
const commented = document.createElement('div');
|
||||
commented.innerHTML = '<!-- a very long comment that renders as nothing -->';
|
||||
document.body.appendChild(commented);
|
||||
testing.expectEqual(commented.clientWidth, commented.scrollWidth);
|
||||
|
||||
// A display:none child takes up no space.
|
||||
const withHidden = document.createElement('div');
|
||||
const hiddenChild = document.createElement('span');
|
||||
hiddenChild.style.display = 'none';
|
||||
withHidden.appendChild(hiddenChild);
|
||||
document.body.appendChild(withHidden);
|
||||
testing.expectEqual(withHidden.clientWidth, withHidden.scrollWidth);
|
||||
|
||||
// A hidden element reports 0, matching clientWidth/offsetWidth.
|
||||
const hidden = document.createElement('div');
|
||||
hidden.style.display = 'none';
|
||||
hidden.appendChild(document.createElement('span'));
|
||||
document.body.appendChild(hidden);
|
||||
testing.expectEqual(0, hidden.scrollWidth);
|
||||
|
||||
// A visible width:0 box (an off-screen measurement container) is not
|
||||
// hidden: its content still overflows it.
|
||||
const zeroWidth = document.createElement('div');
|
||||
zeroWidth.style.width = '0';
|
||||
zeroWidth.appendChild(document.createElement('span'));
|
||||
document.body.appendChild(zeroWidth);
|
||||
testing.expectEqual(0, zeroWidth.clientWidth);
|
||||
testing.expectTrue(zeroWidth.scrollWidth > 0);
|
||||
|
||||
// The root containers keep their synthetic size rather than summing
|
||||
// children, so horizontal page-overflow checks stay stable.
|
||||
testing.expectEqual(document.body.clientWidth, document.body.scrollWidth);
|
||||
testing.expectEqual(
|
||||
document.documentElement.clientWidth,
|
||||
document.documentElement.scrollWidth,
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="scrollWidthMarqueeLoop">
|
||||
{
|
||||
// Regression: the infinite-marquee idiom duplicates content until it is
|
||||
// twice the width of its container. scrollWidth has to respond to the
|
||||
// insertion or the loop never terminates.
|
||||
const track = document.createElement('div');
|
||||
const lane = document.createElement('div');
|
||||
lane.innerHTML = '<span>item</span>';
|
||||
track.appendChild(lane);
|
||||
document.body.appendChild(track);
|
||||
|
||||
// Doubling the markup doubles the measured content width.
|
||||
const before = lane.scrollWidth;
|
||||
lane.innerHTML += lane.innerHTML;
|
||||
testing.expectEqual(before * 2, lane.scrollWidth);
|
||||
|
||||
let guard = 0;
|
||||
while (lane.scrollWidth < track.offsetWidth * 2 && guard < 50) {
|
||||
lane.innerHTML += lane.innerHTML;
|
||||
guard++;
|
||||
}
|
||||
testing.expectTrue(guard < 50);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="scrollHeightFromContent">
|
||||
{
|
||||
// An empty element has no content to overflow its box.
|
||||
const empty = document.createElement('div');
|
||||
document.body.appendChild(empty);
|
||||
testing.expectEqual(empty.clientHeight, empty.scrollHeight);
|
||||
|
||||
// Children stack, so each one strictly increases scrollHeight.
|
||||
const box = document.createElement('div');
|
||||
document.body.appendChild(box);
|
||||
box.appendChild(document.createElement('div'));
|
||||
const oneChild = box.scrollHeight;
|
||||
testing.expectTrue(oneChild >= box.clientHeight);
|
||||
|
||||
box.appendChild(document.createElement('div'));
|
||||
testing.expectTrue(box.scrollHeight > oneChild);
|
||||
testing.expectTrue(box.scrollHeight > box.clientHeight);
|
||||
|
||||
// Text contributes no height, as it contributes no width: a text child's
|
||||
// line box is what the element's own size already stands in for.
|
||||
const short = document.createElement('div');
|
||||
const long = document.createElement('div');
|
||||
short.textContent = 'ab';
|
||||
long.textContent = 'abcdefghijklmnop';
|
||||
document.body.appendChild(short);
|
||||
document.body.appendChild(long);
|
||||
testing.expectEqual(short.clientHeight, short.scrollHeight);
|
||||
testing.expectEqual(long.clientHeight, long.scrollHeight);
|
||||
testing.expectEqual(short.scrollHeight, long.scrollHeight);
|
||||
|
||||
// A display:none child takes up no space.
|
||||
const withHidden = document.createElement('div');
|
||||
const hiddenChild = document.createElement('div');
|
||||
hiddenChild.style.display = 'none';
|
||||
withHidden.appendChild(hiddenChild);
|
||||
document.body.appendChild(withHidden);
|
||||
testing.expectEqual(withHidden.clientHeight, withHidden.scrollHeight);
|
||||
|
||||
// A hidden element reports 0, matching clientHeight/offsetHeight.
|
||||
const hidden = document.createElement('div');
|
||||
hidden.style.display = 'none';
|
||||
hidden.appendChild(document.createElement('div'));
|
||||
document.body.appendChild(hidden);
|
||||
testing.expectEqual(0, hidden.scrollHeight);
|
||||
|
||||
// The collapse/expand idiom: scrollHeight is read while the panel is
|
||||
// collapsed at height:0 to know how far to open it. A zero-height box is
|
||||
// not hidden; its content is still measured.
|
||||
const panel = document.createElement('div');
|
||||
panel.style.height = '0';
|
||||
panel.appendChild(document.createElement('div'));
|
||||
panel.appendChild(document.createElement('div'));
|
||||
document.body.appendChild(panel);
|
||||
testing.expectEqual(0, panel.clientHeight);
|
||||
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.
|
||||
testing.expectEqual(document.body.clientHeight, document.body.scrollHeight);
|
||||
testing.expectEqual(
|
||||
document.documentElement.clientHeight,
|
||||
document.documentElement.scrollHeight,
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="scrollHeightMarqueeLoop">
|
||||
{
|
||||
// The vertical counterpart of scrollWidthMarqueeLoop: a ticker that grows
|
||||
// its lane until it is twice the height of its container.
|
||||
const track = document.createElement('div');
|
||||
const lane = document.createElement('div');
|
||||
lane.innerHTML = '<div>item</div>';
|
||||
track.appendChild(lane);
|
||||
document.body.appendChild(track);
|
||||
|
||||
// Doubling the markup doubles the measured content height.
|
||||
const before = lane.scrollHeight;
|
||||
lane.innerHTML += lane.innerHTML;
|
||||
testing.expectEqual(before * 2, lane.scrollHeight);
|
||||
|
||||
let guard = 0;
|
||||
while (lane.scrollHeight < track.offsetHeight * 2 && guard < 50) {
|
||||
lane.innerHTML += lane.innerHTML;
|
||||
guard++;
|
||||
}
|
||||
testing.expectTrue(guard < 50);
|
||||
}
|
||||
</script>
|
||||
<script id="scrollPosition">
|
||||
{
|
||||
const test1 = $('#test1');
|
||||
|
||||
@@ -1377,13 +1377,106 @@ pub fn setScrollLeft(self: *Element, value: i32, frame: *Frame) !void {
|
||||
}
|
||||
|
||||
pub fn getScrollHeight(self: *Element, frame: *Frame) f64 {
|
||||
// In our dummy layout engine, content doesn't overflow
|
||||
return self.getClientHeight(frame);
|
||||
var visibility_cache: VisibilityCache = .{};
|
||||
if (!self.checkVisibilityCached(&visibility_cache, frame)) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
const height = self.getElementDimensions(frame).height;
|
||||
|
||||
const tag = self.getTag();
|
||||
// As in getScrollWidth: the root containers carry artificial giant
|
||||
// defaults, and page-level overflow checks read them.
|
||||
if (tag == .html or tag == .body) {
|
||||
return height;
|
||||
}
|
||||
|
||||
return @max(height, self.contentHeight(frame, &visibility_cache));
|
||||
}
|
||||
|
||||
// The height of the direct child elements stacked vertically, the counterpart
|
||||
// of contentWidth.
|
||||
//
|
||||
// Note that the two assume contradictory arrangements — contentWidth lays the
|
||||
// children out in a row, this stacks them. That is deliberate. We can't detect
|
||||
// the real layout mode (see contentWidth), so each axis independently assumes
|
||||
// the arrangement that produces overflow. Together they bound the content
|
||||
// extent per axis rather than describing one coherent layout: reporting no
|
||||
// overflow when there is some is what wedges measure-then-mutate loops,
|
||||
// while the reverse merely over-reports.
|
||||
fn contentHeight(self: *Element, frame: *Frame, visibility_cache: *VisibilityCache) f64 {
|
||||
var total: f64 = 0;
|
||||
|
||||
var child = self.asNode().firstChild();
|
||||
while (child) |node| : (child = node.nextSibling()) {
|
||||
if (node.is(Element)) |el| {
|
||||
if (el.checkVisibilityCached(visibility_cache, frame)) {
|
||||
total += el.getElementDimensions(frame).height;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
pub fn getScrollWidth(self: *Element, frame: *Frame) f64 {
|
||||
// In our dummy layout engine, content doesn't overflow
|
||||
return self.getClientWidth(frame);
|
||||
var visibility_cache: VisibilityCache = .{};
|
||||
if (!self.checkVisibilityCached(&visibility_cache, frame)) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
const width = self.getElementDimensions(frame).width;
|
||||
|
||||
const tag = self.getTag();
|
||||
// The root containers carry artificial giant defaults (1920 and
|
||||
// 100_000_000, see getElementDimensions). Stacking their children on
|
||||
// top would inflate a value sites read to detect page overflow.
|
||||
if (tag == .html or tag == .body) {
|
||||
return width;
|
||||
}
|
||||
|
||||
return @max(width, self.contentWidth(frame, &visibility_cache));
|
||||
}
|
||||
|
||||
// The width of the direct child elements laid end to end on a single row.
|
||||
//
|
||||
// The dummy layout engine has no line-breaking, and an element only overflows
|
||||
// horizontally when its children don't wrap (white-space:nowrap, a flex row, an
|
||||
// inline-block strip), so the single-row assumption covers the case that
|
||||
// matters. We can't detect the layout mode to do better: getStyle() sees only
|
||||
// the inline `style=` attribute, and the computed cascade resolves stylesheet
|
||||
// rules for `display:none` and `visibility` alone.
|
||||
//
|
||||
// Only direct children are measured, never the whole subtree. This runs on
|
||||
// every scrollWidth read, and recursing would make an element's cost O(subtree)
|
||||
// rather than O(fan-out).
|
||||
//
|
||||
// Growing with the child count is the point: JS that appends content until
|
||||
// `scrollWidth` passes a threshold (the infinite-marquee idiom) never
|
||||
// terminates when the metric ignores what it just inserted.
|
||||
//
|
||||
// Text children are not measured, matching contentHeight. Estimating a text run
|
||||
// from its length would need a per-character advance, which in turn has to track
|
||||
// font-size or "shrink the font until it fits" loops stop converging — and it
|
||||
// would report overflow for practically every element containing text, since a
|
||||
// few words already exceed the default box. Element children are what content
|
||||
// grown by script actually consists of.
|
||||
fn contentWidth(self: *Element, frame: *Frame, visibility_cache: *VisibilityCache) f64 {
|
||||
var total: f64 = 0;
|
||||
|
||||
// The cache arrives seeded by the caller's own visibility walk, and
|
||||
// siblings share that ancestor chain, so the loop costs one own-element
|
||||
// check per child rather than N ancestor walks.
|
||||
var child = self.asNode().firstChild();
|
||||
while (child) |node| : (child = node.nextSibling()) {
|
||||
if (node.is(Element)) |el| {
|
||||
if (el.checkVisibilityCached(visibility_cache, frame)) {
|
||||
total += el.getElementDimensions(frame).width;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
pub fn getOffsetHeight(self: *Element, frame: *Frame) f64 {
|
||||
|
||||
Reference in New Issue
Block a user