Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 117 additions & 1 deletion src/browser/browser.zig
Original file line number Diff line number Diff line change
Expand Up @@ -298,10 +298,14 @@ pub const Page = struct {
// current_script could by fetch module to resolve module's url to fetch.
current_script: ?*const Script = null,

renderer: FlatRenderer,

fn init(session: *Session) Page {
const arena = session.browser.page_arena.allocator();
return .{
.arena = arena,
.session = session,
.arena = session.browser.page_arena.allocator(),
.renderer = FlatRenderer.init(arena),
};
}

Expand Down Expand Up @@ -423,6 +427,53 @@ pub const Page = struct {
}
}

pub const ClickResult = union(enum) {
navigate: std.Uri,
};

pub const MouseEvent = struct {
x: i32,
y: i32,
type: Type,

const Type = enum {
pressed,
released,
};
};

pub fn mouseEvent(self: *Page, allocator: Allocator, me: MouseEvent) !?ClickResult {
if (me.type != .pressed) {
return null;
}

const element = self.renderer.getElementAtPosition(me.x, me.y) orelse return null;

const event = try parser.mouseEventCreate();
defer parser.mouseEventDestroy(event);
try parser.mouseEventInit(event, "click", .{
.bubbles = true,
.cancelable = true,
.x = me.x,
.y = me.y,
});
_ = try parser.elementDispatchEvent(element, @ptrCast(event));

if ((try parser.mouseEventDefaultPrevented(event)) == true) {
return null;
}

const node = parser.elementToNode(element);
const tag = try parser.nodeName(node);
if (std.ascii.eqlIgnoreCase(tag, "a")) {
const href = (try parser.elementGetAttribute(element, "href")) orelse return null;
var buf = try allocator.alloc(u8, 1024);
return .{ .navigate = try std.Uri.resolve_inplace(self.uri, href, &buf) };
}

return null;
}

