Add various element position properties

clientTop, clientLeft, scrollTop, scrollLeft, scrollHeight, scrollWidth,
offsetTop, offsetLeft, offsetWidth, offsetHeight.

These are all dummy implementation that hook, as much as possible, into what
layout information we have.

Explicitly set scroll information is stored on the page.
This commit is contained in:
Karl Seguin
2026-02-12 17:38:00 +08:00
parent 0cae6ceca3
commit 4b90c8fd45
3 changed files with 209 additions and 0 deletions

View File

@@ -106,6 +106,7 @@ _element_rel_lists: Element.RelListLookup = .empty,
_element_shadow_roots: Element.ShadowRootLookup = .empty,
_node_owner_documents: Node.OwnerDocumentLookup = .empty,
_element_assigned_slots: Element.AssignedSlotLookup = .empty,
_element_scroll_positions: Element.ScrollPositionLookup = .empty,
/// Lazily-created inline event listeners (or listeners provided as attributes).
/// Avoids bloating all elements with extra function fields for rare usage.

View File

@@ -0,0 +1,116 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<div id="test1">Test Element</div>
<div id="test2">Another Element</div>
<script id="clientDimensions">
{
const test1 = $('#test1');
// clientWidth/Height - default is 5px in dummy layout
testing.expectEqual('number', typeof test1.clientWidth);
testing.expectEqual('number', typeof test1.clientHeight);
testing.expectTrue(test1.clientWidth >= 0);
testing.expectTrue(test1.clientHeight >= 0);
// clientTop/Left should be 0 (no borders in dummy layout)
testing.expectEqual(0, test1.clientTop);
testing.expectEqual(0, test1.clientLeft);
}
</script>
<script id="scrollDimensions">
{
const test1 = $('#test1');
// In dummy layout, scroll dimensions equal client dimensions (no overflow)
testing.expectEqual(test1.clientWidth, test1.scrollWidth);
testing.expectEqual(test1.clientHeight, test1.scrollHeight);
}
</script>
<script id="scrollPosition">
{
const test1 = $('#test1');
// Initial scroll position should be 0
testing.expectEqual(0, test1.scrollTop);
testing.expectEqual(0, test1.scrollLeft);
// Setting scroll position
test1.scrollTop = 50;
testing.expectEqual(50, test1.scrollTop);
test1.scrollLeft = 25;
testing.expectEqual(25, test1.scrollLeft);
// Negative values should be clamped to 0
test1.scrollTop = -10;
testing.expectEqual(0, test1.scrollTop);
test1.scrollLeft = -5;
testing.expectEqual(0, test1.scrollLeft);
// Each element has independent scroll position
const test2 = $('#test2');
testing.expectEqual(0, test2.scrollTop);
testing.expectEqual(0, test2.scrollLeft);
test2.scrollTop = 100;
testing.expectEqual(100, test2.scrollTop);
testing.expectEqual(0, test1.scrollTop); // test1 should still be 0
}
</script>
<script id="offsetDimensions">
{
const test1 = $('#test1');
// offsetWidth/Height should be numbers
testing.expectEqual('number', typeof test1.offsetWidth);
testing.expectEqual('number', typeof test1.offsetHeight);
testing.expectTrue(test1.offsetWidth >= 0);
testing.expectTrue(test1.offsetHeight >= 0);
// Should equal client dimensions
testing.expectEqual(test1.clientWidth, test1.offsetWidth);
testing.expectEqual(test1.clientHeight, test1.offsetHeight);
}
</script>
<script id="offsetPosition">
{
const test1 = $('#test1');
const test2 = $('#test2');
// offsetTop/Left should be calculated from tree position
// These values are based on the heuristic layout engine
const top1 = test1.offsetTop;
const left1 = test1.offsetLeft;
const top2 = test2.offsetTop;
const left2 = test2.offsetLeft;
// Position values should be numbers
testing.expectEqual('number', typeof top1);
testing.expectEqual('number', typeof left1);
testing.expectEqual('number', typeof top2);
testing.expectEqual('number', typeof left2);
// Siblings should have different positions (either different x or y)
testing.expectTrue(top1 !== top2 || left1 !== left2);
}
</script>
<script id="offsetVsBounding">
{
const test1 = $('#test1');
// offsetTop/Left should match getBoundingClientRect
const rect = test1.getBoundingClientRect();
testing.expectEqual(rect.y, test1.offsetTop);
testing.expectEqual(rect.x, test1.offsetLeft);
testing.expectEqual(rect.width, test1.offsetWidth);
testing.expectEqual(rect.height, test1.offsetHeight);
}
</script>

View File

