Merge pull request #1287 from lightpanda-io/window.scrollTo

Add Window.scrollTo
This commit is contained in:
Karl Seguin
2025-12-24 07:16:13 +08:00
committed by GitHub
2 changed files with 140 additions and 4 deletions

View File

@@ -0,0 +1,42 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<!-- Chrome don't scroll if the body isn't big enough. -->
<body style=height:4000px;width:4000px></body>
<script id=scroll_evt>
testing.async(async (restore) => {
let scrollevt = 0;
let scrollendevt = 0;
await new Promise((resolve) => {
document.addEventListener("scroll", (event) => {
scrollevt++;
});
document.addEventListener("scrollend", (event) => {
scrollendevt++;
});
window.scrollTo(10, 20);
testing.expectEqual(0, scrollevt);
testing.expectEqual(0, scrollendevt);
// scroll immediately: the scroll event must be throttled.
window.scrollTo(20, 40);
testing.expectEqual(0, scrollevt);
testing.expectEqual(0, scrollendevt);
// wait 10ms and scroll again: we should have a 2nd scroll, but no scrollend.
window.setTimeout(() => {
window.scrollTo(30, 40);
}, 10);
// wait until scrollend happens.
window.setTimeout(() => {
resolve();
}, 100);
});
restore();
testing.expectEqual(2, scrollevt);
testing.expectEqual(1, scrollendevt);
});
</script>

View File

@@ -33,6 +33,7 @@ const Performance = @import("Performance.zig");
const Document = @import("Document.zig");
const Location = @import("Location.zig");
const Fetch = @import("net/Fetch.zig");
const Event = @import("Event.zig");
const EventTarget = @import("EventTarget.zig");
const ErrorEvent = @import("event/ErrorEvent.zig");
const MessageEvent = @import("event/MessageEvent.zig");
@@ -62,6 +63,19 @@ _location: *Location,
_timer_id: u30 = 0,
_timers: std.AutoHashMapUnmanaged(u32, *ScheduleCallback) = .{},
_custom_elements: CustomElementRegistry = .{},
_scroll_pos: struct {
x: u32,
y: u32,
state: enum {
scroll,
end,
done,
},
} = .{
.x = 0,
.y = 0,
.state = .done,
},
pub fn asEventTarget(self: *Window) *EventTarget {
return self._proto;
@@ -355,12 +369,90 @@ pub fn getInnerHeight(_: *const Window) u32 {
return 1080;
}
pub fn getScrollX(_: *const Window) u32 {
return 0;
pub fn getScrollX(self: *const Window) u32 {
return self._scroll_pos.x;
}
pub fn getScrollY(_: *const Window) u32 {
return 0;
pub fn getScrollY(self: *const Window) u32 {
return self._scroll_pos.y;
}
const ScrollToOpts = union(enum) {
x: i32,
opts: Opts,
const Opts = struct {
top: i32,
left: i32,
behavior: []const u8 = "",
};
};
pub fn scrollTo(self: *Window, opts: ScrollToOpts, y: ?i32, page: *Page) !void {
switch (opts) {
.x => |x| {
self._scroll_pos.x = @intCast(@max(x, 0));
self._scroll_pos.y = @intCast(@max(0, y orelse 0));
},
.opts => |o| {
self._scroll_pos.x = @intCast(@max(0, o.left));
self._scroll_pos.y = @intCast(@max(0, o.top));
},
}
self._scroll_pos.state = .scroll;
// We dispatch scroll event asynchronously after 10ms. So we can throttle
// them.
try page.scheduler.add(
page,
struct {
fn dispatch(_page: *anyopaque) anyerror!?u32 {
const p: *Page = @ptrCast(@alignCast(_page));
const pos = &p.window._scroll_pos;
// If the state isn't scroll, we can ignore safely to throttle
// the events.
if (pos.state != .scroll) {
return null;
}
const event = try Event.init("scroll", .{ .bubbles = true }, p);
try p._event_manager.dispatch(p.document.asEventTarget(), event);
pos.state = .end;
return null;
}
}.dispatch,
10,
.{ .low_priority = true },
);
// We dispatch scrollend event asynchronously after 20ms.
try page.scheduler.add(
page,
struct {
fn dispatch(_page: *anyopaque) anyerror!?u32 {
const p: *Page = @ptrCast(@alignCast(_page));
const pos = &p.window._scroll_pos;
// Dispatch only if the state is .end.
// If a scroll is pending, retry in 10ms.
// If the state is .end, the event has been dispatched, so
// ignore safely.
switch (pos.state) {
.scroll => return 10,
.end => {},
.done => return null,
}
const event = try Event.init("scrollend", .{ .bubbles = true }, p);
try p._event_manager.dispatch(p.document.asEventTarget(), event);
pos.state = .done;
return null;
}
}.dispatch,
20,
.{ .low_priority = true },
);
}
const ScheduleOpts = struct {
@@ -571,6 +663,8 @@ pub const JsApi = struct {
pub const scrollY = bridge.accessor(Window.getScrollY, null, .{ .cache = "scrollY" });
pub const pageXOffset = bridge.accessor(Window.getScrollX, null, .{ .cache = "pageXOffset" });
pub const pageYOffset = bridge.accessor(Window.getScrollY, null, .{ .cache = "pageYOffset" });
pub const scrollTo = bridge.function(Window.scrollTo, .{});
pub const scroll = bridge.function(Window.scrollTo, .{});
};
const testing = @import("../../testing.zig");