mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-30 17:18:57 +00:00
This commit involves a number of changes to finalizers, all aimed towards better consistency and reliability. A big part of this has to do with v8::Inspector's ability to move objects across IsolatedWorlds. There has been a few previous efforts on this, the most significant being https://github.com/lightpanda-io/browser/pull/1901. To recap, a Zig instance can map to 0-N v8::Objects. Where N is the total number of IsolatedWorlds. Generally, IsolatedWorlds between origins are...isolated...but the v8::Inspector isn't bound by this. So a Zig instance cannot be tied to a Context/Identity/IsolatedWorld...it has to live until all references, possibly from different IsolatedWorlds, are released (or the page is reset). Finalizers could previously be managed via reference counting or explicitly toggling the instance as weak/strong. Now, only reference counting is supported. weak/strong can essentially be seen as an acquireRef (rc += 1) and releaseRef (rc -= 1). Explicit setting did make some things easier, like not having to worry so much about double-releasing (e.g. XHR abort being called multiple times), but it was only used in a few places AND it simply doesn't work with objects shared between IsolatedWorlds. It is never a boolean now, as 3 different IsolatedWorlds can each hold a reference. Temps and Globals are tracked on the Session. Previously, they were tracked on the Identity, but that makes no sense. If a Zig instance can outlive an Identity, then any of its Temp references can too. This hasn't been a problem because we've only seen MutationObserver and IntersectionObserver be used cross-origin, but the right CDP script can make this crash with a use-after-free (e.g. `MessageEvent.data` is released when the Identity is done, but `MessageEvent` is still referenced by a different IsolateWorld). Rather than deinit with a `comptime shutdown: bool`, there is now an explicit `releaseRef` and `deinit`. Bridge registration has been streamlined. Previously, types had to register their finalizer AND acquireRef/releaseRef/deinit had to be declared on the entire prototype chain, even if these methods just delegated to their proto. Finalizers are now automatically enabled if a type has a `acquireRef` function. If a type has an `acquireRef`, then it must have a `releaseRef` and a `deinit`. So if there's custom cleanup to do in `deinit`, then you also have to define `acquireRef` and `releaseRef` which will just delegate to the _proto. Furthermore these finalizer methods can be defined anywhere on the chain. Previously: ```zig const KeywboardEvent = struct { _proto: *Event, ... pub fn deinit(self: *KeyboardEvent, session: *Session) void { self._proto.deinit(session); } pub fn releaseRef(self: *KeyboardEvent, session: *Session) void { self._proto.releaseRef(session); } } ``` ```zig const KeyboardEvent = struct { _proto: *Event, ... // no deinit, releaseRef, acquireref } ``` Since the `KeyboardEvent` doesn't participate in finalization directly, it doesn't have to define anything. The bridge will detect the most specific place they are defined and call them there.
318 lines
8.4 KiB
Zig
318 lines
8.4 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 String = @import("../../../string.zig").String;
|
|
|
|
const js = @import("../../js/js.zig");
|
|
const Page = @import("../../Page.zig");
|
|
const Session = @import("../../Session.zig");
|
|
|
|
const Event = @import("../Event.zig");
|
|
const UIEvent = @import("UIEvent.zig");
|
|
const Allocator = std.mem.Allocator;
|
|
|
|
const KeyboardEvent = @This();
|
|
|
|
_proto: *UIEvent,
|
|
_key: Key,
|
|
_code: []const u8,
|
|
_ctrl_key: bool,
|
|
_shift_key: bool,
|
|
_alt_key: bool,
|
|
_meta_key: bool,
|
|
_location: Location,
|
|
_repeat: bool,
|
|
_is_composing: bool,
|
|
|
|
// https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values
|
|
pub const Key = union(enum) {
|
|
// Special Key Values
|
|
Dead,
|
|
Undefined,
|
|
Unidentified,
|
|
|
|
// Modifier Keys
|
|
Alt,
|
|
AltGraph,
|
|
CapsLock,
|
|
Control,
|
|
Fn,
|
|
FnLock,
|
|
Hyper,
|
|
Meta,
|
|
NumLock,
|
|
ScrollLock,
|
|
Shift,
|
|
Super,
|
|
Symbol,
|
|
SymbolLock,
|
|
|
|
// Whitespace Keys
|
|
Enter,
|
|
Tab,
|
|
|
|
// Navigation Keys
|
|
ArrowDown,
|
|
ArrowLeft,
|
|
ArrowRight,
|
|
ArrowUp,
|
|
End,
|
|
Home,
|
|
PageDown,
|
|
PageUp,
|
|
|
|
// Editing Keys
|
|
Backspace,
|
|
Clear,
|
|
Copy,
|
|
CrSel,
|
|
Cut,
|
|
Delete,
|
|
EraseEof,
|
|
ExSel,
|
|
Insert,
|
|
Paste,
|
|
Redo,
|
|
Undo,
|
|
|
|
// UI Keys
|
|
Accept,
|
|
Again,
|
|
Attn,
|
|
Cancel,
|
|
ContextMenu,
|
|
Escape,
|
|
Execute,
|
|
Find,
|
|
Finish,
|
|
Help,
|
|
Pause,
|
|
Play,
|
|
Props,
|
|
Select,
|
|
ZoomIn,
|
|
ZoomOut,
|
|
|
|
// Function Keys
|
|
F1,
|
|
F2,
|
|
F3,
|
|
F4,
|
|
F5,
|
|
F6,
|
|
F7,
|
|
F8,
|
|
F9,
|
|
F10,
|
|
F11,
|
|
F12,
|
|
|
|
// Printable keys (single character, space, etc.)
|
|
standard: []const u8,
|
|
|
|
pub fn fromString(allocator: std.mem.Allocator, str: []const u8) !Key {
|
|
const key_type_info = @typeInfo(Key);
|
|
inline for (key_type_info.@"union".fields) |field| {
|
|
if (comptime std.mem.eql(u8, field.name, "standard")) continue;
|
|
|
|
if (std.mem.eql(u8, field.name, str)) {
|
|
return @unionInit(Key, field.name, {});
|
|
}
|
|
}
|
|
|
|
const duped = try allocator.dupe(u8, str);
|
|
return .{ .standard = duped };
|
|
}
|
|
|
|
/// Returns true if this key represents a printable character that should be
|
|
/// inserted into text input elements. This includes alphanumeric characters,
|
|
/// punctuation, symbols, and space.
|
|
pub fn isPrintable(self: Key) bool {
|
|
return switch (self) {
|
|
.standard => |s| s.len > 0,
|
|
else => false,
|
|
};
|
|
}
|
|
|
|
/// Returns the string representation that should be inserted into text input.
|
|
/// For most keys this is just the key itself, but some keys like Enter need
|
|
/// special handling (e.g., newline for textarea, form submission for input).
|
|
pub fn asString(self: Key) []const u8 {
|
|
return switch (self) {
|
|
.standard => |s| s,
|
|
else => |k| @tagName(k),
|
|
};
|
|
}
|
|
};
|
|
|
|
pub const Location = enum(i32) {
|
|
DOM_KEY_LOCATION_STANDARD = 0,
|
|
DOM_KEY_LOCATION_LEFT = 1,
|
|
DOM_KEY_LOCATION_RIGHT = 2,
|
|
DOM_KEY_LOCATION_NUMPAD = 3,
|
|
};
|
|
|
|
pub const KeyboardEventOptions = struct {
|
|
key: []const u8 = "",
|
|
code: ?[]const u8 = null,
|
|
location: i32 = 0,
|
|
repeat: bool = false,
|
|
isComposing: bool = false,
|
|
ctrlKey: bool = false,
|
|
shiftKey: bool = false,
|
|
altKey: bool = false,
|
|
metaKey: bool = false,
|
|
};
|
|
|
|
const Options = Event.inheritOptions(
|
|
KeyboardEvent,
|
|
KeyboardEventOptions,
|
|
);
|
|
|
|
pub fn initTrusted(typ: String, _opts: ?Options, page: *Page) !*KeyboardEvent {
|
|
const arena = try page.getArena(.{ .debug = "KeyboardEvent.trusted" });
|
|
errdefer page.releaseArena(arena);
|
|
return initWithTrusted(arena, typ, _opts, true, page);
|
|
}
|
|
|
|
pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*KeyboardEvent {
|
|
const arena = try page.getArena(.{ .debug = "KeyboardEvent" });
|
|
errdefer page.releaseArena(arena);
|
|
const type_string = try String.init(arena, typ, .{});
|
|
return initWithTrusted(arena, type_string, _opts, false, page);
|
|
}
|
|
|
|
fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool, page: *Page) !*KeyboardEvent {
|
|
const opts = _opts orelse Options{};
|
|
|
|
const event = try page._factory.uiEvent(
|
|
arena,
|
|
typ,
|
|
KeyboardEvent{
|
|
._proto = undefined,
|
|
._key = try Key.fromString(arena, opts.key),
|
|
._location = std.meta.intToEnum(Location, opts.location) catch return error.TypeError,
|
|
._code = if (opts.code) |c| try arena.dupe(u8, c) else "",
|
|
._repeat = opts.repeat,
|
|
._is_composing = opts.isComposing,
|
|
._ctrl_key = opts.ctrlKey,
|
|
._shift_key = opts.shiftKey,
|
|
._alt_key = opts.altKey,
|
|
._meta_key = opts.metaKey,
|
|
},
|
|
);
|
|
|
|
Event.populatePrototypes(event, opts, trusted);
|
|
|
|
// https://w3c.github.io/uievents/#event-type-keyup
|
|
const rootevt = event._proto._proto;
|
|
rootevt._bubbles = true;
|
|
rootevt._cancelable = true;
|
|
rootevt._composed = true;
|
|
|
|
return event;
|
|
}
|
|
|
|
pub fn asEvent(self: *KeyboardEvent) *Event {
|
|
return self._proto.asEvent();
|
|
}
|
|
|
|
pub fn getAltKey(self: *const KeyboardEvent) bool {
|
|
return self._alt_key;
|
|
}
|
|
|
|
pub fn getCtrlKey(self: *const KeyboardEvent) bool {
|
|
return self._ctrl_key;
|
|
}
|
|
|
|
pub fn getIsComposing(self: *const KeyboardEvent) bool {
|
|
return self._is_composing;
|
|
}
|
|
|
|
pub fn getKey(self: *const KeyboardEvent) Key {
|
|
return self._key;
|
|
}
|
|
|
|
pub fn getCode(self: *const KeyboardEvent) []const u8 {
|
|
return self._code;
|
|
}
|
|
|
|
pub fn getLocation(self: *const KeyboardEvent) i32 {
|
|
return @intFromEnum(self._location);
|
|
}
|
|
|
|
pub fn getMetaKey(self: *const KeyboardEvent) bool {
|
|
return self._meta_key;
|
|
}
|
|
|
|
pub fn getRepeat(self: *const KeyboardEvent) bool {
|
|
return self._repeat;
|
|
}
|
|
|
|
pub fn getShiftKey(self: *const KeyboardEvent) bool {
|
|
return self._shift_key;
|
|
}
|
|
|
|
pub fn getModifierState(self: *const KeyboardEvent, str: []const u8) !bool {
|
|
const key = try Key.fromString(self._proto._proto._arena, str);
|
|
|
|
switch (key) {
|
|
.Alt, .AltGraph => return self._alt_key,
|
|
.Shift => return self._shift_key,
|
|
.Control => return self._ctrl_key,
|
|
.Meta => return self._meta_key,
|
|
.standard => |s| if (std.mem.eql(u8, s, "Accel")) {
|
|
return self._ctrl_key or self._meta_key;
|
|
},
|
|
else => {},
|
|
}
|
|
return false;
|
|
}
|
|
|
|
pub const JsApi = struct {
|
|
pub const bridge = js.Bridge(KeyboardEvent);
|
|
|
|
pub const Meta = struct {
|
|
pub const name = "KeyboardEvent";
|
|
pub const prototype_chain = bridge.prototypeChain();
|
|
pub var class_id: bridge.ClassId = undefined;
|
|
};
|
|
|
|
pub const constructor = bridge.constructor(KeyboardEvent.init, .{});
|
|
pub const altKey = bridge.accessor(KeyboardEvent.getAltKey, null, .{});
|
|
pub const ctrlKey = bridge.accessor(KeyboardEvent.getCtrlKey, null, .{});
|
|
pub const isComposing = bridge.accessor(KeyboardEvent.getIsComposing, null, .{});
|
|
pub const key = bridge.accessor(struct {
|
|
fn keyAsString(self: *const KeyboardEvent) []const u8 {
|
|
return self._key.asString();
|
|
}
|
|
}.keyAsString, null, .{});
|
|
pub const code = bridge.accessor(KeyboardEvent.getCode, null, .{});
|
|
pub const location = bridge.accessor(KeyboardEvent.getLocation, null, .{});
|
|
pub const metaKey = bridge.accessor(KeyboardEvent.getMetaKey, null, .{});
|
|
pub const repeat = bridge.accessor(KeyboardEvent.getRepeat, null, .{});
|
|
pub const shiftKey = bridge.accessor(KeyboardEvent.getShiftKey, null, .{});
|
|
pub const getModifierState = bridge.function(KeyboardEvent.getModifierState, .{});
|
|
};
|
|
|
|
const testing = @import("../../../testing.zig");
|
|
test "WebApi: KeyboardEvent" {
|
|
try testing.htmlRunner("event/keyboard.html", .{});
|
|
}
|