From 2b10b1c17a478823ddc28744fe575d4ff6a313f9 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Tue, 23 Dec 2025 12:07:07 +0100 Subject: [PATCH 1/2] webapi: add window.scrollTo --- src/browser/webapi/Window.zig | 48 ++++++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index 7cf26b6c..9e85efc0 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -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,7 @@ _location: *Location, _timer_id: u30 = 0, _timers: std.AutoHashMapUnmanaged(u32, *ScheduleCallback) = .{}, _custom_elements: CustomElementRegistry = .{}, +_scroll_pos: struct { x: u32, y: u32 } = .{ .x = 0, .y = 0 }, pub fn asEventTarget(self: *Window) *EventTarget { return self._proto; @@ -355,12 +357,48 @@ 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)); + }, + } + + { + // TODO According to the doc, scroll event should be throttled. + // see https://developer.mozilla.org/en-US/docs/Web/API/Document/scroll_event#scroll_event_throttling + const event = try Event.init("scroll", .{ .bubbles = true }, page); + try page._event_manager.dispatch(self._document.asEventTarget(), event); + } + + { + // TODO scrollend must be dispatched once the scroll really ends. + const event = try Event.init("scrollend", .{ .bubbles = true }, page); + try page._event_manager.dispatch(self._document.asEventTarget(), event); + } } const ScheduleOpts = struct { @@ -571,6 +609,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"); From 2bd38608e9e83c148c563d9f81891fa8cc96cd51 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Tue, 23 Dec 2025 16:06:33 +0100 Subject: [PATCH 2/2] throttle scroll event --- src/browser/tests/window/scroll.html | 42 +++++++++++++++ src/browser/webapi/Window.zig | 78 +++++++++++++++++++++++----- 2 files changed, 108 insertions(+), 12 deletions(-) create mode 100644 src/browser/tests/window/scroll.html diff --git a/src/browser/tests/window/scroll.html b/src/browser/tests/window/scroll.html new file mode 100644 index 00000000..fccddce0 --- /dev/null +++ b/src/browser/tests/window/scroll.html @@ -0,0 +1,42 @@ + + + + + + diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index 9e85efc0..b9060aaf 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -63,7 +63,19 @@ _location: *Location, _timer_id: u30 = 0, _timers: std.AutoHashMapUnmanaged(u32, *ScheduleCallback) = .{}, _custom_elements: CustomElementRegistry = .{}, -_scroll_pos: struct { x: u32, y: u32 } = .{ .x = 0, .y = 0 }, +_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; @@ -387,18 +399,60 @@ pub fn scrollTo(self: *Window, opts: ScrollToOpts, y: ?i32, page: *Page) !void { }, } - { - // TODO According to the doc, scroll event should be throttled. - // see https://developer.mozilla.org/en-US/docs/Web/API/Document/scroll_event#scroll_event_throttling - const event = try Event.init("scroll", .{ .bubbles = true }, page); - try page._event_manager.dispatch(self._document.asEventTarget(), event); - } + self._scroll_pos.state = .scroll; - { - // TODO scrollend must be dispatched once the scroll really ends. - const event = try Event.init("scrollend", .{ .bubbles = true }, page); - try page._event_manager.dispatch(self._document.asEventTarget(), event); - } + // 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 {