complete History impl backed by Navigation

This commit is contained in:
Muki Kiboigo
2025-12-08 05:16:48 -08:00
parent ee7852665e
commit 01d71323fc
7 changed files with 157 additions and 55 deletions

View File

@@ -34,7 +34,6 @@ const Mime = @import("Mime.zig");
const Factory = @import("Factory.zig");
const Session = @import("Session.zig");
const Scheduler = @import("Scheduler.zig");
const History = @import("webapi/History.zig");
const EventManager = @import("EventManager.zig");
const ScriptManager = @import("ScriptManager.zig");
@@ -211,7 +210,6 @@ fn reset(self: *Page, comptime initializing: bool) !void {
self.window = try self._factory.eventTarget(Window{
._document = self.document,
._storage_bucket = storage_bucket,
._history = History.init(self),
._performance = Performance.init(),
._proto = undefined,
._location = &default_location,
@@ -1903,6 +1901,12 @@ const IdleNotification = union(enum) {
}
};
pub fn isSameOrigin(self: *const Page, url: [:0]const u8) !bool {
const URLRaw = @import("URL.zig");
const current_origin = (try URLRaw.getOrigin(self.arena, self.url)) orelse return false;
return std.mem.startsWith(u8, url, current_origin);
}
pub const NavigateReason = enum {
anchor,
address_bar,

View File

@@ -23,6 +23,7 @@ const log = @import("../log.zig");
const js = @import("js/js.zig");
const storage = @import("webapi/storage/storage.zig");
const Navigation = @import("webapi/navigation/Navigation.zig");
const History = @import("webapi/History.zig");
const Page = @import("Page.zig");
const Browser = @import("Browser.zig");
@@ -55,6 +56,7 @@ executor: js.ExecutionWorld,
cookie_jar: storage.Cookie.Jar,
storage_shed: storage.Shed,
history: History,
navigation: Navigation,
page: ?*Page = null,
@@ -80,6 +82,7 @@ pub fn init(self: *Session, browser: *Browser) !void {
.arena = session_allocator,
.cookie_jar = storage.Cookie.Jar.init(allocator),
.navigation = Navigation.init(session_allocator),
.history = .{},
.transfer_arena = browser.transfer_arena.allocator(),
};
}

View File

@@ -569,6 +569,7 @@ pub const JsApis = flattenTypes(&.{
@import("../webapi/event/ProgressEvent.zig"),
@import("../webapi/event/NavigationCurrentEntryChangeEvent.zig"),
@import("../webapi/event/PageTransitionEvent.zig"),
@import("../webapi/event/PopStateEvent.zig"),
@import("../webapi/MessageChannel.zig"),
@import("../webapi/MessagePort.zig"),
@import("../webapi/media/MediaError.zig"),

View File

@@ -58,6 +58,7 @@ pub const Type = union(enum) {
composition_event: *@import("event/CompositionEvent.zig"),
navigation_current_entry_change_event: *@import("event/NavigationCurrentEntryChangeEvent.zig"),
page_transition_event: *@import("event/PageTransitionEvent.zig"),
pop_state_event: *@import("event/PopStateEvent.zig"),
};
const Options = struct {

View File

@@ -20,67 +20,75 @@ const std = @import("std");
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const PopStateEvent = @import("event/PopStateEvent.zig");
const History = @This();
_page: *Page,
_length: u32 = 1,
_state: ?js.Object = null,
pub fn init(page: *Page) History {
return .{
._page = page,
};
pub fn getLength(_: *const History, page: *Page) u32 {
return @intCast(page._session.navigation._entries.items.len);
}
pub fn deinit(self: *History) void {
if (self._state) |state| {
js.q.JS_FreeValue(self._page.js.ctx, state.value);
pub fn getState(_: *const History, page: *Page) !?js.Value {
if (page._session.navigation.getCurrentEntry()._state.value) |state| {
const value = try js.Value.fromJson(page.js, state);
return value;
} else return null;
}
pub fn pushState(_: *History, state: js.Object, _: []const u8, _url: ?[]const u8, page: *Page) !void {
const arena = page._session.arena;
const url = if (_url) |u| try arena.dupeZ(u8, u) else try arena.dupeZ(u8, page.url);
const json = state.toJson(arena) catch return error.DateClone;
_ = try page._session.navigation.pushEntry(url, .{ .source = .history, .value = json }, page, true);
}
pub fn replaceState(_: *History, state: js.Object, _: []const u8, _url: ?[]const u8, page: *Page) !void {
const arena = page._session.arena;
const url = if (_url) |u| try arena.dupeZ(u8, u) else try arena.dupeZ(u8, page.url);
const json = state.toJson(arena) catch return error.DateClone;
_ = try page._session.navigation.replaceEntry(url, .{ .source = .history, .value = json }, page, true);
}
fn goInner(delta: i32, page: *Page) !void {
// 0 behaves the same as no argument, both reloadig the page.
const current = page._session.navigation._index;
const index_s: i64 = @intCast(@as(i64, @intCast(current)) + @as(i64, @intCast(delta)));
if (index_s < 0 or index_s > page._session.navigation._entries.items.len - 1) {
return;
}
const index = @as(usize, @intCast(index_s));
const entry = page._session.navigation._entries.items[index];
if (entry._url) |url| {
if (try page.isSameOrigin(url)) {
const event = try PopStateEvent.init("popstate", .{ .state = entry._state.value }, page);
try page._event_manager.dispatchWithFunction(
page.window.asEventTarget(),
event.asEvent(),
page.window._on_popstate,
.{ .context = "Pop State" },
);
}
}
_ = try page._session.navigation.navigateInner(entry._url, .{ .traverse = index }, page);
}
pub fn getLength(self: *const History) u32 {
return self._length;
pub fn back(_: *History, page: *Page) !void {
try goInner(-1, page);
}
pub fn getState(self: *const History) ?js.Object {
return self._state;
pub fn forward(_: *History, page: *Page) !void {
try goInner(1, page);
}
pub fn pushState(self: *History, state: js.Object, _title: []const u8, url: ?[]const u8, page: *Page) !void {
_ = _title; // title is ignored in modern browsers
_ = url; // For minimal implementation, we don't actually navigate
_ = page;
self._state = try state.persist();
self._length += 1;
}
pub fn replaceState(self: *History, state: js.Object, _title: []const u8, url: ?[]const u8, page: *Page) !void {
_ = _title;
_ = url;
_ = page;
self._state = try state.persist();
// Note: replaceState doesn't change length
}
pub fn back(self: *History, page: *Page) void {
_ = self;
_ = page;
// Minimal implementation: no-op
}
pub fn forward(self: *History, page: *Page) void {
_ = self;
_ = page;
// Minimal implementation: no-op
}
pub fn go(self: *History, delta: i32, page: *Page) void {
_ = self;
_ = delta;
_ = page;
// Minimal implementation: no-op
pub fn go(_: *History, delta: ?i32, page: *Page) !void {
try goInner(delta orelse 0, page);
}
pub const JsApi = struct {

View File

@@ -52,10 +52,10 @@ _console: Console = .init,
_navigator: Navigator = .init,
_screen: Screen = .init,
_performance: Performance,
_history: History,
_storage_bucket: *storage.Bucket,
_on_load: ?js.Function = null,
_on_pageshow: ?js.Function = null,
_on_popstate: ?js.Function = null,
_on_error: ?js.Function = null, // TODO: invoke on error?
_on_unhandled_rejection: ?js.Function = null, // TODO: invoke on error
_location: *Location,
@@ -115,8 +115,8 @@ pub fn getLocation(self: *const Window) *Location {
return self._location;
}
pub fn getHistory(self: *Window) *History {
return &self._history;
pub fn getHistory(_: *Window, page: *Page) *History {
return &page._session.history;
}
pub fn getNavigation(_: *Window, page: *Page) *Navigation {
@@ -151,6 +151,18 @@ pub fn setOnPageShow(self: *Window, cb_: ?js.Function) !void {
}
}
pub fn getOnPopState(self: *const Window) ?js.Function {
return self._on_popstate;
}
pub fn setOnPopState(self: *Window, cb_: ?js.Function) !void {
if (cb_) |cb| {
self._on_popstate = cb;
} else {
self._on_popstate = null;
}
}
pub fn getOnError(self: *const Window) ?js.Function {
return self._on_error;
}
@@ -504,13 +516,14 @@ pub const JsApi = struct {
pub const sessionStorage = bridge.accessor(Window.getSessionStorage, null, .{ .cache = "sessionStorage" });
pub const document = bridge.accessor(Window.getDocument, null, .{ .cache = "document" });
pub const location = bridge.accessor(Window.getLocation, null, .{ .cache = "location" });
pub const history = bridge.accessor(Window.getHistory, null, .{ .cache = "history" });
pub const history = bridge.accessor(Window.getHistory, null, .{});
pub const navigation = bridge.accessor(Window.getNavigation, null, .{});
pub const crypto = bridge.accessor(Window.getCrypto, null, .{ .cache = "crypto" });
pub const CSS = bridge.accessor(Window.getCSS, null, .{ .cache = "CSS" });
pub const customElements = bridge.accessor(Window.getCustomElements, null, .{ .cache = "customElements" });
pub const onload = bridge.accessor(Window.getOnLoad, Window.setOnLoad, .{});
pub const onpageshow = bridge.accessor(Window.getOnPageShow, Window.setOnPageShow, .{});
pub const onpopstate = bridge.accessor(Window.getOnPopState, Window.setOnPopState, .{});
pub const onerror = bridge.accessor(Window.getOnError, Window.getOnError, .{});
pub const onunhandledrejection = bridge.accessor(Window.getOnUnhandledRejection, Window.setOnUnhandledRejection, .{});
pub const fetch = bridge.function(Window.fetch, .{});

View File

@@ -0,0 +1,72 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const log = @import("../../../log.zig");
// const Window = @import("../html/window.zig").Window;
const Event = @import("../Event.zig");
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
// https://developer.mozilla.org/en-US/docs/Web/API/PopStateEvent
const PopStateEvent = @This();
const EventOptions = struct {
state: ?[]const u8 = null,
};
_proto: *Event,
_state: ?[]const u8,
pub fn init(typ: []const u8, _options: ?EventOptions, page: *Page) !*PopStateEvent {
const options = _options orelse EventOptions{};
return page._factory.event(typ, PopStateEvent{
._proto = undefined,
._state = options.state,
});
}
pub fn asEvent(self: *PopStateEvent) *Event {
return self._proto;
}
pub fn getState(self: *PopStateEvent, page: *Page) !?js.Value {
if (self._state == null) return null;
const value = try js.Value.fromJson(page.js, self._state.?);
return value;
}
pub fn getUAVisualTransition(_: *PopStateEvent) bool {
// Not currently supported so we always return false;
return false;
}
pub const JsApi = struct {
pub const bridge = js.Bridge(PopStateEvent);
pub const Meta = struct {
pub const name = "PopStateEvent";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const constructor = bridge.constructor(PopStateEvent.init, .{});
pub const state = bridge.accessor(PopStateEvent.getState, null, .{});
pub const hasUAVisualTransition = bridge.accessor(PopStateEvent.getUAVisualTransition, null, .{});
};