apprt/gtk-ng: surface drag and drop

This commit is contained in:
Mitchell Hashimoto
2025-07-23 11:05:16 -07:00
parent 9ea87ec096
commit 550d29d930
4 changed files with 261 additions and 25 deletions

View File

@ -284,6 +284,9 @@ pub const Surface = extern struct {
/// True when we have a precision scroll in progress
precision_scroll: bool = false,
// Template binds
drop_target: *gtk.DropTarget,
pub var offset: c_int = 0;
};
@ -793,6 +796,16 @@ pub const Surface = extern struct {
priv.im_composing = false;
priv.im_len = 0;
// Set up to handle items being dropped on our surface. Files can be dropped
// from Nautilus and strings can be dropped from many programs. The order
// of these types matter.
var drop_target_types = [_]gobject.Type{
gdk.FileList.getGObjectType(),
gio.File.getGObjectType(),
gobject.ext.types.string,
};
priv.drop_target.setGtypes(&drop_target_types, drop_target_types.len);
// Initialize our GLArea. We only set the values we can't set
// in our blueprint file.
const gl_area = priv.gl_area;
@ -1002,6 +1015,100 @@ pub const Surface = extern struct {
//---------------------------------------------------------------
// Signal Handlers
fn dtDrop(
_: *gtk.DropTarget,
value: *gobject.Value,
_: f64,
_: f64,
self: *Self,
) callconv(.c) c_int {
const alloc = Application.default().allocator();
if (g_value_holds(
value,
gdk.FileList.getGObjectType(),
)) {
var data = std.ArrayList(u8).init(alloc);
defer data.deinit();
var shell_escape_writer: internal_os.ShellEscapeWriter(std.ArrayList(u8).Writer) = .{
.child_writer = data.writer(),
};
const writer = shell_escape_writer.writer();
const list: ?*glib.SList = list: {
const unboxed = value.getBoxed() orelse return 0;
const fl: *gdk.FileList = @ptrCast(@alignCast(unboxed));
break :list fl.getFiles();
};
defer if (list) |v| v.free();
{
var current: ?*glib.SList = list;
while (current) |item| : (current = item.f_next) {
const file: *gio.File = @ptrCast(@alignCast(item.f_data orelse continue));
const path = file.getPath() orelse continue;
const slice = std.mem.span(path);
defer glib.free(path);
writer.writeAll(slice) catch |err| {
log.err("unable to write path to buffer: {}", .{err});
continue;
};
writer.writeAll("\n") catch |err| {
log.err("unable to write to buffer: {}", .{err});
continue;
};
}
}
const string = data.toOwnedSliceSentinel(0) catch |err| {
log.err("unable to convert to a slice: {}", .{err});
return 0;
};
defer alloc.free(string);
Clipboard.paste(self, string);
return 1;
}
if (g_value_holds(value, gio.File.getGObjectType())) {
const object = value.getObject() orelse return 0;
const file = gobject.ext.cast(gio.File, object) orelse return 0;
const path = file.getPath() orelse return 0;
var data = std.ArrayList(u8).init(alloc);
defer data.deinit();
var shell_escape_writer: internal_os.ShellEscapeWriter(std.ArrayList(u8).Writer) = .{
.child_writer = data.writer(),
};
const writer = shell_escape_writer.writer();
writer.writeAll(std.mem.span(path)) catch |err| {
log.err("unable to write path to buffer: {}", .{err});
return 0;
};
writer.writeAll("\n") catch |err| {
log.err("unable to write to buffer: {}", .{err});
return 0;
};
const string = data.toOwnedSliceSentinel(0) catch |err| {
log.err("unable to convert to a slice: {}", .{err});
return 0;
};
defer alloc.free(string);
return 1;
}
if (g_value_holds(value, gobject.ext.types.string)) {
if (value.getString()) |string| {
Clipboard.paste(self, std.mem.span(string));
}
return 1;
}
return 1;
}
fn ecKeyPressed(
ec_key: *gtk.EventControllerKey,
keyval: c_uint,
@ -1669,6 +1776,7 @@ pub const Surface = extern struct {
class.bindTemplateChildPrivate("url_left", .{});
class.bindTemplateChildPrivate("url_right", .{});
class.bindTemplateChildPrivate("resize_overlay", .{});
class.bindTemplateChildPrivate("drop_target", .{});
class.bindTemplateChildPrivate("im_context", .{});
// Template Callbacks
@ -1683,6 +1791,7 @@ pub const Surface = extern struct {
class.bindTemplateCallback("scroll", &ecMouseScroll);
class.bindTemplateCallback("scroll_begin", &ecMouseScrollPrecisionBegin);
class.bindTemplateCallback("scroll_end", &ecMouseScrollPrecisionEnd);
class.bindTemplateCallback("drop", &dtDrop);
class.bindTemplateCallback("gl_realize", &glareaRealize);
class.bindTemplateCallback("gl_unrealize", &glareaUnrealize);
class.bindTemplateCallback("gl_render", &glareaRender);
@ -1758,17 +1867,6 @@ fn translateMouseButton(button: c_uint) input.MouseButton {
/// A namespace for our clipboard-related functions so Surface isn't SO large.
const Clipboard = struct {
/// Get the specific type of clipboard for a widget.
pub fn get(
widget: *gtk.Widget,
clipboard: apprt.Clipboard,
) ?*gdk.Clipboard {
return switch (clipboard) {
.standard => widget.getClipboard(),
.selection, .primary => widget.getPrimaryClipboard(),
};
}
/// Set the clipboard contents.
pub fn set(
self: *Surface,
@ -1837,6 +1935,52 @@ const Clipboard = struct {
);
}
/// Paste explicit text directly into the surface, regardless of the
/// actual clipboard contents.
pub fn paste(
self: *Surface,
text: [:0]const u8,
) void {
if (text.len == 0) return;
const surface = self.private().core_surface orelse return;
surface.completeClipboardRequest(
.paste,
text,
false,
) catch |err| switch (err) {
error.UnsafePaste,
error.UnauthorizedPaste,
=> {
showClipboardConfirmation(
self,
.paste,
text,
);
return;
},
else => {
log.warn(
"failed to complete clipboard request err={}",
.{err},
);
return;
},
};
}
/// Get the specific type of clipboard for a widget.
fn get(
widget: *gtk.Widget,
clipboard: apprt.Clipboard,
) ?*gdk.Clipboard {
return switch (clipboard) {
.standard => widget.getClipboard(),
.selection, .primary => widget.getPrimaryClipboard(),
};
}
fn showClipboardConfirmation(
self: *Surface,
req: apprt.ClipboardRequest,
@ -2007,3 +2151,13 @@ const Clipboard = struct {
state: apprt.ClipboardRequest,
};
};
/// Check a GValue to see what's type its wrapping. This is equivalent to GTK's
/// `G_VALUE_HOLDS` macro but Zig's C translator does not like it.
fn g_value_holds(value_: ?*gobject.Value, g_type: gobject.Type) bool {
if (value_) |value| {
if (value.f_g_type == g_type) return true;
return gobject.typeCheckValueHolds(value, g_type) != 0;
}
return false;
}

View File

@ -89,6 +89,11 @@ template $GhosttySurface: Adw.Bin {
released => $mouse_up();
button: 0;
}
DropTarget drop_target {
drop => $drop();
actions: copy;
}
}
IMMulticontext im_context {

View File

@ -2232,15 +2232,22 @@ fn gtkDrop(
};
const writer = shell_escape_writer.writer();
const list: ?*glib.SList = list: {
const unboxed = value.getBoxed() orelse return 0;
const fl: *gdk.FileList = @ptrCast(@alignCast(unboxed));
var list: ?*glib.SList = fl.getFiles();
break :list fl.getFiles();
};
defer if (list) |v| v.free();
while (list) |item| : (list = item.f_next) {
{
var current: ?*glib.SList = list;
while (current) |item| : (current = item.f_next) {
const file: *gio.File = @ptrCast(@alignCast(item.f_data orelse continue));
const path = file.getPath() orelse continue;
const slice = std.mem.span(path);
defer glib.free(path);
writer.writeAll(std.mem.span(path)) catch |err| {
writer.writeAll(slice) catch |err| {
log.err("unable to write path to buffer: {}", .{err});
continue;
};
@ -2249,6 +2256,7 @@ fn gtkDrop(
continue;
};
}
}
const string = data.toOwnedSliceSentinel(0) catch |err| {
log.err("unable to convert to a slice: {}", .{err});

View File

@ -13,6 +13,75 @@
# You must gracefully exit Ghostty (do not SIGINT) by closing all windows
# and quitting. Otherwise, we leave a number of GTK resources around.
{
GDK Drag and Drop Leaks Data
Memcheck:Leak
match-leak-kinds: definite
fun:malloc
fun:g_malloc
fun:g_slice_alloc
fun:g_slice_alloc0
fun:g_error_allocate
fun:g_error_new_literal
fun:g_set_error_literal
fun:g_output_stream_set_pending
fun:g_output_stream_write
fun:portal_file_deserializer_finish
fun:g_task_return_now
fun:g_task_return
fun:async_ready_splice_callback_wrapper
fun:g_task_return_now
fun:g_task_return
fun:real_splice_async_complete_cb
fun:async_ready_close_callback_wrapper
fun:g_task_return_now
fun:complete_in_idle_cb
fun:g_idle_dispatch
fun:g_main_context_dispatch_unlocked
fun:g_main_context_iterate_unlocked.isra.0
fun:g_main_context_iteration
fun:apprt.gtk-ng.class.application.Application.run
fun:apprt.gtk-ng.App.run
fun:main_ghostty.main
fun:callMain
fun:callMainWithArgs
fun:main
}
{
GDK Drag and Drop Leaks Task
Memcheck:Leak
match-leak-kinds: definite
fun:calloc
fun:g_malloc0
fun:g_type_create_instance
fun:g_object_new_internal.part.0
fun:g_object_new_with_properties
fun:g_object_new
fun:g_task_new
fun:file_transfer_portal_retrieve_files
fun:portal_file_deserializer_finish
fun:g_task_return_now
fun:g_task_return
fun:async_ready_splice_callback_wrapper
fun:g_task_return_now
fun:g_task_return
fun:real_splice_async_complete_cb
fun:async_ready_close_callback_wrapper
fun:g_task_return_now
fun:complete_in_idle_cb
fun:g_idle_dispatch
fun:g_main_context_dispatch_unlocked
fun:g_main_context_iterate_unlocked.isra.0
fun:g_main_context_iteration
fun:apprt.gtk-ng.class.application.Application.run
fun:apprt.gtk-ng.App.run
fun:main_ghostty.main
fun:callMain
fun:callMainWithArgs
fun:main
}
{
GSK Renderer GPU Stuff
Memcheck:Leak