mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-09-05 19:08:17 +00:00
apprt/gtk-ng: resize_split
action (#8215)
Ports the resize split action (tied to the `resize_split` binding action). This also includes fixes for splits that are exactly `0` or `1` ratio width (full width either direction). This would previously cause crashes.
This commit is contained in:
@@ -597,6 +597,8 @@ pub const Application = extern struct {
|
||||
|
||||
.render => Action.render(target),
|
||||
|
||||
.resize_split => return Action.resizeSplit(target, value),
|
||||
|
||||
.ring_bell => Action.ringBell(target),
|
||||
|
||||
.set_title => Action.setTitle(target, value),
|
||||
@@ -618,7 +620,6 @@ pub const Application = extern struct {
|
||||
.prompt_title,
|
||||
.inspector,
|
||||
// TODO: splits
|
||||
.resize_split,
|
||||
.toggle_split_zoom,
|
||||
=> {
|
||||
log.warn("unimplemented action={}", .{action});
|
||||
@@ -2003,6 +2004,43 @@ const Action = struct {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resizeSplit(
|
||||
target: apprt.Target,
|
||||
value: apprt.action.ResizeSplit,
|
||||
) bool {
|
||||
switch (target) {
|
||||
.app => {
|
||||
log.warn("resize_split to app is unexpected", .{});
|
||||
return false;
|
||||
},
|
||||
.surface => |core| {
|
||||
const surface = core.rt_surface.surface;
|
||||
const tree = ext.getAncestor(
|
||||
SplitTree,
|
||||
surface.as(gtk.Widget),
|
||||
) orelse {
|
||||
log.warn("surface is not in a split tree, ignoring goto_split", .{});
|
||||
return false;
|
||||
};
|
||||
|
||||
return tree.resize(
|
||||
switch (value.direction) {
|
||||
.up => .up,
|
||||
.down => .down,
|
||||
.left => .left,
|
||||
.right => .right,
|
||||
},
|
||||
value.amount,
|
||||
) catch |err| switch (err) {
|
||||
error.OutOfMemory => {
|
||||
log.warn("unable to resize split, out of memory", .{});
|
||||
return false;
|
||||
},
|
||||
};
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ringBell(target: apprt.Target) void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
|
@@ -261,6 +261,51 @@ pub const SplitTree = extern struct {
|
||||
self.setTree(&new_tree);
|
||||
}
|
||||
|
||||
pub fn resize(
|
||||
self: *Self,
|
||||
direction: Surface.Tree.Split.Direction,
|
||||
amount: u16,
|
||||
) Allocator.Error!bool {
|
||||
// Avoid useless work
|
||||
if (amount == 0) return false;
|
||||
|
||||
const old_tree = self.getTree() orelse return false;
|
||||
const active = self.getActiveSurfaceHandle() orelse return false;
|
||||
|
||||
// Get all our dimensions we're going to need to turn our
|
||||
// amount into a percentage.
|
||||
const priv = self.private();
|
||||
const width = priv.tree_bin.as(gtk.Widget).getWidth();
|
||||
const height = priv.tree_bin.as(gtk.Widget).getHeight();
|
||||
if (width == 0 or height == 0) return false;
|
||||
const width_f64: f64 = @floatFromInt(width);
|
||||
const height_f64: f64 = @floatFromInt(height);
|
||||
const amount_f64: f64 = @floatFromInt(amount);
|
||||
|
||||
// Get our ratio and use positive/neg for directions.
|
||||
const ratio: f64 = switch (direction) {
|
||||
.right => amount_f64 / width_f64,
|
||||
.left => -(amount_f64 / width_f64),
|
||||
.down => amount_f64 / height_f64,
|
||||
.up => -(amount_f64 / height_f64),
|
||||
};
|
||||
|
||||
const layout: Surface.Tree.Split.Layout = switch (direction) {
|
||||
.left, .right => .horizontal,
|
||||
.up, .down => .vertical,
|
||||
};
|
||||
|
||||
var new_tree = try old_tree.resize(
|
||||
Application.default().allocator(),
|
||||
active,
|
||||
layout,
|
||||
@floatCast(ratio),
|
||||
);
|
||||
defer new_tree.deinit();
|
||||
self.setTree(&new_tree);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Move focus from the currently focused surface to the given
|
||||
/// direction. Returns true if focus switched to a new surface.
|
||||
pub fn goto(self: *Self, to: Surface.Tree.Goto) bool {
|
||||
|
@@ -698,6 +698,114 @@ pub fn SplitTree(comptime V: type) type {
|
||||
};
|
||||
}
|
||||
|
||||
/// Resize the nearest split matching the layout by the given ratio.
|
||||
/// Positive is right and down.
|
||||
///
|
||||
/// The ratio is a value between 0 and 1 representing the percentage
|
||||
/// to move the divider in the given direction. The percentage is
|
||||
/// of the entire grid size, not just the specific split size.
|
||||
/// We use the entire grid size because that's what Ghostty's
|
||||
/// `resize_split` keybind does, because it maps to a general human
|
||||
/// understanding of moving a split relative to the entire window
|
||||
/// (generally).
|
||||
///
|
||||
/// For example, a ratio of 0.1 and a layout of `vertical` will find
|
||||
/// the nearest vertical split and move the divider down by 10% of
|
||||
/// the total grid height.
|
||||
///
|
||||
/// If no matching split is found, this does nothing, but will always
|
||||
/// still return a cloned tree.
|
||||
pub fn resize(
|
||||
self: *const Self,
|
||||
gpa: Allocator,
|
||||
from: Node.Handle,
|
||||
layout: Split.Layout,
|
||||
ratio: f16,
|
||||
) Allocator.Error!Self {
|
||||
assert(ratio >= 0 and ratio <= 1);
|
||||
assert(!std.math.isNan(ratio));
|
||||
assert(!std.math.isInf(ratio));
|
||||
|
||||
// Fast path empty trees.
|
||||
if (self.isEmpty()) return .empty;
|
||||
|
||||
// From this point forward worst case we return a clone.
|
||||
var result = try self.clone(gpa);
|
||||
errdefer result.deinit();
|
||||
|
||||
// Find our nearest parent split node matching the layout.
|
||||
const parent_handle = switch (self.findParentSplit(
|
||||
layout,
|
||||
from,
|
||||
0,
|
||||
)) {
|
||||
.deadend, .backtrack => return result,
|
||||
.result => |v| v,
|
||||
};
|
||||
|
||||
// Get our spatial layout, because we need the dimensions of this
|
||||
// split with regards to the entire grid.
|
||||
var sp = try result.spatial(gpa);
|
||||
defer sp.deinit(gpa);
|
||||
|
||||
// Get the ratio of the split relative to the full grid.
|
||||
const full_ratio = full_ratio: {
|
||||
// Our scale is the amount we need to multiply our individual
|
||||
// ratio by to get the full ratio. Its actually a ratio on its
|
||||
// own but I'm trying to avoid that word: its the ratio of
|
||||
// our spatial width/height to the total.
|
||||
const scale = switch (layout) {
|
||||
.horizontal => sp.slots[parent_handle].width / sp.slots[0].width,
|
||||
.vertical => sp.slots[parent_handle].height / sp.slots[0].height,
|
||||
};
|
||||
|
||||
const current = result.nodes[parent_handle].split.ratio;
|
||||
break :full_ratio current * scale;
|
||||
};
|
||||
|
||||
// Set the final new ratio, clamping it to [0, 1]
|
||||
result.resizeInPlace(
|
||||
parent_handle,
|
||||
@min(@max(full_ratio + ratio, 0), 1),
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
fn findParentSplit(
|
||||
self: *const Self,
|
||||
layout: Split.Layout,
|
||||
from: Node.Handle,
|
||||
current: Node.Handle,
|
||||
) Backtrack {
|
||||
if (from == current) return .backtrack;
|
||||
return switch (self.nodes[current]) {
|
||||
.leaf => .deadend,
|
||||
.split => |s| switch (self.findParentSplit(
|
||||
layout,
|
||||
from,
|
||||
s.left,
|
||||
)) {
|
||||
.result => |v| .{ .result = v },
|
||||
.backtrack => if (s.layout == layout)
|
||||
.{ .result = current }
|
||||
else
|
||||
.backtrack,
|
||||
.deadend => switch (self.findParentSplit(
|
||||
layout,
|
||||
from,
|
||||
s.right,
|
||||
)) {
|
||||
.deadend => .deadend,
|
||||
.result => |v| .{ .result = v },
|
||||
.backtrack => if (s.layout == layout)
|
||||
.{ .result = current }
|
||||
else
|
||||
.backtrack,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// Spatial representation of the split tree. See spatial.
|
||||
pub const Spatial = struct {
|
||||
/// The slots of the spatial representation in the same order
|
||||
@@ -732,11 +840,11 @@ pub fn SplitTree(comptime V: type) type {
|
||||
/// Spatial representation of the split tree. This can be used to
|
||||
/// better understand the layout of the tree in a 2D space.
|
||||
///
|
||||
/// The bounds of the representation are always based on each split
|
||||
/// being exactly 1 unit wide and high. The x and y coordinates
|
||||
/// are offsets into that space. This means that the spatial
|
||||
/// representation is a normalized representation of the actual
|
||||
/// space.
|
||||
/// The bounds of the representation are always based on the total
|
||||
/// 2D space being 1x1. The x/y coordinates and width/height dimensions
|
||||
/// of each individual split and leaf are relative to this.
|
||||
/// This means that the spatial representation is a normalized
|
||||
/// representation of the actual space.
|
||||
///
|
||||
/// The top-left corner of the tree is always (0, 0).
|
||||
///
|
||||
@@ -766,6 +874,14 @@ pub fn SplitTree(comptime V: type) type {
|
||||
};
|
||||
self.fillSpatialSlots(slots, 0);
|
||||
|
||||
// Normalize the dimensions to 1x1 grid.
|
||||
for (slots) |*slot| {
|
||||
slot.x /= @floatFromInt(dim.width);
|
||||
slot.y /= @floatFromInt(dim.height);
|
||||
slot.width /= @floatFromInt(dim.width);
|
||||
slot.height /= @floatFromInt(dim.height);
|
||||
}
|
||||
|
||||
return .{ .slots = slots };
|
||||
}
|
||||
|
||||
@@ -774,7 +890,7 @@ pub fn SplitTree(comptime V: type) type {
|
||||
slots: []Spatial.Slot,
|
||||
current: Node.Handle,
|
||||
) void {
|
||||
assert(slots[current].width > 0 and slots[current].height > 0);
|
||||
assert(slots[current].width >= 0 and slots[current].height >= 0);
|
||||
|
||||
switch (self.nodes[current]) {
|
||||
// Leaf node, current slot is already filled by caller.
|
||||
@@ -926,8 +1042,8 @@ pub fn SplitTree(comptime V: type) type {
|
||||
var min_w: f16 = 1;
|
||||
var min_h: f16 = 1;
|
||||
for (sp.slots) |slot| {
|
||||
min_w = @min(min_w, slot.width);
|
||||
min_h = @min(min_h, slot.height);
|
||||
if (slot.width > 0) min_w = @min(min_w, slot.width);
|
||||
if (slot.height > 0) min_h = @min(min_h, slot.height);
|
||||
}
|
||||
|
||||
const ratio_w: f16 = 1 / min_w;
|
||||
@@ -1007,6 +1123,9 @@ pub fn SplitTree(comptime V: type) type {
|
||||
.split => continue,
|
||||
}
|
||||
|
||||
// If our width/height is zero then we skip this.
|
||||
if (slot.width == 0 or slot.height == 0) continue;
|
||||
|
||||
var x: usize = @intFromFloat(@floor(slot.x));
|
||||
var y: usize = @intFromFloat(@floor(slot.y));
|
||||
var width: usize = @intFromFloat(@max(@floor(slot.width), 1));
|
||||
@@ -1378,6 +1497,142 @@ test "SplitTree: split vertical" {
|
||||
);
|
||||
}
|
||||
|
||||
test "SplitTree: split horizontal with zero ratio" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var v1: TestTree.View = .{ .label = "A" };
|
||||
var t1: TestTree = try .init(alloc, &v1);
|
||||
defer t1.deinit();
|
||||
var v2: TestTree.View = .{ .label = "B" };
|
||||
var t2: TestTree = try .init(alloc, &v2);
|
||||
defer t2.deinit();
|
||||
|
||||
// A | B horizontal
|
||||
var splitAB = try t1.split(
|
||||
alloc,
|
||||
0, // at root
|
||||
.right, // split right
|
||||
0,
|
||||
&t2, // insert t2
|
||||
);
|
||||
defer splitAB.deinit();
|
||||
const split = splitAB;
|
||||
|
||||
{
|
||||
const str = try std.fmt.allocPrint(alloc, "{diagram}", .{split});
|
||||
defer alloc.free(str);
|
||||
try testing.expectEqualStrings(str,
|
||||
\\+---+
|
||||
\\| B |
|
||||
\\+---+
|
||||
\\
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
test "SplitTree: split vertical with zero ratio" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var v1: TestTree.View = .{ .label = "A" };
|
||||
var t1: TestTree = try .init(alloc, &v1);
|
||||
defer t1.deinit();
|
||||
var v2: TestTree.View = .{ .label = "B" };
|
||||
var t2: TestTree = try .init(alloc, &v2);
|
||||
defer t2.deinit();
|
||||
|
||||
// A | B horizontal
|
||||
var splitAB = try t1.split(
|
||||
alloc,
|
||||
0, // at root
|
||||
.down, // split right
|
||||
0,
|
||||
&t2, // insert t2
|
||||
);
|
||||
defer splitAB.deinit();
|
||||
const split = splitAB;
|
||||
|
||||
{
|
||||
const str = try std.fmt.allocPrint(alloc, "{diagram}", .{split});
|
||||
defer alloc.free(str);
|
||||
try testing.expectEqualStrings(str,
|
||||
\\+---+
|
||||
\\| B |
|
||||
\\+---+
|
||||
\\
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
test "SplitTree: split horizontal with full width" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var v1: TestTree.View = .{ .label = "A" };
|
||||
var t1: TestTree = try .init(alloc, &v1);
|
||||
defer t1.deinit();
|
||||
var v2: TestTree.View = .{ .label = "B" };
|
||||
var t2: TestTree = try .init(alloc, &v2);
|
||||
defer t2.deinit();
|
||||
|
||||
// A | B horizontal
|
||||
var splitAB = try t1.split(
|
||||
alloc,
|
||||
0, // at root
|
||||
.right, // split right
|
||||
1,
|
||||
&t2, // insert t2
|
||||
);
|
||||
defer splitAB.deinit();
|
||||
const split = splitAB;
|
||||
|
||||
{
|
||||
const str = try std.fmt.allocPrint(alloc, "{diagram}", .{split});
|
||||
defer alloc.free(str);
|
||||
try testing.expectEqualStrings(str,
|
||||
\\+---+
|
||||
\\| A |
|
||||
\\+---+
|
||||
\\
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
test "SplitTree: split vertical with full width" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var v1: TestTree.View = .{ .label = "A" };
|
||||
var t1: TestTree = try .init(alloc, &v1);
|
||||
defer t1.deinit();
|
||||
var v2: TestTree.View = .{ .label = "B" };
|
||||
var t2: TestTree = try .init(alloc, &v2);
|
||||
defer t2.deinit();
|
||||
|
||||
// A | B horizontal
|
||||
var splitAB = try t1.split(
|
||||
alloc,
|
||||
0, // at root
|
||||
.down, // split right
|
||||
1,
|
||||
&t2, // insert t2
|
||||
);
|
||||
defer splitAB.deinit();
|
||||
const split = splitAB;
|
||||
|
||||
{
|
||||
const str = try std.fmt.allocPrint(alloc, "{diagram}", .{split});
|
||||
defer alloc.free(str);
|
||||
try testing.expectEqualStrings(str,
|
||||
\\+---+
|
||||
\\| A |
|
||||
\\+---+
|
||||
\\
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
test "SplitTree: remove leaf" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
@@ -1639,6 +1894,65 @@ test "SplitTree: spatial goto" {
|
||||
}
|
||||
}
|
||||
|
||||
test "SplitTree: resize" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var v1: TestTree.View = .{ .label = "A" };
|
||||
var t1: TestTree = try .init(alloc, &v1);
|
||||
defer t1.deinit();
|
||||
var v2: TestTree.View = .{ .label = "B" };
|
||||
var t2: TestTree = try .init(alloc, &v2);
|
||||
defer t2.deinit();
|
||||
|
||||
// A | B horizontal
|
||||
var split = try t1.split(
|
||||
alloc,
|
||||
0, // at root
|
||||
.right, // split right
|
||||
0.5,
|
||||
&t2, // insert t2
|
||||
);
|
||||
defer split.deinit();
|
||||
|
||||
{
|
||||
const str = try std.fmt.allocPrint(alloc, "{diagram}", .{split});
|
||||
defer alloc.free(str);
|
||||
try testing.expectEqualStrings(str,
|
||||
\\+---++---+
|
||||
\\| A || B |
|
||||
\\+---++---+
|
||||
\\
|
||||
);
|
||||
}
|
||||
|
||||
// Resize
|
||||
{
|
||||
var resized = try split.resize(
|
||||
alloc,
|
||||
at: {
|
||||
var it = split.iterator();
|
||||
break :at while (it.next()) |entry| {
|
||||
if (std.mem.eql(u8, entry.view.label, "B")) {
|
||||
break entry.handle;
|
||||
}
|
||||
} else return error.NotFound;
|
||||
},
|
||||
.horizontal, // resize right
|
||||
0.25,
|
||||
);
|
||||
defer resized.deinit();
|
||||
const str = try std.fmt.allocPrint(alloc, "{diagram}", .{resized});
|
||||
defer alloc.free(str);
|
||||
try testing.expectEqualStrings(str,
|
||||
\\+-------------++---+
|
||||
\\| A || B |
|
||||
\\+-------------++---+
|
||||
\\
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
test "SplitTree: clone empty tree" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
Reference in New Issue
Block a user