diff --git a/src/browser/EventManager.zig b/src/browser/EventManager.zig
index c598d5305..5ff52cf08 100644
--- a/src/browser/EventManager.zig
+++ b/src/browser/EventManager.zig
@@ -25,9 +25,11 @@ const EventManagerBase = @import("EventManagerBase.zig");
const Node = @import("webapi/Node.zig");
const Event = @import("webapi/Event.zig");
+const Window = @import("webapi/Window.zig");
const EventTarget = @import("webapi/EventTarget.zig");
const Element = @import("webapi/Element.zig");
const ShadowRoot = @import("webapi/ShadowRoot.zig");
+const XMLHttpRequestEventTarget = @import("webapi/net/XMLHttpRequestEventTarget.zig");
const log = lp.log;
const Allocator = std.mem.Allocator;
@@ -83,15 +85,15 @@ pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) Dispat
}
switch (target._type) {
- .node => |node| try self.dispatchNode(node, event),
- .xhr => |xhr| try self.dispatchDirect(target, event, xhr.inlineHandler(event._type_string), .{ .context = "dispatch" }),
- .window => |w| try self.dispatchDirect(target, event, windowInlineHandler(w, event._type_string), .{ .context = "dispatch" }),
+ .node => try self.dispatchNode(target.subtype(Node), event),
+ .xhr => try self.dispatchDirect(target, event, target.subtype(XMLHttpRequestEventTarget).inlineHandler(event._type_string), .{ .context = "dispatch" }),
+ .window => try self.dispatchDirect(target, event, windowInlineHandler(target.subtype(Window), event._type_string), .{ .context = "dispatch" }),
else => try self.dispatchDirect(target, event, null, .{ .context = "dispatch" }),
}
}
// Resolves the Window's property event handler for the given event type.
-fn windowInlineHandler(window: *@import("webapi/Window.zig"), typ: lp.String) ?js.Function.Global {
+fn windowInlineHandler(window: *Window, typ: lp.String) ?js.Function.Global {
const global_event_handlers = @import("webapi/global_event_handlers.zig");
const handler_type = global_event_handlers.fromEventType(typ.str()) orelse return null;
return switch (handler_type) {
@@ -244,9 +246,10 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event) !void {
// root is the document (not for detached trees, and not when propagation
// stopped at a shadow boundary). The only explicit exception is "load".
if (event._type_string.eql(comptime .wrap("load")) == false and path_len < path_buffer.len) {
- const root_is_document = path_len > 0 and switch (path_buffer[path_len - 1]._type) {
- .node => |n| n._type == .document,
- else => false,
+ const root_is_document = blk: {
+ if (path_len == 0) break :blk false;
+ const root = path_buffer[path_len - 1].is(Node) orelse break :blk false;
+ break :blk root._type == .document;
};
if (root_is_document) {
path_buffer[path_len] = frame.window.asEventTarget();
@@ -508,17 +511,20 @@ fn getInlineHandler(self: *EventManager, target: *EventTarget, event: *Event) ?j
// Look up the inline handler for this target
const html_element = switch (target._type) {
- .node => |n| n.is(Element.Html) orelse return null,
+ .node => target.subtype(Node).is(Element.Html) orelse return null,
// The Window stores its event handlers in dedicated fields; an event
// propagating to the window must fire them too.
- .window => |w| return switch (handler_type) {
- .onerror => w._on_error,
- .onload => w._on_load,
- .onblur => w._on_blur,
- .onfocus => w._on_focus,
- .onresize => w._on_resize,
- .onscroll => w._on_scroll,
- else => null,
+ .window => {
+ const w = target.subtype(Window);
+ return switch (handler_type) {
+ .onerror => w._on_error,
+ .onload => w._on_load,
+ .onblur => w._on_blur,
+ .onfocus => w._on_focus,
+ .onresize => w._on_resize,
+ .onscroll => w._on_scroll,
+ else => null,
+ };
},
else => return null,
};
@@ -639,14 +645,8 @@ fn eventPathParent(node: *Node, event: *Event, target_root: *Node, frame: ?*Fram
// DOM spec "retarget": walk original_target out of shadow trees until the
// node is visible from current_target's tree.
fn getAdjustedTarget(original_target: ?*EventTarget, current_target: *EventTarget) ?*EventTarget {
- const orig_node = switch ((original_target orelse return null)._type) {
- .node => |n| n,
- else => return original_target,
- };
- const curr_node = switch (current_target._type) {
- .node => |n| n,
- else => return original_target,
- };
+ const orig_node = (original_target orelse return null).is(Node) orelse return original_target;
+ const curr_node = current_target.is(Node) orelse return original_target;
var node = orig_node;
while (true) {
@@ -678,10 +678,8 @@ fn isShadowIncludingInclusiveAncestor(ancestor: *Node, node: *Node) bool {
// shadow root. Used for the spec's post-dispatch "clear targets" step.
fn rootIsShadowRoot(target_: ?*EventTarget) bool {
const target = target_ orelse return false;
- return switch (target._type) {
- .node => |n| n.containingShadowRoot() != null,
- else => false,
- };
+ const node = target.is(Node) orelse return false;
+ return node.containingShadowRoot() != null;
}
// Check if ancestor is an ancestor of (or the same as) node
diff --git a/src/browser/Factory.zig b/src/browser/Factory.zig
index f156055b3..959c378a5 100644
--- a/src/browser/Factory.zig
+++ b/src/browser/Factory.zig
@@ -68,7 +68,7 @@ pub fn eventTargetWithAllocator(_: *const Factory, allocator: Allocator, child:
const event_ptr = chain.get(0);
event_ptr.* = .{
- ._type = unionInit(EventTarget.Type, chain.get(1)),
+ ._type = typeInit(EventTarget, chain.get(1)),
};
chain.setLeaf(1, child);
@@ -621,3 +621,6 @@ fn unionFieldName(comptime T: type, comptime V: type) []const u8 {
}
@compileError(@typeName(V) ++ " is not a valid type for " ++ @typeName(T) ++ ".type");
}
+
+
+
diff --git a/src/browser/Session.zig b/src/browser/Session.zig
index f24c58fc8..09233a32b 100644
--- a/src/browser/Session.zig
+++ b/src/browser/Session.zig
@@ -161,10 +161,9 @@ pub fn init(self: *Session, browser: *Browser, notification: *Notification) !voi
errdefer arena.release();
const navigation = try Factory.chainedWithAllocator(arena.allocator(), .{
- EventTarget{ ._type = undefined },
+ EventTarget{ ._type = .navigation },
Navigation{ ._proto = undefined },
});
- navigation._proto._type = .{ .navigation = navigation };
self.* = .{
.arena = arena,
diff --git a/src/browser/webapi/Event.zig b/src/browser/webapi/Event.zig
index 95c427598..c62730864 100644
--- a/src/browser/webapi/Event.zig
+++ b/src/browser/webapi/Event.zig
@@ -322,10 +322,7 @@ pub fn composedPath(self: *Event, exec: *Execution) ![]const *EventTarget {
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 &.{},
- };
+ const target_node = target.is(Node) orelse return &.{};
const frame_ = switch (exec.js.global) {
.frame => |frame| frame,
@@ -341,9 +338,9 @@ pub fn composedPath(self: *Event, exec: *Execution) ![]const *EventTarget {
// Window follows the document at the end of the path. A path that stopped
// early — at a shadow boundary, or at the relatedTarget — doesn't end on
// the document and so doesn't reach it.
- const root_is_document = switch (path_buffer[path_len - 1]._type) {
- .node => |n| n._type == .document,
- else => false,
+ const root_is_document = blk: {
+ const root = path_buffer[path_len - 1].is(Node) orelse break :blk false;
+ break :blk root._type == .document;
};
if (root_is_document and path_len < path_buffer.len) {
if (frame_) |frame| {
@@ -356,10 +353,7 @@ pub fn composedPath(self: *Event, exec: *Execution) ![]const *EventTarget {
// it is inside that root and hidden from a currentTarget outside it.
var closed_host_index: ?usize = null;
for (path_buffer[0..path_len], 0..) |entry, i| {
- const node = switch (entry._type) {
- .node => |n| n,
- else => continue,
- };
+ const node = entry.is(Node) orelse continue;
const shadow = node.is(Node.ShadowRoot) orelse continue;
if (shadow._mode == .closed) {
closed_host_index = i + 1;
diff --git a/src/browser/webapi/EventTarget.zig b/src/browser/webapi/EventTarget.zig
index 3d6df52a5..99403becd 100644
--- a/src/browser/webapi/EventTarget.zig
+++ b/src/browser/webapi/EventTarget.zig
@@ -17,50 +17,142 @@
// along with this program. If not, see .
const std = @import("std");
+const lp = @import("lightpanda");
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
+const Factory = @import("../Factory.zig");
const EventManager = @import("../EventManager.zig");
+const Node = @import("Node.zig");
const Event = @import("Event.zig");
+const Screen = @import("Screen.zig");
+const Worker = @import("Worker.zig");
+const Window = @import("Window.zig");
const AbortSignal = @import("AbortSignal.zig");
+const MessagePort = @import("MessagePort.zig");
+const FileReader = @import("FileReader.zig");
+const WebSocket = @import("net/WebSocket.zig");
+const Navigation = @import("navigation/Navigation.zig");
+const Notification = @import("Notification.zig");
+const EventSource = @import("net/EventSource.zig");
+const CookieStore = @import("storage/CookieStore.zig");
+const IDBRequest = @import("storage/idb/IDBRequest.zig");
+const SharedWorker = @import("SharedWorker.zig");
+const FontFaceSet = @import("css/FontFaceSet.zig");
+const IDBDatabase = @import("storage/idb/IDBDatabase.zig");
+const TextTrackCue = @import("media/TextTrackCue.zig");
+const VisualViewport = @import("VisualViewport.zig");
+const MediaQueryList = @import("css/MediaQueryList.zig");
+const IDBTransaction = @import("storage/idb/IDBTransaction.zig");
+const BroadcastChannel = @import("BroadcastChannel.zig");
+const WorkerGlobalScope = @import("WorkerGlobalScope.zig");
+const NavigationHistoryEntry = @import("navigation/NavigationHistoryEntry.zig");
+const XMLHttpRequestEventTarget = @import("net/XMLHttpRequestEventTarget.zig");
const RegisterOptions = EventManager.RegisterOptions;
const EventTarget = @This();
pub const _prototype_root = true;
-_type: Type,
-pub const Type = union(enum) {
- generic: void,
- node: *@import("Node.zig"),
- window: *@import("Window.zig"),
- worker: *@import("Worker.zig"),
- shared_worker: *@import("SharedWorker.zig"),
- worker_global_scope: *@import("WorkerGlobalScope.zig"),
- xhr: *@import("net/XMLHttpRequestEventTarget.zig"),
- abort_signal: *@import("AbortSignal.zig"),
- media_query_list: *@import("css/MediaQueryList.zig"),
- message_port: *@import("MessagePort.zig"),
- broadcast_channel: *@import("BroadcastChannel.zig"),
- text_track_cue: *@import("media/TextTrackCue.zig"),
- navigation: *@import("navigation/Navigation.zig"),
- navigation_history_entry: *@import("navigation/NavigationHistoryEntry.zig"),
- screen: *@import("Screen.zig"),
- screen_orientation: *@import("Screen.zig").Orientation,
- visual_viewport: *@import("VisualViewport.zig"),
- file_reader: *@import("FileReader.zig"),
- font_face_set: *@import("css/FontFaceSet.zig"),
- websocket: *@import("net/WebSocket.zig"),
- event_source: *@import("net/EventSource.zig"),
- cookie_store: *@import("storage/CookieStore.zig"),
- idb_request: *@import("storage/idb/IDBRequest.zig"),
- idb_database: *@import("storage/idb/IDBDatabase.zig"),
- idb_transaction: *@import("storage/idb/IDBTransaction.zig"),
- notification: *@import("Notification.zig"),
+// `global_event_handlers.Key` reuses the low 3 bits of an EventTarget pointer,
+// so the type has to stay 8-byte aligned even though the tag is a single byte.
+// This costs nothing in a chain: EventTarget is always at offset 0 and every
+// subtype that follows it is itself 8-aligned.
+_type: Type align(8),
+
+pub const Type = enum(u8) {
+ generic,
+ node,
+ window,
+ worker,
+ shared_worker,
+ worker_global_scope,
+ xhr,
+ abort_signal,
+ media_query_list,
+ message_port,
+ broadcast_channel,
+ text_track_cue,
+ navigation,
+ navigation_history_entry,
+ screen,
+ screen_orientation,
+ visual_viewport,
+ file_reader,
+ font_face_set,
+ websocket,
+ event_source,
+ cookie_store,
+ idb_request,
+ idb_database,
+ idb_transaction,
+ notification,
};
+// `.generic` maps to EventTarget itself: a standalone `new EventTarget()` has
+// no chain member of its own.
+pub fn Subtype(comptime tag: Type) type {
+ return switch (tag) {
+ .generic => EventTarget,
+ .node => Node,
+ .window => Window,
+ .worker => Worker,
+ .shared_worker => SharedWorker,
+ .worker_global_scope => WorkerGlobalScope,
+ .xhr => XMLHttpRequestEventTarget,
+ .abort_signal => AbortSignal,
+ .media_query_list => MediaQueryList,
+ .message_port => MessagePort,
+ .broadcast_channel => BroadcastChannel,
+ .text_track_cue => TextTrackCue,
+ .navigation => Navigation,
+ .navigation_history_entry => NavigationHistoryEntry,
+ .screen => Screen,
+ .screen_orientation => Screen.Orientation,
+ .visual_viewport => VisualViewport,
+ .file_reader => FileReader,
+ .font_face_set => FontFaceSet,
+ .websocket => WebSocket,
+ .event_source => EventSource,
+ .cookie_store => CookieStore,
+ .idb_request => IDBRequest,
+ .idb_database => IDBDatabase,
+ .idb_transaction => IDBTransaction,
+ .notification => Notification,
+ };
+}
+
+pub fn subtype(self: *const EventTarget, comptime T: type) *T {
+ const offset = comptime Factory.chainOffsetOf(T, T) - Factory.chainOffsetOf(T, EventTarget);
+ const sub: *T = @ptrFromInt(@intFromPtr(self) + offset);
+ if (comptime lp.IS_DEBUG) {
+ // This pointer dance only works because the factory allocates the chain
+ // in a contiguous block of memory. In debug, we assert this holds via
+ // the _proto_canary back pointer.
+ std.debug.assert(Factory.protoOf(sub) == self);
+ }
+ return sub;
+}
+
+// Returns the target as a more specific type, or null if it isn't a `T`.
+pub fn is(self: *EventTarget, comptime T: type) ?*T {
+ switch (self._type) {
+ .generic => {},
+ inline else => |tag| {
+ if (Subtype(tag) == T) {
+ return self.subtype(T);
+ }
+ },
+ }
+ return null;
+}
+
+pub fn as(self: *EventTarget, comptime T: type) *T {
+ return self.is(T).?;
+}
+
pub fn init(page: *Page) !*EventTarget {
return page.factory.create(EventTarget{
._type = .generic,
@@ -121,8 +213,9 @@ fn defaultPassiveValue(self: *EventTarget, typ: []const u8) bool {
switch (self._type) {
.window => return true,
- .node => |n| {
+ .node => {
const Element = @import("Element.zig");
+ const n = self.subtype(Node);
if (n._type == .document) {
return true;
}
@@ -212,7 +305,7 @@ pub fn removeEventListener(self: *EventTarget, typ: []const u8, callback_: js.Nu
pub fn format(self: *EventTarget, writer: *std.Io.Writer) !void {
return switch (self._type) {
- .node => |n| n.format(writer),
+ .node => self.subtype(Node).format(writer),
.generic => writer.writeAll(""),
.window => writer.writeAll(""),
.worker => writer.writeAll(""),
@@ -293,6 +386,8 @@ test "WebApi: EventTarget" {
testing.silenceLog(&.{ .js, .event });
// we create thousands of these per frame. Nothing should bloat it.
- try testing.expectEqual(16, @sizeOf(EventTarget));
+ // The tag is 1 byte; the rest is the align(8) that `Key.fuse` depends on.
+ try testing.expectEqual(8, @sizeOf(EventTarget));
+ try testing.expectEqual(8, @alignOf(EventTarget));
try testing.htmlRunner("events.html", .{});
}
diff --git a/src/browser/webapi/WorkerGlobalScope.zig b/src/browser/webapi/WorkerGlobalScope.zig
index 38c2e53a7..74601fd73 100644
--- a/src/browser/webapi/WorkerGlobalScope.zig
+++ b/src/browser/webapi/WorkerGlobalScope.zig
@@ -143,7 +143,7 @@ pub fn init(
const factory = frame._factory;
const leaf = try Factory.chainedWithAllocator(arena, .{
- EventTarget{ ._type = undefined },
+ EventTarget{ ._type = .worker_global_scope },
WorkerGlobalScope{
.url = url,
.arena = arena,
@@ -173,7 +173,6 @@ pub fn init(
});
const self = leaf._proto;
self._type = @unionInit(Type, @tagName(tag), leaf);
- self._proto._type = .{ .worker_global_scope = self };
self._http_owner = .init(&frame._page.blob_urls, &self.origin);
diff --git a/src/browser/webapi/navigation/Navigation.zig b/src/browser/webapi/navigation/Navigation.zig
index 8c96b5418..b188032f6 100644
--- a/src/browser/webapi/navigation/Navigation.zig
+++ b/src/browser/webapi/navigation/Navigation.zig
@@ -216,7 +216,7 @@ pub fn pushEntry(
const id_str = try std.fmt.allocPrint(arena.allocator(), "{d}", .{id});
const entry = try Factory.chainedWithAllocator(arena.allocator(), .{
- EventTarget{ ._type = undefined },
+ EventTarget{ ._type = .navigation_history_entry },
NavigationHistoryEntry{
._proto = undefined,
._id = id_str,
@@ -225,7 +225,6 @@ pub fn pushEntry(
._state = state,
},
});
- entry._proto._type = .{ .navigation_history_entry = entry };
// we don't always have a current entry...
const previous = if (self._entries.items.len > 0) self.getCurrentEntry() else null;
@@ -263,7 +262,7 @@ pub fn replaceEntry(
const id_str = try std.fmt.allocPrint(arena.allocator(), "{d}", .{id});
const entry = try Factory.chainedWithAllocator(arena.allocator(), .{
- EventTarget{ ._type = undefined },
+ EventTarget{ ._type = .navigation_history_entry },
NavigationHistoryEntry{
._proto = undefined,
._id = id_str,
@@ -272,7 +271,6 @@ pub fn replaceEntry(
._state = state,
},
});
- entry._proto._type = .{ .navigation_history_entry = entry };
const old_entry = self._entries.items[self._index];
self._entries.items[self._index] = entry;