mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 16:26:08 +03:00
apprt/gtk: working on new Split
This commit is contained in:
@ -56,9 +56,13 @@ pub fn init(self: *Paned, sibling: *Surface, direction: input.SplitDirection) !v
|
||||
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 } });
|
||||
const alloc = sibling.app.core_app.alloc;
|
||||
var surface = try Surface.create(alloc, sibling.app, .{
|
||||
.parent2 = &sibling.core_surface,
|
||||
.parent = .{ .paned = .{ self, .end } },
|
||||
});
|
||||
errdefer surface.destroy(alloc);
|
||||
surface.container = sibling.container; // TODO
|
||||
|
||||
self.addChild1(.{ .surface = sibling });
|
||||
self.addChild2(.{ .surface = surface });
|
||||
|
260
src/apprt/gtk/Split.zig
Normal file
260
src/apprt/gtk/Split.zig
Normal file
@ -0,0 +1,260 @@
|
||||
/// 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 font = @import("../../font/main.zig");
|
||||
const input = @import("../../input.zig");
|
||||
const CoreSurface = @import("../../Surface.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,
|
||||
|
||||
/// The container for this split panel.
|
||||
container: Surface.Container,
|
||||
|
||||
/// The elements of this split panel.
|
||||
top_left: Elem,
|
||||
bottom_right: Elem,
|
||||
|
||||
/// Elem is the possible element of the split.
|
||||
pub const Elem = union(enum) {
|
||||
/// A surface is a leaf element of the split -- a terminal surface.
|
||||
surface: *Surface,
|
||||
|
||||
/// A split is a nested split within a split. This lets you for example
|
||||
/// have a horizontal split with a vertical split on the left side
|
||||
/// (amongst all other possible combinations).
|
||||
split: *Split,
|
||||
|
||||
/// Returns the GTK widget to add to the paned for the given element
|
||||
pub fn widget(self: Child) *c.GtkWidget {
|
||||
return switch (self) {
|
||||
.surface => |surface| @ptrCast(surface.gl_area),
|
||||
.split => |split| @ptrCast(@alignCast(split.paned)),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// 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: input.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: input.SplitDirection,
|
||||
) !void {
|
||||
// Create the new child surface
|
||||
const alloc = sibling.app.core_app.alloc;
|
||||
var surface = try Surface.create(alloc, sibling.app, .{
|
||||
.parent2 = &sibling.core_surface,
|
||||
.parent = .{ .paned = .{ self, .end } },
|
||||
});
|
||||
errdefer surface.destroy(alloc);
|
||||
|
||||
// Create the actual GTKPaned, attach the proper children.
|
||||
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);
|
||||
|
||||
// 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;
|
||||
sibling.container = .{ .split_tl = &self.top_left };
|
||||
surface.container = .{ .split_br = &self.bottom_right };
|
||||
|
||||
// If the sibling is already in a split, then we need to
|
||||
// nest them properly. This gets the pointer to the split element
|
||||
// that the original split was in, then updates it to point to this
|
||||
// split. This split then contains the surface as an element.
|
||||
if (container.splitElem()) |parent_elem| {
|
||||
parent_elem.* = .{ .split = self };
|
||||
}
|
||||
|
||||
self.* = .{
|
||||
.paned = @ptrCast(paned),
|
||||
.container = container,
|
||||
.top_left = .{ .surface = sibling },
|
||||
.bottom_right = .{ .surface = surface },
|
||||
};
|
||||
}
|
||||
|
||||
/// Set the parent of Split.
|
||||
pub fn setParent(self: *Split, parent: Parent) void {
|
||||
self.parent = parent;
|
||||
}
|
||||
|
||||
/// Focus on first Surface that can be found in given position. If there's a
|
||||
/// Split in the position, it will focus on the first surface in that position.
|
||||
pub fn focusFirstSurfaceInPosition(self: *Split, 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 Split with two surfaces.
|
||||
pub fn splitSurfaceInPosition(self: *Split, 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 Split
|
||||
// NOTE: We cannot use `replaceChildInPosition` here because we need to
|
||||
// first remove the surface before we create a new pane.
|
||||
const paned = try Split.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: *Split, 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.GtkSplit start/end children to null.
|
||||
pub fn removeChildren(self: *Split) void {
|
||||
self.removeChildInPosition(.start);
|
||||
self.removeChildInPosition(.end);
|
||||
}
|
||||
|
||||
/// Deinit the Split by deiniting its child Split, if they exist.
|
||||
pub fn deinit(self: *Split, 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: *Split, 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);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the paned children to represent the current state.
|
||||
/// This should be called anytime the top/left or bottom/right
|
||||
/// element is changed.
|
||||
fn updateChildren(self: *const Split) void {
|
||||
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(),
|
||||
);
|
||||
}
|
||||
|
||||
fn addChild1(self: *Split, 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: *Split, 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: *Split, position: Position) Child {
|
||||
return switch (position) {
|
||||
.start => self.child1,
|
||||
.end => self.child2,
|
||||
};
|
||||
}
|
||||
|
||||
fn surfaceInPosition(self: *Split, position: Position) ?*Surface {
|
||||
return switch (self.childInPosition(position)) {
|
||||
.surface => |surface| surface,
|
||||
else => null,
|
||||
};
|
||||
}
|
@ -13,7 +13,7 @@ const terminal = @import("../../terminal/main.zig");
|
||||
const CoreSurface = @import("../../Surface.zig");
|
||||
|
||||
const App = @import("App.zig");
|
||||
const Paned = @import("Paned.zig");
|
||||
const Split = @import("Split.zig");
|
||||
const Tab = @import("Tab.zig");
|
||||
const Window = @import("Window.zig");
|
||||
const ClipboardConfirmationWindow = @import("ClipboardConfirmationWindow.zig");
|
||||
@ -47,15 +47,20 @@ pub const Container = union(enum) {
|
||||
/// Directly attached to a tab. (i.e. no splits)
|
||||
tab_: *Tab,
|
||||
|
||||
/// A split within a split hierarchy.
|
||||
paned: *Paned,
|
||||
/// A split within a split hierarchy. The key determines the
|
||||
/// position of the split within the parent split.
|
||||
split_tl: *Split.Elem,
|
||||
split_br: *Split.Elem,
|
||||
|
||||
/// Returns the window that this surface is attached to.
|
||||
pub fn window(self: Container) ?*Window {
|
||||
return switch (self) {
|
||||
.none => null,
|
||||
.tab_ => |v| v.window,
|
||||
else => @panic("TODO"),
|
||||
.split_tl, .split_br => split: {
|
||||
const s = self.split() orelse break :split null;
|
||||
break :split s.container.window();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -64,7 +69,29 @@ pub const Container = union(enum) {
|
||||
return switch (self) {
|
||||
.none => null,
|
||||
.tab_ => |v| v,
|
||||
else => @panic("TODO"),
|
||||
.split_tl, .split_br => split: {
|
||||
const s = self.split() orelse break :split null;
|
||||
break :split s.container.tab();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the split containing this surface (if any).
|
||||
pub fn split(self: Container) ?*Split {
|
||||
return switch (self) {
|
||||
.none, .tab_ => null,
|
||||
.split_tl => |ptr| @fieldParentPtr(Split, "top_left", ptr),
|
||||
.split_br => |ptr| @fieldParentPtr(Split, "bottom_right", ptr),
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the element of the split that this container
|
||||
/// is attached to.
|
||||
pub fn splitElem(self: Container) ?*Split.Elem {
|
||||
return switch (self) {
|
||||
.none, .tab_ => null,
|
||||
.split_tl => |ptr| ptr,
|
||||
.split_br => |ptr| ptr,
|
||||
};
|
||||
}
|
||||
};
|
||||
@ -422,7 +449,7 @@ pub fn getTitleLabel(self: *Surface) ?*c.GtkWidget {
|
||||
}
|
||||
|
||||
pub fn newSplit(self: *Surface, direction: input.SplitDirection) !void {
|
||||
log.debug("splitting surface, direction: {}", .{direction});
|
||||
log.debug("splitting direction={}", .{direction});
|
||||
|
||||
switch (self.parent) {
|
||||
.none => return,
|
||||
|
@ -149,22 +149,6 @@ pub fn deinit(self: *Tab) void {
|
||||
}
|
||||
}
|
||||
|
||||
/// Allocates and initializes a new Surface, but doesn't add it to the Tab yet.
|
||||
/// Can also be added to a Paned.
|
||||
pub fn newSurface(self: *Tab, parent_: ?*CoreSurface) !*Surface {
|
||||
const alloc = self.window.app.core_app.alloc;
|
||||
var surface = try Surface.create(alloc, self.window.app, .{
|
||||
.parent2 = parent_,
|
||||
.parent = .{
|
||||
.tab = self,
|
||||
},
|
||||
});
|
||||
errdefer surface.destroy(alloc);
|
||||
surface.setContainer(.{ .tab_ = self });
|
||||
|
||||
return surface;
|
||||
}
|
||||
|
||||
/// Splits the current child surface into a Paned in given direction. Child of
|
||||
/// Tab must be a Surface.
|
||||
pub fn splitSurface(self: *Tab, direction: input.SplitDirection) !void {
|
||||
|
Reference in New Issue
Block a user