diff --git a/src/browser/browser.zig b/src/browser/browser.zig index 8133140d..5d2a0e61 100644 --- a/src/browser/browser.zig +++ b/src/browser/browser.zig @@ -134,6 +134,10 @@ pub const Session = struct { storage_shed: storage.Shed, 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, http_client: *http.Client, @@ -158,6 +162,7 @@ pub const Session = struct { const allocator = app.allocator; self.* = .{ .app = app, + .aux_data = null, .browser = browser, .notify_ctx = any_ctx, .inspector = undefined, @@ -250,8 +255,12 @@ pub const Session = struct { // load polyfills try polyfill.load(self.arena.allocator(), self.executor); + if (aux_data) |ad| { + self.aux_data = try self.arena.allocator().dupe(u8, ad); + } + // inspector - self.contextCreated(page, aux_data); + self.contextCreated(page); return page; } @@ -279,9 +288,28 @@ pub const Session = struct { 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", .{}); - 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 { @@ -361,7 +389,7 @@ pub const Page = struct { // spec reference: https://html.spec.whatwg.org/#document-lifecycle // - aux_data: extra data forwarded to the Inspector // 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 session = self.session; @@ -387,7 +415,12 @@ pub const Page = struct { var request = try self.newHTTPRequest(.GET, url, .{ .navigation = true }); 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(.{}); // 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); if (mime.isHTML()) { - try self.loadHTMLDoc(&response, mime.charset orelse "utf-8", aux_data); + try self.loadHTMLDoc(&response, mime.charset orelse "utf-8"); } else { log.info("non-HTML document: {s}", .{ct}); var arr: std.ArrayListUnmanaged(u8) = .{}; @@ -428,44 +461,14 @@ pub const Page = struct { self.raw_data = arr.items; } - 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)); + session.notify(&.{ .page_navigated = .{ + .url = url, + .timestamp = timestamp(), + } }); } // 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; // start netsurf memory arena. @@ -507,7 +510,7 @@ pub const Page = struct { // https://html.spec.whatwg.org/#read-html // inspector - session.contextCreated(self, aux_data); + session.contextCreated(self); { // update the sessions state @@ -738,6 +741,35 @@ pub const Page = struct { 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 { const self: *Page = @alignCast(@ptrCast(ctx)); self._windowClicked(event) catch |err| { @@ -746,8 +778,6 @@ pub const Page = struct { } fn _windowClicked(self: *Page, event: *parser.Event) !void { - _ = self; - const target = (try parser.eventTarget(event)) orelse return; const node = parser.eventTargetToNode(target); @@ -755,10 +785,15 @@ pub const Page = struct { return; } - const element: *parser.ElementHTML = @ptrCast(node); - const tag_name = try parser.elementHTMLGetTagType(element); - // TODO https://github.com/lightpanda-io/browser/pull/501 - _ = tag_name; + const html_element: *parser.ElementHTML = @ptrCast(node); + switch (try parser.elementHTMLGetTagType(html_element)) { + .a => { + const element: *parser.Element = @ptrCast(node); + const href = (try parser.elementGetAttribute(element, "href")) orelse return; + return self.session.pageNavigate(href); + }, + else => {}, + } } 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 -// 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; // This "renderer" positions elements in a single row in an unspecified order. diff --git a/src/browser/html/elements.zig b/src/browser/html/elements.zig index ff7ad400..2f9aa97b 100644 --- a/src/browser/html/elements.zig +++ b/src/browser/html/elements.zig @@ -132,21 +132,15 @@ pub const HTMLElement = struct { } pub fn _click(e: *parser.ElementHTML) !void { - _ = e; - // TODO needs: https://github.com/lightpanda-io/browser/pull/501 - // TODO: when the above is merged, should we get the element coordinates? - - // const event = try parser.mouseEventCreate(); - // defer parser.mouseEventDestroy(event); - // try parser.mouseEventInit(event, "click", .{ - // .bubbles = true, - // .cancelable = true, - // - // // get the coordinates? - // .x = 0, - // .y = 0, - // }); - // _ = try parser.elementDispatchEvent(@ptrCast(e), @ptrCast(event)); + const event = try parser.mouseEventCreate(); + defer parser.mouseEventDestroy(event); + try parser.mouseEventInit(event, "click", .{ + .x = 0, + .y = 0, + .bubbles = true, + .cancelable = true, + }); + _ = try parser.elementDispatchEvent(@ptrCast(e), @ptrCast(event)); } }; @@ -1000,4 +994,12 @@ test "Browser.HTML.Element" { .{ "document.getElementById('content').innerText", "foo" }, .{ "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" }, + }, .{}); } diff --git a/src/cdp/domains/page.zig b/src/cdp/domains/page.zig index 1a7bc9bd..94c569ab 100644 --- a/src/cdp/domains/page.zig +++ b/src/cdp/domains/page.zig @@ -151,13 +151,6 @@ fn navigate(cmd: anytype) !void { 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().?; bc.loader_id = bc.cdp.loader_id_gen.next(); try cmd.sendResult(.{ @@ -165,10 +158,13 @@ fn navigate(cmd: anytype) !void { .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 // have these things setup. std.debug.assert(bc.session.page != null); @@ -180,6 +176,22 @@ pub fn pageNavigate(bc: anytype, event: *const Notification.PageEvent) !void { 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 try cdp.sendEvent("Page.frameStartedNavigating", .{ .frameId = target_id, @@ -202,12 +214,18 @@ pub fn pageNavigate(bc: anytype, event: *const Notification.PageEvent) !void { }, .{ .session_id = session_id }); } + if (event.reason == .anchor) { + try cdp.sendEvent("Page.frameClearedScheduledNavigation", .{ + .frameId = target_id, + }, .{ .session_id = session_id }); + } + // Send Runtime.executionContextsCleared event // TODO: noop event, we have no env context at this point, is it necesarry? 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 // have these things setup. std.debug.assert(bc.session.page != null); diff --git a/src/cdp/testing.zig b/src/cdp/testing.zig index 1f15caf6..1ca67c03 100644 --- a/src/cdp/testing.zig +++ b/src/cdp/testing.zig @@ -161,9 +161,9 @@ const Page = struct { aux_data: []const u8 = "", 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; - _ = aux_data; + _ = opts; } const MouseEvent = @import("../browser/browser.zig").Page.MouseEvent; diff --git a/src/main.zig b/src/main.zig index 2dfc339e..2dcbfffb 100644 --- a/src/main.zig +++ b/src/main.zig @@ -102,7 +102,7 @@ pub fn main() !void { // page 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 => { log.err("'{s}' is not a valid URL ({any})\n", .{ url, err }); return args.printUsageAndExit(false); diff --git a/src/notification.zig b/src/notification.zig index 28c5162e..24310ae4 100644 --- a/src/notification.zig +++ b/src/notification.zig @@ -1,10 +1,17 @@ const URL = @import("url.zig").URL; +const browser = @import("browser/browser.zig"); pub const Notification = union(enum) { - page_navigate: PageEvent, - page_navigated: PageEvent, + page_navigate: PageNavigate, + 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, url: *const URL, }; diff --git a/src/url.zig b/src/url.zig index a5baef08..53c59085 100644 --- a/src/url.zig +++ b/src/url.zig @@ -48,7 +48,7 @@ pub const URL = struct { 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. pub fn host(self: *const URL) []const u8 { return self.uri.host.?.percent_encoded;