// https://html.spec.whatwg.org/#read-html
fn loadHTMLDoc(self: *Page, reader: anytype, charset: []const u8, aux_data: ?[]const u8) !void {
const arena = self.arena;
Expand Down Expand Up @@ -462,6 +513,7 @@ pub const Page = struct {
try session.env.setUserContext(.{
.uri = self.uri,
.document = html_doc,
.renderer = @ptrCast(&self.renderer),
.cookie_jar = @ptrCast(&self.session.cookie_jar),
.http_client = @ptrCast(self.session.http_client),
});
Expand Down Expand Up @@ -753,6 +805,70 @@ pub const Page = struct {
};
};

// provide very poor abstration to the rest of the code. In theory, we can change
// the FlatRendere to a different implementation, and it'll all just work.
pub const Renderer = FlatRenderer;

// This "renderer" positions elements in a single row in an unspecified order.
// The important thing is that elements have a consistent position/index within
// that row, which can be turned into a rectangle.
const FlatRenderer = struct {
allocator: Allocator,

// key is a @ptrFromInt of the element
// value is the index position
positions: std.AutoHashMapUnmanaged(u64, u32),

// given an index, get the element
elements: std.ArrayListUnmanaged(u64),

const Element = @import("../dom/element.zig").Element;

// we expect allocator to be an arena
pub fn init(allocator: Allocator) FlatRenderer {
return .{
.elements = .{},
.positions = .{},
.allocator = allocator,
};
}

pub fn getRect(self: *FlatRenderer, e: *parser.Element) !Element.DOMRect {
var elements = &self.elements;
const gop = try self.positions.getOrPut(self.allocator, @intFromPtr(e));
var x: u32 = gop.value_ptr.*;
if (gop.found_existing == false) {
try elements.append(self.allocator, @intFromPtr(e));
x = @intCast(elements.items.len);
gop.value_ptr.* = x;
}

return .{
.x = @floatFromInt(x),
.y = 0.0,
.width = 1.0,
.height = 1.0,
};
}

pub fn width(self: *const FlatRenderer) u32 {
return @intCast(self.elements.items.len);
}

pub fn height(_: *const FlatRenderer) u32 {
return 1;
}

pub fn getElementAtPosition(self: *const FlatRenderer, x: i32, y: i32) ?*parser.Element {
if (y != 1 or x < 0) {
return null;
}

const elements = self.elements.items;
return if (x < elements.len) @ptrFromInt(elements[@intCast(x)]) else null;
}
};

const NoopInspector = struct {
pub fn onInspectorResponse(_: *anyopaque, _: u32, _: []const u8) void {}
pub fn onInspectorEvent(_: *anyopaque, _: []const u8) void {}
Expand Down
11 changes: 8 additions & 3 deletions src/cdp/cdp.zig
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ pub const CDP = CDPT(struct {

const SessionIdGen = Incrementing(u32, "SID");
const TargetIdGen = Incrementing(u32, "TID");
const LoaderIdGen = Incrementing(u32, "LID");
const BrowserContextIdGen = Incrementing(u32, "BID");

// Generic so that we can inject mocks into it.
Expand All @@ -54,6 +55,7 @@ pub fn CDPT(comptime TypeProvider: type) type {
target_auto_attach: bool = false,

target_id_gen: TargetIdGen = .{},
loader_id_gen: LoaderIdGen = .{},
session_id_gen: SessionIdGen = .{},
browser_context_id_gen: BrowserContextIdGen = .{},

Expand Down Expand Up @@ -183,6 +185,7 @@ pub fn CDPT(comptime TypeProvider: type) type {
},
5 => switch (@as(u40, @bitCast(domain[0..5].*))) {
asUint("Fetch") => return @import("domains/fetch.zig").processMessage(command),
asUint("Input") => return @import("domains/input.zig").processMessage(command),
else => {},
},
6 => switch (@as(u48, @bitCast(domain[0..6].*))) {
Expand Down Expand Up @@ -281,8 +284,6 @@ pub fn BrowserContext(comptime CDP_T: type) type {
// we should reject it.
session_id: ?[]const u8,

// State
url: []const u8,
loader_id: []const u8,
security_origin: []const u8,
page_life_cycle_events: bool,
Expand All @@ -303,7 +304,6 @@ pub fn BrowserContext(comptime CDP_T: type) type {
.cdp = cdp,
.target_id = null,
.session_id = null,
.url = URL_BASE,
.security_origin = URL_BASE,
.secure_context_type = "Secure", // TODO = enum
.loader_id = LOADER_ID,
Expand Down Expand Up @@ -333,6 +333,11 @@ pub fn BrowserContext(comptime CDP_T: type) type {
};
}

pub fn getURL(self: *const Self) ?[]const u8 {
const page = self.session.currentPage() orelse return null;
return page.rawuri;
}

pub fn onInspectorResponse(ctx: *anyopaque, _: u32, msg: []const u8) void {
if (std.log.defaultLogEnabled(.debug)) {
// msg should be {"id":<id>,...
Expand Down
100 changes: 100 additions & 0 deletions src/cdp/domains/input.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// 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 <https://www.gnu.org/licenses/>.

const std = @import("std");
const Page = @import("../../browser/browser.zig").Page;

pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum {
dispatchMouseEvent,
}, cmd.input.action) orelse return error.UnknownMethod;

switch (action) {
.dispatchMouseEvent => return dispatchMouseEvent(cmd),
}
}

// https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchMouseEvent
fn dispatchMouseEvent(cmd: anytype) !void {
const params = (try cmd.params(struct {
x: i32,
y: i32,
type: Type,

const Type = enum {
mousePressed,
mouseReleased,
mouseMoved,
mouseWheel,
};
})) orelse return error.InvalidParams;

try cmd.sendResult(null, .{});

// quickly ignore types we know we don't handle
switch (params.type) {
.mouseMoved, .mouseWheel => return,
else => {},
}

const bc = cmd.browser_context orelse return;
const page = bc.session.currentPage() orelse return;

const mouse_event = Page.MouseEvent{
.x = params.x,
.y = params.y,
.type = switch (params.type) {
.mousePressed => .pressed,
.mouseReleased => .released,
else => unreachable,
},
};
const click_result = (try page.mouseEvent(cmd.arena, mouse_event)) orelse return;

switch (click_result) {
.navigate => |uri| try clickNavigate(cmd, uri),
}
// result already sent
}

fn clickNavigate(cmd: anytype, uri: std.Uri) !void {
const bc = cmd.browser_context.?;

var url_buf: std.ArrayListUnmanaged(u8) = .{};
try uri.writeToStream(.{
.scheme = true,
.authentication = true,
.authority = true,
.port = true,
.path = true,
.query = true,
}, url_buf.writer(cmd.arena));
const url = url_buf.items;

try cmd.sendEvent("Page.frameRequestedNavigation", .{
.url = url,
.frameId = bc.target_id.?,
.reason = "anchorClick",
.disposition = "currentTab",
}, .{ .session_id = bc.session_id.? });

bc.session.removePage();
_ = try bc.session.createPage(null);

try @import("page.zig").navigateToUrl(cmd, url, false);
}
43 changes: 23 additions & 20 deletions src/cdp/domains/page.zig
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,10 @@ fn getFrameTree(cmd: anytype) !void {
return cmd.sendResult(.{
.frameTree = .{
.frame = Frame{
.url = bc.url,
.id = target_id,
.loaderId = bc.loader_id,
.securityOrigin = bc.security_origin,
.url = bc.getURL() orelse "about:blank",
.secureContextType = bc.secure_context_type,
},
},
Expand Down Expand Up @@ -129,6 +129,18 @@ fn createIsolatedWorld(cmd: anytype) !void {
}

fn navigate(cmd: anytype) !void {
const params = (try cmd.params(struct {
url: []const u8,
// referrer: ?[]const u8 = null,
// transitionType: ?[]const u8 = null, // TODO: enum
// frameId: ?[]const u8 = null,
// referrerPolicy: ?[]const u8 = null, // TODO: enum
})) orelse return error.InvalidParams;

return navigateToUrl(cmd, params.url, true);
}

pub fn navigateToUrl(cmd: anytype, url: []const u8, send_result: bool) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;

// didn't create?
Expand All @@ -140,20 +152,9 @@ fn navigate(cmd: anytype) !void {
// if we have a target_id we have to have a page;
std.debug.assert(bc.session.page != null);

const params = (try cmd.params(struct {
url: []const u8,
referrer: ?[]const u8 = null,
transitionType: ?[]const u8 = null, // TODO: enum
frameId: ?[]const u8 = null,
referrerPolicy: ?[]const u8 = null, // TODO: enum
})) orelse return error.InvalidParams;

// change state
bc.reset();
bc.url = params.url;

// TODO: hard coded ID
bc.loader_id = "AF8667A203C5392DBE9AC290044AA4C2";
bc.loader_id = cmd.cdp.loader_id_gen.next();

const LifecycleEvent = struct {
frameId: []const u8,
Expand All @@ -180,10 +181,12 @@ fn navigate(cmd: anytype) !void {
}

// output
try cmd.sendResult(.{
.frameId = target_id,
.loaderId = bc.loader_id,
}, .{});
if (send_result) {
try cmd.sendResult(.{
.frameId = target_id,
.loaderId = bc.loader_id,
}, .{});
}

// TODO: at this point do we need async the following actions to be async?

Expand All @@ -199,7 +202,7 @@ fn navigate(cmd: anytype) !void {
);

var page = bc.session.currentPage().?;
try page.navigate(params.url, aux_data);
try page.navigate(url, aux_data);

// Events

Expand All @@ -218,7 +221,7 @@ fn navigate(cmd: anytype) !void {
.type = "Navigation",
.frame = Frame{
.id = target_id,
.url = bc.url,
.url = url,
.securityOrigin = bc.security_origin,
.secureContextType = bc.secure_context_type,
.loaderId = bc.loader_id,
Expand Down Expand Up @@ -281,7 +284,7 @@ test "cdp.page: getFrameTree" {
.frame = .{
.id = "TID-3",
.loaderId = bc.loader_id,
.url = bc.url,
.url = "about:blank",
.domainAndRegistry = "",
.securityOrigin = bc.security_origin,
.mimeType = "text/html",
Expand Down
Loading
Loading