diff --git a/src/browser/EventManager.zig b/src/browser/EventManager.zig index 2d866733..2600e288 100644 --- a/src/browser/EventManager.zig +++ b/src/browser/EventManager.zig @@ -184,6 +184,7 @@ pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) !void .navigation, .screen, .screen_orientation, + .visual_viewport, .generic, => { const list = self.lookup.get(.{ diff --git a/src/browser/Page.zig b/src/browser/Page.zig index b5baafdb..27700443 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -51,6 +51,7 @@ const Document = @import("webapi/Document.zig"); const ShadowRoot = @import("webapi/ShadowRoot.zig"); const Performance = @import("webapi/Performance.zig"); const Screen = @import("webapi/Screen.zig"); +const VisualViewport = @import("webapi/VisualViewport.zig"); const PerformanceObserver = @import("webapi/PerformanceObserver.zig"); const MutationObserver = @import("webapi/MutationObserver.zig"); const IntersectionObserver = @import("webapi/IntersectionObserver.zig"); @@ -306,6 +307,7 @@ fn reset(self: *Page, comptime initializing: bool) !void { const storage_bucket = try self._factory.create(storage.Bucket{}); const screen = try Screen.init(self); + const visual_viewport = try VisualViewport.init(self); self.window = try self._factory.eventTarget(Window{ ._document = self.document, ._storage_bucket = storage_bucket, @@ -313,6 +315,7 @@ fn reset(self: *Page, comptime initializing: bool) !void { ._proto = undefined, ._location = &default_location, ._screen = screen, + ._visual_viewport = visual_viewport, }); self.window._document = self.document; self.window._location = &default_location; diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index de9a61dd..5d77590f 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -912,6 +912,7 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/Blob.zig"), @import("../webapi/File.zig"), @import("../webapi/Screen.zig"), + @import("../webapi/VisualViewport.zig"), @import("../webapi/PerformanceObserver.zig"), @import("../webapi/navigation/Navigation.zig"), @import("../webapi/navigation/NavigationEventTarget.zig"), diff --git a/src/browser/tests/window/visual_viewport.html b/src/browser/tests/window/visual_viewport.html new file mode 100644 index 00000000..bcde9ace --- /dev/null +++ b/src/browser/tests/window/visual_viewport.html @@ -0,0 +1,14 @@ + + + + diff --git a/src/browser/webapi/EventTarget.zig b/src/browser/webapi/EventTarget.zig index e15dc243..39613f67 100644 --- a/src/browser/webapi/EventTarget.zig +++ b/src/browser/webapi/EventTarget.zig @@ -42,6 +42,7 @@ pub const Type = union(enum) { navigation: *@import("navigation/NavigationEventTarget.zig"), screen: *@import("Screen.zig"), screen_orientation: *@import("Screen.zig").Orientation, + visual_viewport: *@import("VisualViewport.zig"), }; pub fn init(page: *Page) !*EventTarget { @@ -132,6 +133,7 @@ pub fn format(self: *EventTarget, writer: *std.Io.Writer) !void { .navigation => writer.writeAll(""), .screen => writer.writeAll(""), .screen_orientation => writer.writeAll(""), + .visual_viewport => writer.writeAll(""), }; } @@ -148,6 +150,7 @@ pub fn toString(self: *EventTarget) []const u8 { .navigation => return "[object Navigation]", .screen => return "[object Screen]", .screen_orientation => return "[object ScreenOrientation]", + .visual_viewport => return "[object VisualViewport]", }; } diff --git a/src/browser/webapi/VisualViewport.zig b/src/browser/webapi/VisualViewport.zig new file mode 100644 index 00000000..afbab13e --- /dev/null +++ b/src/browser/webapi/VisualViewport.zig @@ -0,0 +1,64 @@ +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +const js = @import("../js/js.zig"); +const Page = @import("../Page.zig"); +const EventTarget = @import("EventTarget.zig"); +const Window = @import("Window.zig"); + +const VisualViewport = @This(); + +_proto: *EventTarget, + +pub fn init(page: *Page) !*VisualViewport { + return page._factory.eventTarget(VisualViewport{ + ._proto = undefined, + }); +} + +pub fn asEventTarget(self: *VisualViewport) *EventTarget { + return self._proto; +} + +pub fn getPageLeft(_: *const VisualViewport, page: *Page) u32 { + return page.window.getScrollX(); +} + +pub fn getPageTop(_: *const VisualViewport, page: *Page) u32 { + return page.window.getScrollY(); +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(VisualViewport); + + pub const Meta = struct { + pub const name = "VisualViewport"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + // Static viewport properties for headless browser + // No pinch-zoom or mobile viewport, so values are straightforward + pub const offsetLeft = bridge.property(0, .{ .template = false }); + pub const offsetTop = bridge.property(0, .{ .template = false }); + pub const pageLeft = bridge.accessor(VisualViewport.getPageLeft, null, .{}); + pub const pageTop = bridge.accessor(VisualViewport.getPageTop, null, .{}); + pub const width = bridge.property(1920, .{ .template = false }); + pub const height = bridge.property(1080, .{ .template = false }); + pub const scale = bridge.property(1.0, .{ .template = false }); +}; diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index f252211d..8bf2ebfd 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -29,6 +29,7 @@ const Crypto = @import("Crypto.zig"); const CSS = @import("CSS.zig"); const Navigator = @import("Navigator.zig"); const Screen = @import("Screen.zig"); +const VisualViewport = @import("VisualViewport.zig"); const Performance = @import("Performance.zig"); const Document = @import("Document.zig"); const Location = @import("Location.zig"); @@ -55,6 +56,7 @@ _crypto: Crypto = .init, _console: Console = .init, _navigator: Navigator = .init, _screen: *Screen, +_visual_viewport: *VisualViewport, _performance: Performance, _storage_bucket: *storage.Bucket, _on_load: ?js.Function.Global = null, @@ -108,6 +110,10 @@ pub fn getScreen(self: *Window) *Screen { return self._screen; } +pub fn getVisualViewport(self: *const Window) *VisualViewport { + return self._visual_viewport; +} + pub fn getCrypto(self: *Window) *Crypto { return &self._crypto; } @@ -690,6 +696,7 @@ pub const JsApi = struct { pub const console = bridge.accessor(Window.getConsole, null, .{}); pub const navigator = bridge.accessor(Window.getNavigator, null, .{}); pub const screen = bridge.accessor(Window.getScreen, null, .{}); + pub const visualViewport = bridge.accessor(Window.getVisualViewport, null, .{}); pub const performance = bridge.accessor(Window.getPerformance, null, .{}); pub const localStorage = bridge.accessor(Window.getLocalStorage, null, .{}); pub const sessionStorage = bridge.accessor(Window.getSessionStorage, null, .{});