From fef5586ff5c5c3f36c02df2f4f1b1980f25f7bf6 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Sat, 14 Feb 2026 16:22:19 +0800 Subject: [PATCH] add FocusEvent, TextEvent and WheelEvent --- src/browser/js/bridge.zig | 3 + src/browser/tests/event/focus.html | 22 ++++ src/browser/tests/event/text.html | 17 +++ src/browser/tests/event/wheel.html | 30 ++++++ src/browser/webapi/Document.zig | 10 ++ src/browser/webapi/event/FocusEvent.zig | 91 ++++++++++++++++ src/browser/webapi/event/MouseEvent.zig | 2 + src/browser/webapi/event/TextEvent.zig | 115 ++++++++++++++++++++ src/browser/webapi/event/UIEvent.zig | 4 + src/browser/webapi/event/WheelEvent.zig | 136 ++++++++++++++++++++++++ 10 files changed, 430 insertions(+) create mode 100644 src/browser/tests/event/focus.html create mode 100644 src/browser/tests/event/text.html create mode 100644 src/browser/tests/event/wheel.html create mode 100644 src/browser/webapi/event/FocusEvent.zig create mode 100644 src/browser/webapi/event/TextEvent.zig create mode 100644 src/browser/webapi/event/WheelEvent.zig diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index c6f677a0..623d898e 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -817,6 +817,9 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/event/MouseEvent.zig"), @import("../webapi/event/PointerEvent.zig"), @import("../webapi/event/KeyboardEvent.zig"), + @import("../webapi/event/FocusEvent.zig"), + @import("../webapi/event/WheelEvent.zig"), + @import("../webapi/event/TextEvent.zig"), @import("../webapi/event/PromiseRejectionEvent.zig"), @import("../webapi/MessageChannel.zig"), @import("../webapi/MessagePort.zig"), diff --git a/src/browser/tests/event/focus.html b/src/browser/tests/event/focus.html new file mode 100644 index 00000000..ac5b2a51 --- /dev/null +++ b/src/browser/tests/event/focus.html @@ -0,0 +1,22 @@ + + + + + + + diff --git a/src/browser/tests/event/text.html b/src/browser/tests/event/text.html new file mode 100644 index 00000000..f885c293 --- /dev/null +++ b/src/browser/tests/event/text.html @@ -0,0 +1,17 @@ + + + + + diff --git a/src/browser/tests/event/wheel.html b/src/browser/tests/event/wheel.html new file mode 100644 index 00000000..55a5c925 --- /dev/null +++ b/src/browser/tests/event/wheel.html @@ -0,0 +1,30 @@ + + + + + + + diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig index 8a3f695b..eb8f0c41 100644 --- a/src/browser/webapi/Document.zig +++ b/src/browser/webapi/Document.zig @@ -358,6 +358,16 @@ pub fn createEvent(_: *const Document, event_type: []const u8, page: *Page) !*@i return (try UIEvent.init("", null, page)).asEvent(); } + if (std.mem.eql(u8, normalized, "focusevent") or std.mem.eql(u8, normalized, "focusevents")) { + const FocusEvent = @import("event/FocusEvent.zig"); + return (try FocusEvent.init("", null, page)).asEvent(); + } + + if (std.mem.eql(u8, normalized, "textevent") or std.mem.eql(u8, normalized, "textevents")) { + const TextEvent = @import("event/TextEvent.zig"); + return (try TextEvent.init("", null, page)).asEvent(); + } + return error.NotSupported; } diff --git a/src/browser/webapi/event/FocusEvent.zig b/src/browser/webapi/event/FocusEvent.zig new file mode 100644 index 00000000..46032bee --- /dev/null +++ b/src/browser/webapi/event/FocusEvent.zig @@ -0,0 +1,91 @@ +// 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 String = @import("../../../string.zig").String; +const Page = @import("../../Page.zig"); +const js = @import("../../js/js.zig"); + +const Event = @import("../Event.zig"); +const EventTarget = @import("../EventTarget.zig"); +const UIEvent = @import("UIEvent.zig"); + +const FocusEvent = @This(); + +_proto: *UIEvent, +_related_target: ?*EventTarget = null, + +pub const FocusEventOptions = struct { + relatedTarget: ?*EventTarget = null, +}; + +pub const Options = Event.inheritOptions( + FocusEvent, + FocusEventOptions, +); + +pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*FocusEvent { + const arena = try page.getArena(.{ .debug = "FocusEvent" }); + errdefer page.releaseArena(arena); + const type_string = try String.init(arena, typ, .{}); + + const opts = _opts orelse Options{}; + + const event = try page._factory.uiEvent( + arena, + type_string, + FocusEvent{ + ._proto = undefined, + ._related_target = opts.relatedTarget, + }, + ); + + Event.populatePrototypes(event, opts, false); + return event; +} + +pub fn deinit(self: *FocusEvent, shutdown: bool) void { + self._proto.deinit(shutdown); +} + +pub fn asEvent(self: *FocusEvent) *Event { + return self._proto.asEvent(); +} + +pub fn getRelatedTarget(self: *const FocusEvent) ?*EventTarget { + return self._related_target; +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(FocusEvent); + + pub const Meta = struct { + pub const name = "FocusEvent"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + pub const weak = true; + pub const finalizer = bridge.finalizer(FocusEvent.deinit); + }; + + pub const constructor = bridge.constructor(FocusEvent.init, .{}); + pub const relatedTarget = bridge.accessor(FocusEvent.getRelatedTarget, null, .{}); +}; + +const testing = @import("../../../testing.zig"); +test "WebApi: FocusEvent" { + try testing.htmlRunner("event/focus.html", .{}); +} diff --git a/src/browser/webapi/event/MouseEvent.zig b/src/browser/webapi/event/MouseEvent.zig index f2eac422..a687aeaf 100644 --- a/src/browser/webapi/event/MouseEvent.zig +++ b/src/browser/webapi/event/MouseEvent.zig @@ -40,6 +40,7 @@ pub const MouseButton = enum(u8) { pub const Type = union(enum) { generic, pointer_event: *PointerEvent, + wheel_event: *@import("WheelEvent.zig"), }; _type: Type, @@ -123,6 +124,7 @@ 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, + .wheel_event => |e| return if (T == @import("WheelEvent.zig")) e else null, } return null; } diff --git a/src/browser/webapi/event/TextEvent.zig b/src/browser/webapi/event/TextEvent.zig new file mode 100644 index 00000000..65b16db8 --- /dev/null +++ b/src/browser/webapi/event/TextEvent.zig @@ -0,0 +1,115 @@ +// 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 String = @import("../../../string.zig").String; +const Page = @import("../../Page.zig"); +const js = @import("../../js/js.zig"); + +const Event = @import("../Event.zig"); +const UIEvent = @import("UIEvent.zig"); + +const TextEvent = @This(); + +_proto: *UIEvent, +_data: []const u8 = "", + +pub const TextEventOptions = struct { + data: ?[]const u8 = null, +}; + +pub const Options = Event.inheritOptions( + TextEvent, + TextEventOptions, +); + +pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*TextEvent { + const arena = try page.getArena(.{ .debug = "TextEvent" }); + errdefer page.releaseArena(arena); + const type_string = try String.init(arena, typ, .{}); + + const opts = _opts orelse Options{}; + + const event = try page._factory.uiEvent( + arena, + type_string, + TextEvent{ + ._proto = undefined, + ._data = if (opts.data) |str| try arena.dupe(u8, str) else "", + }, + ); + + Event.populatePrototypes(event, opts, false); + return event; +} + +pub fn deinit(self: *TextEvent, shutdown: bool) void { + self._proto.deinit(shutdown); +} + +pub fn asEvent(self: *TextEvent) *Event { + return self._proto.asEvent(); +} + +pub fn getData(self: *const TextEvent) []const u8 { + return self._data; +} + +pub fn initTextEvent( + self: *TextEvent, + typ: []const u8, + bubbles: bool, + cancelable: bool, + view: ?*@import("../Window.zig"), + data: []const u8, +) !void { + _ = view; // view parameter is ignored in modern implementations + + const event = self._proto._proto; + if (event._event_phase != .none) { + // Only allow initialization if event hasn't been dispatched + return; + } + + const arena = event._arena; + event._type_string = try String.init(arena, typ, .{}); + event._bubbles = bubbles; + event._cancelable = cancelable; + self._data = try arena.dupe(u8, data); +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(TextEvent); + + pub const Meta = struct { + pub const name = "TextEvent"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + pub const weak = true; + pub const finalizer = bridge.finalizer(TextEvent.deinit); + }; + + // No constructor - TextEvent is created via document.createEvent('TextEvent') + pub const data = bridge.accessor(TextEvent.getData, null, .{}); + pub const initTextEvent = bridge.function(TextEvent.initTextEvent, .{}); +}; + +const testing = @import("../../../testing.zig"); +test "WebApi: TextEvent" { + try testing.htmlRunner("event/text.html", .{}); +} diff --git a/src/browser/webapi/event/UIEvent.zig b/src/browser/webapi/event/UIEvent.zig index 6e50603e..b708dbd9 100644 --- a/src/browser/webapi/event/UIEvent.zig +++ b/src/browser/webapi/event/UIEvent.zig @@ -34,6 +34,8 @@ pub const Type = union(enum) { generic, mouse_event: *@import("MouseEvent.zig"), keyboard_event: *@import("KeyboardEvent.zig"), + focus_event: *@import("FocusEvent.zig"), + text_event: *@import("TextEvent.zig"), }; pub const UIEventOptions = struct { @@ -83,6 +85,8 @@ pub fn is(self: *UIEvent, comptime T: type) ?*T { return e.is(T); }, .keyboard_event => |e| return if (T == @import("KeyboardEvent.zig")) e else null, + .focus_event => |e| return if (T == @import("FocusEvent.zig")) e else null, + .text_event => |e| return if (T == @import("TextEvent.zig")) e else null, } return null; } diff --git a/src/browser/webapi/event/WheelEvent.zig b/src/browser/webapi/event/WheelEvent.zig new file mode 100644 index 00000000..665e1d49 --- /dev/null +++ b/src/browser/webapi/event/WheelEvent.zig @@ -0,0 +1,136 @@ +// 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 String = @import("../../../string.zig").String; +const Page = @import("../../Page.zig"); +const js = @import("../../js/js.zig"); + +const Event = @import("../Event.zig"); +const MouseEvent = @import("MouseEvent.zig"); + +const WheelEvent = @This(); + +_proto: *MouseEvent, +_delta_x: f64, +_delta_y: f64, +_delta_z: f64, +_delta_mode: u32, + +pub const DOM_DELTA_PIXEL: u32 = 0x00; +pub const DOM_DELTA_LINE: u32 = 0x01; +pub const DOM_DELTA_PAGE: u32 = 0x02; + +pub const WheelEventOptions = struct { + deltaX: f64 = 0.0, + deltaY: f64 = 0.0, + deltaZ: f64 = 0.0, + deltaMode: u32 = 0, +}; + +pub const Options = Event.inheritOptions( + WheelEvent, + WheelEventOptions, +); + +pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*WheelEvent { + const arena = try page.getArena(.{ .debug = "WheelEvent" }); + errdefer page.releaseArena(arena); + const type_string = try String.init(arena, typ, .{}); + + const opts = _opts orelse Options{}; + + const event = try page._factory.mouseEvent( + arena, + type_string, + MouseEvent{ + ._type = .{ .wheel_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, + }, + WheelEvent{ + ._proto = undefined, + ._delta_x = opts.deltaX, + ._delta_y = opts.deltaY, + ._delta_z = opts.deltaZ, + ._delta_mode = opts.deltaMode, + }, + ); + + Event.populatePrototypes(event, opts, false); + return event; +} + +pub fn deinit(self: *WheelEvent, shutdown: bool) void { + self._proto.deinit(shutdown); +} + +pub fn asEvent(self: *WheelEvent) *Event { + return self._proto.asEvent(); +} + +pub fn getDeltaX(self: *const WheelEvent) f64 { + return self._delta_x; +} + +pub fn getDeltaY(self: *const WheelEvent) f64 { + return self._delta_y; +} + +pub fn getDeltaZ(self: *const WheelEvent) f64 { + return self._delta_z; +} + +pub fn getDeltaMode(self: *const WheelEvent) u32 { + return self._delta_mode; +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(WheelEvent); + + pub const Meta = struct { + pub const name = "WheelEvent"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + pub const weak = true; + pub const finalizer = bridge.finalizer(WheelEvent.deinit); + }; + + pub const constructor = bridge.constructor(WheelEvent.init, .{}); + pub const deltaX = bridge.accessor(WheelEvent.getDeltaX, null, .{}); + pub const deltaY = bridge.accessor(WheelEvent.getDeltaY, null, .{}); + pub const deltaZ = bridge.accessor(WheelEvent.getDeltaZ, null, .{}); + pub const deltaMode = bridge.accessor(WheelEvent.getDeltaMode, null, .{}); + pub const DOM_DELTA_PIXEL = bridge.property(WheelEvent.DOM_DELTA_PIXEL, .{ .template = true }); + pub const DOM_DELTA_LINE = bridge.property(WheelEvent.DOM_DELTA_LINE, .{ .template = true }); + pub const DOM_DELTA_PAGE = bridge.property(WheelEvent.DOM_DELTA_PAGE, .{ .template = true }); +}; + +const testing = @import("../../../testing.zig"); +test "WebApi: WheelEvent" { + try testing.htmlRunner("event/wheel.html", .{}); +}