ghostty/src/apprt/gtk/Paned.zig
2023-11-30 21:44:16 -08:00

202 lines
6.3 KiB
Zig

const Paned = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const font = @import("../../font/main.zig");
const input = @import("../../input.zig");
const CoreSurface = @import("../../Surface.zig");
const Window = @import("Window.zig");
const Surface = @import("Surface.zig");
const Tab = @import("Tab.zig");
const Position = @import("relation.zig").Position;
const Parent = @import("relation.zig").Parent;
const Child = @import("relation.zig").Child;
const c = @import("c.zig");
const log = std.log.scoped(.gtk);
/// Our actual GtkPaned widget
paned: *c.GtkPaned,
// We have two children, each of which can be either a Surface, another pane,
// or empty. We're going to keep track of which each child is here.
child1: Child,
child2: Child,
// We also hold a reference to our parent widget, so that when we close we can either
// maximize the parent pane, or close the tab.
parent: Parent,
pub fn create(alloc: Allocator, sibling: *Surface, direction: input.SplitDirection) !*Paned {
var paned = try alloc.create(Paned);
errdefer alloc.destroy(paned);
try paned.init(sibling, direction);
return paned;
}
pub fn init(self: *Paned, sibling: *Surface, direction: input.SplitDirection) !void {
self.* = .{
.paned = undefined,
.child1 = .none,
.child2 = .none,
.parent = undefined,
};
errdefer self.* = undefined;
const orientation: c_uint = switch (direction) {
.right => c.GTK_ORIENTATION_HORIZONTAL,
.down => c.GTK_ORIENTATION_VERTICAL,
};
const paned = c.gtk_paned_new(orientation);
errdefer c.g_object_unref(paned);
const gtk_paned: *c.GtkPaned = @ptrCast(paned);
self.paned = gtk_paned;
const tab = sibling.container.tab().?; // TODO
const surface = try tab.newSurface(&sibling.core_surface);
surface.setParent(.{ .paned = .{ self, .end } });
self.addChild1(.{ .surface = sibling });
self.addChild2(.{ .surface = surface });
}
/// Set the parent of Paned.
pub fn setParent(self: *Paned, parent: Parent) void {
self.parent = parent;
}
/// Focus on first Surface that can be found in given position. If there's a
/// Paned in the position, it will focus on the first surface in that position.
pub fn focusFirstSurfaceInPosition(self: *Paned, position: Position) void {
const child = self.childInPosition(position);
switch (child) {
.surface => |s| s.grabFocus(),
.paned => |p| p.focusFirstSurfaceInPosition(position),
.none => {
log.warn("attempted to focus on first surface, found none", .{});
return;
},
}
}
/// Split the Surface in the given position into a Paned with two surfaces.
pub fn splitSurfaceInPosition(self: *Paned, position: Position, direction: input.SplitDirection) !void {
const surface: *Surface = self.surfaceInPosition(position) orelse return;
// Keep explicit reference to surface gl_area before we remove it.
const object: *c.GObject = @ptrCast(surface.gl_area);
_ = c.g_object_ref(object);
defer c.g_object_unref(object);
// Keep position of divider
const parent_paned_position_before = c.gtk_paned_get_position(self.paned);
// Now remove it
self.removeChildInPosition(position);
// Create new Paned
// NOTE: We cannot use `replaceChildInPosition` here because we need to
// first remove the surface before we create a new pane.
const paned = try Paned.create(surface.app.core_app.alloc, surface, direction);
switch (position) {
.start => self.addChild1(.{ .paned = paned }),
.end => self.addChild2(.{ .paned = paned }),
}
// Restore position
c.gtk_paned_set_position(self.paned, parent_paned_position_before);
// Focus on new surface
paned.focusFirstSurfaceInPosition(.end);
}
/// Replace the existing .start or .end Child with the given new Child.
pub fn replaceChildInPosition(self: *Paned, child: Child, position: Position) void {
// Keep position of divider
const parent_paned_position_before = c.gtk_paned_get_position(self.paned);
// Focus on the sibling, otherwise we'll get a GTK warning
self.focusFirstSurfaceInPosition(if (position == .start) .end else .start);
// Now we can remove the other one
self.removeChildInPosition(position);
switch (position) {
.start => self.addChild1(child),
.end => self.addChild2(child),
}
// Restore position
c.gtk_paned_set_position(self.paned, parent_paned_position_before);
}
/// Remove both children, setting *c.GtkPaned start/end children to null.
pub fn removeChildren(self: *Paned) void {
self.removeChildInPosition(.start);
self.removeChildInPosition(.end);
}
/// Deinit the Paned by deiniting its child Paneds, if they exist.
pub fn deinit(self: *Paned, alloc: Allocator) void {
for ([_]Child{ self.child1, self.child2 }) |child| {
switch (child) {
.none, .surface => continue,
.paned => |paned| {
paned.deinit(alloc);
alloc.destroy(paned);
},
}
}
}
fn removeChildInPosition(self: *Paned, position: Position) void {
switch (position) {
.start => {
assert(self.child1 != .none);
self.child1 = .none;
c.gtk_paned_set_start_child(@ptrCast(self.paned), null);
},
.end => {
assert(self.child2 != .none);
self.child2 = .none;
c.gtk_paned_set_end_child(@ptrCast(self.paned), null);
},
}
}
fn addChild1(self: *Paned, child: Child) void {
assert(self.child1 == .none);
const widget = child.widget() orelse return;
c.gtk_paned_set_start_child(@ptrCast(self.paned), widget);
self.child1 = child;
child.setParent(.{ .paned = .{ self, .start } });
}
fn addChild2(self: *Paned, child: Child) void {
assert(self.child2 == .none);
const widget = child.widget() orelse return;
c.gtk_paned_set_end_child(@ptrCast(self.paned), widget);
self.child2 = child;
child.setParent(.{ .paned = .{ self, .end } });
}
fn childInPosition(self: *Paned, position: Position) Child {
return switch (position) {
.start => self.child1,
.end => self.child2,
};
}
fn surfaceInPosition(self: *Paned, position: Position) ?*Surface {
return switch (self.childInPosition(position)) {
.surface => |surface| surface,
else => null,
};
}