mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00

This reverts commit 09470ede55c26e042a3c9805a8175e972b7cc89b, reversing changes made to 6139cb00cf6a50df2d47989dfb91b97286dd7879.
409 lines
13 KiB
Zig
409 lines
13 KiB
Zig
/// Split represents a surface split where two surfaces are shown side-by-side
|
|
/// within the same window either vertically or horizontally.
|
|
const Split = @This();
|
|
|
|
const std = @import("std");
|
|
const Allocator = std.mem.Allocator;
|
|
const assert = std.debug.assert;
|
|
const apprt = @import("../../apprt.zig");
|
|
const font = @import("../../font/main.zig");
|
|
const CoreSurface = @import("../../Surface.zig");
|
|
|
|
const Surface = @import("Surface.zig");
|
|
const Tab = @import("Tab.zig");
|
|
const c = @import("c.zig").c;
|
|
|
|
const log = std.log.scoped(.gtk);
|
|
|
|
/// The split orientation.
|
|
pub const Orientation = enum {
|
|
horizontal,
|
|
vertical,
|
|
|
|
pub fn fromDirection(direction: apprt.action.SplitDirection) Orientation {
|
|
return switch (direction) {
|
|
.right, .left => .horizontal,
|
|
.down, .up => .vertical,
|
|
};
|
|
}
|
|
|
|
pub fn fromResizeDirection(direction: apprt.action.ResizeSplit.Direction) Orientation {
|
|
return switch (direction) {
|
|
.up, .down => .vertical,
|
|
.left, .right => .horizontal,
|
|
};
|
|
}
|
|
};
|
|
|
|
/// Our actual GtkPaned widget
|
|
paned: *c.GtkPaned,
|
|
|
|
/// The container for this split panel.
|
|
container: Surface.Container,
|
|
|
|
/// The orientation of this split panel.
|
|
orientation: Orientation,
|
|
|
|
/// The elements of this split panel.
|
|
top_left: Surface.Container.Elem,
|
|
bottom_right: Surface.Container.Elem,
|
|
|
|
/// Create a new split panel with the given sibling surface in the given
|
|
/// direction. The direction is where the new surface will be initialized.
|
|
///
|
|
/// The sibling surface can be in a split already or it can be within a
|
|
/// tab. This properly handles updating the surface container so that
|
|
/// it represents the new split.
|
|
pub fn create(
|
|
alloc: Allocator,
|
|
sibling: *Surface,
|
|
direction: apprt.action.SplitDirection,
|
|
) !*Split {
|
|
var split = try alloc.create(Split);
|
|
errdefer alloc.destroy(split);
|
|
try split.init(sibling, direction);
|
|
return split;
|
|
}
|
|
|
|
pub fn init(
|
|
self: *Split,
|
|
sibling: *Surface,
|
|
direction: apprt.action.SplitDirection,
|
|
) !void {
|
|
// If our sibling is too small to be split in half then we don't
|
|
// allow the split to happen. This avoids a situation where the
|
|
// split becomes too small.
|
|
//
|
|
// This is kind of a hack. Ideally we'd use gtk_widget_set_size_request
|
|
// properly along the path to ensure minimum sizes. I don't know if
|
|
// GTK even respects that all but any way GTK does this for us seems
|
|
// better than this.
|
|
{
|
|
// This is the min size of the sibling split. This means the
|
|
// smallest split is half of this.
|
|
const multiplier = 4;
|
|
|
|
const size = &sibling.core_surface.size;
|
|
const small = switch (direction) {
|
|
.right, .left => size.screen.width < size.cell.width * multiplier,
|
|
.down, .up => size.screen.height < size.cell.height * multiplier,
|
|
};
|
|
if (small) return error.SplitTooSmall;
|
|
}
|
|
|
|
// Create the new child surface for the other direction.
|
|
const alloc = sibling.app.core_app.alloc;
|
|
var surface = try Surface.create(alloc, sibling.app, .{
|
|
.parent = &sibling.core_surface,
|
|
});
|
|
errdefer surface.destroy(alloc);
|
|
sibling.dimSurface();
|
|
sibling.setSplitZoom(false);
|
|
|
|
// Create the actual GTKPaned, attach the proper children.
|
|
const orientation: c_uint = switch (direction) {
|
|
.right, .left => c.GTK_ORIENTATION_HORIZONTAL,
|
|
.down, .up => c.GTK_ORIENTATION_VERTICAL,
|
|
};
|
|
const paned = c.gtk_paned_new(orientation);
|
|
errdefer c.g_object_unref(paned);
|
|
|
|
// Keep a long-lived reference, which we unref in destroy.
|
|
_ = c.g_object_ref(paned);
|
|
|
|
// Update all of our containers to point to the right place.
|
|
// The split has to point to where the sibling pointed to because
|
|
// we're inheriting its parent. The sibling points to its location
|
|
// in the split, and the surface points to the other location.
|
|
const container = sibling.container;
|
|
const tl: *Surface, const br: *Surface = switch (direction) {
|
|
.right, .down => right_down: {
|
|
sibling.container = .{ .split_tl = &self.top_left };
|
|
surface.container = .{ .split_br = &self.bottom_right };
|
|
break :right_down .{ sibling, surface };
|
|
},
|
|
|
|
.left, .up => left_up: {
|
|
sibling.container = .{ .split_br = &self.bottom_right };
|
|
surface.container = .{ .split_tl = &self.top_left };
|
|
break :left_up .{ surface, sibling };
|
|
},
|
|
};
|
|
|
|
self.* = .{
|
|
.paned = @ptrCast(paned),
|
|
.container = container,
|
|
.top_left = .{ .surface = tl },
|
|
.bottom_right = .{ .surface = br },
|
|
.orientation = Orientation.fromDirection(direction),
|
|
};
|
|
|
|
// Replace the previous containers element with our split.
|
|
// This allows a non-split to become a split, a split to
|
|
// become a nested split, etc.
|
|
container.replace(.{ .split = self });
|
|
|
|
// Update our children so that our GL area is properly
|
|
// added to the paned.
|
|
self.updateChildren();
|
|
|
|
// The new surface should always grab focus
|
|
surface.grabFocus();
|
|
}
|
|
|
|
pub fn destroy(self: *Split, alloc: Allocator) void {
|
|
self.top_left.deinit(alloc);
|
|
self.bottom_right.deinit(alloc);
|
|
|
|
// Clean up our GTK reference. This will trigger all the destroy callbacks
|
|
// that are necessary for the surfaces to clean up.
|
|
c.g_object_unref(self.paned);
|
|
|
|
alloc.destroy(self);
|
|
}
|
|
|
|
/// Remove the top left child.
|
|
pub fn removeTopLeft(self: *Split) void {
|
|
self.removeChild(self.top_left, self.bottom_right);
|
|
}
|
|
|
|
/// Remove the top left child.
|
|
pub fn removeBottomRight(self: *Split) void {
|
|
self.removeChild(self.bottom_right, self.top_left);
|
|
}
|
|
|
|
fn removeChild(
|
|
self: *Split,
|
|
remove: Surface.Container.Elem,
|
|
keep: Surface.Container.Elem,
|
|
) void {
|
|
const window = self.container.window() orelse return;
|
|
const alloc = window.app.core_app.alloc;
|
|
|
|
// Remove our children since we are going to no longer be
|
|
// a split anyways. This prevents widgets with multiple parents.
|
|
self.removeChildren();
|
|
|
|
// Our container must become whatever our top left is
|
|
self.container.replace(keep);
|
|
|
|
// Grab focus of the left-over side
|
|
keep.grabFocus();
|
|
|
|
// When a child is removed we are no longer a split, so destroy ourself
|
|
remove.deinit(alloc);
|
|
alloc.destroy(self);
|
|
}
|
|
|
|
/// Move the divider in the given direction by the given amount.
|
|
pub fn moveDivider(
|
|
self: *Split,
|
|
direction: apprt.action.ResizeSplit.Direction,
|
|
amount: u16,
|
|
) void {
|
|
const min_pos = 10;
|
|
|
|
const pos = c.gtk_paned_get_position(self.paned);
|
|
const new = switch (direction) {
|
|
.up, .left => @max(pos - amount, min_pos),
|
|
.down, .right => new_pos: {
|
|
const max_pos: u16 = @as(u16, @intFromFloat(self.maxPosition())) - min_pos;
|
|
break :new_pos @min(pos + amount, max_pos);
|
|
},
|
|
};
|
|
|
|
c.gtk_paned_set_position(self.paned, new);
|
|
}
|
|
|
|
/// Equalize the splits in this split panel. Each split is equalized based on
|
|
/// its weight, i.e. the number of Surfaces it contains.
|
|
///
|
|
/// It works recursively by equalizing the children of each split.
|
|
///
|
|
/// It returns this split's weight.
|
|
pub fn equalize(self: *Split) f64 {
|
|
// Calculate weights of top_left/bottom_right
|
|
const top_left_weight = self.top_left.equalize();
|
|
const bottom_right_weight = self.bottom_right.equalize();
|
|
const weight = top_left_weight + bottom_right_weight;
|
|
|
|
// Ratio of top_left weight to overall weight, which gives the split ratio
|
|
const ratio = top_left_weight / weight;
|
|
|
|
// Convert split ratio into new position for divider
|
|
c.gtk_paned_set_position(self.paned, @intFromFloat(self.maxPosition() * ratio));
|
|
|
|
return weight;
|
|
}
|
|
|
|
// maxPosition returns the maximum position of the GtkPaned, which is the
|
|
// "max-position" attribute.
|
|
fn maxPosition(self: *Split) f64 {
|
|
var value: c.GValue = std.mem.zeroes(c.GValue);
|
|
defer c.g_value_unset(&value);
|
|
|
|
_ = c.g_value_init(&value, c.G_TYPE_INT);
|
|
c.g_object_get_property(
|
|
@ptrCast(@alignCast(self.paned)),
|
|
"max-position",
|
|
&value,
|
|
);
|
|
|
|
return @floatFromInt(c.g_value_get_int(&value));
|
|
}
|
|
|
|
// This replaces the element at the given pointer with a new element.
|
|
// The ptr must be either top_left or bottom_right (asserted in debug).
|
|
// The memory of the old element must be freed or otherwise handled by
|
|
// the caller.
|
|
pub fn replace(
|
|
self: *Split,
|
|
ptr: *Surface.Container.Elem,
|
|
new: Surface.Container.Elem,
|
|
) void {
|
|
// We can write our element directly. There's nothing special.
|
|
assert(&self.top_left == ptr or &self.bottom_right == ptr);
|
|
ptr.* = new;
|
|
|
|
// Update our paned children. This will reset the divider
|
|
// position but we want to keep it in place so save and restore it.
|
|
const pos = c.gtk_paned_get_position(self.paned);
|
|
defer c.gtk_paned_set_position(self.paned, pos);
|
|
self.updateChildren();
|
|
}
|
|
|
|
// grabFocus grabs the focus of the top-left element.
|
|
pub fn grabFocus(self: *Split) void {
|
|
self.top_left.grabFocus();
|
|
}
|
|
|
|
/// Update the paned children to represent the current state.
|
|
/// This should be called anytime the top/left or bottom/right
|
|
/// element is changed.
|
|
pub fn updateChildren(self: *const Split) void {
|
|
// We have to set both to null. If we overwrite the pane with
|
|
// the same value, then GTK bugs out (the GL area unrealizes
|
|
// and never rerealizes).
|
|
self.removeChildren();
|
|
|
|
// Set our current children
|
|
c.gtk_paned_set_start_child(
|
|
@ptrCast(self.paned),
|
|
self.top_left.widget(),
|
|
);
|
|
c.gtk_paned_set_end_child(
|
|
@ptrCast(self.paned),
|
|
self.bottom_right.widget(),
|
|
);
|
|
}
|
|
|
|
/// A mapping of direction to the element (if any) in that direction.
|
|
pub const DirectionMap = std.EnumMap(
|
|
apprt.action.GotoSplit,
|
|
?*Surface,
|
|
);
|
|
|
|
pub const Side = enum { top_left, bottom_right };
|
|
|
|
/// Returns the map that can be used to determine elements in various
|
|
/// directions (primarily for gotoSplit).
|
|
pub fn directionMap(self: *const Split, from: Side) DirectionMap {
|
|
var result = DirectionMap.initFull(null);
|
|
|
|
if (self.directionPrevious(from)) |prev| {
|
|
result.put(.previous, prev.surface);
|
|
if (!prev.wrapped) {
|
|
// This behavior matches the behavior of macOS at the time of writing
|
|
// this. There is an open issue (#524) to make this depend on the
|
|
// actual physical location of the current split.
|
|
result.put(.up, prev.surface);
|
|
result.put(.left, prev.surface);
|
|
}
|
|
}
|
|
|
|
if (self.directionNext(from)) |next| {
|
|
result.put(.next, next.surface);
|
|
if (!next.wrapped) {
|
|
result.put(.down, next.surface);
|
|
result.put(.right, next.surface);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
fn directionPrevious(self: *const Split, from: Side) ?struct {
|
|
surface: *Surface,
|
|
wrapped: bool,
|
|
} {
|
|
switch (from) {
|
|
// From the bottom right, our previous is the deepest surface
|
|
// in the top-left of our own split.
|
|
.bottom_right => return .{
|
|
.surface = self.top_left.deepestSurface(.bottom_right) orelse return null,
|
|
.wrapped = false,
|
|
},
|
|
|
|
// From the top left its more complicated. It is the de
|
|
.top_left => {
|
|
// If we have no parent split then there can be no unwrapped prev.
|
|
// We can still have a wrapped previous.
|
|
const parent = self.container.split() orelse return .{
|
|
.surface = self.bottom_right.deepestSurface(.bottom_right) orelse return null,
|
|
.wrapped = true,
|
|
};
|
|
|
|
// The previous value is the previous of the side that we are.
|
|
const side = self.container.splitSide() orelse return null;
|
|
return switch (side) {
|
|
.top_left => parent.directionPrevious(.top_left),
|
|
.bottom_right => parent.directionPrevious(.bottom_right),
|
|
};
|
|
},
|
|
}
|
|
}
|
|
|
|
fn directionNext(self: *const Split, from: Side) ?struct {
|
|
surface: *Surface,
|
|
wrapped: bool,
|
|
} {
|
|
switch (from) {
|
|
// From the top left, our next is the earliest surface in the
|
|
// top-left direction of the bottom-right side of our split. Fun!
|
|
.top_left => return .{
|
|
.surface = self.bottom_right.deepestSurface(.top_left) orelse return null,
|
|
.wrapped = false,
|
|
},
|
|
|
|
// From the bottom right is more compliated. It is the deepest
|
|
// (last) surface in the
|
|
.bottom_right => {
|
|
// If we have no parent split then there can be no next.
|
|
const parent = self.container.split() orelse return .{
|
|
.surface = self.top_left.deepestSurface(.top_left) orelse return null,
|
|
.wrapped = true,
|
|
};
|
|
|
|
// The previous value is the previous of the side that we are.
|
|
const side = self.container.splitSide() orelse return null;
|
|
return switch (side) {
|
|
.top_left => parent.directionNext(.top_left),
|
|
.bottom_right => parent.directionNext(.bottom_right),
|
|
};
|
|
},
|
|
}
|
|
}
|
|
|
|
pub fn detachTopLeft(self: *const Split) void {
|
|
c.gtk_paned_set_start_child(self.paned, null);
|
|
}
|
|
|
|
pub fn detachBottomRight(self: *const Split) void {
|
|
c.gtk_paned_set_end_child(self.paned, null);
|
|
}
|
|
|
|
fn removeChildren(self: *const Split) void {
|
|
self.detachTopLeft();
|
|
self.detachBottomRight();
|
|
}
|