@@ -49,6 +49,12 @@ pub const RelListLookup = std.AutoHashMapUnmanaged(*Element, *collections.DOMTok
pub const ShadowRootLookup = std.AutoHashMapUnmanaged(*Element, *ShadowRoot);
pub const AssignedSlotLookup = std.AutoHashMapUnmanaged(*Element, *Html.Slot);
pub const ScrollPosition = struct {
x: u32 = 0,
y: u32 = 0,
};
pub const ScrollPositionLookup = std.AutoHashMapUnmanaged(*Element, ScrollPosition);
pub const Namespace = enum(u8) {
html,
svg,
@@ -1027,6 +1033,82 @@ pub fn getClientRects(self: *Element, page: *Page) ![]DOMRect {
return ptr[0..1];
}
pub fn getScrollTop(self: *Element, page: *Page) u32 {
const pos = page._element_scroll_positions.get(self) orelse return 0;
return pos.y;
}
pub fn setScrollTop(self: *Element, value: i32, page: *Page) !void {
const gop = try page._element_scroll_positions.getOrPut(page.arena, self);
if (!gop.found_existing) {
gop.value_ptr.* = .{};
}
gop.value_ptr.y = @intCast(@max(0, value));
}
pub fn getScrollLeft(self: *Element, page: *Page) u32 {
const pos = page._element_scroll_positions.get(self) orelse return 0;
return pos.x;
}
pub fn setScrollLeft(self: *Element, value: i32, page: *Page) !void {
const gop = try page._element_scroll_positions.getOrPut(page.arena, self);
if (!gop.found_existing) {
gop.value_ptr.* = .{};
}
gop.value_ptr.x = @intCast(@max(0, value));
}
pub fn getScrollHeight(self: *Element, page: *Page) !f64 {
// In our dummy layout engine, content doesn't overflow
return self.getClientHeight(page);
}
pub fn getScrollWidth(self: *Element, page: *Page) !f64 {
// In our dummy layout engine, content doesn't overflow
return self.getClientWidth(page);
}
pub fn getOffsetHeight(self: *Element, page: *Page) !f64 {
if (!try self.checkVisibility(page)) {
return 0.0;
}
const dims = try self.getElementDimensions(page);
return dims.height;
}
pub fn getOffsetWidth(self: *Element, page: *Page) !f64 {
if (!try self.checkVisibility(page)) {
return 0.0;
}
const dims = try self.getElementDimensions(page);
return dims.width;
}
pub fn getOffsetTop(self: *Element, page: *Page) !f64 {
if (!try self.checkVisibility(page)) {
return 0.0;
}
return calculateDocumentPosition(self.asNode());
}
pub fn getOffsetLeft(self: *Element, page: *Page) !f64 {
if (!try self.checkVisibility(page)) {
return 0.0;
}
return calculateSiblingPosition(self.asNode());
}
pub fn getClientTop(_: *Element) f64 {
// Border width - in our dummy layout, we don't apply borders to layout
return 0.0;
}
pub fn getClientLeft(_: *Element) f64 {
// Border width - in our dummy layout, we don't apply borders to layout
return 0.0;
}
// Calculates document position by counting all nodes that appear before this one
// in tree order, but only traversing the "left side" of the tree.
//
@@ -1502,6 +1584,16 @@ pub const JsApi = struct {
pub const checkVisibility = bridge.function(Element.checkVisibility, .{});
pub const clientWidth = bridge.accessor(Element.getClientWidth, null, .{});
pub const clientHeight = bridge.accessor(Element.getClientHeight, null, .{});
pub const clientTop = bridge.accessor(Element.getClientTop, null, .{});
pub const clientLeft = bridge.accessor(Element.getClientLeft, null, .{});
pub const scrollTop = bridge.accessor(Element.getScrollTop, Element.setScrollTop, .{});
pub const scrollLeft = bridge.accessor(Element.getScrollLeft, Element.setScrollLeft, .{});
pub const scrollHeight = bridge.accessor(Element.getScrollHeight, null, .{});
pub const scrollWidth = bridge.accessor(Element.getScrollWidth, null, .{});
pub const offsetTop = bridge.accessor(Element.getOffsetTop, null, .{});
pub const offsetLeft = bridge.accessor(Element.getOffsetLeft, null, .{});
pub const offsetWidth = bridge.accessor(Element.getOffsetWidth, null, .{});
pub const offsetHeight = bridge.accessor(Element.getOffsetHeight, null, .{});
pub const getClientRects = bridge.function(Element.getClientRects, .{});
pub const getBoundingClientRect = bridge.function(Element.getBoundingClientRect, .{});
pub const getElementsByTagName = bridge.function(Element.getElementsByTagName, .{});