From 596d5906a0491e2ebc56e1c1bbe8ecf79b23f398 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 20 Jan 2026 18:38:03 +0800 Subject: [PATCH] add PointerEvent --- src/browser/Factory.zig | 26 ++- src/browser/js/bridge.zig | 1 + src/browser/tests/event/pointer.html | 165 ++++++++++++++++++ src/browser/webapi/event/MouseEvent.zig | 24 ++- src/browser/webapi/event/PointerEvent.zig | 202 ++++++++++++++++++++++ src/browser/webapi/event/UIEvent.zig | 5 +- 6 files changed, 419 insertions(+), 4 deletions(-) create mode 100644 src/browser/tests/event/pointer.html create mode 100644 src/browser/webapi/event/PointerEvent.zig diff --git a/src/browser/Factory.zig b/src/browser/Factory.zig index 36eba138..60a622b4 100644 --- a/src/browser/Factory.zig +++ b/src/browser/Factory.zig @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2025 Lightpanda (Selecy SAS) +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) // // Francis Bouvier // Pierre Tachoire @@ -29,6 +29,7 @@ const Page = @import("Page.zig"); const Node = @import("webapi/Node.zig"); const Event = @import("webapi/Event.zig"); const UIEvent = @import("webapi/event/UIEvent.zig"); +const MouseEvent = @import("webapi/event/MouseEvent.zig"); const Element = @import("webapi/Element.zig"); const Document = @import("webapi/Document.zig"); const EventTarget = @import("webapi/EventTarget.zig"); @@ -213,6 +214,29 @@ pub fn uiEvent(self: *Factory, typ: []const u8, child: anytype) !*@TypeOf(child) return chain.get(2); } +pub fn mouseEvent(self: *Factory, typ: []const u8, mouse: MouseEvent, child: anytype) !*@TypeOf(child) { + const allocator = self._slab.allocator(); + + const chain = try PrototypeChain( + &.{ Event, UIEvent, MouseEvent, @TypeOf(child) }, + ).allocate(allocator); + + // Special case: Event has a _type_string field, so we need manual setup + const event_ptr = chain.get(0); + event_ptr.* = try eventInit(typ, chain.get(1), self._page); + chain.setMiddle(1, UIEvent.Type); + + // Set MouseEvent with all its fields + const mouse_ptr = chain.get(2); + mouse_ptr.* = mouse; + mouse_ptr._proto = chain.get(1); + mouse_ptr._type = unionInit(MouseEvent.Type, chain.get(3)); + + chain.setLeaf(3, child); + + return chain.get(3); +} + pub fn blob(self: *Factory, child: anytype) !*@TypeOf(child) { const allocator = self._slab.allocator(); diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index 4c4abb8f..eab18ef5 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -692,6 +692,7 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/event/PopStateEvent.zig"), @import("../webapi/event/UIEvent.zig"), @import("../webapi/event/MouseEvent.zig"), + @import("../webapi/event/PointerEvent.zig"), @import("../webapi/event/KeyboardEvent.zig"), @import("../webapi/MessageChannel.zig"), @import("../webapi/MessagePort.zig"), diff --git a/src/browser/tests/event/pointer.html b/src/browser/tests/event/pointer.html new file mode 100644 index 00000000..931c90f9 --- /dev/null +++ b/src/browser/tests/event/pointer.html @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/browser/webapi/event/MouseEvent.zig b/src/browser/webapi/event/MouseEvent.zig index c0bd14c7..81e2d7a6 100644 --- a/src/browser/webapi/event/MouseEvent.zig +++ b/src/browser/webapi/event/MouseEvent.zig @@ -22,10 +22,11 @@ const UIEvent = @import("UIEvent.zig"); const EventTarget = @import("../EventTarget.zig"); const Page = @import("../../Page.zig"); const js = @import("../../js/js.zig"); +const PointerEvent = @import("PointerEvent.zig"); const MouseEvent = @This(); -const MouseButton = enum(u8) { +pub const MouseButton = enum(u8) { main = 0, auxillary = 1, secondary = 2, @@ -33,6 +34,12 @@ const MouseButton = enum(u8) { fifth = 4, }; +pub const Type = union(enum) { + generic, + pointer_event: *PointerEvent, +}; + +_type: Type, _proto: *UIEvent, _alt_key: bool, @@ -62,7 +69,7 @@ pub const MouseEventOptions = struct { relatedTarget: ?*EventTarget = null, }; -const Options = Event.inheritOptions( +pub const Options = Event.inheritOptions( MouseEvent, MouseEventOptions, ); @@ -73,6 +80,7 @@ pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*MouseEvent { const event = try page._factory.uiEvent( typ, MouseEvent{ + ._type = .generic, ._proto = undefined, ._screen_x = opts.screenX, ._screen_y = opts.screenY, @@ -95,6 +103,18 @@ pub fn asEvent(self: *MouseEvent) *Event { return self._proto.asEvent(); } +pub fn as(self: *MouseEvent, comptime T: type) *T { + return self.is(T).?; +} + +pub fn is(self: *MouseEvent, comptime T: type) ?*T { + switch (self._type) { + .generic => return if (T == MouseEvent) self else null, + .pointer_event => |e| return if (T == PointerEvent) e else null, + } + return null; +} + pub fn getAltKey(self: *const MouseEvent) bool { return self._alt_key; } diff --git a/src/browser/webapi/event/PointerEvent.zig b/src/browser/webapi/event/PointerEvent.zig new file mode 100644 index 00000000..fab647ba --- /dev/null +++ b/src/browser/webapi/event/PointerEvent.zig @@ -0,0 +1,202 @@ +// 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 js = @import("../../js/js.zig"); +const Page = @import("../../Page.zig"); +const Event = @import("../Event.zig"); +const MouseEvent = @import("MouseEvent.zig"); + +const PointerEvent = @This(); + +const PointerType = enum { + empty, + mouse, + pen, + touch, + + fn fromString(s: []const u8) PointerType { + if (std.mem.eql(u8, s, "")) return .empty; + if (std.mem.eql(u8, s, "mouse")) return .mouse; + if (std.mem.eql(u8, s, "pen")) return .pen; + if (std.mem.eql(u8, s, "touch")) return .touch; + return .empty; + } + + fn toString(self: PointerType) []const u8 { + return switch (self) { + .empty => "", + inline else => |pt| @tagName(pt), + }; + } +}; + +_proto: *MouseEvent, +_pointer_id: i32, +_pointer_type: PointerType, +_width: f64, +_height: f64, +_pressure: f64, +_tangential_pressure: f64, +_tilt_x: i32, +_tilt_y: i32, +_twist: i32, +_altitude_angle: f64, +_azimuth_angle: f64, +_is_primary: bool, + +pub const PointerEventOptions = struct { + pointerId: i32 = 0, + pointerType: []const u8 = "", + width: f64 = 1.0, + height: f64 = 1.0, + pressure: f64 = 0.0, + tangentialPressure: f64 = 0.0, + tiltX: i32 = 0, + tiltY: i32 = 0, + twist: i32 = 0, + altitudeAngle: f64 = std.math.pi / 2.0, + azimuthAngle: f64 = 0.0, + isPrimary: bool = false, +}; + +const Options = Event.inheritOptions( + PointerEvent, + PointerEventOptions, +); + +pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*PointerEvent { + const opts = _opts orelse Options{}; + + const event = try page._factory.mouseEvent( + typ, + MouseEvent{ + ._type = .{ .pointer_event = undefined }, + ._proto = undefined, + ._screen_x = opts.screenX, + ._screen_y = opts.screenY, + ._client_x = opts.clientX, + ._client_y = opts.clientY, + ._ctrl_key = opts.ctrlKey, + ._shift_key = opts.shiftKey, + ._alt_key = opts.altKey, + ._meta_key = opts.metaKey, + ._button = std.meta.intToEnum(MouseEvent.MouseButton, opts.button) catch return error.TypeError, + ._related_target = opts.relatedTarget, + }, + PointerEvent{ + ._proto = undefined, + ._pointer_id = opts.pointerId, + ._pointer_type = PointerType.fromString(opts.pointerType), + ._width = opts.width, + ._height = opts.height, + ._pressure = opts.pressure, + ._tangential_pressure = opts.tangentialPressure, + ._tilt_x = opts.tiltX, + ._tilt_y = opts.tiltY, + ._twist = opts.twist, + ._altitude_angle = opts.altitudeAngle, + ._azimuth_angle = opts.azimuthAngle, + ._is_primary = opts.isPrimary, + }, + ); + + Event.populatePrototypes(event, opts, false); + return event; +} + +pub fn asEvent(self: *PointerEvent) *Event { + return self._proto.asEvent(); +} + +pub fn getPointerId(self: *const PointerEvent) i32 { + return self._pointer_id; +} + +pub fn getPointerType(self: *const PointerEvent) []const u8 { + return self._pointer_type.toString(); +} + +pub fn getWidth(self: *const PointerEvent) f64 { + return self._width; +} + +pub fn getHeight(self: *const PointerEvent) f64 { + return self._height; +} + +pub fn getPressure(self: *const PointerEvent) f64 { + return self._pressure; +} + +pub fn getTangentialPressure(self: *const PointerEvent) f64 { + return self._tangential_pressure; +} + +pub fn getTiltX(self: *const PointerEvent) i32 { + return self._tilt_x; +} + +pub fn getTiltY(self: *const PointerEvent) i32 { + return self._tilt_y; +} + +pub fn getTwist(self: *const PointerEvent) i32 { + return self._twist; +} + +pub fn getAltitudeAngle(self: *const PointerEvent) f64 { + return self._altitude_angle; +} + +pub fn getAzimuthAngle(self: *const PointerEvent) f64 { + return self._azimuth_angle; +} + +pub fn getIsPrimary(self: *const PointerEvent) bool { + return self._is_primary; +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(PointerEvent); + + pub const Meta = struct { + pub const name = "PointerEvent"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const constructor = bridge.constructor(PointerEvent.init, .{}); + pub const pointerId = bridge.accessor(PointerEvent.getPointerId, null, .{}); + pub const pointerType = bridge.accessor(PointerEvent.getPointerType, null, .{}); + pub const width = bridge.accessor(PointerEvent.getWidth, null, .{}); + pub const height = bridge.accessor(PointerEvent.getHeight, null, .{}); + pub const pressure = bridge.accessor(PointerEvent.getPressure, null, .{}); + pub const tangentialPressure = bridge.accessor(PointerEvent.getTangentialPressure, null, .{}); + pub const tiltX = bridge.accessor(PointerEvent.getTiltX, null, .{}); + pub const tiltY = bridge.accessor(PointerEvent.getTiltY, null, .{}); + pub const twist = bridge.accessor(PointerEvent.getTwist, null, .{}); + pub const altitudeAngle = bridge.accessor(PointerEvent.getAltitudeAngle, null, .{}); + pub const azimuthAngle = bridge.accessor(PointerEvent.getAzimuthAngle, null, .{}); + pub const isPrimary = bridge.accessor(PointerEvent.getIsPrimary, null, .{}); +}; + +const testing = @import("../../../testing.zig"); +test "WebApi: PointerEvent" { + try testing.htmlRunner("event/pointer.html", .{}); +} diff --git a/src/browser/webapi/event/UIEvent.zig b/src/browser/webapi/event/UIEvent.zig index 2ea51f7a..8ad24858 100644 --- a/src/browser/webapi/event/UIEvent.zig +++ b/src/browser/webapi/event/UIEvent.zig @@ -68,7 +68,10 @@ pub fn as(self: *UIEvent, comptime T: type) *T { pub fn is(self: *UIEvent, comptime T: type) ?*T { switch (self._type) { .generic => return if (T == UIEvent) self else null, - .mouse_event => |e| return if (T == @import("MouseEvent.zig")) e else null, + .mouse_event => |e| { + if (T == @import("MouseEvent.zig")) return e; + return e.is(T); + }, .keyboard_event => |e| return if (T == @import("KeyboardEvent.zig")) e else null, } return null;