Merge pull request #1388 from lightpanda-io/pointer_event

add PointerEvent
This commit is contained in:
Karl Seguin
2026-01-21 07:10:32 +08:00
committed by GitHub
6 changed files with 419 additions and 4 deletions

View File

@@ -1,4 +1,4 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
@@ -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();

View File

@@ -694,6 +694,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"),

View File

@@ -0,0 +1,165 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<script id=default>
{
let event = new PointerEvent('pointerdown');
testing.expectEqual('pointerdown', event.type);
testing.expectEqual(true, event instanceof PointerEvent);
testing.expectEqual(true, event instanceof MouseEvent);
testing.expectEqual(true, event instanceof UIEvent);
testing.expectEqual(true, event instanceof Event);
testing.expectEqual(0, event.pointerId);
testing.expectEqual('', event.pointerType);
testing.expectEqual(1.0, event.width);
testing.expectEqual(1.0, event.height);
testing.expectEqual(0.0, event.pressure);
testing.expectEqual(0.0, event.tangentialPressure);
testing.expectEqual(0, event.tiltX);
testing.expectEqual(0, event.tiltY);
testing.expectEqual(0, event.twist);
testing.expectEqual(false, event.isPrimary);
}
</script>
<script id=parameters>
{
let new_event = new PointerEvent('pointerdown', {
pointerId: 42,
pointerType: 'pen',
width: 10.5,
height: 20.5,
pressure: 0.75,
tangentialPressure: -0.25,
tiltX: 30,
tiltY: 45,
twist: 90,
isPrimary: true,
clientX: 100,
clientY: 200,
screenX: 300,
screenY: 400
});
testing.expectEqual(42, new_event.pointerId);
testing.expectEqual('pen', new_event.pointerType);
testing.expectEqual(10.5, new_event.width);
testing.expectEqual(20.5, new_event.height);
testing.expectEqual(0.75, new_event.pressure);
testing.expectEqual(-0.25, new_event.tangentialPressure);
testing.expectEqual(30, new_event.tiltX);
testing.expectEqual(45, new_event.tiltY);
testing.expectEqual(90, new_event.twist);
testing.expectEqual(true, new_event.isPrimary);
testing.expectEqual(100, new_event.clientX);
testing.expectEqual(200, new_event.clientY);
testing.expectEqual(300, new_event.screenX);
testing.expectEqual(400, new_event.screenY);
}
</script>
<script id=mousePointerType>
{
let mouse_event = new PointerEvent('pointerdown', { pointerType: 'mouse' });
testing.expectEqual('mouse', mouse_event.pointerType);
}
</script>
<script id=touchPointerType>
{
let touch_event = new PointerEvent('pointerdown', { pointerType: 'touch', pointerId: 1, pressure: 0.5 });
testing.expectEqual('touch', touch_event.pointerType);
testing.expectEqual(1, touch_event.pointerId);
testing.expectEqual(0.5, touch_event.pressure);
}
</script>
<script id=listener>
{
let pe = new PointerEvent('pointerdown', { pointerId: 123 });
testing.expectEqual(true, pe instanceof PointerEvent);
testing.expectEqual(true, pe instanceof MouseEvent);
testing.expectEqual(true, pe instanceof Event);
var evt = null;
document.addEventListener('pointerdown', function (e) {
evt = e;
});
document.dispatchEvent(pe);
testing.expectEqual('pointerdown', evt.type);
testing.expectEqual(true, evt instanceof PointerEvent);
testing.expectEqual(123, evt.pointerId);
}
</script>
<script id=isTrusted>
{
let pointerEvent = new PointerEvent('pointerup');
testing.expectEqual(false, pointerEvent.isTrusted);
let pointerIsTrusted = null;
document.addEventListener('pointertest', (e) => {
pointerIsTrusted = e.isTrusted;
testing.expectEqual(true, e instanceof PointerEvent);
});
document.dispatchEvent(new PointerEvent('pointertest'));
testing.expectEqual(false, pointerIsTrusted);
}
</script>
<script id=eventTypes>
{
let down = new PointerEvent('pointerdown');
testing.expectEqual('pointerdown', down.type);
let up = new PointerEvent('pointerup');
testing.expectEqual('pointerup', up.type);
let move = new PointerEvent('pointermove');
testing.expectEqual('pointermove', move.type);
let enter = new PointerEvent('pointerenter');
testing.expectEqual('pointerenter', enter.type);
let leave = new PointerEvent('pointerleave');
testing.expectEqual('pointerleave', leave.type);
let over = new PointerEvent('pointerover');
testing.expectEqual('pointerover', over.type);
let out = new PointerEvent('pointerout');
testing.expectEqual('pointerout', out.type);
let cancel = new PointerEvent('pointercancel');
testing.expectEqual('pointercancel', cancel.type);
}
</script>
<script id=inheritedMouseProperties>
{
let pe = new PointerEvent('pointerdown', {
button: 2,
buttons: 4,
altKey: true,
ctrlKey: true,
shiftKey: true,
metaKey: true
});
testing.expectEqual(2, pe.button);
testing.expectEqual(true, pe.altKey);
testing.expectEqual(true, pe.ctrlKey);
testing.expectEqual(true, pe.shiftKey);
testing.expectEqual(true, pe.metaKey);
}
</script>
<script id=inheritedUIEventProperties>
{
let pe = new PointerEvent('pointerdown', {
detail: 5,
bubbles: true,
cancelable: true
});
testing.expectEqual(5, pe.detail);
testing.expectEqual(true, pe.bubbles);
testing.expectEqual(true, pe.cancelable);
}
</script>

View File

@@ -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;
}

View File

@@ -0,0 +1,202 @@
// 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 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", .{});
}

View File

@@ -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;