mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-06-11 01:25:53 -04:00
webapi: add DragEvent carrying dataTransfer; wire InputEvent.dataTransfer
DragEvent extends MouseEvent (new drag_event arm in the MouseEvent type union) and exposes a nullable dataTransfer, so JS can build a DataTransfer and dispatch a drop/dragover event whose .dataTransfer.files is readable. Also resolves the InputEvent dataTransfer TODO with a real field + accessor. Both events hold a refcount on the DataTransfer for their lifetime and release it in deinit (via the leaf acquireRef/releaseRef/deinit trio, like MessageEvent holds a Blob), so the store's arena can't be freed out from under an event whose JS wrapper is collected first. Refs #2043
This commit is contained in:
@@ -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"),
|
||||
|
||||
69
src/browser/tests/event/drag.html
Normal file
69
src/browser/tests/event/drag.html
Normal file
@@ -0,0 +1,69 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
|
||||
<script id=default>
|
||||
{
|
||||
const event = new DragEvent('drop');
|
||||
testing.expectEqual('drop', event.type);
|
||||
testing.expectEqual(true, event instanceof DragEvent);
|
||||
testing.expectEqual(true, event instanceof MouseEvent);
|
||||
testing.expectEqual(true, event instanceof UIEvent);
|
||||
testing.expectEqual(true, event instanceof Event);
|
||||
// no dataTransfer supplied -> null
|
||||
testing.expectEqual(null, event.dataTransfer);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=mouse_inherited>
|
||||
{
|
||||
const event = new DragEvent('dragover', { clientX: 5, clientY: 9, button: 0 });
|
||||
testing.expectEqual(5, event.clientX);
|
||||
testing.expectEqual(9, event.clientY);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=carries_data_transfer>
|
||||
{
|
||||
const dt = new DataTransfer();
|
||||
dt.items.add(new File(["x"], "drop.txt", { type: "text/plain" }));
|
||||
|
||||
const event = new DragEvent('drop', { dataTransfer: dt, bubbles: true });
|
||||
testing.expectEqual(dt, event.dataTransfer);
|
||||
testing.expectEqual(1, event.dataTransfer.files.length);
|
||||
testing.expectEqual("drop.txt", event.dataTransfer.files[0].name);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=dispatched_drop>
|
||||
{
|
||||
const dt = new DataTransfer();
|
||||
dt.items.add(new File(["x"], "drop.txt", { type: "text/plain" }));
|
||||
|
||||
const el = document.createElement('div');
|
||||
let seen = -1;
|
||||
el.addEventListener('drop', function (e) {
|
||||
testing.expectEqual(true, e instanceof DragEvent);
|
||||
seen = e.dataTransfer.files.length;
|
||||
});
|
||||
el.dispatchEvent(new DragEvent('drop', { dataTransfer: dt, bubbles: true }));
|
||||
testing.expectEqual(1, seen);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=refcount_balance>
|
||||
{
|
||||
// A DragEvent holds a ref on its DataTransfer so the store outlives the event
|
||||
// even if the JS wrapper is dropped first. Build many event/store pairs and
|
||||
// retain none: at teardown every acquire must be matched by a release, or the
|
||||
// leak-checking test runner fails. Encodes the acquire/release contract that a
|
||||
// plain "read the files" assertion wouldn't exercise.
|
||||
let last = 0;
|
||||
for (let i = 0; i < 25; i++) {
|
||||
const dt = new DataTransfer();
|
||||
dt.items.add(new File(["payload"], "f" + i + ".txt", { type: "text/plain" }));
|
||||
const ev = new DragEvent('drop', { dataTransfer: dt });
|
||||
last = ev.dataTransfer.files.length;
|
||||
}
|
||||
testing.expectEqual(1, last);
|
||||
}
|
||||
</script>
|
||||
134
src/browser/webapi/event/DragEvent.zig
Normal file
134
src/browser/webapi/event/DragEvent.zig
Normal file
@@ -0,0 +1,134 @@
|
||||
// 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 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", .{});
|
||||
}
|
||||
@@ -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, .{});
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user