diff --git a/src/browser/Factory.zig b/src/browser/Factory.zig index 6a0de8037..eb0613b16 100644 --- a/src/browser/Factory.zig +++ b/src/browser/Factory.zig @@ -30,6 +30,7 @@ const SlabAllocator = @import("../slab.zig").SlabAllocator; 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 Element = @import("webapi/Element.zig"); const Document = @import("webapi/Document.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) { const allocator = self._slab.allocator(); - // Special case: Event has a _type_string field, so we need manual setup const chain = try PrototypeChain( &.{ Event, @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)), @@ -185,6 +186,25 @@ pub fn event(self: *Factory, typ: []const u8, child: anytype) !*@TypeOf(child) { 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) { const allocator = self._slab.allocator(); diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index a37097e17..e3c7813f1 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -576,6 +576,9 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/event/NavigationCurrentEntryChangeEvent.zig"), @import("../webapi/event/PageTransitionEvent.zig"), @import("../webapi/event/PopStateEvent.zig"), + @import("../webapi/event/UIEvent.zig"), + @import("../webapi/event/MouseEvent.zig"), + @import("../webapi/event/KeyboardEvent.zig"), @import("../webapi/MessageChannel.zig"), @import("../webapi/MessagePort.zig"), @import("../webapi/media/MediaError.zig"), diff --git a/src/browser/tests/event/keyboard.html b/src/browser/tests/event/keyboard.html new file mode 100644 index 000000000..3d14cfca5 --- /dev/null +++ b/src/browser/tests/event/keyboard.html @@ -0,0 +1,89 @@ + + + + + + + + + + + + diff --git a/src/browser/tests/event/mouse.html b/src/browser/tests/event/mouse.html new file mode 100644 index 000000000..4f8dd17fa --- /dev/null +++ b/src/browser/tests/event/mouse.html @@ -0,0 +1,37 @@ + + + + + + + diff --git a/src/browser/tests/event/ui.html b/src/browser/tests/event/ui.html new file mode 100644 index 000000000..50d880ec3 --- /dev/null +++ b/src/browser/tests/event/ui.html @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + diff --git a/src/browser/webapi/Event.zig b/src/browser/webapi/Event.zig index e99d71ece..23a0fa0e5 100644 --- a/src/browser/webapi/Event.zig +++ b/src/browser/webapi/Event.zig @@ -18,7 +18,6 @@ const std = @import("std"); const js = @import("../js/js.zig"); -const reflect = @import("../reflect.zig"); const Page = @import("../Page.zig"); const EventTarget = @import("EventTarget.zig"); @@ -60,9 +59,10 @@ pub const Type = union(enum) { navigation_current_entry_change_event: *@import("event/NavigationCurrentEntryChangeEvent.zig"), page_transition_event: *@import("event/PageTransitionEvent.zig"), pop_state_event: *@import("event/PopStateEvent.zig"), + ui_event: *@import("event/UIEvent.zig"), }; -const Options = struct { +pub const Options = struct { bubbles: bool = false, cancelable: bool = false, composed: bool = false, @@ -210,7 +210,7 @@ pub fn inheritOptions(comptime T: type, comptime additions: anytype) type { inline for (t_fields) |field| { 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")) { const parent_options = @typeInfo(ProtoType.Options); all_fields = all_fields ++ parent_options.@"struct".fields; diff --git a/src/browser/webapi/event/KeyboardEvent.zig b/src/browser/webapi/event/KeyboardEvent.zig new file mode 100644 index 000000000..288a5ccbb --- /dev/null +++ b/src/browser/webapi/event/KeyboardEvent.zig @@ -0,0 +1,196 @@ +// Copyright (C) 2023-2025 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 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 KeyboardEvent = @This(); + +_proto: *UIEvent, +_key: Key, +_ctrl_key: bool, +_shift_key: bool, +_alt_key: bool, +_meta_key: bool, +_location: Location, +_repeat: bool, +_is_composing: bool, + +// https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values +pub const Key = union(enum) { + // Special Key Values + Dead, + Undefined, + Alt, + AltGraph, + CapsLock, + Control, + Fn, + FnLock, + Hyper, + Meta, + NumLock, + ScrollLock, + Shift, + Super, + Symbol, + SymbolLock, + standard: []const u8, + + pub fn fromString(allocator: std.mem.Allocator, str: []const u8) !Key { + const key_type_info = @typeInfo(Key); + inline for (key_type_info.@"union".fields) |field| { + if (comptime std.mem.eql(u8, field.name, "standard")) continue; + + if (std.mem.eql(u8, field.name, str)) { + return @unionInit(Key, field.name, {}); + } + } + + const duped = try allocator.dupe(u8, str); + return .{ .standard = duped }; + } +}; + +pub const Location = enum(i32) { + DOM_KEY_LOCATION_STANDARD = 0, + DOM_KEY_LOCATION_LEFT = 1, + DOM_KEY_LOCATION_RIGHT = 2, + DOM_KEY_LOCATION_NUMPAD = 3, +}; + +pub const KeyboardEventOptions = struct { + key: []const u8 = "", + // TODO: code but it is not baseline. + location: i32 = 0, + repeat: bool = false, + isComposing: bool = false, + ctrlKey: bool = false, + shiftKey: bool = false, + altKey: bool = false, + metaKey: bool = false, +}; + +pub const Options = Event.inheritOptions( + KeyboardEvent, + KeyboardEventOptions, +); + +pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*KeyboardEvent { + const opts = _opts orelse Options{}; + + const event = try page._factory.uiEvent( + typ, + KeyboardEvent{ + ._proto = undefined, + ._key = try Key.fromString(page.arena, opts.key), + ._location = std.meta.intToEnum(Location, opts.location) catch return error.TypeError, + ._repeat = opts.repeat, + ._is_composing = opts.isComposing, + ._ctrl_key = opts.ctrlKey, + ._shift_key = opts.shiftKey, + ._alt_key = opts.altKey, + ._meta_key = opts.metaKey, + }, + ); + + Event.populatePrototypes(event, opts); + return event; +} + +pub fn asEvent(self: *KeyboardEvent) *Event { + return self._proto.asEvent(); +} + +pub fn getAltKey(self: *const KeyboardEvent) bool { + return self._alt_key; +} + +pub fn getCtrlKey(self: *const KeyboardEvent) bool { + return self._ctrl_key; +} + +pub fn getIsComposing(self: *const KeyboardEvent) bool { + return self._is_composing; +} + +pub fn getKey(self: *const KeyboardEvent) []const u8 { + return switch (self._key) { + .standard => |key| key, + else => |x| @tagName(x), + }; +} + +pub fn getLocation(self: *const KeyboardEvent) i32 { + return @intFromEnum(self._location); +} + +pub fn getMetaKey(self: *const KeyboardEvent) bool { + return self._meta_key; +} + +pub fn getRepeat(self: *const KeyboardEvent) bool { + return self._repeat; +} + +pub fn getShiftKey(self: *const KeyboardEvent) bool { + return self._shift_key; +} + +pub fn getModifierState(self: *const KeyboardEvent, str: []const u8, page: *Page) !bool { + const key = try Key.fromString(page.arena, str); + + switch (key) { + .Alt, .AltGraph => return self._alt_key, + .Shift => return self._shift_key, + .Control => return self._ctrl_key, + .Meta => return self._meta_key, + else => return false, + } +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(KeyboardEvent); + + pub const Meta = struct { + pub const name = "KeyboardEvent"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const constructor = bridge.constructor(KeyboardEvent.init, .{}); + pub const altKey = bridge.accessor(KeyboardEvent.getAltKey, null, .{}); + pub const ctrlKey = bridge.accessor(KeyboardEvent.getCtrlKey, null, .{}); + pub const isComposing = bridge.accessor(KeyboardEvent.getIsComposing, null, .{}); + pub const key = bridge.accessor(KeyboardEvent.getKey, null, .{}); + pub const location = bridge.accessor(KeyboardEvent.getLocation, null, .{}); + pub const metaKey = bridge.accessor(KeyboardEvent.getMetaKey, null, .{}); + pub const repeat = bridge.accessor(KeyboardEvent.getRepeat, null, .{}); + pub const shiftKey = bridge.accessor(KeyboardEvent.getShiftKey, null, .{}); + pub const getModifierState = bridge.function(KeyboardEvent.getModifierState, .{}); +}; + +const testing = @import("../../../testing.zig"); +test "WebApi: KeyboardEvent" { + try testing.htmlRunner("event/keyboard.html", .{}); +} diff --git a/src/browser/webapi/event/MouseEvent.zig b/src/browser/webapi/event/MouseEvent.zig new file mode 100644 index 000000000..305d4c86d --- /dev/null +++ b/src/browser/webapi/event/MouseEvent.zig @@ -0,0 +1,188 @@ +// Copyright (C) 2023-2025 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 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", .{}); +} diff --git a/src/browser/webapi/event/UIEvent.zig b/src/browser/webapi/event/UIEvent.zig new file mode 100644 index 000000000..75a58cd61 --- /dev/null +++ b/src/browser/webapi/event/UIEvent.zig @@ -0,0 +1,102 @@ +// Copyright (C) 2023-2025 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 Event = @import("../Event.zig"); +const Window = @import("../Window.zig"); +const Page = @import("../../Page.zig"); +const js = @import("../../js/js.zig"); + +const UIEvent = @This(); + +_type: Type, +_proto: *Event, +_detail: u32 = 0, +_view: ?*Window = null, + +pub const Type = union(enum) { + generic, + mouse_event: *@import("MouseEvent.zig"), + keyboard_event: *@import("KeyboardEvent.zig"), +}; + +pub const UIEventOptions = struct { + detail: u32 = 0, + view: ?*Window = null, +}; + +pub const Options = Event.inheritOptions( + UIEvent, + UIEventOptions, +); + +pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*UIEvent { + const opts = _opts orelse Options{}; + + const event = try page._factory.event( + typ, + UIEvent{ + ._type = .generic, + ._proto = undefined, + ._detail = opts.detail, + ._view = opts.view orelse page.window, + }, + ); + + Event.populatePrototypes(event, opts); + return event; +} + +pub fn populateFromOptions(self: *UIEvent, opts: anytype) void { + self._detail = opts.detail; + self._view = opts.view; +} + +pub fn asEvent(self: *UIEvent) *Event { + return self._proto; +} + +pub fn getDetail(self: *UIEvent) u32 { + return self._detail; +} + +// sourceCapabilities not implemented + +pub fn getView(self: *UIEvent, page: *Page) *Window { + return self._view orelse page.window; +} + +// deprecated `initUIEvent()` not implemented + +pub const JsApi = struct { + pub const bridge = js.Bridge(UIEvent); + + pub const Meta = struct { + pub const name = "UIEvent"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const constructor = bridge.constructor(UIEvent.init, .{}); + pub const detail = bridge.accessor(UIEvent.getDetail, null, .{}); + pub const view = bridge.accessor(UIEvent.getView, null, .{}); +}; + +const testing = @import("../../../testing.zig"); +test "WebApi: UIEvent" { + try testing.htmlRunner("event/ui.html", .{}); +}