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;