diff --git a/src/browser/js/Caller.zig b/src/browser/js/Caller.zig index eff8769e0..a1844976f 100644 --- a/src/browser/js/Caller.zig +++ b/src/browser/js/Caller.zig @@ -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); diff --git a/src/browser/js/Snapshot.zig b/src/browser/js/Snapshot.zig index 80b660de5..7441051ea 100644 --- a/src/browser/js/Snapshot.zig +++ b/src/browser/js/Snapshot.zig @@ -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 diff --git a/src/browser/js/TaggedOpaque.zig b/src/browser/js/TaggedOpaque.zig index 3a8250ce9..ca20b3ef5 100644 --- a/src/browser/js/TaggedOpaque.zig +++ b/src/browser/js/TaggedOpaque.zig @@ -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); diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index 100e1520b..7bbd62a48 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -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; + } + } +} diff --git a/src/browser/tests/event/custom_event.html b/src/browser/tests/event/custom_event.html index 1835b205a..b4c87fab6 100644 --- a/src/browser/tests/event/custom_event.html +++ b/src/browser/tests/event/custom_event.html @@ -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); } diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig index c0b270ff8..b34de50ba 100644 --- a/src/browser/webapi/Document.zig +++ b/src/browser/webapi/Document.zig @@ -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"); diff --git a/src/browser/webapi/Event.zig b/src/browser/webapi/Event.zig index a0323c86f..c344a761f 100644 --- a/src/browser/webapi/Event.zig +++ b/src/browser/webapi/Event.zig @@ -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, diff --git a/src/browser/webapi/collections/node_live.zig b/src/browser/webapi/collections/node_live.zig index ab96ed661..82769cd07 100644 --- a/src/browser/webapi/collections/node_live.zig +++ b/src/browser/webapi/collections/node_live.zig @@ -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; diff --git a/src/browser/webapi/element/Html.zig b/src/browser/webapi/element/Html.zig index fead6c238..2caf21a2c 100644 --- a/src/browser/webapi/element/Html.zig +++ b/src/browser/webapi/element/Html.zig @@ -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, .{}); diff --git a/src/browser/webapi/event/BeforeUnloadEvent.zig b/src/browser/webapi/event/BeforeUnloadEvent.zig new file mode 100644 index 000000000..00b2fdbff --- /dev/null +++ b/src/browser/webapi/event/BeforeUnloadEvent.zig @@ -0,0 +1,89 @@ +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// 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 . + +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, .{}); +}; diff --git a/src/browser/webapi/event/DeviceMotionEvent.zig b/src/browser/webapi/event/DeviceMotionEvent.zig new file mode 100644 index 000000000..6da445f79 --- /dev/null +++ b/src/browser/webapi/event/DeviceMotionEvent.zig @@ -0,0 +1,96 @@ +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// 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 . + +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, .{}); +}; diff --git a/src/browser/webapi/event/DeviceOrientationEvent.zig b/src/browser/webapi/event/DeviceOrientationEvent.zig new file mode 100644 index 000000000..267611c85 --- /dev/null +++ b/src/browser/webapi/event/DeviceOrientationEvent.zig @@ -0,0 +1,104 @@ +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// 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 . + +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, .{}); +}; diff --git a/src/browser/webapi/event/StorageEvent.zig b/src/browser/webapi/event/StorageEvent.zig new file mode 100644 index 000000000..bb299a2b9 --- /dev/null +++ b/src/browser/webapi/event/StorageEvent.zig @@ -0,0 +1,147 @@ +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// 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 . + +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, .{}); +}; diff --git a/src/browser/webapi/event/TouchEvent.zig b/src/browser/webapi/event/TouchEvent.zig new file mode 100644 index 000000000..4956ed0ad --- /dev/null +++ b/src/browser/webapi/event/TouchEvent.zig @@ -0,0 +1,124 @@ +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// 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 . + +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, .{}); +}; diff --git a/src/browser/webapi/event/UIEvent.zig b/src/browser/webapi/event/UIEvent.zig index ded62c17a..72e8fc795 100644 --- a/src/browser/webapi/event/UIEvent.zig +++ b/src/browser/webapi/event/UIEvent.zig @@ -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; } diff --git a/src/browser/webapi/global_event_handlers.zig b/src/browser/webapi/global_event_handlers.zig index 64843c26f..cd6555583 100644 --- a/src/browser/webapi/global_event_handlers.zig +++ b/src/browser/webapi/global_event_handlers.zig @@ -156,6 +156,10 @@ pub const Handler = enum(u7) { onsuspend, ontimeupdate, ontoggle, + ontouchcancel, + ontouchend, + ontouchmove, + ontouchstart, ontransitioncancel, ontransitionend, ontransitionrun,