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
163 changes: 120 additions & 43 deletions src/browser/browser.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Expand All @@ -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,
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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;

Expand All @@ -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
Expand Down Expand Up @@ -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) = .{};
Expand All @@ -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.
Expand All @@ -481,6 +484,16 @@ pub const Page = struct {
// save a document's pointer in the page.
self.doc = doc;

const document_element = (try parser.documentGetDocumentElement(doc)) orelse return error.DocumentElementError;
try parser.eventTargetAddZigListener(
parser.toEventTarget(parser.Element, document_element),
arena,
"click",
windowClicked,
self,
false,
);

// TODO set document.readyState to interactive
// https://html.spec.whatwg.org/#reporting-document-loading-status

Expand All @@ -497,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
Expand Down Expand Up @@ -728,6 +741,61 @@ 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| {
log.err("window click handler: {}", .{err});
};
}

fn _windowClicked(self: *Page, event: *parser.Event) !void {
const target = (try parser.eventTarget(event)) orelse return;

const node = parser.eventTargetToNode(target);
if (try parser.nodeType(node) != .element) {
return;
}

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 {
element: *parser.Element,
kind: Kind,
Expand Down Expand Up @@ -792,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.
Expand Down
14 changes: 7 additions & 7 deletions src/browser/dom/mutation_observer.zig
Original file line number Diff line number Diff line change
Expand Up @@ -104,15 +104,15 @@ pub const MutationObserver = struct {
arena,
"DOMNodeInserted",
EventHandler,
.{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc },
.{ .cbk = self.cbk, .ctx = o, .deinitFunc = deinitFunc },
false,
);
try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node),
arena,
"DOMNodeRemoved",
EventHandler,
.{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc },
.{ .cbk = self.cbk, .ctx = o, .deinitFunc = deinitFunc },
false,
);
}
Expand All @@ -122,7 +122,7 @@ pub const MutationObserver = struct {
arena,
"DOMAttrModified",
EventHandler,
.{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc },
.{ .cbk = self.cbk, .ctx = o, .deinitFunc = deinitFunc },
false,
);
}
Expand All @@ -132,7 +132,7 @@ pub const MutationObserver = struct {
arena,
"DOMCharacterDataModified",
EventHandler,
.{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc },
.{ .cbk = self.cbk, .ctx = o, .deinitFunc = deinitFunc },
false,
);
}
Expand All @@ -142,7 +142,7 @@ pub const MutationObserver = struct {
arena,
"DOMSubtreeModified",
EventHandler,
.{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc },
.{ .cbk = self.cbk, .ctx = o, .deinitFunc = deinitFunc },
false,
);
}
Expand Down Expand Up @@ -261,7 +261,7 @@ const EventHandler = struct {
return false;
}

fn handle(evt: ?*parser.Event, data: parser.EventHandlerData) void {
fn handle(evt: ?*parser.Event, data: *const parser.JSEventHandlerData) void {
if (evt == null) return;

var mrs: MutationRecords = .{};
Expand All @@ -277,7 +277,7 @@ const EventHandler = struct {
const node = parser.eventTargetToNode(et);

// retrieve the observer from the data.
const o: *MutationObserver.Observer = @ptrCast(@alignCast(data.data));
const o: *MutationObserver.Observer = @ptrCast(@alignCast(data.ctx));

if (!apply(o, node)) return;

Expand Down
2 changes: 1 addition & 1 deletion src/browser/events/event.zig
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ pub const Event = struct {
};

pub const EventHandler = struct {
fn handle(event: ?*parser.Event, data: parser.EventHandlerData) void {
fn handle(event: ?*parser.Event, data: *const parser.JSEventHandlerData) void {
var result: Callback.Result = undefined;
data.cbk.tryCall(.{if (event) |evt| Event.toInterface(evt) catch unreachable else null}, &result) catch {
log.err("event handler error: {s}", .{result.exception});
Expand Down
20 changes: 20 additions & 0 deletions src/browser/html/elements.zig
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,18 @@ pub const HTMLElement = struct {
// attach the text node.
_ = try parser.nodeAppendChild(n, @as(*parser.Node, @ptrCast(t)));
}

pub fn _click(e: *parser.ElementHTML) !void {
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));
}
};

// Deprecated HTMLElements in Chrome (2023/03/15)
Expand Down Expand Up @@ -982,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" },
}, .{});
}
Loading
Loading