mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-24 17:33:06 -04:00
At a high level, this does for Events what was recently done for XHR, Fetch and
Observers. Events are self-contained in their own arena from the ArenaPool and
are registered with v8 to be finalized.
But events are more complicated than those other types. For one, events have
a prototype chain. (XHR also does, but it's always the top-level object that's
created, whereas it's valid to create a base Event or something that inherits
from Event). But the _real_ complication is that Events, unlike previous types,
can be created from Zig or from V8.
This is something that Fetch had to deal with too, because the Response is only
given to V8 on success. So in Fetch, there's a period of time where Zig is
solely responsible for the Response, until it's passed to v8. But with events
it's a lot more subtle.
There are 3 possibilities:
1 - An Event is created from v8. This is the simplest, and it simply becomes a
a weak reference for us. When v8 is done with it, the finalizer is called.
2 - An Event is created in Zig (e.g. window.load) and dispatched to v8. Again
we can rely on the v8 finalizer.
3 - An event is created in Zig, but not dispatched to v8 (e.g. there are no
listeners), Zig has to release the event.
(It's worth pointing out that one thing that still keeps this relatively
straightforward is that we never hold on to Events past some pretty clear point)
Now, it would seem that #3 is the only issue we have to deal with, and maybe
we can do something like:
```
if (event_manager.hasListener("load", capture)) {
try event_manager.dispatch(event);
} else {
event.deinit();
}
```
In fact, in many cases, we could use this to optimize not even creating the
event:
```
if (event_manager.hasListener("load, capture)) {
const event = try createEvent("load", capture);
try event_manager.dispatch(event);
}
```
And that's an optimization worth considering, but it isn't good enough to
properly manage memory. Do you see the issue? There could be a listener (so we
think v8 owns it), but we might never give the value to v8. Any failure between
hasListener and actually handing the value to v8 would result in a leak.
To solve this, the bridge will now set a _v8_handover flag (if present) once it
has created the finalizer_callback entry. So dispatching code now becomes:
```
const event = try createEvent("load", capture);
defer if (!event._v8_handover) event.deinit(false);
try event_manager.dispatch(event);
```
The v8 finalizer callback was also improved. Previously, we just embedded the
pointer to the zig object. In the v8 callback, we could cast that back to T
and call deinit. But, because of possible timing issues between when (if) v8
calls the finalizer, and our own cleanup, the code would check in the context to
see if the ptr was still valid. Wait, what? We're using the ptr to get the
context to see if the ptr is valid?
We now store a pointer to the FinalizerCallback which contains the context.
So instead of something stupid like:
```
// note, if the identity_map doesn't contain the value, then value is likely
// invalid, and value.page will segfault
value.page.js.identity_map.contains(@intFromPtr(value))
```
We do:
```
if (fc.ctx.finalizer_callbacks.contains(@intFromPtr(fc.value)) {
// fc.value is safe to use
}
```
442 lines
15 KiB
Zig
442 lines
15 KiB
Zig
// 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 js = @import("../js/js.zig");
|
|
|
|
const Page = @import("../Page.zig");
|
|
const EventTarget = @import("EventTarget.zig");
|
|
const Node = @import("Node.zig");
|
|
const String = @import("../../string.zig").String;
|
|
|
|
const Allocator = std.mem.Allocator;
|
|
|
|
pub const Event = @This();
|
|
|
|
pub const _prototype_root = true;
|
|
_type: Type,
|
|
_page: *Page,
|
|
_arena: Allocator,
|
|
_bubbles: bool = false,
|
|
_cancelable: bool = false,
|
|
_composed: bool = false,
|
|
_type_string: String,
|
|
_target: ?*EventTarget = null,
|
|
_current_target: ?*EventTarget = null,
|
|
_dispatch_target: ?*EventTarget = null, // Original target for composedPath()
|
|
_prevent_default: bool = false,
|
|
_stop_propagation: bool = false,
|
|
_stop_immediate_propagation: bool = false,
|
|
_event_phase: EventPhase = .none,
|
|
_time_stamp: u64,
|
|
_needs_retargeting: bool = false,
|
|
_isTrusted: bool = false,
|
|
|
|
// There's a period of time between creating an event and handing it off to v8
|
|
// where things can fail. If it does fail, we need to deinit the event. This flag
|
|
// when true, tells us the event is registered in the js.Contxt and thus, at
|
|
// the very least, will be finalized on context shutdown.
|
|
_v8_handoff: bool = false,
|
|
|
|
pub const EventPhase = enum(u8) {
|
|
none = 0,
|
|
capturing_phase = 1,
|
|
at_target = 2,
|
|
bubbling_phase = 3,
|
|
};
|
|
|
|
pub const Type = union(enum) {
|
|
generic,
|
|
error_event: *@import("event/ErrorEvent.zig"),
|
|
custom_event: *@import("event/CustomEvent.zig"),
|
|
message_event: *@import("event/MessageEvent.zig"),
|
|
progress_event: *@import("event/ProgressEvent.zig"),
|
|
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"),
|
|
ui_event: *@import("event/UIEvent.zig"),
|
|
};
|
|
|
|
pub const Options = struct {
|
|
bubbles: bool = false,
|
|
cancelable: bool = false,
|
|
composed: bool = false,
|
|
};
|
|
|
|
pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*Event {
|
|
const arena = try page.getArena(.{ .debug = "Event" });
|
|
errdefer page.releaseArena(arena);
|
|
const str = try String.init(arena, typ, .{});
|
|
return initWithTrusted(arena, str, opts_, false, page);
|
|
}
|
|
|
|
pub fn initTrusted(typ: String, opts_: ?Options, page: *Page) !*Event {
|
|
const arena = try page.getArena(.{ .debug = "Event.trusted" });
|
|
errdefer page.releaseArena(arena);
|
|
return initWithTrusted(arena, typ, opts_, true, page);
|
|
}
|
|
|
|
fn initWithTrusted(arena: Allocator, typ: String, opts_: ?Options, trusted: bool, page: *Page) !*Event {
|
|
const opts = opts_ orelse Options{};
|
|
|
|
// Round to 2ms for privacy (browsers do this)
|
|
const raw_timestamp = @import("../../datetime.zig").milliTimestamp(.monotonic);
|
|
const time_stamp = (raw_timestamp / 2) * 2;
|
|
|
|
const event = try arena.create(Event);
|
|
event.* = .{
|
|
._page = page,
|
|
._arena = arena,
|
|
._type = .generic,
|
|
._bubbles = opts.bubbles,
|
|
._time_stamp = time_stamp,
|
|
._cancelable = opts.cancelable,
|
|
._composed = opts.composed,
|
|
._type_string = typ,
|
|
._isTrusted = trusted,
|
|
};
|
|
return event;
|
|
}
|
|
|
|
pub fn initEvent(
|
|
self: *Event,
|
|
event_string: []const u8,
|
|
bubbles: ?bool,
|
|
cancelable: ?bool,
|
|
) !void {
|
|
if (self._event_phase != .none) {
|
|
return;
|
|
}
|
|
|
|
self._type_string = try String.init(self._arena, event_string, .{});
|
|
self._bubbles = bubbles orelse false;
|
|
self._cancelable = cancelable orelse false;
|
|
self._stop_propagation = false;
|
|
}
|
|
|
|
pub fn deinit(self: *Event, shutdown: bool) void {
|
|
_ = shutdown;
|
|
self._page.releaseArena(self._arena);
|
|
}
|
|
|
|
pub fn as(self: *Event, comptime T: type) *T {
|
|
return self.is(T).?;
|
|
}
|
|
|
|
pub fn is(self: *Event, comptime T: type) ?*T {
|
|
switch (self._type) {
|
|
.generic => return if (T == Event) self else null,
|
|
.error_event => |e| return if (T == @import("event/ErrorEvent.zig")) e else null,
|
|
.custom_event => |e| return if (T == @import("event/CustomEvent.zig")) e else null,
|
|
.message_event => |e| return if (T == @import("event/MessageEvent.zig")) e else null,
|
|
.progress_event => |e| return if (T == @import("event/ProgressEvent.zig")) e else null,
|
|
.composition_event => |e| return if (T == @import("event/CompositionEvent.zig")) e else null,
|
|
.navigation_current_entry_change_event => |e| return if (T == @import("event/NavigationCurrentEntryChangeEvent.zig")) e else null,
|
|
.page_transition_event => |e| return if (T == @import("event/PageTransitionEvent.zig")) e else null,
|
|
.pop_state_event => |e| return if (T == @import("event/PopStateEvent.zig")) e else null,
|
|
.ui_event => |e| {
|
|
if (T == @import("event/UIEvent.zig")) {
|
|
return e;
|
|
}
|
|
return e.is(T);
|
|
},
|
|
}
|
|
return null;
|
|
}
|
|
|
|
pub fn getType(self: *const Event) []const u8 {
|
|
return self._type_string.str();
|
|
}
|
|
|
|
pub fn getBubbles(self: *const Event) bool {
|
|
return self._bubbles;
|
|
}
|
|
|
|
pub fn getCancelable(self: *const Event) bool {
|
|
return self._cancelable;
|
|
}
|
|
|
|
pub fn getComposed(self: *const Event) bool {
|
|
return self._composed;
|
|
}
|
|
|
|
pub fn getTarget(self: *const Event) ?*EventTarget {
|
|
return self._target;
|
|
}
|
|
|
|
pub fn getCurrentTarget(self: *const Event) ?*EventTarget {
|
|
return self._current_target;
|
|
}
|
|
|
|
pub fn preventDefault(self: *Event) void {
|
|
self._prevent_default = true;
|
|
}
|
|
|
|
pub fn stopPropagation(self: *Event) void {
|
|
self._stop_propagation = true;
|
|
}
|
|
|
|
pub fn stopImmediatePropagation(self: *Event) void {
|
|
self._stop_immediate_propagation = true;
|
|
self._stop_propagation = true;
|
|
}
|
|
|
|
pub fn getDefaultPrevented(self: *const Event) bool {
|
|
return self._prevent_default;
|
|
}
|
|
|
|
pub fn getReturnValue(self: *const Event) bool {
|
|
return !self._prevent_default;
|
|
}
|
|
|
|
pub fn setReturnValue(self: *Event, v: bool) void {
|
|
self._prevent_default = !v;
|
|
}
|
|
|
|
pub fn getCancelBubble(self: *const Event) bool {
|
|
return self._stop_propagation;
|
|
}
|
|
|
|
pub fn setCancelBubble(self: *Event) void {
|
|
self.stopPropagation();
|
|
}
|
|
|
|
pub fn getEventPhase(self: *const Event) u8 {
|
|
return @intFromEnum(self._event_phase);
|
|
}
|
|
|
|
pub fn getTimeStamp(self: *const Event) u64 {
|
|
return self._time_stamp;
|
|
}
|
|
|
|
pub fn setTrusted(self: *Event) void {
|
|
self._isTrusted = true;
|
|
}
|
|
|
|
pub fn setUntrusted(self: *Event) void {
|
|
self._isTrusted = false;
|
|
}
|
|
|
|
pub fn getIsTrusted(self: *const Event) bool {
|
|
return self._isTrusted;
|
|
}
|
|
|
|
pub fn composedPath(self: *Event, page: *Page) ![]const *EventTarget {
|
|
// Return empty array if event is not being dispatched
|
|
if (self._event_phase == .none) {
|
|
return &.{};
|
|
}
|
|
|
|
// Use dispatch_target (original target) if available, otherwise fall back to target
|
|
// This is important because _target gets retargeted during event dispatch
|
|
const target = self._dispatch_target orelse self._target orelse return &.{};
|
|
|
|
// Only nodes have a propagation path
|
|
const target_node = switch (target._type) {
|
|
.node => |n| n,
|
|
else => return &.{},
|
|
};
|
|
|
|
// Build the path by walking up from target
|
|
var path_len: usize = 0;
|
|
var path_buffer: [128]*EventTarget = undefined;
|
|
var stopped_at_shadow_boundary = false;
|
|
|
|
// Track closed shadow boundaries (position in path and host position)
|
|
var closed_shadow_boundary: ?struct { shadow_end: usize, host_start: usize } = null;
|
|
|
|
var node: ?*Node = target_node;
|
|
while (node) |n| {
|
|
if (path_len >= path_buffer.len) {
|
|
break;
|
|
}
|
|
path_buffer[path_len] = n.asEventTarget();
|
|
path_len += 1;
|
|
|
|
// Check if this node is a shadow root
|
|
if (n._type == .document_fragment) {
|
|
if (n._type.document_fragment._type == .shadow_root) {
|
|
const shadow = n._type.document_fragment._type.shadow_root;
|
|
|
|
// If event is not composed, stop at shadow boundary
|
|
if (!self._composed) {
|
|
stopped_at_shadow_boundary = true;
|
|
break;
|
|
}
|
|
|
|
// Track the first closed shadow boundary we encounter
|
|
if (shadow._mode == .closed and closed_shadow_boundary == null) {
|
|
// Mark where the shadow root is in the path
|
|
// The next element will be the host
|
|
closed_shadow_boundary = .{
|
|
.shadow_end = path_len - 1, // index of shadow root
|
|
.host_start = path_len, // index where host will be
|
|
};
|
|
}
|
|
|
|
// Jump to the shadow host and continue
|
|
node = shadow._host.asNode();
|
|
continue;
|
|
}
|
|
}
|
|
|
|
node = n._parent;
|
|
}
|
|
|
|
// Add window at the end (unless we stopped at shadow boundary)
|
|
if (!stopped_at_shadow_boundary) {
|
|
if (path_len < path_buffer.len) {
|
|
path_buffer[path_len] = page.window.asEventTarget();
|
|
path_len += 1;
|
|
}
|
|
}
|
|
|
|
// Determine visible path based on current_target and closed shadow boundaries
|
|
var visible_start_index: usize = 0;
|
|
|
|
if (closed_shadow_boundary) |boundary| {
|
|
// Check if current_target is outside the closed shadow
|
|
// If current_target is null or is at/after the host position, hide shadow internals
|
|
const current_target = self._current_target;
|
|
|
|
if (current_target) |ct| {
|
|
// Find current_target in the path
|
|
var ct_index: ?usize = null;
|
|
for (path_buffer[0..path_len], 0..) |elem, i| {
|
|
if (elem == ct) {
|
|
ct_index = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If current_target is at or after the host (outside the closed shadow),
|
|
// hide everything from target up to the host
|
|
if (ct_index) |idx| {
|
|
if (idx >= boundary.host_start) {
|
|
visible_start_index = boundary.host_start;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Calculate the visible portion of the path
|
|
const visible_path_len = if (path_len > visible_start_index) path_len - visible_start_index else 0;
|
|
|
|
// Allocate and return the visible path using call_arena (short-lived)
|
|
const path = try page.call_arena.alloc(*EventTarget, visible_path_len);
|
|
@memcpy(path, path_buffer[visible_start_index..path_len]);
|
|
return path;
|
|
}
|
|
|
|
pub fn populateFromOptions(self: *Event, opts: anytype) void {
|
|
self._bubbles = opts.bubbles;
|
|
self._cancelable = opts.cancelable;
|
|
self._composed = opts.composed;
|
|
}
|
|
|
|
pub fn inheritOptions(comptime T: type, comptime additions: anytype) type {
|
|
var all_fields: []const std.builtin.Type.StructField = &.{};
|
|
|
|
if (@hasField(T, "_proto")) {
|
|
const t_fields = @typeInfo(T).@"struct".fields;
|
|
|
|
inline for (t_fields) |field| {
|
|
if (std.mem.eql(u8, field.name, "_proto")) {
|
|
const ProtoType = @typeInfo(field.type).pointer.child;
|
|
if (@hasDecl(ProtoType, "Options")) {
|
|
const parent_options = @typeInfo(ProtoType.Options);
|
|
all_fields = all_fields ++ parent_options.@"struct".fields;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const additions_info = @typeInfo(additions);
|
|
all_fields = all_fields ++ additions_info.@"struct".fields;
|
|
|
|
return @Type(.{
|
|
.@"struct" = .{
|
|
.layout = .auto,
|
|
.fields = all_fields,
|
|
.decls = &.{},
|
|
.is_tuple = false,
|
|
},
|
|
});
|
|
}
|
|
|
|
pub fn populatePrototypes(self: anytype, opts: anytype, trusted: bool) void {
|
|
const T = @TypeOf(self.*);
|
|
|
|
if (@hasField(T, "_proto")) {
|
|
populatePrototypes(self._proto, opts, trusted);
|
|
}
|
|
|
|
if (@hasDecl(T, "populateFromOptions")) {
|
|
T.populateFromOptions(self, opts);
|
|
}
|
|
|
|
// Set isTrusted at the Event level (base of prototype chain)
|
|
if (T == Event or @hasField(T, "_isTrusted")) {
|
|
self._isTrusted = trusted;
|
|
}
|
|
}
|
|
|
|
pub const JsApi = struct {
|
|
pub const bridge = js.Bridge(Event);
|
|
|
|
pub const Meta = struct {
|
|
pub const name = "Event";
|
|
|
|
pub const prototype_chain = bridge.prototypeChain();
|
|
pub var class_id: bridge.ClassId = undefined;
|
|
pub const weak = true;
|
|
pub const finalizer = bridge.finalizer(Event.deinit);
|
|
};
|
|
|
|
pub const constructor = bridge.constructor(Event.init, .{});
|
|
pub const @"type" = bridge.accessor(Event.getType, null, .{});
|
|
pub const bubbles = bridge.accessor(Event.getBubbles, null, .{});
|
|
pub const cancelable = bridge.accessor(Event.getCancelable, null, .{});
|
|
pub const composed = bridge.accessor(Event.getComposed, null, .{});
|
|
pub const target = bridge.accessor(Event.getTarget, null, .{});
|
|
pub const srcElement = bridge.accessor(Event.getTarget, null, .{});
|
|
pub const currentTarget = bridge.accessor(Event.getCurrentTarget, null, .{});
|
|
pub const eventPhase = bridge.accessor(Event.getEventPhase, null, .{});
|
|
pub const defaultPrevented = bridge.accessor(Event.getDefaultPrevented, null, .{});
|
|
pub const timeStamp = bridge.accessor(Event.getTimeStamp, null, .{});
|
|
pub const isTrusted = bridge.accessor(Event.getIsTrusted, null, .{});
|
|
pub const preventDefault = bridge.function(Event.preventDefault, .{});
|
|
pub const stopPropagation = bridge.function(Event.stopPropagation, .{});
|
|
pub const stopImmediatePropagation = bridge.function(Event.stopImmediatePropagation, .{});
|
|
pub const composedPath = bridge.function(Event.composedPath, .{});
|
|
pub const initEvent = bridge.function(Event.initEvent, .{});
|
|
// deprecated
|
|
pub const returnValue = bridge.accessor(Event.getReturnValue, Event.setReturnValue, .{});
|
|
// deprecated
|
|
pub const cancelBubble = bridge.accessor(Event.getCancelBubble, Event.setCancelBubble, .{});
|
|
|
|
// Event phase constants
|
|
pub const NONE = bridge.property(@intFromEnum(EventPhase.none), .{ .template = true });
|
|
pub const CAPTURING_PHASE = bridge.property(@intFromEnum(EventPhase.capturing_phase), .{ .template = true });
|
|
pub const AT_TARGET = bridge.property(@intFromEnum(EventPhase.at_target), .{ .template = true });
|
|
pub const BUBBLING_PHASE = bridge.property(@intFromEnum(EventPhase.bubbling_phase), .{ .template = true });
|
|
};
|
|
|
|
// tested in event_target
|