mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-29 23:23:28 +00:00
JS clicks and MouseInput clicks trigger page navigation
This commit is contained in:
@@ -134,6 +134,10 @@ pub const Session = struct {
|
|||||||
storage_shed: storage.Shed,
|
storage_shed: storage.Shed,
|
||||||
cookie_jar: storage.CookieJar,
|
cookie_jar: storage.CookieJar,
|
||||||
|
|
||||||
|
// arbitrary that we pass to the inspector, which the inspector will include
|
||||||
|
// in any response/event that it emits.
|
||||||
|
aux_data: ?[]const u8 = null,
|
||||||
|
|
||||||
page: ?Page = null,
|
page: ?Page = null,
|
||||||
http_client: *http.Client,
|
http_client: *http.Client,
|
||||||
|
|
||||||
@@ -158,6 +162,7 @@ pub const Session = struct {
|
|||||||
const allocator = app.allocator;
|
const allocator = app.allocator;
|
||||||
self.* = .{
|
self.* = .{
|
||||||
.app = app,
|
.app = app,
|
||||||
|
.aux_data = null,
|
||||||
.browser = browser,
|
.browser = browser,
|
||||||
.notify_ctx = any_ctx,
|
.notify_ctx = any_ctx,
|
||||||
.inspector = undefined,
|
.inspector = undefined,
|
||||||
@@ -250,8 +255,12 @@ pub const Session = struct {
|
|||||||
// load polyfills
|
// load polyfills
|
||||||
try polyfill.load(self.arena.allocator(), self.executor);
|
try polyfill.load(self.arena.allocator(), self.executor);
|
||||||
|
|
||||||
|
if (aux_data) |ad| {
|
||||||
|
self.aux_data = try self.arena.allocator().dupe(u8, ad);
|
||||||
|
}
|
||||||
|
|
||||||
// inspector
|
// inspector
|
||||||
self.contextCreated(page, aux_data);
|
self.contextCreated(page);
|
||||||
|
|
||||||
return page;
|
return page;
|
||||||
}
|
}
|
||||||
@@ -279,9 +288,28 @@ pub const Session = struct {
|
|||||||
return &(self.page orelse return null);
|
return &(self.page orelse return null);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn contextCreated(self: *Session, page: *Page, aux_data: ?[]const u8) void {
|
fn pageNavigate(self: *Session, url_string: []const u8) !void {
|
||||||
|
// currently, this is only called from the page, so let's hope
|
||||||
|
// it isn't null!
|
||||||
|
std.debug.assert(self.page != null);
|
||||||
|
|
||||||
|
// can't use the page arena, because we're about to reset it
|
||||||
|
// and don't want to use the session's arena, because that'll start to
|
||||||
|
// look like a leak if we navigate from page to page a lot.
|
||||||
|
var buf: [1024]u8 = undefined;
|
||||||
|
var fba = std.heap.FixedBufferAllocator.init(&buf);
|
||||||
|
const url = try self.page.?.url.?.resolve(fba.allocator(), url_string);
|
||||||
|
|
||||||
|
self.removePage();
|
||||||
|
var page = try self.createPage(null);
|
||||||
|
return page.navigate(url, .{
|
||||||
|
.reason = .anchor,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn contextCreated(self: *Session, page: *Page) void {
|
||||||
log.debug("inspector context created", .{});
|
log.debug("inspector context created", .{});
|
||||||
self.inspector.contextCreated(self.executor, "", (page.origin() catch "://") orelse "://", aux_data);
|
self.inspector.contextCreated(self.executor, "", (page.origin() catch "://") orelse "://", self.aux_data);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn notify(self: *const Session, notification: *const Notification) void {
|
fn notify(self: *const Session, notification: *const Notification) void {
|
||||||
@@ -361,7 +389,7 @@ pub const Page = struct {
|
|||||||
// spec reference: https://html.spec.whatwg.org/#document-lifecycle
|
// spec reference: https://html.spec.whatwg.org/#document-lifecycle
|
||||||
// - aux_data: extra data forwarded to the Inspector
|
// - aux_data: extra data forwarded to the Inspector
|
||||||
// see Inspector.contextCreated
|
// see Inspector.contextCreated
|
||||||
pub fn navigate(self: *Page, request_url: URL, aux_data: ?[]const u8) !void {
|
pub fn navigate(self: *Page, request_url: URL, opts: NavigateOpts) !void {
|
||||||
const arena = self.arena;
|
const arena = self.arena;
|
||||||
const session = self.session;
|
const session = self.session;
|
||||||
|
|
||||||
@@ -387,7 +415,12 @@ pub const Page = struct {
|
|||||||
var request = try self.newHTTPRequest(.GET, url, .{ .navigation = true });
|
var request = try self.newHTTPRequest(.GET, url, .{ .navigation = true });
|
||||||
defer request.deinit();
|
defer request.deinit();
|
||||||
|
|
||||||
session.notify(&.{ .page_navigate = .{ .url = url, .timestamp = timestamp() } });
|
session.notify(&.{ .page_navigate = .{
|
||||||
|
.url = url,
|
||||||
|
.reason = opts.reason,
|
||||||
|
.timestamp = timestamp(),
|
||||||
|
} });
|
||||||
|
|
||||||
var response = try request.sendSync(.{});
|
var response = try request.sendSync(.{});
|
||||||
|
|
||||||
// would be different than self.url in the case of a redirect
|
// would be different than self.url in the case of a redirect
|
||||||
@@ -417,7 +450,7 @@ pub const Page = struct {
|
|||||||
var mime = try Mime.parse(arena, ct);
|
var mime = try Mime.parse(arena, ct);
|
||||||
|
|
||||||
if (mime.isHTML()) {
|
if (mime.isHTML()) {
|
||||||
try self.loadHTMLDoc(&response, mime.charset orelse "utf-8", aux_data);
|
try self.loadHTMLDoc(&response, mime.charset orelse "utf-8");
|
||||||
} else {
|
} else {
|
||||||
log.info("non-HTML document: {s}", .{ct});
|
log.info("non-HTML document: {s}", .{ct});
|
||||||
var arr: std.ArrayListUnmanaged(u8) = .{};
|
var arr: std.ArrayListUnmanaged(u8) = .{};
|
||||||
@@ -428,44 +461,14 @@ pub const Page = struct {
|
|||||||
self.raw_data = arr.items;
|
self.raw_data = arr.items;
|
||||||
}
|
}
|
||||||
|
|
||||||
session.notify(&.{ .page_navigated = .{ .url = url, .timestamp = timestamp() } });
|
session.notify(&.{ .page_navigated = .{
|
||||||
}
|
.url = url,
|
||||||
|
.timestamp = timestamp(),
|
||||||
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, me: MouseEvent) !void {
|
|
||||||
if (me.type != .pressed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const element = self.renderer.getElementAtPosition(me.x, me.y) orelse return;
|
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://html.spec.whatwg.org/#read-html
|
// https://html.spec.whatwg.org/#read-html
|
||||||
fn loadHTMLDoc(self: *Page, reader: anytype, charset: []const u8, aux_data: ?[]const u8) !void {
|
fn loadHTMLDoc(self: *Page, reader: anytype, charset: []const u8) !void {
|
||||||
const arena = self.arena;
|
const arena = self.arena;
|
||||||
|
|
||||||
// start netsurf memory arena.
|
// start netsurf memory arena.
|
||||||
@@ -507,7 +510,7 @@ pub const Page = struct {
|
|||||||
// https://html.spec.whatwg.org/#read-html
|
// https://html.spec.whatwg.org/#read-html
|
||||||
|
|
||||||
// inspector
|
// inspector
|
||||||
session.contextCreated(self, aux_data);
|
session.contextCreated(self);
|
||||||
|
|
||||||
{
|
{
|
||||||
// update the sessions state
|
// update the sessions state
|
||||||
@@ -738,6 +741,35 @@ pub const Page = struct {
|
|||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const MouseEvent = struct {
|
||||||
|
x: i32,
|
||||||
|
y: i32,
|
||||||
|
type: Type,
|
||||||
|
|
||||||
|
const Type = enum {
|
||||||
|
pressed,
|
||||||
|
released,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn mouseEvent(self: *Page, me: MouseEvent) !void {
|
||||||
|
if (me.type != .pressed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const element = self.renderer.getElementAtPosition(me.x, me.y) orelse return;
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
fn windowClicked(ctx: *anyopaque, event: *parser.Event) void {
|
fn windowClicked(ctx: *anyopaque, event: *parser.Event) void {
|
||||||
const self: *Page = @alignCast(@ptrCast(ctx));
|
const self: *Page = @alignCast(@ptrCast(ctx));
|
||||||
self._windowClicked(event) catch |err| {
|
self._windowClicked(event) catch |err| {
|
||||||
@@ -746,8 +778,6 @@ pub const Page = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn _windowClicked(self: *Page, event: *parser.Event) !void {
|
fn _windowClicked(self: *Page, event: *parser.Event) !void {
|
||||||
_ = self;
|
|
||||||
|
|
||||||
const target = (try parser.eventTarget(event)) orelse return;
|
const target = (try parser.eventTarget(event)) orelse return;
|
||||||
|
|
||||||
const node = parser.eventTargetToNode(target);
|
const node = parser.eventTargetToNode(target);
|
||||||
@@ -755,10 +785,15 @@ pub const Page = struct {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const element: *parser.ElementHTML = @ptrCast(node);
|
const html_element: *parser.ElementHTML = @ptrCast(node);
|
||||||
const tag_name = try parser.elementHTMLGetTagType(element);
|
switch (try parser.elementHTMLGetTagType(html_element)) {
|
||||||
// TODO https://github.com/lightpanda-io/browser/pull/501
|
.a => {
|
||||||
_ = tag_name;
|
const element: *parser.Element = @ptrCast(node);
|
||||||
|
const href = (try parser.elementGetAttribute(element, "href")) orelse return;
|
||||||
|
return self.session.pageNavigate(href);
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const Script = struct {
|
const Script = struct {
|
||||||
@@ -825,8 +860,17 @@ pub const Page = struct {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub const NavigateReason = enum {
|
||||||
|
anchor,
|
||||||
|
address_bar,
|
||||||
|
};
|
||||||
|
|
||||||
|
const NavigateOpts = struct {
|
||||||
|
reason: NavigateReason = .address_bar,
|
||||||
|
};
|
||||||
|
|
||||||
// provide very poor abstration to the rest of the code. In theory, we can change
|
// 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.
|
// the FlatRenderer to a different implementation, and it'll all just work.
|
||||||
pub const Renderer = FlatRenderer;
|
pub const Renderer = FlatRenderer;
|
||||||
|
|
||||||
// This "renderer" positions elements in a single row in an unspecified order.
|
// This "renderer" positions elements in a single row in an unspecified order.
|
||||||
|
|||||||
@@ -132,21 +132,15 @@ pub const HTMLElement = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn _click(e: *parser.ElementHTML) !void {
|
pub fn _click(e: *parser.ElementHTML) !void {
|
||||||
_ = e;
|
const event = try parser.mouseEventCreate();
|
||||||
// TODO needs: https://github.com/lightpanda-io/browser/pull/501
|
defer parser.mouseEventDestroy(event);
|
||||||
// TODO: when the above is merged, should we get the element coordinates?
|
try parser.mouseEventInit(event, "click", .{
|
||||||
|
.x = 0,
|
||||||
// const event = try parser.mouseEventCreate();
|
.y = 0,
|
||||||
// defer parser.mouseEventDestroy(event);
|
.bubbles = true,
|
||||||
// try parser.mouseEventInit(event, "click", .{
|
.cancelable = true,
|
||||||
// .bubbles = true,
|
});
|
||||||
// .cancelable = true,
|
_ = try parser.elementDispatchEvent(@ptrCast(e), @ptrCast(event));
|
||||||
//
|
|
||||||
// // get the coordinates?
|
|
||||||
// .x = 0,
|
|
||||||
// .y = 0,
|
|
||||||
// });
|
|
||||||
// _ = try parser.elementDispatchEvent(@ptrCast(e), @ptrCast(event));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1000,4 +994,12 @@ test "Browser.HTML.Element" {
|
|||||||
.{ "document.getElementById('content').innerText", "foo" },
|
.{ "document.getElementById('content').innerText", "foo" },
|
||||||
.{ "document.getElementById('content').innerHTML = backup; true;", "true" },
|
.{ "document.getElementById('content').innerHTML = backup; true;", "true" },
|
||||||
}, .{});
|
}, .{});
|
||||||
|
|
||||||
|
try runner.testCases(&.{
|
||||||
|
.{ "let click_count = 0;", "undefined" },
|
||||||
|
.{ "let clickCbk = function() { click_count++ }", "undefined" },
|
||||||
|
.{ "document.getElementById('content').addEventListener('click', clickCbk);", "undefined" },
|
||||||
|
.{ "document.getElementById('content').click()", "undefined" },
|
||||||
|
.{ "click_count", "1" },
|
||||||
|
}, .{});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,13 +151,6 @@ fn navigate(cmd: anytype) !void {
|
|||||||
|
|
||||||
const url = try URL.parse(params.url, "https");
|
const url = try URL.parse(params.url, "https");
|
||||||
|
|
||||||
const aux_data = try std.fmt.allocPrint(
|
|
||||||
cmd.arena,
|
|
||||||
// NOTE: we assume this is the default web page
|
|
||||||
"{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}",
|
|
||||||
.{target_id},
|
|
||||||
);
|
|
||||||
|
|
||||||
var page = bc.session.currentPage().?;
|
var page = bc.session.currentPage().?;
|
||||||
bc.loader_id = bc.cdp.loader_id_gen.next();
|
bc.loader_id = bc.cdp.loader_id_gen.next();
|
||||||
try cmd.sendResult(.{
|
try cmd.sendResult(.{
|
||||||
@@ -165,10 +158,13 @@ fn navigate(cmd: anytype) !void {
|
|||||||
.loaderId = bc.loader_id,
|
.loaderId = bc.loader_id,
|
||||||
}, .{});
|
}, .{});
|
||||||
|
|
||||||
try page.navigate(url, aux_data);
|
std.debug.print("page: {s}\n", .{target_id});
|
||||||
|
try page.navigate(url, .{
|
||||||
|
.reason = .address_bar,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pageNavigate(bc: anytype, event: *const Notification.PageEvent) !void {
|
pub fn pageNavigate(bc: anytype, event: *const Notification.PageNavigate) !void {
|
||||||
// I don't think it's possible that we get these notifications and don't
|
// I don't think it's possible that we get these notifications and don't
|
||||||
// have these things setup.
|
// have these things setup.
|
||||||
std.debug.assert(bc.session.page != null);
|
std.debug.assert(bc.session.page != null);
|
||||||
@@ -180,6 +176,22 @@ pub fn pageNavigate(bc: anytype, event: *const Notification.PageEvent) !void {
|
|||||||
|
|
||||||
bc.reset();
|
bc.reset();
|
||||||
|
|
||||||
|
if (event.reason == .anchor) {
|
||||||
|
try cdp.sendEvent("Page.frameScheduledNavigation", .{
|
||||||
|
.frameId = target_id,
|
||||||
|
.delay = 0,
|
||||||
|
.reason = "anchorClick",
|
||||||
|
.url = event.url.raw,
|
||||||
|
}, .{ .session_id = session_id });
|
||||||
|
|
||||||
|
try cdp.sendEvent("Page.frameRequestedNavigation", .{
|
||||||
|
.frameId = target_id,
|
||||||
|
.reason = "anchorClick",
|
||||||
|
.url = event.url.raw,
|
||||||
|
.disposition = "currentTab",
|
||||||
|
}, .{ .session_id = session_id });
|
||||||
|
}
|
||||||
|
|
||||||
// frameStartedNavigating event
|
// frameStartedNavigating event
|
||||||
try cdp.sendEvent("Page.frameStartedNavigating", .{
|
try cdp.sendEvent("Page.frameStartedNavigating", .{
|
||||||
.frameId = target_id,
|
.frameId = target_id,
|
||||||
@@ -202,12 +214,18 @@ pub fn pageNavigate(bc: anytype, event: *const Notification.PageEvent) !void {
|
|||||||
}, .{ .session_id = session_id });
|
}, .{ .session_id = session_id });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (event.reason == .anchor) {
|
||||||
|
try cdp.sendEvent("Page.frameClearedScheduledNavigation", .{
|
||||||
|
.frameId = target_id,
|
||||||
|
}, .{ .session_id = session_id });
|
||||||
|
}
|
||||||
|
|
||||||
// Send Runtime.executionContextsCleared event
|
// Send Runtime.executionContextsCleared event
|
||||||
// TODO: noop event, we have no env context at this point, is it necesarry?
|
// TODO: noop event, we have no env context at this point, is it necesarry?
|
||||||
try cdp.sendEvent("Runtime.executionContextsCleared", null, .{ .session_id = session_id });
|
try cdp.sendEvent("Runtime.executionContextsCleared", null, .{ .session_id = session_id });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pageNavigated(bc: anytype, event: *const Notification.PageEvent) !void {
|
pub fn pageNavigated(bc: anytype, event: *const Notification.PageNavigated) !void {
|
||||||
// I don't think it's possible that we get these notifications and don't
|
// I don't think it's possible that we get these notifications and don't
|
||||||
// have these things setup.
|
// have these things setup.
|
||||||
std.debug.assert(bc.session.page != null);
|
std.debug.assert(bc.session.page != null);
|
||||||
|
|||||||
@@ -161,9 +161,9 @@ const Page = struct {
|
|||||||
aux_data: []const u8 = "",
|
aux_data: []const u8 = "",
|
||||||
doc: ?*parser.Document = null,
|
doc: ?*parser.Document = null,
|
||||||
|
|
||||||
pub fn navigate(_: *Page, url: URL, aux_data: []const u8) !void {
|
pub fn navigate(_: *Page, url: URL, opts: anytype) !void {
|
||||||
_ = url;
|
_ = url;
|
||||||
_ = aux_data;
|
_ = opts;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MouseEvent = @import("../browser/browser.zig").Page.MouseEvent;
|
const MouseEvent = @import("../browser/browser.zig").Page.MouseEvent;
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ pub fn main() !void {
|
|||||||
// page
|
// page
|
||||||
const page = try session.createPage(null);
|
const page = try session.createPage(null);
|
||||||
|
|
||||||
_ = page.navigate(url, null) catch |err| switch (err) {
|
_ = page.navigate(url, .{}) catch |err| switch (err) {
|
||||||
error.UnsupportedUriScheme, error.UriMissingHost => {
|
error.UnsupportedUriScheme, error.UriMissingHost => {
|
||||||
log.err("'{s}' is not a valid URL ({any})\n", .{ url, err });
|
log.err("'{s}' is not a valid URL ({any})\n", .{ url, err });
|
||||||
return args.printUsageAndExit(false);
|
return args.printUsageAndExit(false);
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
const URL = @import("url.zig").URL;
|
const URL = @import("url.zig").URL;
|
||||||
|
const browser = @import("browser/browser.zig");
|
||||||
|
|
||||||
pub const Notification = union(enum) {
|
pub const Notification = union(enum) {
|
||||||
page_navigate: PageEvent,
|
page_navigate: PageNavigate,
|
||||||
page_navigated: PageEvent,
|
page_navigated: PageNavigated,
|
||||||
|
|
||||||
pub const PageEvent = struct {
|
pub const PageNavigate = struct {
|
||||||
|
timestamp: u32,
|
||||||
|
url: *const URL,
|
||||||
|
reason: browser.NavigateReason,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const PageNavigated = struct {
|
||||||
timestamp: u32,
|
timestamp: u32,
|
||||||
url: *const URL,
|
url: *const URL,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ pub const URL = struct {
|
|||||||
return parse(buf.items, null);
|
return parse(buf.items, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Above, in `parse, we error if a host doesn't exist
|
// Above, in `parse`, we error if a host doesn't exist
|
||||||
// In other words, we can't have a URL with a null host.
|
// In other words, we can't have a URL with a null host.
|
||||||
pub fn host(self: *const URL) []const u8 {
|
pub fn host(self: *const URL) []const u8 {
|
||||||
return self.uri.host.?.percent_encoded;
|
return self.uri.host.?.percent_encoded;
|
||||||
|
|||||||
Reference in New Issue
Block a user