mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-07-31 09:46:05 -04:00
Merge pull request #2664 from rohitsux/feat/cdp-set-device-metrics
feat(cdp): honor viewport in Emulation.setDeviceMetricsOverride
This commit is contained in:
@@ -25,6 +25,7 @@ const v8 = js.v8;
|
||||
const Frame = @import("Frame.zig");
|
||||
const Session = @import("Session.zig");
|
||||
const Factory = @import("Factory.zig");
|
||||
const Viewport = @import("Viewport.zig");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
const IS_DEBUG = builtin.mode == .Debug;
|
||||
@@ -123,6 +124,18 @@ closed_frames: std.ArrayList(*Frame) = .empty,
|
||||
// notification (so CDP doesn't reset its node registry yet) and
|
||||
_state: enum { active, pending } = .active,
|
||||
|
||||
// Runtime viewport override set via Emulation.setDeviceMetricsOverride and
|
||||
// cleared via Emulation.clearDeviceMetricsOverride. Null means use the
|
||||
// compile-time Viewport.default. Read every viewport consumer through
|
||||
// getViewport so they all observe the same (possibly overridden) value.
|
||||
viewport_override: ?Viewport = null,
|
||||
|
||||
// The viewport every consumer should read: the runtime override if set,
|
||||
// otherwise the compile-time default.
|
||||
pub fn getViewport(self: *const Page) Viewport {
|
||||
return self.viewport_override orelse Viewport.default;
|
||||
}
|
||||
|
||||
// Initialize a Page and its root Frame.
|
||||
pub fn init(self: *Page, session: *Session, frame_id: u32) !void {
|
||||
const frame_arena = try session.arena_pool.acquire(.large, "Page.frame_arena");
|
||||
|
||||
@@ -20,7 +20,6 @@ const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const Frame = @import("Frame.zig");
|
||||
const Viewport = @import("Viewport.zig");
|
||||
|
||||
const CssParser = @import("css/Parser.zig");
|
||||
const MediaQuery = @import("css/MediaQuery.zig");
|
||||
@@ -148,7 +147,7 @@ fn applyMediaAtRule(self: *StyleManager, text: []const u8, depth: u8) !void {
|
||||
const query = std.mem.trim(u8, rest[0..open], &std.ascii.whitespace);
|
||||
const inner = rest[open + 1 .. close];
|
||||
|
||||
if (!MediaQuery.matches(query, Viewport.default)) return;
|
||||
if (!MediaQuery.matches(query, self.frame._page.getViewport())) return;
|
||||
|
||||
var it = CssParser.parseStylesheet(inner);
|
||||
while (it.next()) |nested_rule| {
|
||||
|
||||
@@ -1543,7 +1543,7 @@ pub fn scrollIntoViewIfNeeded(self: *Element, center_if_needed: ?bool, frame: *F
|
||||
_ = center_if_needed;
|
||||
const y = calculateDocumentPosition(self.asNode());
|
||||
const scroll_y: f64 = @floatFromInt(frame.window.getScrollY());
|
||||
const viewport_height: f64 = @floatFromInt(frame.window.getInnerHeight());
|
||||
const viewport_height: f64 = @floatFromInt(frame.window.getInnerHeight(frame));
|
||||
if (y >= scroll_y and y <= scroll_y + viewport_height) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ const js = @import("../js/js.zig");
|
||||
|
||||
const Page = @import("../Page.zig");
|
||||
const Frame = @import("../Frame.zig");
|
||||
const Viewport = @import("../Viewport.zig");
|
||||
|
||||
const Node = @import("Node.zig");
|
||||
const Element = @import("Element.zig");
|
||||
@@ -208,13 +207,15 @@ fn calculateIntersection(
|
||||
// Use root element's rect or the faux-layout viewport.
|
||||
const root_rect = if (self._root) |root|
|
||||
root.getBoundingClientRect(frame)
|
||||
else
|
||||
DOMRect{
|
||||
else blk: {
|
||||
const viewport = frame._page.getViewport();
|
||||
break :blk DOMRect{
|
||||
._x = 0.0,
|
||||
._y = 0.0,
|
||||
._width = @floatFromInt(Viewport.default.width),
|
||||
._height = @floatFromInt(Viewport.default.height),
|
||||
._width = @floatFromInt(viewport.width),
|
||||
._height = @floatFromInt(viewport.height),
|
||||
};
|
||||
};
|
||||
|
||||
// For a headless browser without real layout, we treat all elements as fully visible.
|
||||
// This avoids fingerprinting issues (massive viewports) and matches the behavior
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
|
||||
const js = @import("../js/js.zig");
|
||||
const Frame = @import("../Frame.zig");
|
||||
const Viewport = @import("../Viewport.zig");
|
||||
const EventTarget = @import("EventTarget.zig");
|
||||
|
||||
pub fn registerTypes() []const type {
|
||||
@@ -46,6 +45,14 @@ pub fn getOrientation(self: *Screen, frame: *Frame) !*Orientation {
|
||||
return orientation;
|
||||
}
|
||||
|
||||
pub fn getWidth(_: *const Screen, frame: *Frame) u32 {
|
||||
return frame._page.getViewport().width;
|
||||
}
|
||||
|
||||
pub fn getHeight(_: *const Screen, frame: *Frame) u32 {
|
||||
return frame._page.getViewport().height;
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(Screen);
|
||||
|
||||
@@ -55,9 +62,9 @@ pub const JsApi = struct {
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const width = bridge.property(Viewport.default.width, .{ .template = false });
|
||||
pub const height = bridge.property(Viewport.default.height, .{ .template = false });
|
||||
pub const availWidth = bridge.property(Viewport.default.width, .{ .template = false });
|
||||
pub const width = bridge.accessor(Screen.getWidth, null, .{});
|
||||
pub const height = bridge.accessor(Screen.getHeight, null, .{});
|
||||
pub const availWidth = bridge.accessor(Screen.getWidth, null, .{});
|
||||
pub const availHeight = bridge.property(1040, .{ .template = false });
|
||||
pub const colorDepth = bridge.property(24, .{ .template = false });
|
||||
pub const pixelDepth = bridge.property(24, .{ .template = false });
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
|
||||
const js = @import("../js/js.zig");
|
||||
const Frame = @import("../Frame.zig");
|
||||
const Viewport = @import("../Viewport.zig");
|
||||
const EventTarget = @import("EventTarget.zig");
|
||||
|
||||
const VisualViewport = @This();
|
||||
@@ -37,6 +36,14 @@ pub fn getPageTop(_: *const VisualViewport, frame: *Frame) u32 {
|
||||
return frame.window.getScrollY();
|
||||
}
|
||||
|
||||
pub fn getWidth(_: *const VisualViewport, frame: *Frame) u32 {
|
||||
return frame._page.getViewport().width;
|
||||
}
|
||||
|
||||
pub fn getHeight(_: *const VisualViewport, frame: *Frame) u32 {
|
||||
return frame._page.getViewport().height;
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(VisualViewport);
|
||||
|
||||
@@ -52,7 +59,7 @@ pub const JsApi = struct {
|
||||
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(Viewport.default.width, .{ .template = false });
|
||||
pub const height = bridge.property(Viewport.default.height, .{ .template = false });
|
||||
pub const width = bridge.accessor(VisualViewport.getWidth, null, .{});
|
||||
pub const height = bridge.accessor(VisualViewport.getHeight, null, .{});
|
||||
pub const scale = bridge.property(1.0, .{ .template = false });
|
||||
};
|
||||
|
||||
@@ -23,7 +23,6 @@ const builtin = @import("builtin");
|
||||
const js = @import("../js/js.zig");
|
||||
const URL = @import("../URL.zig");
|
||||
const Frame = @import("../Frame.zig");
|
||||
const Viewport = @import("../Viewport.zig");
|
||||
const Console = @import("Console.zig");
|
||||
const History = @import("History.zig");
|
||||
const Navigation = @import("navigation/Navigation.zig");
|
||||
@@ -195,6 +194,14 @@ pub fn setLength(_: *Window, value: js.Value) void {
|
||||
replaceGlobalProperty(value, "length");
|
||||
}
|
||||
|
||||
pub fn setInnerWidth(_: *Window, value: js.Value) void {
|
||||
replaceGlobalProperty(value, "innerWidth");
|
||||
}
|
||||
|
||||
pub fn setInnerHeight(_: *Window, value: js.Value) void {
|
||||
replaceGlobalProperty(value, "innerHeight");
|
||||
}
|
||||
|
||||
pub fn setScrollX(_: *Window, value: js.Value) void {
|
||||
replaceGlobalProperty(value, "scrollX");
|
||||
}
|
||||
@@ -727,10 +734,14 @@ pub fn getScrollY(self: *const Window) u32 {
|
||||
return self._scroll_pos.y;
|
||||
}
|
||||
|
||||
pub fn getInnerWidth(_: *const Window, frame: *Frame) u32 {
|
||||
return frame._page.getViewport().width;
|
||||
}
|
||||
|
||||
// Faux-layout viewport height, used to decide whether an element is already
|
||||
// within view (e.g. scrollIntoViewIfNeeded).
|
||||
pub fn getInnerHeight(_: *const Window) u32 {
|
||||
return Viewport.default.height;
|
||||
pub fn getInnerHeight(_: *const Window, frame: *Frame) u32 {
|
||||
return frame._page.getViewport().height;
|
||||
}
|
||||
|
||||
const ScrollToOpts = union(enum) {
|
||||
@@ -1042,9 +1053,11 @@ pub const JsApi = struct {
|
||||
// sites not to try to access those features
|
||||
pub const isSecureContext = bridge.property(false, .{ .template = false });
|
||||
|
||||
// [Replaceable] (CSSOM-View): writable so assignment overwrites rather than throws.
|
||||
pub const innerWidth = bridge.property(Viewport.default.width, .{ .template = false, .readonly = false });
|
||||
pub const innerHeight = bridge.property(Viewport.default.height, .{ .template = false, .readonly = false });
|
||||
// [Replaceable] (CSSOM-View): the getter reads the page's runtime viewport
|
||||
// (overridable via Emulation.setDeviceMetricsOverride); the setter overwrites
|
||||
// the attribute rather than throwing.
|
||||
pub const innerWidth = bridge.accessor(Window.getInnerWidth, Window.setInnerWidth, .{});
|
||||
pub const innerHeight = bridge.accessor(Window.getInnerHeight, Window.setInnerHeight, .{});
|
||||
pub const devicePixelRatio = bridge.property(1, .{ .template = false, .readonly = false });
|
||||
|
||||
pub const opener = bridge.accessor(Window.getOpener, null, .{});
|
||||
|
||||
@@ -18,9 +18,9 @@
|
||||
|
||||
// zlint-disable unused-decls
|
||||
const js = @import("../../js/js.zig");
|
||||
const Frame = @import("../../Frame.zig");
|
||||
const EventTarget = @import("../EventTarget.zig");
|
||||
const MediaQuery = @import("../../css/MediaQuery.zig");
|
||||
const Viewport = @import("../../Viewport.zig");
|
||||
|
||||
const MediaQueryList = @This();
|
||||
|
||||
@@ -40,18 +40,17 @@ pub fn getMedia(self: *const MediaQueryList) []const u8 {
|
||||
}
|
||||
|
||||
/// Re-evaluates the stored query against the current viewport on every call
|
||||
/// so the result stays in sync if viewport emulation later lands. The
|
||||
/// viewport currently comes from `Viewport.default` (1920×1080), matching
|
||||
/// `Window.innerWidth` / `innerHeight`.
|
||||
pub fn getMatches(self: *const MediaQueryList) bool {
|
||||
return MediaQuery.matches(self._media, Viewport.default);
|
||||
/// so the result stays in sync with viewport emulation. The viewport comes
|
||||
/// from the page (overridable via Emulation.setDeviceMetricsOverride),
|
||||
/// matching `Window.innerWidth` / `innerHeight`.
|
||||
pub fn getMatches(self: *const MediaQueryList, frame: *Frame) bool {
|
||||
return MediaQuery.matches(self._media, frame._page.getViewport());
|
||||
}
|
||||
|
||||
// TODO: once viewport emulation lands, `change` events need to fire on
|
||||
// every listener registered here (and via `addEventListener("change", ...)`
|
||||
// through the EventTarget proto) when the viewport crosses the breakpoint
|
||||
// that flips `matches`. Today the viewport is a constant, so no event can
|
||||
// ever fire — the registrations are no-ops rather than stored hooks.
|
||||
// TODO: `change` events need to fire on every listener registered here (and
|
||||
// via `addEventListener("change", ...)` through the EventTarget proto) when a
|
||||
// viewport override crosses the breakpoint that flips `matches`. Today these
|
||||
// registrations are no-ops rather than stored hooks.
|
||||
pub fn addListener(_: *const MediaQueryList, _: js.Function) void {}
|
||||
pub fn removeListener(_: *const MediaQueryList, _: js.Function) void {}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ pub fn processMessage(cmd: *CDP.Command) !void {
|
||||
setEmulatedMedia,
|
||||
setFocusEmulationEnabled,
|
||||
setDeviceMetricsOverride,
|
||||
clearDeviceMetricsOverride,
|
||||
setTouchEmulationEnabled,
|
||||
setUserAgentOverride,
|
||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||
@@ -37,6 +38,7 @@ pub fn processMessage(cmd: *CDP.Command) !void {
|
||||
.setEmulatedMedia => return setEmulatedMedia(cmd),
|
||||
.setFocusEmulationEnabled => return setFocusEmulationEnabled(cmd),
|
||||
.setDeviceMetricsOverride => return setDeviceMetricsOverride(cmd),
|
||||
.clearDeviceMetricsOverride => return clearDeviceMetricsOverride(cmd),
|
||||
.setTouchEmulationEnabled => return setTouchEmulationEnabled(cmd),
|
||||
.setUserAgentOverride => return setUserAgentOverride(cmd),
|
||||
}
|
||||
@@ -63,8 +65,53 @@ fn setFocusEmulationEnabled(cmd: *CDP.Command) !void {
|
||||
return cmd.sendResult(null, .{});
|
||||
}
|
||||
|
||||
// TODO: noop method
|
||||
fn setDeviceMetricsOverride(cmd: *CDP.Command) !void {
|
||||
const params = (try cmd.params(struct {
|
||||
width: u32,
|
||||
height: u32,
|
||||
deviceScaleFactor: ?f64 = null,
|
||||
mobile: ?bool = null,
|
||||
scale: ?f64 = null,
|
||||
screenWidth: ?u32 = null,
|
||||
screenHeight: ?u32 = null,
|
||||
})) orelse return error.InvalidParams;
|
||||
|
||||
// Not-yet-emulated parameters: accept them but warn so the caller knows
|
||||
// they are ignored.
|
||||
if (params.deviceScaleFactor) |v| {
|
||||
if (v != 0) log.warn(.not_implemented, "Emulation.setDeviceMetricsOverride", .{ .param = "deviceScaleFactor", .value = v });
|
||||
}
|
||||
if (params.mobile) |v| {
|
||||
if (v) log.warn(.not_implemented, "Emulation.setDeviceMetricsOverride", .{ .param = "mobile", .value = v });
|
||||
}
|
||||
if (params.scale) |v| {
|
||||
if (v != 0) log.warn(.not_implemented, "Emulation.setDeviceMetricsOverride", .{ .param = "scale", .value = v });
|
||||
}
|
||||
if (params.screenWidth) |v| {
|
||||
if (v != 0) log.warn(.not_implemented, "Emulation.setDeviceMetricsOverride", .{ .param = "screenWidth", .value = v });
|
||||
}
|
||||
if (params.screenHeight) |v| {
|
||||
if (v != 0) log.warn(.not_implemented, "Emulation.setDeviceMetricsOverride", .{ .param = "screenHeight", .value = v });
|
||||
}
|
||||
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
const page = bc.session.currentPage() orelse return error.FrameNotLoaded;
|
||||
|
||||
// CDP convention: a 0 width/height means "don't override that dimension",
|
||||
// so keep the current value for any dimension passed as 0.
|
||||
const current = page.getViewport();
|
||||
page.viewport_override = .{
|
||||
.width = if (params.width > 0) params.width else current.width,
|
||||
.height = if (params.height > 0) params.height else current.height,
|
||||
};
|
||||
|
||||
return cmd.sendResult(null, .{});
|
||||
}
|
||||
|
||||
fn clearDeviceMetricsOverride(cmd: *CDP.Command) !void {
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
const page = bc.session.currentPage() orelse return error.FrameNotLoaded;
|
||||
page.viewport_override = null;
|
||||
return cmd.sendResult(null, .{});
|
||||
}
|
||||
|
||||
@@ -216,3 +263,35 @@ test "cdp.Emulation: setUserAgentOverride can be called multiple times" {
|
||||
|
||||
try ctx.expectSentResult(null, .{ .id = 7 });
|
||||
}
|
||||
|
||||
test "cdp.Emulation: setDeviceMetricsOverride and clear" {
|
||||
var ctx = try testing.context();
|
||||
defer ctx.deinit();
|
||||
|
||||
const bc = try ctx.loadBrowserContext(.{ .id = "BID-DM1" });
|
||||
_ = try bc.session.createPage();
|
||||
const page = bc.session.currentPage().?;
|
||||
|
||||
// Defaults to the compile-time viewport before any override.
|
||||
try testing.expectEqual(1920, page.getViewport().width);
|
||||
try testing.expectEqual(1080, page.getViewport().height);
|
||||
|
||||
try ctx.processMessage(.{
|
||||
.id = 8,
|
||||
.method = "Emulation.setDeviceMetricsOverride",
|
||||
.params = .{ .width = 375, .height = 812 },
|
||||
});
|
||||
|
||||
try ctx.expectSentResult(null, .{ .id = 8 });
|
||||
try testing.expectEqual(375, page.getViewport().width);
|
||||
try testing.expectEqual(812, page.getViewport().height);
|
||||
|
||||
try ctx.processMessage(.{
|
||||
.id = 9,
|
||||
.method = "Emulation.clearDeviceMetricsOverride",
|
||||
});
|
||||
|
||||
try ctx.expectSentResult(null, .{ .id = 9 });
|
||||
try testing.expectEqual(1920, page.getViewport().width);
|
||||
try testing.expectEqual(1080, page.getViewport().height);
|
||||
}
|
||||
|
||||
@@ -926,7 +926,11 @@ fn printToPDF(cmd: *CDP.Command) !void {
|
||||
}
|
||||
|
||||
fn getLayoutMetrics(cmd: *CDP.Command) !void {
|
||||
const viewport = @import("../../browser/Viewport.zig").default;
|
||||
const BrowserViewport = @import("../../browser/Viewport.zig");
|
||||
const viewport = if (cmd.browser_context) |bc|
|
||||
if (bc.session.currentPage()) |page| page.getViewport() else BrowserViewport.default
|
||||
else
|
||||
BrowserViewport.default;
|
||||
const width = viewport.width;
|
||||
const height = viewport.height;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user