diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index 4d7ad032..72cb14a6 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -926,6 +926,7 @@ pub const PageJsApis = flattenTypes(&.{ @import("../webapi/event/KeyboardEvent.zig"), @import("../webapi/event/FocusEvent.zig"), @import("../webapi/event/WheelEvent.zig"), + @import("../webapi/event/DragEvent.zig"), @import("../webapi/event/TextEvent.zig"), @import("../webapi/event/InputEvent.zig"), @import("../webapi/event/PromiseRejectionEvent.zig"), diff --git a/src/browser/tests/event/drag.html b/src/browser/tests/event/drag.html new file mode 100644 index 00000000..db3ddaa0 --- /dev/null +++ b/src/browser/tests/event/drag.html @@ -0,0 +1,69 @@ + + + + + + + + + + + + diff --git a/src/browser/webapi/event/DragEvent.zig b/src/browser/webapi/event/DragEvent.zig new file mode 100644 index 00000000..fe0def98 --- /dev/null +++ b/src/browser/webapi/event/DragEvent.zig @@ -0,0 +1,134 @@ +// 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 lp = @import("lightpanda"); + +const js = @import("../../js/js.zig"); +const Frame = @import("../../Frame.zig"); +const Page = @import("../../Page.zig"); + +const Event = @import("../Event.zig"); +const MouseEvent = @import("MouseEvent.zig"); +const DataTransfer = @import("../DataTransfer.zig"); + +const String = lp.String; + +const DragEvent = @This(); + +_proto: *MouseEvent, +_data_transfer: ?*DataTransfer, + +pub const DragEventOptions = struct { + dataTransfer: ?*DataTransfer = null, +}; + +pub const Options = Event.inheritOptions(DragEvent, DragEventOptions); + +pub fn init(typ: []const u8, _opts: ?Options, frame: *Frame) !*DragEvent { + return initWithTrusted(typ, _opts, false, frame); +} + +pub fn initTrusted(typ: []const u8, _opts: ?Options, frame: *Frame) !*DragEvent { + return initWithTrusted(typ, _opts, true, frame); +} + +fn initWithTrusted(typ: []const u8, _opts: ?Options, trusted: bool, frame: *Frame) !*DragEvent { + const arena = try frame.getArena(.medium, "DragEvent"); + errdefer frame.releaseArena(arena); + const type_string = try String.init(arena, typ, .{}); + + const opts = _opts orelse Options{}; + + const event = try frame._factory.mouseEvent( + arena, + type_string, + MouseEvent{ + ._type = .{ .drag_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, + ._buttons = opts.buttons, + ._related_target = opts.relatedTarget, + }, + DragEvent{ + ._proto = undefined, + ._data_transfer = opts.dataTransfer, + }, + ); + + Event.populatePrototypes(event, opts, trusted); + + // Hold a ref on the DataTransfer so its arena outlives this event even if the + // JS wrapper is collected first; released in deinit (mirrors MessageEvent's + // Blob handling). The shared refcount lives on the Event base, so reach it + // through asEvent() rather than the immediate _proto. + if (opts.dataTransfer) |dt| { + dt.acquireRef(); + } + + return event; +} + +pub fn deinit(self: *DragEvent, page: *Page) void { + if (self._data_transfer) |dt| { + dt.releaseRef(page); + } + self.asEvent().deinit(page); +} + +pub fn acquireRef(self: *DragEvent) void { + self.asEvent().acquireRef(); +} + +pub fn releaseRef(self: *DragEvent, page: *Page) void { + self.asEvent()._rc.release(self, page); +} + +pub fn asEvent(self: *DragEvent) *Event { + return self._proto.asEvent(); +} + +pub fn getDataTransfer(self: *const DragEvent) ?*DataTransfer { + return self._data_transfer; +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(DragEvent); + + pub const Meta = struct { + pub const name = "DragEvent"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const constructor = bridge.constructor(DragEvent.init, .{}); + pub const dataTransfer = bridge.accessor(DragEvent.getDataTransfer, null, .{}); +}; + +const testing = @import("../../../testing.zig"); +test "WebApi: DragEvent" { + try testing.htmlRunner("event/drag.html", .{}); +} diff --git a/src/browser/webapi/event/InputEvent.zig b/src/browser/webapi/event/InputEvent.zig index c74e8e03..f942c161 100644 --- a/src/browser/webapi/event/InputEvent.zig +++ b/src/browser/webapi/event/InputEvent.zig @@ -21,9 +21,11 @@ const lp = @import("lightpanda"); const js = @import("../../js/js.zig"); const Frame = @import("../../Frame.zig"); +const Page = @import("../../Page.zig"); const Event = @import("../Event.zig"); const UIEvent = @import("UIEvent.zig"); +const DataTransfer = @import("../DataTransfer.zig"); const String = lp.String; const Allocator = std.mem.Allocator; @@ -32,12 +34,13 @@ const InputEvent = @This(); _proto: *UIEvent, _data: ?[]const u8, -// TODO: add dataTransfer +_data_transfer: ?*DataTransfer = null, _input_type: []const u8, _is_composing: bool, pub const InputEventOptions = struct { data: ?[]const u8 = null, + dataTransfer: ?*DataTransfer = null, inputType: ?[]const u8 = null, isComposing: bool = false, }; @@ -69,6 +72,7 @@ fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool InputEvent{ ._proto = undefined, ._data = if (opts.data) |d| try arena.dupe(u8, d) else null, + ._data_transfer = opts.dataTransfer, ._input_type = if (opts.inputType) |it| try arena.dupe(u8, it) else "", ._is_composing = opts.isComposing, }, @@ -82,9 +86,31 @@ fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool rootevt._cancelable = false; rootevt._composed = true; + // Hold a ref on the DataTransfer (when present) for this event's lifetime; + // released in deinit. Almost always null for input events, but keeps the + // refcount protocol intact when it isn't. + if (opts.dataTransfer) |dt| { + dt.acquireRef(); + } + return event; } +pub fn deinit(self: *InputEvent, page: *Page) void { + if (self._data_transfer) |dt| { + dt.releaseRef(page); + } + self.asEvent().deinit(page); +} + +pub fn acquireRef(self: *InputEvent) void { + self.asEvent().acquireRef(); +} + +pub fn releaseRef(self: *InputEvent, page: *Page) void { + self.asEvent()._rc.release(self, page); +} + pub fn asEvent(self: *InputEvent) *Event { return self._proto.asEvent(); } @@ -93,6 +119,10 @@ pub fn getData(self: *const InputEvent) ?[]const u8 { return self._data; } +pub fn getDataTransfer(self: *const InputEvent) ?*DataTransfer { + return self._data_transfer; +} + pub fn getInputType(self: *const InputEvent) []const u8 { return self._input_type; } @@ -112,6 +142,7 @@ pub const JsApi = struct { pub const constructor = bridge.constructor(InputEvent.init, .{}); pub const data = bridge.accessor(InputEvent.getData, null, .{}); + pub const dataTransfer = bridge.accessor(InputEvent.getDataTransfer, null, .{}); pub const inputType = bridge.accessor(InputEvent.getInputType, null, .{}); pub const isComposing = bridge.accessor(InputEvent.getIsComposing, null, .{}); }; diff --git a/src/browser/webapi/event/MouseEvent.zig b/src/browser/webapi/event/MouseEvent.zig index 240ce6c8..04a07ffe 100644 --- a/src/browser/webapi/event/MouseEvent.zig +++ b/src/browser/webapi/event/MouseEvent.zig @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2025 Lightpanda (Selecy SAS) +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) // // Francis Bouvier // Pierre Tachoire @@ -45,6 +45,7 @@ pub const Type = union(enum) { generic, pointer_event: *PointerEvent, wheel_event: *@import("WheelEvent.zig"), + drag_event: *@import("DragEvent.zig"), }; _type: Type, @@ -135,6 +136,7 @@ pub fn is(self: *MouseEvent, comptime T: type) ?*T { .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, + .drag_event => |e| return if (T == @import("DragEvent.zig")) e else null, } return null; }