mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-08-25 00:21:46 +00:00
gtk: implement drag-to-move for splits
One major todo is moving splits across different split trees (i.e. moving across tabs and windows), but that would involve a lot more logic. This MVP version works for now. GTK version of #10090 Closes #10224
This commit is contained in:
@@ -1028,6 +1028,38 @@ pub const Application = extern struct {
|
||||
\\}
|
||||
\\
|
||||
\\/*
|
||||
\\ * Drag and Drop Overlay
|
||||
\\ */
|
||||
\\.drop-overlay.drop-left {
|
||||
\\ background: linear-gradient(
|
||||
\\ to left,
|
||||
\\ transparent, 50%,
|
||||
\\ color-mix(in srgb, var(--accent-bg-color), transparent 80%) 50%
|
||||
\\ );
|
||||
\\}
|
||||
\\.drop-overlay.drop-right {
|
||||
\\ background: linear-gradient(
|
||||
\\ to right,
|
||||
\\ transparent, 50%,
|
||||
\\ color-mix(in srgb, var(--accent-bg-color), transparent 80%) 50%
|
||||
\\ );
|
||||
\\}
|
||||
\\.drop-overlay.drop-top {
|
||||
\\ background: linear-gradient(
|
||||
\\ to top,
|
||||
\\ transparent, 50%,
|
||||
\\ color-mix(in srgb, var(--accent-bg-color), transparent 80%) 50%
|
||||
\\ );
|
||||
\\}
|
||||
\\.drop-overlay.drop-bottom {
|
||||
\\ background: linear-gradient(
|
||||
\\ to bottom,
|
||||
\\ transparent, 50%,
|
||||
\\ color-mix(in srgb, var(--accent-bg-color), transparent 80%) 50%
|
||||
\\ );
|
||||
\\}
|
||||
\\
|
||||
\\/*
|
||||
\\ * Splits
|
||||
\\ */
|
||||
\\
|
||||
|
||||
@@ -387,6 +387,47 @@ pub const SplitTree = extern struct {
|
||||
return true;
|
||||
}
|
||||
|
||||
pub const MoveSplitError = Allocator.Error || error{
|
||||
SourceNotFound,
|
||||
TargetNotFound,
|
||||
};
|
||||
|
||||
/// Move the source split onto the target split in a given direction.
|
||||
/// The target split must be located within this tree.
|
||||
pub fn moveSplit(
|
||||
self: *Self,
|
||||
source: *Surface,
|
||||
target: *Surface,
|
||||
dir: Surface.Tree.Split.Direction,
|
||||
) MoveSplitError!void {
|
||||
const alloc = Application.default().allocator();
|
||||
const tree = self.getTree() orelse return;
|
||||
|
||||
const source_handle = tree.locate(source) orelse return error.SourceNotFound;
|
||||
|
||||
// This really shouldn't fail, but just in case
|
||||
const target_handle = tree.locate(target) orelse return error.TargetNotFound;
|
||||
|
||||
// TODO: is it perhaps possible to condense all of this
|
||||
// into one atomic operation?
|
||||
var branch = try Surface.Tree.init(alloc, source);
|
||||
defer branch.deinit();
|
||||
|
||||
var after_split = try tree.split(
|
||||
alloc,
|
||||
target_handle,
|
||||
dir,
|
||||
0.5,
|
||||
&branch,
|
||||
);
|
||||
defer after_split.deinit();
|
||||
|
||||
var after_remove = try after_split.remove(alloc, source_handle);
|
||||
defer after_remove.deinit();
|
||||
|
||||
self.setTree(&after_remove);
|
||||
}
|
||||
|
||||
fn disconnectSurfaceHandlers(self: *Self) void {
|
||||
const tree = self.getTree() orelse return;
|
||||
var it = tree.iterator();
|
||||
@@ -705,17 +746,8 @@ pub const SplitTree = extern struct {
|
||||
|
||||
// Find the surface in the tree to verify this is valid and
|
||||
// set our pending close handle.
|
||||
priv.pending_close = handle: {
|
||||
const tree = self.getTree() orelse return;
|
||||
var it = tree.iterator();
|
||||
while (it.next()) |entry| {
|
||||
if (entry.view == surface) {
|
||||
break :handle entry.handle;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
};
|
||||
const tree = self.getTree() orelse return;
|
||||
priv.pending_close = tree.locate(surface) orelse return;
|
||||
|
||||
// If we don't need to confirm then just close immediately.
|
||||
if (!core.needsConfirmQuit()) {
|
||||
|
||||
@@ -34,6 +34,7 @@ const ClipboardConfirmationDialog = @import("clipboard_confirmation_dialog.zig")
|
||||
const TitleDialog = @import("title_dialog.zig").TitleDialog;
|
||||
const Window = @import("window.zig").Window;
|
||||
const InspectorWindow = @import("inspector_window.zig").InspectorWindow;
|
||||
const SplitTree = @import("split_tree.zig").SplitTree;
|
||||
const i18n = @import("../../../os/i18n.zig");
|
||||
const media = @import("../media.zig");
|
||||
const global = @import("../../../global.zig");
|
||||
@@ -715,6 +716,9 @@ pub const Surface = extern struct {
|
||||
child_exited_overlay: *ChildExited,
|
||||
context_menu: *gtk.PopoverMenu,
|
||||
drop_target: *gtk.DropTarget,
|
||||
surface_drop_target: *gtk.DropTarget,
|
||||
drag_handle: *gtk.Widget,
|
||||
drop_overlay: *gtk.Widget,
|
||||
progress_bar_overlay: *gtk.ProgressBar,
|
||||
error_page: *adw.StatusPage,
|
||||
terminal_page: *gtk.Overlay,
|
||||
@@ -1830,8 +1834,19 @@ pub const Surface = extern struct {
|
||||
};
|
||||
priv.drop_target.setGtypes(&drop_target_types, drop_target_types.len);
|
||||
|
||||
// Also have to set up the surface drop target to accept other surfaces
|
||||
// (in particular, their surface IDs)
|
||||
var surface_drop_target_types = [_]gobject.Type{
|
||||
gobject.ext.types.uint64,
|
||||
};
|
||||
priv.surface_drop_target.setGtypes(
|
||||
&surface_drop_target_types,
|
||||
surface_drop_target_types.len,
|
||||
);
|
||||
|
||||
// Setup properties we can't set from our Blueprint file.
|
||||
self.as(gtk.Widget).setCursorFromName("text");
|
||||
priv.drag_handle.setCursorFromName("grab");
|
||||
|
||||
// Initialize our config
|
||||
self.propConfig(undefined, null);
|
||||
@@ -3675,6 +3690,176 @@ pub const Surface = extern struct {
|
||||
};
|
||||
}
|
||||
|
||||
fn surfaceDragPrepare(
|
||||
src: *gtk.DragSource,
|
||||
x: f64,
|
||||
y: f64,
|
||||
self: *Self,
|
||||
) callconv(.c) *gdk.ContentProvider {
|
||||
// TODO: Use a static content provider once we make `self.core()`
|
||||
// available immediately when constructing the surface widget
|
||||
_ = src;
|
||||
_ = x;
|
||||
_ = y;
|
||||
var val = gobject.ext.Value.newFrom(self.core().?.id);
|
||||
return gdk.ContentProvider.newForValue(&val);
|
||||
}
|
||||
|
||||
fn surfaceDragBegin(
|
||||
src: *gtk.DragSource,
|
||||
_: *gdk.Drag,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
// The scale of the preview
|
||||
const preview_scale: f32 = 0.2;
|
||||
|
||||
// Snapshot the entire surface widget as the icon preview
|
||||
const paintable = gtk.WidgetPaintable.new(self.as(gtk.Widget));
|
||||
defer paintable.unref();
|
||||
|
||||
// Center the preview
|
||||
const width = self.as(gtk.Widget).getWidth();
|
||||
const height = self.as(gtk.Widget).getHeight();
|
||||
const mid_x = @as(f32, @floatFromInt(width)) / 2;
|
||||
const mid_y = @as(f32, @floatFromInt(height)) / 2;
|
||||
|
||||
// Create a snapshot to render the scaled paintable
|
||||
const snap = gtk.Snapshot.new();
|
||||
snap.scale(preview_scale, preview_scale);
|
||||
paintable.as(gdk.Paintable).snapshot(
|
||||
snap.as(gdk.Snapshot),
|
||||
@floatFromInt(width),
|
||||
@floatFromInt(height),
|
||||
);
|
||||
|
||||
if (snap.freeToPaintable(null)) |scaled| {
|
||||
defer scaled.unref();
|
||||
src.setIcon(
|
||||
scaled,
|
||||
@intFromFloat(mid_x * preview_scale),
|
||||
@intFromFloat(mid_y * preview_scale),
|
||||
);
|
||||
} else {
|
||||
// The scaling process somehow failed.
|
||||
// Use the original paintable as a fail-safe
|
||||
log.warn("preview scaling failed - falling back to original paintable", .{});
|
||||
src.setIcon(
|
||||
paintable.as(gdk.Paintable),
|
||||
@intFromFloat(mid_x),
|
||||
@intFromFloat(mid_y),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn surfaceDrop(
|
||||
_: *gtk.DropTarget,
|
||||
v: *const gobject.Value,
|
||||
x: f64,
|
||||
y: f64,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
const dropped_id = v.getUint64();
|
||||
const dropped = self.core().?.app.findSurfaceByID(dropped_id) orelse return;
|
||||
const from = dropped.rt_surface.gobj();
|
||||
|
||||
// TODO: Find a better way to access the split tree from here
|
||||
const st = ext.getAncestor(
|
||||
SplitTree,
|
||||
self.as(gtk.Widget),
|
||||
) orelse {
|
||||
log.warn("surface is not placed in a split tree", .{});
|
||||
return;
|
||||
};
|
||||
|
||||
const dir = self.calcDropDirection(x, y);
|
||||
|
||||
st.moveSplit(from, self, dir) catch |err| {
|
||||
switch (err) {
|
||||
error.OutOfMemory => log.warn("out of memory", .{}),
|
||||
// FIXME: implement this!
|
||||
error.SourceNotFound => log.debug(
|
||||
"cross-tree drag-and-drop not yet supported",
|
||||
.{},
|
||||
),
|
||||
error.TargetNotFound => log.warn(
|
||||
"can't seem to find surface in tree - this shouldn't happen!",
|
||||
.{},
|
||||
),
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
// Clean up overlay state
|
||||
self.setDropOverlayDirection(null);
|
||||
}
|
||||
|
||||
fn surfaceDropLeave(
|
||||
_: *gtk.DropTarget,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
// Hide overlay
|
||||
self.setDropOverlayDirection(null);
|
||||
}
|
||||
|
||||
fn surfaceDropMotion(
|
||||
_: *gtk.DropTarget,
|
||||
x: f64,
|
||||
y: f64,
|
||||
self: *Self,
|
||||
) callconv(.c) gdk.DragAction {
|
||||
// Recalculate the drop region
|
||||
const dir = self.calcDropDirection(x, y);
|
||||
self.setDropOverlayDirection(dir);
|
||||
return .{ .move = true };
|
||||
}
|
||||
|
||||
fn propDropValue(
|
||||
tgt: *gtk.DropTarget,
|
||||
_: *gobject.ParamSpec,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
// Reject the drop if we're dropping a surface onto itself.
|
||||
// Note that we cannot implement this via the `accept` signal,
|
||||
// since the decision of whether to accept or deny a drop is dependent
|
||||
// on the payload (i.e. the surface being dropped). This is
|
||||
// well-documented in GTK docs.
|
||||
|
||||
const core_surface = self.core() orelse return;
|
||||
const value = tgt.getValue() orelse return;
|
||||
const surface_id = value.getUint64();
|
||||
if (core_surface.id == surface_id) tgt.reject();
|
||||
}
|
||||
|
||||
fn setDropOverlayDirection(self: *Self, dir: ?Tree.Split.Direction) void {
|
||||
const priv = self.private();
|
||||
inline for (&.{ "drop-top", "drop-left", "drop-right", "drop-bottom" }) |c| {
|
||||
priv.drop_overlay.removeCssClass(c);
|
||||
}
|
||||
|
||||
if (dir) |d| priv.drop_overlay.addCssClass(switch (d) {
|
||||
.up => "drop-top",
|
||||
.left => "drop-left",
|
||||
.right => "drop-right",
|
||||
.down => "drop-bottom",
|
||||
});
|
||||
}
|
||||
|
||||
fn calcDropDirection(self: *Self, x: f64, y: f64) Tree.Split.Direction {
|
||||
const width: f64 = @floatFromInt(self.as(gtk.Widget).getWidth());
|
||||
const height: f64 = @floatFromInt(self.as(gtk.Widget).getHeight());
|
||||
|
||||
const l_dist = x / width;
|
||||
const t_dist = y / height;
|
||||
const r_dist = 1 - l_dist;
|
||||
const b_dist = 1 - t_dist;
|
||||
const min = @min(l_dist, t_dist, r_dist, b_dist);
|
||||
|
||||
if (min == l_dist) return .left;
|
||||
if (min == r_dist) return .right;
|
||||
if (min == t_dist) return .up;
|
||||
return .down;
|
||||
}
|
||||
|
||||
const C = Common(Self, Private);
|
||||
pub const as = C.as;
|
||||
pub const ref = C.ref;
|
||||
@@ -3714,6 +3899,9 @@ pub const Surface = extern struct {
|
||||
class.bindTemplateChildPrivate("key_state_overlay", .{});
|
||||
class.bindTemplateChildPrivate("terminal_page", .{});
|
||||
class.bindTemplateChildPrivate("drop_target", .{});
|
||||
class.bindTemplateChildPrivate("surface_drop_target", .{});
|
||||
class.bindTemplateChildPrivate("drag_handle", .{});
|
||||
class.bindTemplateChildPrivate("drop_overlay", .{});
|
||||
class.bindTemplateChildPrivate("im_context", .{});
|
||||
|
||||
// Template Callbacks
|
||||
@@ -3756,6 +3944,12 @@ pub const Surface = extern struct {
|
||||
class.bindTemplateCallback("search_changed", &searchChanged);
|
||||
class.bindTemplateCallback("search_next_match", &searchNextMatch);
|
||||
class.bindTemplateCallback("search_previous_match", &searchPreviousMatch);
|
||||
class.bindTemplateCallback("surface_drag_prepare", &surfaceDragPrepare);
|
||||
class.bindTemplateCallback("surface_drag_begin", &surfaceDragBegin);
|
||||
class.bindTemplateCallback("surface_drop", &surfaceDrop);
|
||||
class.bindTemplateCallback("surface_drop_leave", &surfaceDropLeave);
|
||||
class.bindTemplateCallback("surface_drop_motion", &surfaceDropMotion);
|
||||
class.bindTemplateCallback("notify_drop_value", &propDropValue);
|
||||
|
||||
// Properties
|
||||
gobject.ext.registerProperties(class, &.{
|
||||
|
||||
@@ -127,7 +127,7 @@ label.resize-overlay {
|
||||
}
|
||||
|
||||
.surface .bell-overlay {
|
||||
border-color: rgba(58, 148, 74, 0.5);
|
||||
border-color: rgba(53, 132, 228, 0.5);
|
||||
/* after GTK 4.16 is a requirement, switch to the following: */
|
||||
/* background-color: color-mix(in srgb, var(--accent-color), transparent 50%); */
|
||||
border-width: 3px;
|
||||
@@ -144,6 +144,58 @@ label.resize-overlay {
|
||||
outline-style: solid;
|
||||
outline-width: 1px;
|
||||
}
|
||||
/*
|
||||
* Drag and drop overlay
|
||||
*/
|
||||
.drag-handle {
|
||||
background-color: rgba(0, 0, 0, 0.15);
|
||||
opacity: 0;
|
||||
transition: 0.1s opacity;
|
||||
}
|
||||
.drag-handle:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
.drop-overlay.drop-left {
|
||||
/* after GTK 4.16 is a requirement, switch to the following: */
|
||||
/* background: linear-gradient(to left, transparent, 50%, color-mix(in srgb, var(--accent-color), transparent 50%) 50%); */
|
||||
background: linear-gradient(
|
||||
to left,
|
||||
transparent,
|
||||
50%,
|
||||
rgba(53, 132, 228, 0.2) 50%
|
||||
);
|
||||
}
|
||||
.drop-overlay.drop-right {
|
||||
/* after GTK 4.16 is a requirement, switch to the following: */
|
||||
/* background: linear-gradient(to right, transparent, 50%, color-mix(in srgb, var(--accent-color), transparent 50%) 50%); */
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
transparent,
|
||||
50%,
|
||||
rgba(53, 132, 228, 0.2) 50%
|
||||
);
|
||||
}
|
||||
.drop-overlay.drop-top {
|
||||
/* after GTK 4.16 is a requirement, switch to the following: */
|
||||
/* background: linear-gradient(to top, transparent, 50%, color-mix(in srgb, var(--accent-color), transparent 50%) 50%); */
|
||||
background: linear-gradient(
|
||||
to top,
|
||||
transparent,
|
||||
50%,
|
||||
rgba(53, 132, 228, 0.2) 50%
|
||||
);
|
||||
}
|
||||
.drop-overlay.drop-bottom {
|
||||
/* after GTK 4.16 is a requirement, switch to the following: */
|
||||
/* background: linear-gradient(to bottom, transparent, 50%, color-mix(in srgb, var(--accent-color), transparent 50%) 50%); */
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
transparent,
|
||||
50%,
|
||||
rgba(53, 132, 228, 0.2) 50%
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* Command Palette
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Gtk 4.0;
|
||||
using Gdk 4.0;
|
||||
using Adw 1;
|
||||
|
||||
Adw.StatusPage error_page {
|
||||
@@ -221,10 +222,56 @@ Overlay terminal_page {
|
||||
}
|
||||
}
|
||||
|
||||
[overlay]
|
||||
CenterBox drag_handle {
|
||||
halign: fill;
|
||||
valign: start;
|
||||
|
||||
styles [
|
||||
"drag-handle",
|
||||
]
|
||||
|
||||
[center]
|
||||
Image {
|
||||
icon-name: "view-more-horizontal-symbolic";
|
||||
}
|
||||
|
||||
DragSource surface_drag_source {
|
||||
actions: move;
|
||||
prepare => $surface_drag_prepare();
|
||||
drag-begin => $surface_drag_begin();
|
||||
}
|
||||
}
|
||||
|
||||
[overlay]
|
||||
DrawingArea drop_overlay {
|
||||
halign: fill;
|
||||
valign: fill;
|
||||
can-focus: false;
|
||||
can-target: false;
|
||||
focusable: false;
|
||||
|
||||
styles [
|
||||
"drop-overlay",
|
||||
]
|
||||
}
|
||||
|
||||
DropTarget drop_target {
|
||||
drop => $drop();
|
||||
actions: copy | move;
|
||||
}
|
||||
|
||||
DropTarget surface_drop_target {
|
||||
// Enabling preload is required since that lets us know
|
||||
// which surface is currently being dragged, and to turn off
|
||||
// the drop overlay if the source and target are the same
|
||||
preload: true;
|
||||
leave => $surface_drop_leave();
|
||||
motion => $surface_drop_motion();
|
||||
drop => $surface_drop();
|
||||
notify::value => $notify_drop_value();
|
||||
actions: move;
|
||||
}
|
||||
}
|
||||
|
||||
template $GhosttySurface: Adw.Bin {
|
||||
|
||||
@@ -183,6 +183,18 @@ pub fn SplitTree(comptime V: type) type {
|
||||
};
|
||||
}
|
||||
|
||||
/// Find the handle within this tree that corresponds
|
||||
/// to the given view. Uses pointer equality.
|
||||
pub fn locate(self: *const Self, view: *const View) ?Node.Handle {
|
||||
var it = self.iterator();
|
||||
while (it.next()) |entry| {
|
||||
if (entry.view == view) {
|
||||
return entry.handle;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// An iterator over all the views in the tree.
|
||||
pub fn iterator(
|
||||
self: *const Self,
|
||||
|
||||
Reference in New Issue
Block a user