add MouseEvent

This commit is contained in:
Muki Kiboigo
2025-12-17 14:11:49 -08:00
parent d63a045534
commit 6f43d9979d
7 changed files with 271 additions and 9 deletions

View File

@@ -30,6 +30,7 @@ const SlabAllocator = @import("../slab.zig").SlabAllocator;
const Page = @import("Page.zig"); const Page = @import("Page.zig");
const Node = @import("webapi/Node.zig"); const Node = @import("webapi/Node.zig");
const Event = @import("webapi/Event.zig"); const Event = @import("webapi/Event.zig");
const UIEvent = @import("webapi/event/UIEvent.zig");
const Element = @import("webapi/Element.zig"); const Element = @import("webapi/Element.zig");
const Document = @import("webapi/Document.zig"); const Document = @import("webapi/Document.zig");
const EventTarget = @import("webapi/EventTarget.zig"); const EventTarget = @import("webapi/EventTarget.zig");
@@ -170,11 +171,11 @@ pub fn eventTarget(self: *Factory, child: anytype) !*@TypeOf(child) {
pub fn event(self: *Factory, typ: []const u8, child: anytype) !*@TypeOf(child) { pub fn event(self: *Factory, typ: []const u8, child: anytype) !*@TypeOf(child) {
const allocator = self._slab.allocator(); const allocator = self._slab.allocator();
// Special case: Event has a _type_string field, so we need manual setup
const chain = try PrototypeChain( const chain = try PrototypeChain(
&.{ Event, @TypeOf(child) }, &.{ Event, @TypeOf(child) },
).allocate(allocator); ).allocate(allocator);
// Special case: Event has a _type_string field, so we need manual setup
const event_ptr = chain.get(0); const event_ptr = chain.get(0);
event_ptr.* = .{ event_ptr.* = .{
._type = unionInit(Event.Type, chain.get(1)), ._type = unionInit(Event.Type, chain.get(1)),
@@ -185,6 +186,25 @@ pub fn event(self: *Factory, typ: []const u8, child: anytype) !*@TypeOf(child) {
return chain.get(1); return chain.get(1);
} }
pub fn uiEvent(self: *Factory, typ: []const u8, child: anytype) !*@TypeOf(child) {
const allocator = self._slab.allocator();
const chain = try PrototypeChain(
&.{ Event, UIEvent, @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.* = .{
._type = unionInit(Event.Type, chain.get(1)),
._type_string = try String.init(self._page.arena, typ, .{}),
};
chain.setMiddle(1, UIEvent.Type);
chain.setLeaf(2, child);
return chain.get(2);
}
pub fn blob(self: *Factory, child: anytype) !*@TypeOf(child) { pub fn blob(self: *Factory, child: anytype) !*@TypeOf(child) {
const allocator = self._slab.allocator(); const allocator = self._slab.allocator();

View File

@@ -577,6 +577,7 @@ pub const JsApis = flattenTypes(&.{
@import("../webapi/event/PageTransitionEvent.zig"), @import("../webapi/event/PageTransitionEvent.zig"),
@import("../webapi/event/PopStateEvent.zig"), @import("../webapi/event/PopStateEvent.zig"),
@import("../webapi/event/UIEvent.zig"), @import("../webapi/event/UIEvent.zig"),
@import("../webapi/event/MouseEvent.zig"),
@import("../webapi/MessageChannel.zig"), @import("../webapi/MessageChannel.zig"),
@import("../webapi/MessagePort.zig"), @import("../webapi/MessagePort.zig"),
@import("../webapi/media/MediaError.zig"), @import("../webapi/media/MediaError.zig"),

View File

@@ -0,0 +1,37 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<script id=default>
let event = new MouseEvent('click');
testing.expectEqual('click', event.type);
testing.expectEqual(true, event instanceof MouseEvent);
testing.expectEqual(true, event instanceof UIEvent);
testing.expectEqual(true, event instanceof Event);
testing.expectEqual(0, event.clientX);
testing.expectEqual(0, event.clientY);
testing.expectEqual(0, event.screenX);
testing.expectEqual(0, event.screenY);
</script>
<script id=parameters>
let new_event = new MouseEvent('click', { 'button': 0, 'clientX': 10, 'clientY': 20, screenX: 200, screenY: 500 });
testing.expectEqual(0, new_event.button);
testing.expectEqual(10, new_event.x);
testing.expectEqual(20, new_event.y);
testing.expectEqual(10, new_event.pageX);
testing.expectEqual(20, new_event.pageY);
testing.expectEqual(200, new_event.screenX);
testing.expectEqual(500, new_event.screenY);
</script>
<script id=listener>
let me = new MouseEvent('click');
testing.expectEqual(true, me instanceof Event);
var evt = null;
document.addEventListener('click', function (e) {
evt = e;
});
document.dispatchEvent(me);
testing.expectEqual('click', evt.type);
testing.expectEqual(true, evt instanceof MouseEvent);
</script>

View File

@@ -47,3 +47,8 @@
testing.expectEqual(0, evt5.detail); testing.expectEqual(0, evt5.detail);
testing.expectEqual(100, evt6.detail); testing.expectEqual(100, evt6.detail);
</script> </script>
<script id=uiEventDetailTypes2>
const evt7 = new UIEvent('custom', { bubbles: true });
testing.expectEqual(0, evt7.detail);
</script>

View File

@@ -18,7 +18,6 @@
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig"); const js = @import("../js/js.zig");
const reflect = @import("../reflect.zig");
const Page = @import("../Page.zig"); const Page = @import("../Page.zig");
const EventTarget = @import("EventTarget.zig"); const EventTarget = @import("EventTarget.zig");
@@ -63,7 +62,7 @@ pub const Type = union(enum) {
ui_event: *@import("event/UIEvent.zig"), ui_event: *@import("event/UIEvent.zig"),
}; };
const Options = struct { pub const Options = struct {
bubbles: bool = false, bubbles: bool = false,
cancelable: bool = false, cancelable: bool = false,
composed: bool = false, composed: bool = false,
@@ -211,7 +210,7 @@ pub fn inheritOptions(comptime T: type, comptime additions: anytype) type {
inline for (t_fields) |field| { inline for (t_fields) |field| {
if (std.mem.eql(u8, field.name, "_proto")) { if (std.mem.eql(u8, field.name, "_proto")) {
const ProtoType = reflect.Struct(field.type); const ProtoType = @typeInfo(field.type).pointer.child;
if (@hasDecl(ProtoType, "Options")) { if (@hasDecl(ProtoType, "Options")) {
const parent_options = @typeInfo(ProtoType.Options); const parent_options = @typeInfo(ProtoType.Options);
all_fields = all_fields ++ parent_options.@"struct".fields; all_fields = all_fields ++ parent_options.@"struct".fields;

View File

@@ -0,0 +1,188 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const Event = @import("../Event.zig");
const UIEvent = @import("UIEvent.zig");
const EventTarget = @import("../EventTarget.zig");
const Window = @import("../Window.zig");
const Page = @import("../../Page.zig");
const js = @import("../../js/js.zig");
const MouseEvent = @This();
const MouseButton = enum(u8) {
main = 0,
auxillary = 1,
secondary = 2,
fourth = 3,
fifth = 4,
};
_proto: *UIEvent,
_alt_key: bool,
_button: MouseButton,
// TODO: _buttons
_client_x: f64,
_client_y: f64,
_ctrl_key: bool,
_meta_key: bool,
// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/relatedTarget
_related_target: ?*EventTarget = null,
_screen_x: f64,
_screen_y: f64,
_shift_key: bool,
pub const MouseEventOptions = struct {
screenX: f64 = 0.0,
screenY: f64 = 0.0,
clientX: f64 = 0.0,
clientY: f64 = 0.0,
ctrlKey: bool = false,
shiftKey: bool = false,
altKey: bool = false,
metaKey: bool = false,
button: i32 = 0,
// TODO: buttons
relatedTarget: ?*EventTarget = null,
};
pub const Options = Event.inheritOptions(
MouseEvent,
MouseEventOptions,
);
pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*MouseEvent {
const opts = _opts orelse Options{};
const event = try page._factory.uiEvent(
typ,
MouseEvent{
._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(MouseButton, opts.button) catch return error.TypeError,
._related_target = opts.relatedTarget,
},
);
Event.populatePrototypes(event, opts);
return event;
}
pub fn asEvent(self: *MouseEvent) *Event {
return self._proto.asEvent();
}
pub fn getAltKey(self: *const MouseEvent) bool {
return self._alt_key;
}
pub fn getButton(self: *const MouseEvent) u8 {
return @intFromEnum(self._button);
}
pub fn getClientX(self: *const MouseEvent) f64 {
return self._client_x;
}
pub fn getClientY(self: *const MouseEvent) f64 {
return self._client_y;
}
pub fn getCtrlKey(self: *const MouseEvent) bool {
return self._ctrl_key;
}
pub fn getMetaKey(self: *const MouseEvent) bool {
return self._meta_key;
}
pub fn getOffsetX(_: *const MouseEvent) f64 {
return 0.0;
}
pub fn getOffsetY(_: *const MouseEvent) f64 {
return 0.0;
}
pub fn getPageX(self: *const MouseEvent) f64 {
// this should be clientX + window.scrollX
return self._client_x;
}
pub fn getPageY(self: *const MouseEvent) f64 {
// this should be clientY + window.scrollY
return self._client_y;
}
pub fn getRelatedTarget(self: *const MouseEvent) ?*EventTarget {
return self._related_target;
}
pub fn getScreenX(self: *const MouseEvent) f64 {
return self._screen_x;
}
pub fn getScreenY(self: *const MouseEvent) f64 {
return self._screen_y;
}
pub fn getShiftKey(self: *const MouseEvent) bool {
return self._shift_key;
}
pub const JsApi = struct {
pub const bridge = js.Bridge(MouseEvent);
pub const Meta = struct {
pub const name = "MouseEvent";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const constructor = bridge.constructor(MouseEvent.init, .{});
pub const altKey = bridge.accessor(getAltKey, null, .{});
pub const button = bridge.accessor(getButton, null, .{});
pub const clientX = bridge.accessor(getClientX, null, .{});
pub const clientY = bridge.accessor(getClientY, null, .{});
pub const ctrlKey = bridge.accessor(getCtrlKey, null, .{});
pub const metaKey = bridge.accessor(getMetaKey, null, .{});
pub const offsetX = bridge.accessor(getOffsetX, null, .{});
pub const offsetY = bridge.accessor(getOffsetY, null, .{});
pub const pageX = bridge.accessor(getPageX, null, .{});
pub const pageY = bridge.accessor(getPageY, null, .{});
pub const relatedTarget = bridge.accessor(getRelatedTarget, null, .{});
pub const screenX = bridge.accessor(getScreenX, null, .{});
pub const screenY = bridge.accessor(getScreenY, null, .{});
pub const shiftKey = bridge.accessor(getShiftKey, null, .{});
pub const x = bridge.accessor(getClientX, null, .{});
pub const y = bridge.accessor(getClientY, null, .{});
};
const testing = @import("../../../testing.zig");
test "WebApi: MouseEvent" {
try testing.htmlRunner("event/mouse.html", .{});
}

View File

@@ -23,16 +23,22 @@ const js = @import("../../js/js.zig");
const UIEvent = @This(); const UIEvent = @This();
_type: Type,
_proto: *Event, _proto: *Event,
_detail: u32, _detail: u32 = 0,
_view: *Window, _view: ?*Window = null,
pub const Type = union(enum) {
generic,
mouse_event: *@import("MouseEvent.zig"),
};
pub const UIEventOptions = struct { pub const UIEventOptions = struct {
detail: u32 = 0, detail: u32 = 0,
view: ?*Window = null, view: ?*Window = null,
}; };
const Options = Event.inheritOptions( pub const Options = Event.inheritOptions(
UIEvent, UIEvent,
UIEventOptions, UIEventOptions,
); );
@@ -43,6 +49,7 @@ pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*UIEvent {
const event = try page._factory.event( const event = try page._factory.event(
typ, typ,
UIEvent{ UIEvent{
._type = .generic,
._proto = undefined, ._proto = undefined,
._detail = opts.detail, ._detail = opts.detail,
._view = opts.view orelse page.window, ._view = opts.view orelse page.window,
@@ -53,6 +60,11 @@ pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*UIEvent {
return event; return event;
} }
pub fn populateFromOptions(self: *UIEvent, opts: anytype) void {
self._detail = opts.detail;
self._view = opts.view;
}
pub fn asEvent(self: *UIEvent) *Event { pub fn asEvent(self: *UIEvent) *Event {
return self._proto; return self._proto;
} }
@@ -63,8 +75,8 @@ pub fn getDetail(self: *UIEvent) u32 {
// sourceCapabilities not implemented // sourceCapabilities not implemented
pub fn getView(self: *UIEvent) *Window { pub fn getView(self: *UIEvent, page: *Page) *Window {
return self._view; return self._view orelse page.window;
} }
// deprecated `initUIEvent()` not implemented // deprecated `initUIEvent()` not implemented