Skip to content

Commit 07e8dfa

Browse files
Merge pull request #501 from karlseguin/renderer
Add a dumb renderer to get coordinates
2 parents 6475752 + 0fbf48a commit 07e8dfa

File tree

11 files changed

+378
-31
lines changed

11 files changed

+378
-31
lines changed

src/browser/browser.zig

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,10 +298,14 @@ pub const Page = struct {
298298
// current_script could by fetch module to resolve module's url to fetch.
299299
current_script: ?*const Script = null,
300300

301+
renderer: FlatRenderer,
302+
301303
fn init(session: *Session) Page {
304+
const arena = session.browser.page_arena.allocator();
302305
return .{
306+
.arena = arena,
303307
.session = session,
304-
.arena = session.browser.page_arena.allocator(),
308+
.renderer = FlatRenderer.init(arena),
305309
};
306310
}
307311

@@ -423,6 +427,53 @@ pub const Page = struct {
423427
}
424428
}
425429

430+
pub const ClickResult = union(enum) {
431+
navigate: std.Uri,
432+
};
433+
434+
pub const MouseEvent = struct {
435+
x: i32,
436+
y: i32,
437+
type: Type,
438+
439+
const Type = enum {
440+
pressed,
441+
released,
442+
};
443+
};
444+
445+
pub fn mouseEvent(self: *Page, allocator: Allocator, me: MouseEvent) !?ClickResult {
446+
if (me.type != .pressed) {
447+
return null;
448+
}
449+
450+
const element = self.renderer.getElementAtPosition(me.x, me.y) orelse return null;
451+
452+
const event = try parser.mouseEventCreate();
453+
defer parser.mouseEventDestroy(event);
454+
try parser.mouseEventInit(event, "click", .{
455+
.bubbles = true,
456+
.cancelable = true,
457+
.x = me.x,
458+
.y = me.y,
459+
});
460+
_ = try parser.elementDispatchEvent(element, @ptrCast(event));
461+
462+
if ((try parser.mouseEventDefaultPrevented(event)) == true) {
463+
return null;
464+
}
465+
466+
const node = parser.elementToNode(element);
467+
const tag = try parser.nodeName(node);
468+
if (std.ascii.eqlIgnoreCase(tag, "a")) {
469+
const href = (try parser.elementGetAttribute(element, "href")) orelse return null;
470+
var buf = try allocator.alloc(u8, 1024);
471+
return .{ .navigate = try std.Uri.resolve_inplace(self.uri, href, &buf) };
472+
}
473+
474+
return null;
475+
}
476+
426477
// https://html.spec.whatwg.org/#read-html
427478
fn loadHTMLDoc(self: *Page, reader: anytype, charset: []const u8, aux_data: ?[]const u8) !void {
428479
const arena = self.arena;
@@ -462,6 +513,7 @@ pub const Page = struct {
462513
try session.env.setUserContext(.{
463514
.uri = self.uri,
464515
.document = html_doc,
516+
.renderer = @ptrCast(&self.renderer),
465517
.cookie_jar = @ptrCast(&self.session.cookie_jar),
466518
.http_client = @ptrCast(self.session.http_client),
467519
});
@@ -753,6 +805,70 @@ pub const Page = struct {
753805
};
754806
};
755807

808+
// provide very poor abstration to the rest of the code. In theory, we can change
809+
// the FlatRendere to a different implementation, and it'll all just work.
810+
pub const Renderer = FlatRenderer;
811+
812+
// This "renderer" positions elements in a single row in an unspecified order.
813+
// The important thing is that elements have a consistent position/index within
814+
// that row, which can be turned into a rectangle.
815+
const FlatRenderer = struct {
816+
allocator: Allocator,
817+
818+
// key is a @ptrFromInt of the element
819+
// value is the index position
820+
positions: std.AutoHashMapUnmanaged(u64, u32),
821+
822+
// given an index, get the element
823+
elements: std.ArrayListUnmanaged(u64),
824+
825+
const Element = @import("../dom/element.zig").Element;
826+
827+
// we expect allocator to be an arena
828+
pub fn init(allocator: Allocator) FlatRenderer {
829+
return .{
830+
.elements = .{},
831+
.positions = .{},
832+
.allocator = allocator,
833+
};
834+
}
835+
836+
pub fn getRect(self: *FlatRenderer, e: *parser.Element) !Element.DOMRect {
837+
var elements = &self.elements;
838+
const gop = try self.positions.getOrPut(self.allocator, @intFromPtr(e));
839+
var x: u32 = gop.value_ptr.*;
840+
if (gop.found_existing == false) {
841+
try elements.append(self.allocator, @intFromPtr(e));
842+
x = @intCast(elements.items.len);
843+
gop.value_ptr.* = x;
844+
}
845+
846+
return .{
847+
.x = @floatFromInt(x),
848+
.y = 0.0,
849+
.width = 1.0,
850+
.height = 1.0,
851+
};
852+
}
853+
854+
pub fn width(self: *const FlatRenderer) u32 {
855+
return @intCast(self.elements.items.len);
856+
}
857+
858+
pub fn height(_: *const FlatRenderer) u32 {
859+
return 1;
860+
}
861+
862+
pub fn getElementAtPosition(self: *const FlatRenderer, x: i32, y: i32) ?*parser.Element {
863+
if (y != 1 or x < 0) {
864+
return null;
865+
}
866+
867+
const elements = self.elements.items;
868+
return if (x < elements.len) @ptrFromInt(elements[@intCast(x)]) else null;
869+
}
870+
};
871+
756872
const NoopInspector = struct {
757873
pub fn onInspectorResponse(_: *anyopaque, _: u32, _: []const u8) void {}
758874
pub fn onInspectorEvent(_: *anyopaque, _: []const u8) void {}

src/cdp/cdp.zig

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ pub const CDP = CDPT(struct {
3737

3838
const SessionIdGen = Incrementing(u32, "SID");
3939
const TargetIdGen = Incrementing(u32, "TID");
40+
const LoaderIdGen = Incrementing(u32, "LID");
4041
const BrowserContextIdGen = Incrementing(u32, "BID");
4142

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

5657
target_id_gen: TargetIdGen = .{},
58+
loader_id_gen: LoaderIdGen = .{},
5759
session_id_gen: SessionIdGen = .{},
5860
browser_context_id_gen: BrowserContextIdGen = .{},
5961

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

284-
// State
285-
url: []const u8,
286287
loader_id: []const u8,
287288
security_origin: []const u8,
288289
page_life_cycle_events: bool,
@@ -303,7 +304,6 @@ pub fn BrowserContext(comptime CDP_T: type) type {
303304
.cdp = cdp,
304305
.target_id = null,
305306
.session_id = null,
306-
.url = URL_BASE,
307307
.security_origin = URL_BASE,
308308
.secure_context_type = "Secure", // TODO = enum
309309
.loader_id = LOADER_ID,
@@ -333,6 +333,11 @@ pub fn BrowserContext(comptime CDP_T: type) type {
333333
};
334334
}
335335

336+
pub fn getURL(self: *const Self) ?[]const u8 {
337+
const page = self.session.currentPage() orelse return null;
338+
return page.rawuri;
339+
}
340+
336341
pub fn onInspectorResponse(ctx: *anyopaque, _: u32, msg: []const u8) void {
337342
if (std.log.defaultLogEnabled(.debug)) {
338343
// msg should be {"id":<id>,...

src/cdp/domains/input.zig

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2+
//
3+
// Francis Bouvier <[email protected]>
4+
// Pierre Tachoire <[email protected]>
5+
//
6+
// This program is free software: you can redistribute it and/or modify
7+
// it under the terms of the GNU Affero General Public License as
8+
// published by the Free Software Foundation, either version 3 of the
9+
// License, or (at your option) any later version.
10+
//
11+
// This program is distributed in the hope that it will be useful,
12+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
// GNU Affero General Public License for more details.
15+
//
16+
// You should have received a copy of the GNU Affero General Public License
17+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
19+
const std = @import("std");
20+
const Page = @import("../../browser/browser.zig").Page;
21+
22+
pub fn processMessage(cmd: anytype) !void {
23+
const action = std.meta.stringToEnum(enum {
24+
dispatchMouseEvent,
25+
}, cmd.input.action) orelse return error.UnknownMethod;
26+
27+
switch (action) {
28+
.dispatchMouseEvent => return dispatchMouseEvent(cmd),
29+
}
30+
}
31+
32+
// https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchMouseEvent
33+
fn dispatchMouseEvent(cmd: anytype) !void {
34+
const params = (try cmd.params(struct {
35+
x: i32,
36+
y: i32,
37+
type: Type,
38+
39+
const Type = enum {
40+
mousePressed,
41+
mouseReleased,
42+
mouseMoved,
43+
mouseWheel,
44+
};
45+
})) orelse return error.InvalidParams;
46+
47+
try cmd.sendResult(null, .{});
48+
49+
// quickly ignore types we know we don't handle
50+
switch (params.type) {
51+
.mouseMoved, .mouseWheel => return,
52+
else => {},
53+
}
54+
55+
const bc = cmd.browser_context orelse return;
56+
const page = bc.session.currentPage() orelse return;
57+
58+
const mouse_event = Page.MouseEvent{
59+
.x = params.x,
60+
.y = params.y,
61+
.type = switch (params.type) {
62+
.mousePressed => .pressed,
63+
.mouseReleased => .released,
64+
else => unreachable,
65+
},
66+
};
67+
const click_result = (try page.mouseEvent(cmd.arena, mouse_event)) orelse return;
68+
69+
switch (click_result) {
70+
.navigate => |uri| try clickNavigate(cmd, uri),
71+
}
72+
// result already sent
73+
}
74+
75+
fn clickNavigate(cmd: anytype, uri: std.Uri) !void {
76+
const bc = cmd.browser_context.?;
77+
78+
var url_buf: std.ArrayListUnmanaged(u8) = .{};
79+
try uri.writeToStream(.{
80+
.scheme = true,
81+
.authentication = true,
82+
.authority = true,
83+
.port = true,
84+
.path = true,
85+
.query = true,
86+
}, url_buf.writer(cmd.arena));
87+
const url = url_buf.items;
88+
89+
try cmd.sendEvent("Page.frameRequestedNavigation", .{
90+
.url = url,
91+
.frameId = bc.target_id.?,
92+
.reason = "anchorClick",
93+
.disposition = "currentTab",
94+
}, .{ .session_id = bc.session_id.? });
95+
96+
bc.session.removePage();
97+
_ = try bc.session.createPage(null);
98+
99+
try @import("page.zig").navigateToUrl(cmd, url, false);
100+
}

src/cdp/domains/page.zig

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,10 @@ fn getFrameTree(cmd: anytype) !void {
6161
return cmd.sendResult(.{
6262
.frameTree = .{
6363
.frame = Frame{
64-
.url = bc.url,
6564
.id = target_id,
6665
.loaderId = bc.loader_id,
6766
.securityOrigin = bc.security_origin,
67+
.url = bc.getURL() orelse "about:blank",
6868
.secureContextType = bc.secure_context_type,
6969
},
7070
},
@@ -129,6 +129,18 @@ fn createIsolatedWorld(cmd: anytype) !void {
129129
}
130130

131131
fn navigate(cmd: anytype) !void {
132+
const params = (try cmd.params(struct {
133+
url: []const u8,
134+
// referrer: ?[]const u8 = null,
135+
// transitionType: ?[]const u8 = null, // TODO: enum
136+
// frameId: ?[]const u8 = null,
137+
// referrerPolicy: ?[]const u8 = null, // TODO: enum
138+
})) orelse return error.InvalidParams;
139+
140+
return navigateToUrl(cmd, params.url, true);
141+
}
142+
143+
pub fn navigateToUrl(cmd: anytype, url: []const u8, send_result: bool) !void {
132144
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
133145

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

143-
const params = (try cmd.params(struct {
144-
url: []const u8,
145-
referrer: ?[]const u8 = null,
146-
transitionType: ?[]const u8 = null, // TODO: enum
147-
frameId: ?[]const u8 = null,
148-
referrerPolicy: ?[]const u8 = null, // TODO: enum
149-
})) orelse return error.InvalidParams;
150-
151155
// change state
152156
bc.reset();
153-
bc.url = params.url;
154-
155-
// TODO: hard coded ID
156-
bc.loader_id = "AF8667A203C5392DBE9AC290044AA4C2";
157+
bc.loader_id = cmd.cdp.loader_id_gen.next();
157158

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

182183
// output
183-
try cmd.sendResult(.{
184-
.frameId = target_id,
185-
.loaderId = bc.loader_id,
186-
}, .{});
184+
if (send_result) {
185+
try cmd.sendResult(.{
186+
.frameId = target_id,
187+
.loaderId = bc.loader_id,
188+
}, .{});
189+
}
187190

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

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

201204
var page = bc.session.currentPage().?;
202-
try page.navigate(params.url, aux_data);
205+
try page.navigate(url, aux_data);
203206

204207
// Events
205208

@@ -218,7 +221,7 @@ fn navigate(cmd: anytype) !void {
218221
.type = "Navigation",
219222
.frame = Frame{
220223
.id = target_id,
221-
.url = bc.url,
224+
.url = url,
222225
.securityOrigin = bc.security_origin,
223226
.secureContextType = bc.secure_context_type,
224227
.loaderId = bc.loader_id,
@@ -281,7 +284,7 @@ test "cdp.page: getFrameTree" {
281284
.frame = .{
282285
.id = "TID-3",
283286
.loaderId = bc.loader_id,
284-
.url = bc.url,
287+
.url = "about:blank",
285288
.domainAndRegistry = "",
286289
.securityOrigin = bc.security_origin,
287290
.mimeType = "text/html",

0 commit comments

Comments
 (0)