From e80c8d5bff5806929527f04fe78b404c34833d33 Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Sun, 12 Oct 2025 16:03:33 -0700 Subject: [PATCH] add functional Navigation --- src/browser/html/Navigation.zig | 215 ++++++++++++++++++++++---------- src/browser/html/html.zig | 1 + src/browser/html/window.zig | 5 + 3 files changed, 153 insertions(+), 68 deletions(-) diff --git a/src/browser/html/Navigation.zig b/src/browser/html/Navigation.zig index a0818828..7968d821 100644 --- a/src/browser/html/Navigation.zig +++ b/src/browser/html/Navigation.zig @@ -20,18 +20,18 @@ const std = @import("std"); const log = @import("../../log.zig"); const URL = @import("../../url.zig").URL; -const Js = @import("../js/js.zig"); +const js = @import("../js/js.zig"); const Page = @import("../page.zig").Page; -// https://developer.mozilla.org/en-US/docs/Web/API/Navigation -const Navigation = @This(); - const EventTarget = @import("../dom/event_target.zig").EventTarget; const EventHandler = @import("../events/event.zig").EventHandler; const parser = @import("../netsurf.zig"); -const Interfaces = .{ +// https://developer.mozilla.org/en-US/docs/Web/API/Navigation +const Navigation = @This(); + +pub const Interfaces = .{ Navigation, NavigationActivation, NavigationHistoryEntry, @@ -51,39 +51,76 @@ const NavigationHistoryEntry = struct { base: parser.EventTargetTBase = parser.EventTargetTBase{ .internal_target_type = .plain }, id: []const u8, - index: usize, key: []const u8, url: ?[]const u8, - same_document: bool, state: ?[]const u8, pub fn get_id(self: *const NavigationHistoryEntry) []const u8 { return self.id; } - pub fn get_index(self: *const NavigationHistoryEntry) usize { - return self.index; + pub fn get_index(self: *const NavigationHistoryEntry, page: *Page) i32 { + const navigation = page.session.navigation; + for (navigation.entries.items, 0..) |*entry, i| { + if (std.mem.eql(u8, entry.id, self.id)) { + return @intCast(i); + } + } + + return -1; } pub fn get_key(self: *const NavigationHistoryEntry) []const u8 { return self.key; } - pub fn get_sameDocument(self: *const NavigationHistoryEntry) bool { - return self.same_document; + pub fn get_sameDocument(self: *const NavigationHistoryEntry, page: *Page) !bool { + const _url = self.url orelse return false; + const url = try URL.parse(_url, null); + return page.url.eqlDocument(&url, page.arena); } pub fn get_url(self: *const NavigationHistoryEntry) ?[]const u8 { return self.url; } - pub fn _getState(self: *const NavigationHistoryEntry, page: *Page) !?Js.Value { + pub fn _getState(self: *const NavigationHistoryEntry, page: *Page) !?js.Value { if (self.state) |state| { - return try Js.Value.fromJson(page.main_context, state); + return try js.Value.fromJson(page.js, state); } else { return null; } } + + pub fn navigate(entry: NavigationHistoryEntry, reload: enum { none, force }, page: *Page) !NavigationReturn { + const arena = page.session.arena; + const url = entry.url orelse return error.MissingURL; + + // https://github.com/WICG/navigation-api/issues/95 + // + // These will only settle on same-origin navigation (mostly intended for SPAs). + // It is fine (and expected) for these to not settle on cross-origin requests :) + const committed = try page.js.createPromiseResolver(.page); + const finished = try page.js.createPromiseResolver(.page); + + const new_url = try URL.parse(url, null); + if (try page.url.eqlDocument(&new_url, arena) or reload == .force) { + page.url = new_url; + try committed.resolve({}); + + // todo: Fire navigate event + + try finished.resolve({}); + } else { + // TODO: Change to history + try page.navigateFromWebAPI(url, .{ .reason = .history }); + } + + return .{ + .committed = committed.promise(), + .finished = finished.promise(), + }; + } }; // https://developer.mozilla.org/en-US/docs/Web/API/NavigationActivation @@ -124,49 +161,61 @@ pub fn get_canGoForward(self: *const Navigation) bool { return self.entries.items.len > self.index + 1; } -pub fn get_currentEntry(_: *const Navigation) NavigationHistoryEntry { - // TODO - unreachable; +pub fn currentEntry(self: *Navigation) *NavigationHistoryEntry { + return &self.entries.items[self.index]; +} + +pub fn get_currentEntry(self: *const Navigation) NavigationHistoryEntry { + return self.entries.items[self.index]; } const NavigationReturn = struct { - comitted: Js.Promise, - finished: Js.Promise, + committed: js.Promise, + finished: js.Promise, }; -pub fn _back(_: *const Navigation) !NavigationReturn { - unreachable; +pub fn _back(self: *Navigation, page: *Page) !NavigationReturn { + if (!self.get_canGoBack()) { + return error.InvalidStateError; + } + + const new_index = self.index - 1; + const next_entry = self.entries.items[new_index]; + self.index = new_index; + + return next_entry.navigate(.none, page); } pub fn _entries(self: *const Navigation) []NavigationHistoryEntry { return self.entries.items; } -pub fn _forward(_: *const Navigation) !NavigationReturn { - unreachable; +pub fn _forward(self: *Navigation, page: *Page) !NavigationReturn { + if (!self.get_canGoForward()) { + return error.InvalidStateError; + } + + const new_index = self.index + 1; + const next_entry = self.entries.items[new_index]; + self.index = new_index; + + return next_entry.navigate(.none, page); } -const NavigateOptions = struct { - const NavigateOptionsHistory = enum { - auto, - push, - replace, - }; - - state: ?Js.Object = null, - info: ?Js.Object = null, - history: NavigateOptionsHistory = .auto, -}; - -pub fn _navigate(self: *Navigation, _url: []const u8, _opts: ?NavigateOptions, page: *Page) !NavigationReturn { +/// Pushes an entry into the Navigation stack WITHOUT actually navigating to it. +/// For that, use `navigate`. +pub fn pushEntry(self: *Navigation, _url: ?[]const u8, _opts: ?NavigateOptions, page: *Page) !NavigationHistoryEntry { const arena = page.session.arena; const options = _opts orelse NavigateOptions{}; - const url = try arena.dupe(u8, _url); + const url = if (_url) |u| try arena.dupe(u8, u) else null; - // TODO: handle push history NotSupportedError. + // truncates our history here. + if (self.entries.items.len > self.index + 1) { + self.entries.shrinkRetainingCapacity(self.index + 1); + } + self.index = self.entries.items.len; - const index = self.entries.items.len; const id = self.next_entry_id; self.next_entry_id += 1; @@ -174,7 +223,7 @@ pub fn _navigate(self: *Navigation, _url: []const u8, _opts: ?NavigateOptions, p const state: ?[]const u8 = blk: { if (options.state) |s| { - break :blk try s.toJson(arena); + break :blk s.toJson(arena) catch return error.DataClone; } else { break :blk null; } @@ -182,40 +231,70 @@ pub fn _navigate(self: *Navigation, _url: []const u8, _opts: ?NavigateOptions, p const entry = NavigationHistoryEntry{ .id = id_str, - .index = index, - .same_document = false, - .url = url, .key = id_str, + .url = url, .state = state, }; try self.entries.append(arena, entry); - // https://github.com/WICG/navigation-api/issues/95 - // - // These will only settle on same-origin navigation (mostly intended for SPAs). - // It is fine (and expected) for these to not settle on cross-origin requests :) - const committed = try page.main_context.createPersistentPromiseResolver(.page); - const finished = try page.main_context.createPersistentPromiseResolver(.page); - - if (entry.same_document) { - page.url = try URL.parse(url, null); - try committed.resolve(void); - - // todo: Fire navigate event - // - - } else { - page.navigateFromWebAPI(url, .{ .reason = .navigation }); - } - - return .{ - .comitted = committed, - .finished = finished, - }; + return entry; } -// const testing = @import("../../testing.zig"); -// test "Browser: Navigation" { -// try testing.htmlRunner("html/navigation.html"); -// } +const NavigateOptions = struct { + const NavigateOptionsHistory = enum { + pub const ENUM_JS_USE_TAG = true; + + auto, + push, + replace, + }; + + state: ?js.Object = null, + info: ?js.Object = null, + history: NavigateOptionsHistory = .auto, +}; + +pub fn _navigate(self: *Navigation, _url: []const u8, _opts: ?NavigateOptions, page: *Page) !NavigationReturn { + const entry = try self.pushEntry(_url, _opts, page); + return entry.navigate(.none, page); +} + +pub const ReloadOptions = struct { + state: ?js.Object = null, + info: ?js.Object = null, +}; + +pub fn _reload(self: *Navigation, _opts: ?ReloadOptions, page: *Page) !NavigationReturn { + const arena = page.session.arena; + + const opts = _opts orelse ReloadOptions{}; + const entry = self.currentEntry(); + if (opts.state) |state| { + entry.state = state.toJson(arena) catch return error.DataClone; + } + + return entry.navigate(.force, page); +} + +pub fn _transition(_: *const Navigation) !NavigationReturn { + unreachable; +} + +pub fn _traverseTo(_: *const Navigation, _: []const u8) !NavigationReturn { + unreachable; +} + +pub const UpdateCurrentEntryOptions = struct { + state: js.Object, +}; + +pub fn _updateCurrentEntry(self: *Navigation, options: UpdateCurrentEntryOptions, page: *Page) !void { + const arena = page.session.arena; + self.currentEntry().state = options.state.toJson(arena) catch return error.DataClone; +} + +const testing = @import("../../testing.zig"); +test "Browser: Navigation" { + try testing.htmlRunner("html/navigation.html"); +} diff --git a/src/browser/html/html.zig b/src/browser/html/html.zig index ef8a99f7..d5aa81a1 100644 --- a/src/browser/html/html.zig +++ b/src/browser/html/html.zig @@ -34,6 +34,7 @@ pub const Interfaces = .{ Window, Navigator, History, + @import("Navigation.zig").Interfaces, Location, MediaQueryList, @import("DataSet.zig"), diff --git a/src/browser/html/window.zig b/src/browser/html/window.zig index ef015492..e6ea1963 100644 --- a/src/browser/html/window.zig +++ b/src/browser/html/window.zig @@ -25,6 +25,7 @@ const Page = @import("../page.zig").Page; const Navigator = @import("navigator.zig").Navigator; const History = @import("History.zig"); +const Navigation = @import("Navigation.zig"); const Location = @import("location.zig").Location; const Crypto = @import("../crypto/crypto.zig").Crypto; const Console = @import("../console/console.zig").Console; @@ -190,6 +191,10 @@ pub const Window = struct { return &page.session.history; } + pub fn get_navigation(_: *Window, page: *Page) *Navigation { + return &page.session.navigation; + } + // The interior height of the window in pixels, including the height of the horizontal scroll bar, if present. pub fn get_innerHeight(_: *Window, page: *Page) u32 { // We do not have scrollbars or padding so this is the same as Element.clientHeight