Skip to content

Commit 12dc0f8

Browse files
committed
JS clicks and MouseInput clicks trigger page navigation
1 parent ba85e41 commit 12dc0f8

File tree

7 files changed

+153
-81
lines changed

7 files changed

+153
-81
lines changed

src/browser/browser.zig

Lines changed: 93 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,10 @@ pub const Session = struct {
133133
storage_shed: storage.Shed,
134134
cookie_jar: storage.CookieJar,
135135

136+
// arbitrary that we pass to the inspector, which the inspector will include
137+
// in any response/event that it emits.
138+
aux_data: ?[]const u8 = null,
139+
136140
page: ?Page = null,
137141
http_client: *http.Client,
138142

@@ -160,6 +164,7 @@ pub const Session = struct {
160164
self.* = .{
161165
.app = app,
162166
.env = undefined,
167+
.aux_data = null,
163168
.browser = browser,
164169
.notify_ctx = any_ctx,
165170
.inspector = undefined,
@@ -247,8 +252,12 @@ pub const Session = struct {
247252
// TODO: change to 'env' when https://github.com/lightpanda-io/zig-js-runtime/pull/285 lands
248253
try polyfill.load(self.arena.allocator(), &self.env);
249254

255+
if (aux_data) |ad| {
256+
self.aux_data = try self.arena.allocator().dupe(u8, ad);
257+
}
258+
250259
// inspector
251-
self.contextCreated(page, aux_data);
260+
self.contextCreated(page);
252261

253262
return page;
254263
}
@@ -276,9 +285,28 @@ pub const Session = struct {
276285
return &(self.page orelse return null);
277286
}
278287

279-
fn contextCreated(self: *Session, page: *Page, aux_data: ?[]const u8) void {
288+
fn pageNavigate(self: *Session, url_string: []const u8) !void {
289+
// currently, this is only called from the page, so let's hope
290+
// it isn't null!
291+
std.debug.assert(self.page != null);
292+
293+
// can't use the page arena, because we're about to reset it
294+
// and don't want to use the session's arena, because that'll start to
295+
// look like a leak if we navigate from page to page a lot.
296+
var buf: [1024]u8 = undefined;
297+
var fba = std.heap.FixedBufferAllocator.init(&buf);
298+
const url = try self.page.?.url.?.resolve(fba.allocator(), url_string);
299+
300+
self.removePage();
301+
var page = try self.createPage(null);
302+
return page.navigate(url, .{
303+
.reason = .anchor,
304+
});
305+
}
306+
307+
fn contextCreated(self: *Session, page: *Page) void {
280308
log.debug("inspector context created", .{});
281-
self.inspector.contextCreated(&self.env, "", (page.origin() catch "://") orelse "://", aux_data);
309+
self.inspector.contextCreated(&self.env, "", (page.origin() catch "://") orelse "://", self.aux_data);
282310
}
283311

284312
fn notify(self: *const Session, notification: *const Notification) void {
@@ -361,7 +389,7 @@ pub const Page = struct {
361389
// spec reference: https://html.spec.whatwg.org/#document-lifecycle
362390
// - aux_data: extra data forwarded to the Inspector
363391
// see Inspector.contextCreated
364-
pub fn navigate(self: *Page, request_url: URL, aux_data: ?[]const u8) !void {
392+
pub fn navigate(self: *Page, request_url: URL, opts: NavigateOpts) !void {
365393
const arena = self.arena;
366394
const session = self.session;
367395

@@ -387,7 +415,12 @@ pub const Page = struct {
387415
var request = try self.newHTTPRequest(.GET, url, .{ .navigation = true });
388416
defer request.deinit();
389417

390-
session.notify(&.{ .page_navigate = .{ .url = url, .timestamp = timestamp() } });
418+
session.notify(&.{ .page_navigate = .{
419+
.url = url,
420+
.reason = opts.reason,
421+
.timestamp = timestamp(),
422+
} });
423+
391424
var response = try request.sendSync(.{});
392425

393426
// would be different than self.url in the case of a redirect
@@ -418,7 +451,7 @@ pub const Page = struct {
418451
defer mime.deinit();
419452

420453
if (mime.isHTML()) {
421-
try self.loadHTMLDoc(&response, mime.charset orelse "utf-8", aux_data);
454+
try self.loadHTMLDoc(&response, mime.charset orelse "utf-8");
422455
} else {
423456
log.info("non-HTML document: {s}", .{ct});
424457
var arr: std.ArrayListUnmanaged(u8) = .{};
@@ -429,44 +462,14 @@ pub const Page = struct {
429462
self.raw_data = arr.items;
430463
}
431464

432-
session.notify(&.{ .page_navigated = .{ .url = url, .timestamp = timestamp() } });
433-
}
434-
435-
pub const ClickResult = union(enum) {
436-
navigate: std.Uri,
437-
};
438-
439-
pub const MouseEvent = struct {
440-
x: i32,
441-
y: i32,
442-
type: Type,
443-
444-
const Type = enum {
445-
pressed,
446-
released,
447-
};
448-
};
449-
450-
pub fn mouseEvent(self: *Page, me: MouseEvent) !void {
451-
if (me.type != .pressed) {
452-
return;
453-
}
454-
455-
const element = self.renderer.getElementAtPosition(me.x, me.y) orelse return;
456-
457-
const event = try parser.mouseEventCreate();
458-
defer parser.mouseEventDestroy(event);
459-
try parser.mouseEventInit(event, "click", .{
460-
.bubbles = true,
461-
.cancelable = true,
462-
.x = me.x,
463-
.y = me.y,
464-
});
465-
_ = try parser.elementDispatchEvent(element, @ptrCast(event));
465+
session.notify(&.{ .page_navigated = .{
466+
.url = url,
467+
.timestamp = timestamp(),
468+
} });
466469
}
467470

468471
// https://html.spec.whatwg.org/#read-html
469-
fn loadHTMLDoc(self: *Page, reader: anytype, charset: []const u8, aux_data: ?[]const u8) !void {
472+
fn loadHTMLDoc(self: *Page, reader: anytype, charset: []const u8) !void {
470473
const arena = self.arena;
471474

472475
// start netsurf memory arena.
@@ -508,7 +511,7 @@ pub const Page = struct {
508511
// https://html.spec.whatwg.org/#read-html
509512

510513
// inspector
511-
session.contextCreated(self, aux_data);
514+
session.contextCreated(self);
512515

513516
// replace the user context document with the new one.
514517
try session.env.setUserContext(.{
@@ -740,6 +743,35 @@ pub const Page = struct {
740743
return request;
741744
}
742745

746+
pub const MouseEvent = struct {
747+
x: i32,
748+
y: i32,
749+
type: Type,
750+
751+
const Type = enum {
752+
pressed,
753+
released,
754+
};
755+
};
756+
757+
pub fn mouseEvent(self: *Page, me: MouseEvent) !void {
758+
if (me.type != .pressed) {
759+
return;
760+
}
761+
762+
const element = self.renderer.getElementAtPosition(me.x, me.y) orelse return;
763+
764+
const event = try parser.mouseEventCreate();
765+
defer parser.mouseEventDestroy(event);
766+
try parser.mouseEventInit(event, "click", .{
767+
.bubbles = true,
768+
.cancelable = true,
769+
.x = me.x,
770+
.y = me.y,
771+
});
772+
_ = try parser.elementDispatchEvent(element, @ptrCast(event));
773+
}
774+
743775
fn windowClicked(ctx: *anyopaque, event: *parser.Event) void {
744776
const self: *Page = @alignCast(@ptrCast(ctx));
745777
self._windowClicked(event) catch |err| {
@@ -748,19 +780,22 @@ pub const Page = struct {
748780
}
749781

750782
fn _windowClicked(self: *Page, event: *parser.Event) !void {
751-
_ = self;
752-
753783
const target = (try parser.eventTarget(event)) orelse return;
754784

755785
const node = parser.eventTargetToNode(target);
756786
if (try parser.nodeType(node) != .element) {
757787
return;
758788
}
759789

760-
const element: *parser.ElementHTML = @ptrCast(node);
761-
const tag_name = try parser.elementHTMLGetTagType(element);
762-
// TODO https://github.com/lightpanda-io/browser/pull/501
763-
_ = tag_name;
790+
const html_element: *parser.ElementHTML = @ptrCast(node);
791+
switch (try parser.elementHTMLGetTagType(html_element)) {
792+
.a => {
793+
const element: *parser.Element = @ptrCast(node);
794+
const href = (try parser.elementGetAttribute(element, "href")) orelse return;
795+
return self.session.pageNavigate(href);
796+
},
797+
else => {},
798+
}
764799
}
765800

766801
const Script = struct {
@@ -827,8 +862,17 @@ pub const Page = struct {
827862
};
828863
};
829864

865+
pub const NavigateReason = enum {
866+
anchor,
867+
address_bar,
868+
};
869+
870+
const NavigateOpts = struct {
871+
reason: NavigateReason = .address_bar,
872+
};
873+
830874
// provide very poor abstration to the rest of the code. In theory, we can change
831-
// the FlatRendere to a different implementation, and it'll all just work.
875+
// the FlatRenderer to a different implementation, and it'll all just work.
832876
pub const Renderer = FlatRenderer;
833877

834878
// This "renderer" positions elements in a single row in an unspecified order.

src/cdp/domains/page.zig

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -151,24 +151,20 @@ fn navigate(cmd: anytype) !void {
151151

152152
const url = try URL.parse(params.url, "https");
153153

154-
const aux_data = try std.fmt.allocPrint(
155-
cmd.arena,
156-
// NOTE: we assume this is the default web page
157-
"{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}",
158-
.{target_id},
159-
);
160-
161154
var page = bc.session.currentPage().?;
162155
bc.loader_id = bc.cdp.loader_id_gen.next();
163156
try cmd.sendResult(.{
164157
.frameId = target_id,
165158
.loaderId = bc.loader_id,
166159
}, .{});
167160

168-
try page.navigate(url, aux_data);
161+
std.debug.print("page: {s}\n", .{target_id});
162+
try page.navigate(url, .{
163+
.reason = .address_bar,
164+
});
169165
}
170166

171-
pub fn pageNavigate(bc: anytype, event: *const Notification.PageEvent) !void {
167+
pub fn pageNavigate(bc: anytype, event: *const Notification.PageNavigate) !void {
172168
// I don't think it's possible that we get these notifications and don't
173169
// have these things setup.
174170
std.debug.assert(bc.session.page != null);
@@ -180,6 +176,22 @@ pub fn pageNavigate(bc: anytype, event: *const Notification.PageEvent) !void {
180176

181177
bc.reset();
182178

179+
if (event.reason == .anchor) {
180+
try cdp.sendEvent("Page.frameScheduledNavigation", .{
181+
.frameId = target_id,
182+
.delay = 0,
183+
.reason = "anchorClick",
184+
.url = event.url.raw,
185+
}, .{ .session_id = session_id });
186+
187+
try cdp.sendEvent("Page.frameRequestedNavigation", .{
188+
.frameId = target_id,
189+
.reason = "anchorClick",
190+
.url = event.url.raw,
191+
.disposition = "currentTab",
192+
}, .{ .session_id = session_id });
193+
}
194+
183195
// frameStartedNavigating event
184196
try cdp.sendEvent("Page.frameStartedNavigating", .{
185197
.frameId = target_id,
@@ -202,12 +214,18 @@ pub fn pageNavigate(bc: anytype, event: *const Notification.PageEvent) !void {
202214
}, .{ .session_id = session_id });
203215
}
204216

217+
if (event.reason == .anchor) {
218+
try cdp.sendEvent("Page.frameClearedScheduledNavigation", .{
219+
.frameId = target_id,
220+
}, .{ .session_id = session_id });
221+
}
222+
205223
// Send Runtime.executionContextsCleared event
206224
// TODO: noop event, we have no env context at this point, is it necesarry?
207225
try cdp.sendEvent("Runtime.executionContextsCleared", null, .{ .session_id = session_id });
208226
}
209227

210-
pub fn pageNavigated(bc: anytype, event: *const Notification.PageEvent) !void {
228+
pub fn pageNavigated(bc: anytype, event: *const Notification.PageNavigated) !void {
211229
// I don't think it's possible that we get these notifications and don't
212230
// have these things setup.
213231
std.debug.assert(bc.session.page != null);

src/cdp/testing.zig

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,9 +161,9 @@ const Page = struct {
161161
aux_data: []const u8 = "",
162162
doc: ?*parser.Document = null,
163163

164-
pub fn navigate(_: *Page, url: URL, aux_data: []const u8) !void {
164+
pub fn navigate(_: *Page, url: URL, opts: anytype) !void {
165165
_ = url;
166-
_ = aux_data;
166+
_ = opts;
167167
}
168168

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

src/html/elements.zig

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -138,21 +138,15 @@ pub const HTMLElement = struct {
138138
}
139139

140140
pub fn _click(e: *parser.ElementHTML) !void {
141-
_ = e;
142-
// TODO needs: https://github.com/lightpanda-io/browser/pull/501
143-
// TODO: when the above is merged, should we get the element coordinates?
144-
145-
// const event = try parser.mouseEventCreate();
146-
// defer parser.mouseEventDestroy(event);
147-
// try parser.mouseEventInit(event, "click", .{
148-
// .bubbles = true,
149-
// .cancelable = true,
150-
//
151-
// // get the coordinates?
152-
// .x = 0,
153-
// .y = 0,
154-
// });
155-
// _ = try parser.elementDispatchEvent(@ptrCast(e), @ptrCast(event));
141+
const event = try parser.mouseEventCreate();
142+
defer parser.mouseEventDestroy(event);
143+
try parser.mouseEventInit(event, "click", .{
144+
.x = 0,
145+
.y = 0,
146+
.bubbles = true,
147+
.cancelable = true,
148+
});
149+
_ = try parser.elementDispatchEvent(@ptrCast(e), @ptrCast(event));
156150
}
157151
};
158152

@@ -1113,4 +1107,13 @@ pub fn testExecFn(
11131107
.{ .src = "document.getElementById('content').innerHTML = backup; true;", .ex = "true" },
11141108
};
11151109
try checkCases(js_env, &innertext);
1110+
1111+
var click = [_]Case{
1112+
.{ .src = "let click_count = 0;", .ex = "undefined" },
1113+
.{ .src = "let clickCbk = function() { click_count++ }", .ex = "undefined" },
1114+
.{ .src = "document.getElementById('content').addEventListener('click', clickCbk);", .ex = "undefined" },
1115+
.{ .src = "document.getElementById('content').click()", .ex = "undefined" },
1116+
.{ .src = "click_count", .ex = "1" },
1117+
};
1118+
try checkCases(js_env, &click);
11161119
}

src/main.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ pub fn main() !void {
108108
// page
109109
const page = try session.createPage(null);
110110

111-
_ = page.navigate(url, null) catch |err| switch (err) {
111+
_ = page.navigate(url, .{}) catch |err| switch (err) {
112112
error.UnsupportedUriScheme, error.UriMissingHost => {
113113
log.err("'{s}' is not a valid URL ({any})\n", .{ url, err });
114114
return args.printUsageAndExit(false);

src/notification.zig

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
const URL = @import("url.zig").URL;
2+
const browser = @import("browser/browser.zig");
23

34
pub const Notification = union(enum) {
4-
page_navigate: PageEvent,
5-
page_navigated: PageEvent,
5+
page_navigate: PageNavigate,
6+
page_navigated: PageNavigated,
67

7-
pub const PageEvent = struct {
8+
pub const PageNavigate = struct {
9+
timestamp: u32,
10+
url: *const URL,
11+
reason: browser.NavigateReason,
12+
};
13+
14+
pub const PageNavigated = struct {
815
timestamp: u32,
916
url: *const URL,
1017
};

0 commit comments

Comments
 (0)