Merge pull request #2950 from lightpanda-io/dom-pr-09-event-types-and-realms

webapi: new event interfaces, HTML-namespace naming, realm-correct exceptions
This commit is contained in:
Karl Seguin
2026-07-15 15:51:52 +08:00
committed by GitHub
16 changed files with 783 additions and 42 deletions

View File

@@ -500,6 +500,65 @@ fn nameToString(local: *const Local, comptime T: type, name: *const v8.Name) !T
return try js.String.toSlice(.{ .local = local, .handle = handle });
}
// Per Web IDL, exceptions belong to the operation's relevant realm — the
// receiver's — which differs from the calling realm for cross-realm calls
// (v8 API callbacks run in the caller's context, and our JS wrappers are
// shared across the page's contexts). For DOM nodes, the relevant realm is
// the node document's frame; otherwise fall back to the calling realm.
fn errorLocal(comptime T: type, local: *const Local, info: anytype) Local {
if (@TypeOf(info) != FunctionCallbackInfo) {
return local.*;
}
const frame = switch (local.ctx.global) {
.frame => |f| f,
.worker => return local.*,
};
const Node = @import("../webapi/Node.zig");
const Document = @import("../webapi/Document.zig");
const is_node_type = comptime blk: {
if (@typeInfo(T) != .@"struct" or !@hasDecl(T, "JsApi")) break :blk false;
break :blk @import("bridge.zig").inheritsOrIs(T.JsApi, Node.JsApi);
};
if (comptime !is_node_type) {
return local.*;
}
const instance = TaggedOpaque.fromJS(*T, info.getThis()) catch return local.*;
const node = protoNode(T, instance);
const doc: *Document = node.ownerDocument(frame) orelse switch (node._type) {
.document => |d| d,
else => return local.*,
};
const doc_frame = doc._frame orelse return local.*;
if (doc_frame == frame) {
return local.*;
}
const ctx = doc_frame.js;
const local_v8_context: *const v8.Context = @ptrCast(v8.v8__Global__Get(&ctx.handle, ctx.isolate.handle) orelse return local.*);
return .{
.ctx = ctx,
.handle = local_v8_context,
.call_arena = ctx.call_arena,
.isolate = ctx.isolate,
};
}
// Upcast a Node-descendant instance to *Node by walking the _proto chain.
// Not every node type defines an asNode() helper (e.g. Comment, Text), but
// inheritsOrIs guarantees Node is in the chain
fn protoNode(comptime T: type, instance: *T) *@import("../webapi/Node.zig") {
if (T == @import("../webapi/Node.zig")) {
return instance;
}
const Proto = @typeInfo(std.meta.fieldInfo(T, ._proto).type).pointer.child;
return protoNode(Proto, instance._proto);
}
fn handleError(comptime T: type, comptime F: type, local: *const Local, err: anyerror, info: anytype) void {
const isolate = local.isolate;
@@ -513,18 +572,34 @@ fn handleError(comptime T: type, comptime F: type, local: *const Local, err: any
}
}
const js_err: *const v8.Value = switch (err) {
// early exit
switch (err) {
error.TryCatchRethrow => return,
// A JS exception is already pending in the isolate (e.g. a value's
// toString threw during argument conversion); throwing anything here
// would replace the original exception the script expects to see.
error.JsException => return,
error.InvalidArgument => isolate.createTypeError("invalid argument"),
error.TypeError => isolate.createTypeError(""),
error.RangeError => isolate.createRangeError(""),
error.OutOfMemory => isolate.createError("out of memory"),
error.IllegalConstructor => isolate.createError("Illegal Constructor"),
else => domExceptionToJs(local, err) orelse isolate.createError(@errorName(err)),
else => {},
}
const err_local = errorLocal(T, local, info);
const js_err: *const v8.Value = blk: {
// Error constructors use the isolate's current context: enter the
// receiver's realm so the exception gets its prototypes.
const entered = err_local.ctx != local.ctx;
if (entered) v8.v8__Context__Enter(err_local.handle);
defer if (entered) v8.v8__Context__Exit(err_local.handle);
break :blk switch (err) {
error.InvalidArgument => isolate.createTypeError("invalid argument"),
error.TypeError => isolate.createTypeError(""),
error.RangeError => isolate.createRangeError(""),
error.OutOfMemory => isolate.createError("out of memory"),
error.IllegalConstructor => isolate.createError("Illegal Constructor"),
error.TryCatchRethrow, error.JsException => unreachable, // early exited a few lines up
else => domExceptionToJs(&err_local, err) orelse isolate.createError(@errorName(err)),
};
};
const js_exception = isolate.throwException(js_err);

View File

@@ -737,7 +737,7 @@ fn inheritsFromHtmlElement(comptime JsApi: type) bool {
if (JsApi.bridge.type == HtmlElement) {
return false;
}
return inheritsOrIs(JsApi, HtmlElement.JsApi);
return bridge.inheritsOrIs(JsApi, HtmlElement.JsApi);
}
const Unforgeable = struct {
@@ -874,7 +874,7 @@ fn attachClass(comptime JsApi: type, comptime flatten: bool, isolate: *v8.Isolat
if (comptime flatten == false) {
inline for (unforgeables) |u| {
if (comptime inheritsOrIs(JsApi, u.Owner)) {
if (comptime bridge.inheritsOrIs(JsApi, u.Owner)) {
// unforgeables attributes aren't only applied directly on the
// instance, they're also applied directly on every child instance
attachAccessorProperty(u.name, u.accessor, isolate, template, signature, instance);
@@ -949,23 +949,6 @@ const unforgeables: []const Unforgeable = blk: {
break :blk list;
};
// Whether Child is Ancestor or inherits from it, following the _proto chain.
fn inheritsOrIs(comptime Child: type, comptime Ancestor: type) bool {
comptime {
const target = Ancestor.bridge.type;
var T = Child.bridge.type;
while (true) {
if (T == target) {
return true;
}
if (!@hasField(T, "_proto")) {
return false;
}
T = @typeInfo(std.meta.fieldInfo(T, ._proto).type).pointer.child;
}
}
}
// Attach JsApi members to a template (public for reuse). This is called on all
// types. But, for globals (window, WGS) it's called twice. The first time, it's
// called like any other interface. The 2nd time, it's called with flatten == true

View File

@@ -106,7 +106,14 @@ pub fn fromJS(comptime R: type, js_obj_handle: *const v8.Object) !R {
@compileError("unknown Zig type: " ++ @typeName(R));
}
const tao_ptr = v8.v8__Object__GetAlignedPointerFromInternalField(js_obj_handle, 0).?;
const tao_ptr = v8.v8__Object__GetAlignedPointerFromInternalField(js_obj_handle, 0) orelse return error.InvalidArgument;
// A wrapped object always embeds an aligned TaggedOpaque pointer. An
// unaligned value is a leftover v8 tagged value: the object was created
// from our template but never mapped to a Zig instance (e.g. a custom
// element whose constructor threw).
if (@intFromPtr(tao_ptr) % @alignOf(TaggedOpaque) != 0) {
return error.InvalidArgument;
}
const tao: *TaggedOpaque = @ptrCast(@alignCast(tao_ptr));
const expected_type_index = bridge.JsApiLookup.getId(JsApi);

View File

@@ -1091,6 +1091,11 @@ pub const PageJsApis = flattenTypes(&.{
@import("../webapi/event/PageTransitionEvent.zig"),
@import("../webapi/event/PopStateEvent.zig"),
@import("../webapi/event/HashChangeEvent.zig"),
@import("../webapi/event/BeforeUnloadEvent.zig"),
@import("../webapi/event/StorageEvent.zig"),
@import("../webapi/event/DeviceMotionEvent.zig"),
@import("../webapi/event/DeviceOrientationEvent.zig"),
@import("../webapi/event/TouchEvent.zig"),
@import("../webapi/event/UIEvent.zig"),
@import("../webapi/event/MouseEvent.zig"),
@import("../webapi/event/PointerEvent.zig"),
@@ -1264,3 +1269,20 @@ pub const JsApis = blk: {
}
break :blk base ++ [_]type{@import("../webapi/WebDriver.zig").JsApi};
};
// Whether Child is Ancestor or inherits from it, following the _proto chain.
pub fn inheritsOrIs(comptime Child: type, comptime Ancestor: type) bool {
comptime {
const target = Ancestor.bridge.type;
var T = Child.bridge.type;
while (true) {
if (T == target) {
return true;
}
if (!@hasField(T, "_proto")) {
return false;
}
T = @typeInfo(std.meta.fieldInfo(T, ._proto).type).pointer.child;
}
}
}

View File

@@ -75,8 +75,10 @@
const event2 = document.createEvent('CUSTOMEVENT');
testing.expectEqual('', event2.type);
const event3 = document.createEvent('CustomEvents');
testing.expectEqual('', event3.type);
// per spec, the pluralized legacy alias is not supported
let threw = false;
try { document.createEvent('CustomEvents'); } catch (e) { threw = e.name === 'NotSupportedError'; }
testing.expectEqual(true, threw);
}
</script>

View File

@@ -495,7 +495,7 @@ pub fn createEvent(_: *const Document, event_type: []const u8, frame: *Frame) !*
break :blk try Event.init("", null, frame._page);
}
if (std.mem.eql(u8, normalized, "customevent") or std.mem.eql(u8, normalized, "customevents")) {
if (std.mem.eql(u8, normalized, "customevent")) {
const CustomEvent = @import("event/CustomEvent.zig");
break :blk (try CustomEvent.init("", null, frame._page)).asEvent();
}
@@ -535,12 +535,12 @@ pub fn createEvent(_: *const Document, event_type: []const u8, frame: *Frame) !*
break :blk (try UIEvent.init("", null, frame)).asEvent();
}
if (std.mem.eql(u8, normalized, "focusevent") or std.mem.eql(u8, normalized, "focusevents")) {
if (std.mem.eql(u8, normalized, "focusevent")) {
const FocusEvent = @import("event/FocusEvent.zig");
break :blk (try FocusEvent.init("", null, frame)).asEvent();
}
if (std.mem.eql(u8, normalized, "textevent") or std.mem.eql(u8, normalized, "textevents")) {
if (std.mem.eql(u8, normalized, "textevent")) {
const TextEvent = @import("event/TextEvent.zig");
break :blk (try TextEvent.init("", null, frame)).asEvent();
}
@@ -550,16 +550,29 @@ pub fn createEvent(_: *const Document, event_type: []const u8, frame: *Frame) !*
break :blk (try CompositionEvent.init("", null, frame)).asEvent();
}
// Aliases the spec requires createEvent to support but whose
// interfaces aren't implemented yet: return a plain Event so the
// caller can at least initialize and dispatch it.
if (std.mem.eql(u8, normalized, "beforeunloadevent") or
std.mem.eql(u8, normalized, "devicemotionevent") or
std.mem.eql(u8, normalized, "deviceorientationevent") or
std.mem.eql(u8, normalized, "storageevent"))
{
log.info(.not_implemented, "createEvent interface", .{ .type = event_type });
break :blk try Event.init("", null, frame._page);
if (std.mem.eql(u8, normalized, "beforeunloadevent")) {
const BeforeUnloadEvent = @import("event/BeforeUnloadEvent.zig");
break :blk (try BeforeUnloadEvent.init("", null, frame)).asEvent();
}
if (std.mem.eql(u8, normalized, "devicemotionevent")) {
const DeviceMotionEvent = @import("event/DeviceMotionEvent.zig");
break :blk (try DeviceMotionEvent.init("", null, frame)).asEvent();
}
if (std.mem.eql(u8, normalized, "deviceorientationevent")) {
const DeviceOrientationEvent = @import("event/DeviceOrientationEvent.zig");
break :blk (try DeviceOrientationEvent.init("", null, frame)).asEvent();
}
if (std.mem.eql(u8, normalized, "storageevent")) {
const StorageEvent = @import("event/StorageEvent.zig");
break :blk (try StorageEvent.init("", null, frame)).asEvent();
}
if (std.mem.eql(u8, normalized, "touchevent")) {
const TouchEvent = @import("event/TouchEvent.zig");
break :blk (try TouchEvent.init("", null, frame)).asEvent();
}
return error.NotSupported;
@@ -1409,6 +1422,10 @@ pub const JsApi = struct {
pub const onselectionchange = bridge.accessor(Document.getOnSelectionChange, Document.setOnSelectionChange, .{});
pub const onclick = bridge.accessor(Document.getOnClick, Document.setOnClick, .{});
pub const ontouchstart = bridge.accessor(handlerAccessor(.ontouchstart).get, handlerAccessor(.ontouchstart).set, .{});
pub const ontouchend = bridge.accessor(handlerAccessor(.ontouchend).get, handlerAccessor(.ontouchend).set, .{});
pub const ontouchmove = bridge.accessor(handlerAccessor(.ontouchmove).get, handlerAccessor(.ontouchmove).set, .{});
pub const ontouchcancel = bridge.accessor(handlerAccessor(.ontouchcancel).get, handlerAccessor(.ontouchcancel).set, .{});
pub const URL = bridge.accessor(Document.getURL, null, .{});
pub const location = bridge.accessor(Document.getLocation, Document.setLocation, .{});
pub const documentURI = bridge.accessor(Document.getURL, null, .{});
@@ -1495,6 +1512,26 @@ pub const JsApi = struct {
return doc_frame.charset;
}
pub const referrer = bridge.property("", .{ .template = false });
// Generates a getter/setter pair backed by the frame's attribute-listener
// map, like onclick above, for other document event handler properties.
fn handlerAccessor(comptime handler: @import("global_event_handlers.zig").Handler) type {
return struct {
pub fn get(self: *Document, frame: *Frame) ?js.Function.Global {
const owner = self._frame orelse frame;
return owner._event_target_attr_listeners.get(.{ .target = self.asEventTarget(), .handler = handler });
}
pub fn set(self: *Document, setter: ?Window.FunctionSetter, frame: *Frame) !void {
const owner = self._frame orelse frame;
if (Window.getFunctionFromSetter(setter)) |cb| {
try owner._event_target_attr_listeners.put(owner.arena, .{ .target = self.asEventTarget(), .handler = handler }, cb);
} else {
_ = owner._event_target_attr_listeners.remove(.{ .target = self.asEventTarget(), .handler = handler });
}
}
};
}
};
const testing = @import("../../testing.zig");

View File

@@ -84,6 +84,10 @@ pub const Type = union(enum) {
page_transition_event: *@import("event/PageTransitionEvent.zig"),
pop_state_event: *@import("event/PopStateEvent.zig"),
hash_change_event: *@import("event/HashChangeEvent.zig"),
before_unload_event: *@import("event/BeforeUnloadEvent.zig"),
storage_event: *@import("event/StorageEvent.zig"),
device_motion_event: *@import("event/DeviceMotionEvent.zig"),
device_orientation_event: *@import("event/DeviceOrientationEvent.zig"),
ui_event: *@import("event/UIEvent.zig"),
promise_rejection_event: *@import("event/PromiseRejectionEvent.zig"),
submit_event: *@import("event/SubmitEvent.zig"),
@@ -193,6 +197,10 @@ pub fn is(self: *Event, comptime T: type) ?*T {
.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,
.hash_change_event => |e| return if (T == @import("event/HashChangeEvent.zig")) e else null,
.before_unload_event => |e| return if (T == @import("event/BeforeUnloadEvent.zig")) e else null,
.storage_event => |e| return if (T == @import("event/StorageEvent.zig")) e else null,
.device_motion_event => |e| return if (T == @import("event/DeviceMotionEvent.zig")) e else null,
.device_orientation_event => |e| return if (T == @import("event/DeviceOrientationEvent.zig")) e else null,
.promise_rejection_event => |e| return if (T == @import("event/PromiseRejectionEvent.zig")) e else null,
.submit_event => |e| return if (T == @import("event/SubmitEvent.zig")) e else null,
.form_data_event => |e| return if (T == @import("event/FormDataEvent.zig")) e else null,

View File

@@ -194,6 +194,11 @@ pub fn NodeLive(comptime mode: Mode) type {
// (like length or getAtIndex)
var tw = self._tw.clone();
while (self.nextTw(&tw)) |element| {
// Per spec, only HTML-namespace elements are exposed via
// their name attribute (ids expose any element).
if (element._namespace != .html) {
continue;
}
const element_name = element.getAttributeSafe(comptime .wrap("name")) orelse continue;
if (std.mem.eql(u8, element_name, name)) {
return element;

View File

@@ -1227,6 +1227,38 @@ pub fn getOnToggle(self: *HtmlElement, frame: *Frame) !?js.Function.Global {
return self.getAttributeFunction(.ontoggle, frame);
}
pub fn setOnTouchCancel(self: *HtmlElement, callback: ?js.Function.Global, frame: *Frame) !void {
return self.setAttributeListener(.ontouchcancel, callback, frame);
}
pub fn getOnTouchCancel(self: *HtmlElement, frame: *Frame) !?js.Function.Global {
return self.getAttributeFunction(.ontouchcancel, frame);
}
pub fn setOnTouchEnd(self: *HtmlElement, callback: ?js.Function.Global, frame: *Frame) !void {
return self.setAttributeListener(.ontouchend, callback, frame);
}
pub fn getOnTouchEnd(self: *HtmlElement, frame: *Frame) !?js.Function.Global {
return self.getAttributeFunction(.ontouchend, frame);
}
pub fn setOnTouchMove(self: *HtmlElement, callback: ?js.Function.Global, frame: *Frame) !void {
return self.setAttributeListener(.ontouchmove, callback, frame);
}
pub fn getOnTouchMove(self: *HtmlElement, frame: *Frame) !?js.Function.Global {
return self.getAttributeFunction(.ontouchmove, frame);
}
pub fn setOnTouchStart(self: *HtmlElement, callback: ?js.Function.Global, frame: *Frame) !void {
return self.setAttributeListener(.ontouchstart, callback, frame);
}
pub fn getOnTouchStart(self: *HtmlElement, frame: *Frame) !?js.Function.Global {
return self.getAttributeFunction(.ontouchstart, frame);
}
pub fn setOnTransitionCancel(self: *HtmlElement, callback: ?js.Function.Global, frame: *Frame) !void {
return self.setAttributeListener(.ontransitioncancel, callback, frame);
}
@@ -1773,6 +1805,10 @@ pub const JsApi = struct {
pub const onsuspend = bridge.accessor(HtmlElement.getOnSuspend, HtmlElement.setOnSuspend, .{});
pub const ontimeupdate = bridge.accessor(HtmlElement.getOnTimeUpdate, HtmlElement.setOnTimeUpdate, .{});
pub const ontoggle = bridge.accessor(HtmlElement.getOnToggle, HtmlElement.setOnToggle, .{});
pub const ontouchcancel = bridge.accessor(HtmlElement.getOnTouchCancel, HtmlElement.setOnTouchCancel, .{});
pub const ontouchend = bridge.accessor(HtmlElement.getOnTouchEnd, HtmlElement.setOnTouchEnd, .{});
pub const ontouchmove = bridge.accessor(HtmlElement.getOnTouchMove, HtmlElement.setOnTouchMove, .{});
pub const ontouchstart = bridge.accessor(HtmlElement.getOnTouchStart, HtmlElement.setOnTouchStart, .{});
pub const ontransitioncancel = bridge.accessor(HtmlElement.getOnTransitionCancel, HtmlElement.setOnTransitionCancel, .{});
pub const ontransitionend = bridge.accessor(HtmlElement.getOnTransitionEnd, HtmlElement.setOnTransitionEnd, .{});
pub const ontransitionrun = bridge.accessor(HtmlElement.getOnTransitionRun, HtmlElement.setOnTransitionRun, .{});

View File

@@ -0,0 +1,89 @@
// Copyright (C) 2023-2026 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 lp = @import("lightpanda");
const js = @import("../../js/js.zig");
const Frame = @import("../../Frame.zig");
const Event = @import("../Event.zig");
const String = lp.String;
const Allocator = std.mem.Allocator;
// https://html.spec.whatwg.org/multipage/browsing-the-web.html#the-beforeunloadevent-interface
const BeforeUnloadEvent = @This();
_proto: *Event,
_return_value: []const u8 = "",
const Options = Event.inheritOptions(BeforeUnloadEvent, struct {});
pub fn init(typ: []const u8, _opts: ?Options, frame: *Frame) !*BeforeUnloadEvent {
const arena = try frame.getArena(.tiny, "BeforeUnloadEvent");
errdefer frame.releaseArena(arena);
const type_string = try String.init(arena, typ, .{});
return initWithTrusted(arena, type_string, _opts, false, frame);
}
pub fn initTrusted(typ: String, _opts: ?Options, frame: *Frame) !*BeforeUnloadEvent {
const arena = try frame.getArena(.tiny, "BeforeUnloadEvent.trusted");
errdefer frame.releaseArena(arena);
return initWithTrusted(arena, typ, _opts, true, frame);
}
fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool, frame: *Frame) !*BeforeUnloadEvent {
const opts = _opts orelse Options{};
const event = try frame._factory.event(
arena,
typ,
BeforeUnloadEvent{
._proto = undefined,
},
);
Event.populatePrototypes(event, opts, trusted);
return event;
}
pub fn asEvent(self: *BeforeUnloadEvent) *Event {
return self._proto;
}
pub fn getReturnValue(self: *const BeforeUnloadEvent) []const u8 {
return self._return_value;
}
pub fn setReturnValue(self: *BeforeUnloadEvent, value: []const u8, frame: *Frame) !void {
self._return_value = try frame.dupeString(value);
}
pub const JsApi = struct {
pub const bridge = js.Bridge(BeforeUnloadEvent);
pub const Meta = struct {
pub const name = "BeforeUnloadEvent";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
// Per spec BeforeUnloadEvent has no constructor.
pub const returnValue = bridge.accessor(BeforeUnloadEvent.getReturnValue, BeforeUnloadEvent.setReturnValue, .{});
};

View File

@@ -0,0 +1,96 @@
// Copyright (C) 2023-2026 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 lp = @import("lightpanda");
const js = @import("../../js/js.zig");
const Frame = @import("../../Frame.zig");
const Event = @import("../Event.zig");
const String = lp.String;
const Allocator = std.mem.Allocator;
// https://w3c.github.io/deviceorientation/#devicemotionevent
const DeviceMotionEvent = @This();
_proto: *Event,
_interval: f64 = 0,
const DeviceMotionEventOptions = struct {
interval: f64 = 0,
};
const Options = Event.inheritOptions(DeviceMotionEvent, DeviceMotionEventOptions);
pub fn init(typ: []const u8, _opts: ?Options, frame: *Frame) !*DeviceMotionEvent {
const arena = try frame.getArena(.tiny, "DeviceMotionEvent");
errdefer frame.releaseArena(arena);
const type_string = try String.init(arena, typ, .{});
const opts = _opts orelse Options{};
const event = try frame._factory.event(
arena,
type_string,
DeviceMotionEvent{
._proto = undefined,
._interval = opts.interval,
},
);
Event.populatePrototypes(event, opts, false);
return event;
}
pub fn asEvent(self: *DeviceMotionEvent) *Event {
return self._proto;
}
// There is no motion sensor: the acceleration and rotation members are null.
pub fn getAcceleration(_: *const DeviceMotionEvent) ?bool {
return null;
}
pub fn getAccelerationIncludingGravity(_: *const DeviceMotionEvent) ?bool {
return null;
}
pub fn getRotationRate(_: *const DeviceMotionEvent) ?bool {
return null;
}
pub fn getInterval(self: *const DeviceMotionEvent) f64 {
return self._interval;
}
pub const JsApi = struct {
pub const bridge = js.Bridge(DeviceMotionEvent);
pub const Meta = struct {
pub const name = "DeviceMotionEvent";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const constructor = bridge.constructor(DeviceMotionEvent.init, .{});
pub const acceleration = bridge.accessor(DeviceMotionEvent.getAcceleration, null, .{});
pub const accelerationIncludingGravity = bridge.accessor(DeviceMotionEvent.getAccelerationIncludingGravity, null, .{});
pub const rotationRate = bridge.accessor(DeviceMotionEvent.getRotationRate, null, .{});
pub const interval = bridge.accessor(DeviceMotionEvent.getInterval, null, .{});
};

View File

@@ -0,0 +1,104 @@
// Copyright (C) 2023-2026 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 lp = @import("lightpanda");
const js = @import("../../js/js.zig");
const Frame = @import("../../Frame.zig");
const Event = @import("../Event.zig");
const String = lp.String;
const Allocator = std.mem.Allocator;
// https://w3c.github.io/deviceorientation/#deviceorientationevent
const DeviceOrientationEvent = @This();
_proto: *Event,
_alpha: ?f64 = null,
_beta: ?f64 = null,
_gamma: ?f64 = null,
_absolute: bool = false,
const DeviceOrientationEventOptions = struct {
alpha: ?f64 = null,
beta: ?f64 = null,
gamma: ?f64 = null,
absolute: bool = false,
};
const Options = Event.inheritOptions(DeviceOrientationEvent, DeviceOrientationEventOptions);
pub fn init(typ: []const u8, _opts: ?Options, frame: *Frame) !*DeviceOrientationEvent {
const arena = try frame.getArena(.tiny, "DeviceOrientationEvent");
errdefer frame.releaseArena(arena);
const type_string = try String.init(arena, typ, .{});
const opts = _opts orelse Options{};
const event = try frame._factory.event(
arena,
type_string,
DeviceOrientationEvent{
._proto = undefined,
._alpha = opts.alpha,
._beta = opts.beta,
._gamma = opts.gamma,
._absolute = opts.absolute,
},
);
Event.populatePrototypes(event, opts, false);
return event;
}
pub fn asEvent(self: *DeviceOrientationEvent) *Event {
return self._proto;
}
pub fn getAlpha(self: *const DeviceOrientationEvent) ?f64 {
return self._alpha;
}
pub fn getBeta(self: *const DeviceOrientationEvent) ?f64 {
return self._beta;
}
pub fn getGamma(self: *const DeviceOrientationEvent) ?f64 {
return self._gamma;
}
pub fn getAbsolute(self: *const DeviceOrientationEvent) bool {
return self._absolute;
}
pub const JsApi = struct {
pub const bridge = js.Bridge(DeviceOrientationEvent);
pub const Meta = struct {
pub const name = "DeviceOrientationEvent";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const constructor = bridge.constructor(DeviceOrientationEvent.init, .{});
pub const alpha = bridge.accessor(DeviceOrientationEvent.getAlpha, null, .{});
pub const beta = bridge.accessor(DeviceOrientationEvent.getBeta, null, .{});
pub const gamma = bridge.accessor(DeviceOrientationEvent.getGamma, null, .{});
pub const absolute = bridge.accessor(DeviceOrientationEvent.getAbsolute, null, .{});
};

View File

@@ -0,0 +1,147 @@
// Copyright (C) 2023-2026 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 lp = @import("lightpanda");
const js = @import("../../js/js.zig");
const Frame = @import("../../Frame.zig");
const Event = @import("../Event.zig");
const String = lp.String;
const Allocator = std.mem.Allocator;
// https://html.spec.whatwg.org/multipage/webstorage.html#the-storageevent-interface
const StorageEvent = @This();
_proto: *Event,
_key: ?[]const u8 = null,
_old_value: ?[]const u8 = null,
_new_value: ?[]const u8 = null,
_url: []const u8 = "",
const StorageEventOptions = struct {
key: ?[]const u8 = null,
oldValue: ?[]const u8 = null,
newValue: ?[]const u8 = null,
url: []const u8 = "",
};
const Options = Event.inheritOptions(StorageEvent, StorageEventOptions);
pub fn init(typ: []const u8, _opts: ?Options, frame: *Frame) !*StorageEvent {
const arena = try frame.getArena(.tiny, "StorageEvent");
errdefer frame.releaseArena(arena);
const type_string = try String.init(arena, typ, .{});
return initWithTrusted(arena, type_string, _opts, false, frame);
}
pub fn initTrusted(typ: String, _opts: ?Options, frame: *Frame) !*StorageEvent {
const arena = try frame.getArena(.tiny, "StorageEvent.trusted");
errdefer frame.releaseArena(arena);
return initWithTrusted(arena, typ, _opts, true, frame);
}
fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool, frame: *Frame) !*StorageEvent {
const opts = _opts orelse Options{};
const event = try frame._factory.event(
arena,
typ,
StorageEvent{
._proto = undefined,
._key = if (opts.key) |k| try arena.dupe(u8, k) else null,
._old_value = if (opts.oldValue) |v| try arena.dupe(u8, v) else null,
._new_value = if (opts.newValue) |v| try arena.dupe(u8, v) else null,
._url = try arena.dupe(u8, opts.url),
},
);
Event.populatePrototypes(event, opts, trusted);
return event;
}
pub fn asEvent(self: *StorageEvent) *Event {
return self._proto;
}
pub fn getKey(self: *const StorageEvent) ?[]const u8 {
return self._key;
}
pub fn getOldValue(self: *const StorageEvent) ?[]const u8 {
return self._old_value;
}
pub fn getNewValue(self: *const StorageEvent) ?[]const u8 {
return self._new_value;
}
pub fn getUrl(self: *const StorageEvent) []const u8 {
return self._url;
}
// The initiating Storage object is not tracked; always null.
pub fn getStorageArea(_: *const StorageEvent) ?bool {
return null;
}
pub fn initStorageEvent(
self: *StorageEvent,
typ: []const u8,
bubbles: ?bool,
cancelable: ?bool,
key: ?[]const u8,
old_value: ?[]const u8,
new_value: ?[]const u8,
url: ?[]const u8,
) !void {
const event = self._proto;
if (event._event_phase != .none) {
return;
}
const arena = event._arena;
event._initialized = true;
event._type_string = try String.init(arena, typ, .{});
event._bubbles = bubbles orelse false;
event._cancelable = cancelable orelse false;
self._key = if (key) |k| try arena.dupe(u8, k) else null;
self._old_value = if (old_value) |v| try arena.dupe(u8, v) else null;
self._new_value = if (new_value) |v| try arena.dupe(u8, v) else null;
self._url = if (url) |u| try arena.dupe(u8, u) else "";
}
pub const JsApi = struct {
pub const bridge = js.Bridge(StorageEvent);
pub const Meta = struct {
pub const name = "StorageEvent";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const constructor = bridge.constructor(StorageEvent.init, .{});
pub const key = bridge.accessor(StorageEvent.getKey, null, .{});
pub const oldValue = bridge.accessor(StorageEvent.getOldValue, null, .{});
pub const newValue = bridge.accessor(StorageEvent.getNewValue, null, .{});
pub const url = bridge.accessor(StorageEvent.getUrl, null, .{});
pub const storageArea = bridge.accessor(StorageEvent.getStorageArea, null, .{});
pub const initStorageEvent = bridge.function(StorageEvent.initStorageEvent, .{});
};

View File

@@ -0,0 +1,124 @@
// Copyright (C) 2023-2026 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 lp = @import("lightpanda");
const js = @import("../../js/js.zig");
const Frame = @import("../../Frame.zig");
const Event = @import("../Event.zig");
const UIEvent = @import("UIEvent.zig");
const String = lp.String;
const Allocator = std.mem.Allocator;
// https://w3c.github.io/touch-events/#touchevent-interface
// There is no touch input source: the touch lists are always empty.
const TouchEvent = @This();
_proto: *UIEvent,
_alt_key: bool = false,
_meta_key: bool = false,
_ctrl_key: bool = false,
_shift_key: bool = false,
pub const TouchEventOptions = struct {
altKey: bool = false,
metaKey: bool = false,
ctrlKey: bool = false,
shiftKey: bool = false,
};
pub const Options = Event.inheritOptions(
TouchEvent,
TouchEventOptions,
);
pub fn init(typ: []const u8, _opts: ?Options, frame: *Frame) !*TouchEvent {
const arena = try frame.getArena(.tiny, "TouchEvent");
errdefer frame.releaseArena(arena);
const type_string = try String.init(arena, typ, .{});
const opts = _opts orelse Options{};
const event = try frame._factory.uiEvent(
arena,
type_string,
TouchEvent{
._proto = undefined,
._alt_key = opts.altKey,
._meta_key = opts.metaKey,
._ctrl_key = opts.ctrlKey,
._shift_key = opts.shiftKey,
},
);
Event.populatePrototypes(event, opts, false);
return event;
}
pub fn asEvent(self: *TouchEvent) *Event {
return self._proto.asEvent();
}
pub fn getTouches(_: *const TouchEvent) []const bool {
return &.{};
}
pub fn getTargetTouches(_: *const TouchEvent) []const bool {
return &.{};
}
pub fn getChangedTouches(_: *const TouchEvent) []const bool {
return &.{};
}
pub fn getAltKey(self: *const TouchEvent) bool {
return self._alt_key;
}
pub fn getMetaKey(self: *const TouchEvent) bool {
return self._meta_key;
}
pub fn getCtrlKey(self: *const TouchEvent) bool {
return self._ctrl_key;
}
pub fn getShiftKey(self: *const TouchEvent) bool {
return self._shift_key;
}
pub const JsApi = struct {
pub const bridge = js.Bridge(TouchEvent);
pub const Meta = struct {
pub const name = "TouchEvent";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const constructor = bridge.constructor(TouchEvent.init, .{});
pub const touches = bridge.accessor(TouchEvent.getTouches, null, .{});
pub const targetTouches = bridge.accessor(TouchEvent.getTargetTouches, null, .{});
pub const changedTouches = bridge.accessor(TouchEvent.getChangedTouches, null, .{});
pub const altKey = bridge.accessor(TouchEvent.getAltKey, null, .{});
pub const metaKey = bridge.accessor(TouchEvent.getMetaKey, null, .{});
pub const ctrlKey = bridge.accessor(TouchEvent.getCtrlKey, null, .{});
pub const shiftKey = bridge.accessor(TouchEvent.getShiftKey, null, .{});
};

View File

@@ -41,6 +41,7 @@ pub const Type = union(enum) {
text_event: *@import("TextEvent.zig"),
input_event: *@import("InputEvent.zig"),
composition_event: *@import("CompositionEvent.zig"),
touch_event: *@import("TouchEvent.zig"),
};
pub const UIEventOptions = struct {
@@ -90,6 +91,7 @@ pub fn is(self: *UIEvent, comptime T: type) ?*T {
.text_event => |e| return if (T == @import("TextEvent.zig")) e else null,
.input_event => |e| return if (T == @import("InputEvent.zig")) e else null,
.composition_event => |e| return if (T == @import("CompositionEvent.zig")) e else null,
.touch_event => |e| return if (T == @import("TouchEvent.zig")) e else null,
}
return null;
}

View File

@@ -156,6 +156,10 @@ pub const Handler = enum(u7) {
onsuspend,
ontimeupdate,
ontoggle,
ontouchcancel,
ontouchend,
ontouchmove,
ontouchstart,
ontransitioncancel,
ontransitionend,
ontransitionrun,