Merge pull request #1576 from lightpanda-io/navigation-is-event-target
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
e2e-test / browser fetch (push) Has been cancelled
zig-test / zig test using v8 in debug mode (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled

Navigation is the EventTarget
This commit is contained in:
Karl Seguin
2026-02-18 07:33:48 +08:00
committed by GitHub
6 changed files with 86 additions and 119 deletions

View File

@@ -175,6 +175,13 @@ pub fn eventTarget(self: *Factory, child: anytype) !*@TypeOf(child) {
return chain.get(1); return chain.get(1);
} }
pub fn standaloneEventTarget(self: *Factory, child: anytype) !*EventTarget {
const allocator = self._slab.allocator();
const et = try allocator.create(EventTarget);
et.* = .{ ._type = unionInit(EventTarget.Type, child) };
return et;
}
// this is a root object // this is a root object
pub fn event(self: *Factory, arena: Allocator, typ: String, child: anytype) !*@TypeOf(child) { pub fn event(self: *Factory, arena: Allocator, typ: String, child: anytype) !*@TypeOf(child) {
const chain = try PrototypeChain( const chain = try PrototypeChain(

View File

@@ -75,7 +75,8 @@ pub fn init(self: *Session, browser: *Browser, notification: *Notification) !voi
.page = null, .page = null,
.arena = arena, .arena = arena,
.history = .{}, .history = .{},
.navigation = .{}, // The prototype (EventTarget) for Navigation is created when a Page is created.
.navigation = .{ ._proto = undefined },
.storage_shed = .{}, .storage_shed = .{},
.browser = browser, .browser = browser,
.notification = notification, .notification = notification,

View File

@@ -861,7 +861,6 @@ pub const JsApis = flattenTypes(&.{
@import("../webapi/VisualViewport.zig"), @import("../webapi/VisualViewport.zig"),
@import("../webapi/PerformanceObserver.zig"), @import("../webapi/PerformanceObserver.zig"),
@import("../webapi/navigation/Navigation.zig"), @import("../webapi/navigation/Navigation.zig"),
@import("../webapi/navigation/NavigationEventTarget.zig"),
@import("../webapi/navigation/NavigationHistoryEntry.zig"), @import("../webapi/navigation/NavigationHistoryEntry.zig"),
@import("../webapi/navigation/NavigationActivation.zig"), @import("../webapi/navigation/NavigationActivation.zig"),
@import("../webapi/canvas/CanvasRenderingContext2D.zig"), @import("../webapi/canvas/CanvasRenderingContext2D.zig"),

View File

@@ -39,7 +39,7 @@ pub const Type = union(enum) {
media_query_list: *@import("css/MediaQueryList.zig"), media_query_list: *@import("css/MediaQueryList.zig"),
message_port: *@import("MessagePort.zig"), message_port: *@import("MessagePort.zig"),
text_track_cue: *@import("media/TextTrackCue.zig"), text_track_cue: *@import("media/TextTrackCue.zig"),
navigation: *@import("navigation/NavigationEventTarget.zig"), navigation: *@import("navigation/Navigation.zig"),
screen: *@import("Screen.zig"), screen: *@import("Screen.zig"),
screen_orientation: *@import("Screen.zig").Orientation, screen_orientation: *@import("Screen.zig").Orientation,
visual_viewport: *@import("VisualViewport.zig"), visual_viewport: *@import("VisualViewport.zig"),

View File

@@ -24,6 +24,8 @@ const URL = @import("../URL.zig");
const js = @import("../../js/js.zig"); const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig"); const Page = @import("../../Page.zig");
const IS_DEBUG = @import("builtin").mode == .Debug;
const EventTarget = @import("../EventTarget.zig"); const EventTarget = @import("../EventTarget.zig");
// https://developer.mozilla.org/en-US/docs/Web/API/Navigation // https://developer.mozilla.org/en-US/docs/Web/API/Navigation
@@ -36,9 +38,10 @@ const NavigationState = @import("root.zig").NavigationState;
const NavigationHistoryEntry = @import("NavigationHistoryEntry.zig"); const NavigationHistoryEntry = @import("NavigationHistoryEntry.zig");
const NavigationCurrentEntryChangeEvent = @import("../event/NavigationCurrentEntryChangeEvent.zig"); const NavigationCurrentEntryChangeEvent = @import("../event/NavigationCurrentEntryChangeEvent.zig");
const NavigationEventTarget = @import("NavigationEventTarget.zig");
_proto: *NavigationEventTarget = undefined, _proto: *EventTarget,
_on_currententrychange: ?js.Function.Global = null,
_current_navigation_kind: ?NavigationKind = null, _current_navigation_kind: ?NavigationKind = null,
_index: usize = 0, _index: usize = 0,
@@ -48,7 +51,7 @@ _next_entry_id: usize = 0,
_activation: ?NavigationActivation = null, _activation: ?NavigationActivation = null,
fn asEventTarget(self: *Navigation) *EventTarget { fn asEventTarget(self: *Navigation) *EventTarget {
return self._proto.asEventTarget(); return self._proto;
} }
pub fn onRemovePage(self: *Navigation) void { pub fn onRemovePage(self: *Navigation) void {
@@ -56,9 +59,7 @@ pub fn onRemovePage(self: *Navigation) void {
} }
pub fn onNewPage(self: *Navigation, page: *Page) !void { pub fn onNewPage(self: *Navigation, page: *Page) !void {
self._proto = try page._factory.eventTarget( self._proto = try page._factory.standaloneEventTarget(self);
NavigationEventTarget{ ._proto = undefined },
);
} }
pub fn getActivation(self: *const Navigation) ?NavigationActivation { pub fn getActivation(self: *const Navigation) ?NavigationActivation {
@@ -124,13 +125,19 @@ pub fn forward(self: *Navigation, page: *Page) !NavigationReturn {
return self.navigateInner(next_entry._url, .{ .traverse = new_index }, page); return self.navigateInner(next_entry._url, .{ .traverse = new_index }, page);
} }
pub fn updateEntries(self: *Navigation, url: [:0]const u8, kind: NavigationKind, page: *Page, dispatch: bool) !void { pub fn updateEntries(
self: *Navigation,
url: [:0]const u8,
kind: NavigationKind,
page: *Page,
should_dispatch: bool,
) !void {
switch (kind) { switch (kind) {
.replace => |state| { .replace => |state| {
_ = try self.replaceEntry(url, .{ .source = .navigation, .value = state }, page, dispatch); _ = try self.replaceEntry(url, .{ .source = .navigation, .value = state }, page, should_dispatch);
}, },
.push => |state| { .push => |state| {
_ = try self.pushEntry(url, .{ .source = .navigation, .value = state }, page, dispatch); _ = try self.pushEntry(url, .{ .source = .navigation, .value = state }, page, should_dispatch);
}, },
.traverse => |index| { .traverse => |index| {
self._index = index; self._index = index;
@@ -166,7 +173,7 @@ pub fn pushEntry(
_url: [:0]const u8, _url: [:0]const u8,
state: NavigationState, state: NavigationState,
page: *Page, page: *Page,
dispatch: bool, should_dispatch: bool,
) !*NavigationHistoryEntry { ) !*NavigationHistoryEntry {
const arena = page._session.arena; const arena = page._session.arena;
const url = try arena.dupeZ(u8, _url); const url = try arena.dupeZ(u8, _url);
@@ -197,13 +204,13 @@ pub fn pushEntry(
self._index = index; self._index = index;
if (previous) |prev| { if (previous) |prev| {
if (dispatch) { if (should_dispatch) {
const event = try NavigationCurrentEntryChangeEvent.initTrusted( const event = try NavigationCurrentEntryChangeEvent.initTrusted(
.wrap("currententrychange"), .wrap("currententrychange"),
.{ .from = prev, .navigationType = @tagName(.push) }, .{ .from = prev, .navigationType = @tagName(.push) },
page, page,
); );
try self._proto.dispatch(.{ .currententrychange = event }, page); try self.dispatch(.{ .currententrychange = event }, page);
} }
} }
@@ -215,7 +222,7 @@ pub fn replaceEntry(
_url: [:0]const u8, _url: [:0]const u8,
state: NavigationState, state: NavigationState,
page: *Page, page: *Page,
dispatch: bool, should_dispatch: bool,
) !*NavigationHistoryEntry { ) !*NavigationHistoryEntry {
const arena = page._session.arena; const arena = page._session.arena;
const url = try arena.dupeZ(u8, _url); const url = try arena.dupeZ(u8, _url);
@@ -236,13 +243,13 @@ pub fn replaceEntry(
self._entries.items[self._index] = entry; self._entries.items[self._index] = entry;
if (dispatch) { if (should_dispatch) {
const event = try NavigationCurrentEntryChangeEvent.initTrusted( const event = try NavigationCurrentEntryChangeEvent.initTrusted(
.wrap("currententrychange"), .wrap("currententrychange"),
.{ .from = previous, .navigationType = @tagName(.replace) }, .{ .from = previous, .navigationType = @tagName(.replace) },
page, page,
); );
try self._proto.dispatch(.{ .currententrychange = event }, page); try self.dispatch(.{ .currententrychange = event }, page);
} }
return entry; return entry;
@@ -334,7 +341,7 @@ pub fn navigateInner(
.{ .from = previous, .navigationType = @tagName(kind) }, .{ .from = previous, .navigationType = @tagName(kind) },
page, page,
); );
try self._proto.dispatch(.{ .currententrychange = event }, page); try self.dispatch(.{ .currententrychange = event }, page);
_ = try committed.persist(); _ = try committed.persist();
_ = try finished.persist(); _ = try finished.persist();
@@ -376,7 +383,7 @@ pub fn reload(self: *Navigation, _opts: ?ReloadOptions, page: *Page) !Navigation
.{ .from = previous, .navigationType = @tagName(.reload) }, .{ .from = previous, .navigationType = @tagName(.reload) },
page, page,
); );
try self._proto.dispatch(.{ .currententrychange = event }, page); try self.dispatch(.{ .currententrychange = event }, page);
} }
return self.navigateInner(entry._url, .reload, page); return self.navigateInner(entry._url, .reload, page);
@@ -418,7 +425,52 @@ pub fn updateCurrentEntry(self: *Navigation, options: UpdateCurrentEntryOptions,
.{ .from = previous, .navigationType = null }, .{ .from = previous, .navigationType = null },
page, page,
); );
try self._proto.dispatch(.{ .currententrychange = event }, page); try self.dispatch(.{ .currententrychange = event }, page);
}
const DispatchType = union(enum) {
currententrychange: *NavigationCurrentEntryChangeEvent,
};
pub fn dispatch(self: *Navigation, event_type: DispatchType, page: *Page) !void {
const event, const field = blk: {
break :blk switch (event_type) {
.currententrychange => |cec| .{ cec.asEvent(), "_on_currententrychange" },
};
};
defer if (!event._v8_handoff) event.deinit(false);
if (comptime IS_DEBUG) {
if (page.js.local == null) {
log.fatal(.bug, "null context scope", .{ .src = "Navigation.dispatch", .url = page.url });
std.debug.assert(page.js.local != null);
}
}
const func = @field(self, field) orelse return;
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
return page._event_manager.dispatchWithFunction(
self.asEventTarget(),
event,
ls.toLocal(func),
.{ .context = "Navigation" },
);
}
fn getOnCurrentEntryChange(self: *Navigation) ?js.Function.Global {
return self._on_currententrychange;
}
pub fn setOnCurrentEntryChange(self: *Navigation, listener: ?js.Function) !void {
if (listener) |listen| {
self._on_currententrychange = try listen.persistWithThis(self);
} else {
self._on_currententrychange = null;
}
} }
pub const JsApi = struct { pub const JsApi = struct {
@@ -441,4 +493,10 @@ pub const JsApi = struct {
pub const navigate = bridge.function(Navigation.navigate, .{}); pub const navigate = bridge.function(Navigation.navigate, .{});
pub const traverseTo = bridge.function(Navigation.traverseTo, .{}); pub const traverseTo = bridge.function(Navigation.traverseTo, .{});
pub const updateCurrentEntry = bridge.function(Navigation.updateCurrentEntry, .{}); pub const updateCurrentEntry = bridge.function(Navigation.updateCurrentEntry, .{});
pub const oncurrententrychange = bridge.accessor(
Navigation.getOnCurrentEntryChange,
Navigation.setOnCurrentEntryChange,
.{},
);
}; };

View File

@@ -1,98 +0,0 @@
// Copyright (C) 2023-2025 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 std = @import("std");
const log = @import("../../../log.zig");
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const IS_DEBUG = @import("builtin").mode == .Debug;
const EventTarget = @import("../EventTarget.zig");
const NavigationCurrentEntryChangeEvent = @import("../event/NavigationCurrentEntryChangeEvent.zig");
pub const NavigationEventTarget = @This();
_proto: *EventTarget,
_on_currententrychange: ?js.Function.Global = null,
pub fn asEventTarget(self: *NavigationEventTarget) *EventTarget {
return self._proto;
}
const DispatchType = union(enum) {
currententrychange: *NavigationCurrentEntryChangeEvent,
};
pub fn dispatch(self: *NavigationEventTarget, event_type: DispatchType, page: *Page) !void {
const event, const field = blk: {
break :blk switch (event_type) {
.currententrychange => |cec| .{ cec.asEvent(), "_on_currententrychange" },
};
};
defer if (!event._v8_handoff) event.deinit(false);
if (comptime IS_DEBUG) {
if (page.js.local == null) {
log.fatal(.bug, "null context scope", .{ .src = "NavigationEventTarget.dispatch", .url = page.url });
std.debug.assert(page.js.local != null);
}
}
const func = @field(self, field) orelse return;
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
return page._event_manager.dispatchWithFunction(
self.asEventTarget(),
event,
ls.toLocal(func),
.{ .context = "Navigation" },
);
}
pub fn getOnCurrentEntryChange(self: *NavigationEventTarget) ?js.Function.Global {
return self._on_currententrychange;
}
pub fn setOnCurrentEntryChange(self: *NavigationEventTarget, listener: ?js.Function) !void {
if (listener) |listen| {
self._on_currententrychange = try listen.persistWithThis(self);
} else {
self._on_currententrychange = null;
}
}
pub const JsApi = struct {
pub const bridge = js.Bridge(NavigationEventTarget);
pub const Meta = struct {
pub const name = "NavigationEventTarget";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const oncurrententrychange = bridge.accessor(
NavigationEventTarget.getOnCurrentEntryChange,
NavigationEventTarget.setOnCurrentEntryChange,
.{},
);
};