mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 00:36:07 +03:00
Merge pull request #778 from mitchellh/mrn/gtk-tabs-splits
gtk: add support for splits
This commit is contained in:
263
src/apprt/gtk/Split.zig
Normal file
263
src/apprt/gtk/Split.zig
Normal file
@ -0,0 +1,263 @@
|
||||
/// 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 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: 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: 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 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);
|
||||
|
||||
// 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);
|
||||
|
||||
// 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;
|
||||
sibling.container = .{ .split_tl = &self.top_left };
|
||||
surface.container = .{ .split_br = &self.bottom_right };
|
||||
|
||||
self.* = .{
|
||||
.paned = @ptrCast(paned),
|
||||
.container = container,
|
||||
.top_left = .{ .surface = sibling },
|
||||
.bottom_right = .{ .surface = surface },
|
||||
};
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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.
|
||||
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(
|
||||
input.SplitFocusDirection,
|
||||
?*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);
|
||||
|
||||
// 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(.top, prev);
|
||||
result.put(.left, prev);
|
||||
}
|
||||
|
||||
if (self.directionNext(from)) |next| {
|
||||
result.put(.next, next);
|
||||
result.put(.bottom, next);
|
||||
result.put(.right, next);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
fn directionPrevious(self: *const Split, from: Side) ?*Surface {
|
||||
switch (from) {
|
||||
// From the bottom right, our previous is the deepest surface
|
||||
// in the top-left of our own split.
|
||||
.bottom_right => return self.top_left.deepestSurface(.bottom_right),
|
||||
|
||||
// From the top left its more complicated. It is the de
|
||||
.top_left => {
|
||||
// If we have no parent split then there can be no previous.
|
||||
const parent = self.container.split() orelse return null;
|
||||
const side = self.container.splitSide() orelse return null;
|
||||
|
||||
// The previous value is the previous of the side that we are.
|
||||
return switch (side) {
|
||||
.top_left => parent.directionPrevious(.top_left),
|
||||
.bottom_right => parent.directionPrevious(.bottom_right),
|
||||
};
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn directionNext(self: *const Split, from: Side) ?*Surface {
|
||||
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 self.bottom_right.deepestSurface(.top_left),
|
||||
|
||||
// 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 null;
|
||||
const side = self.container.splitSide() orelse return null;
|
||||
|
||||
// The previous value is the previous of the side that we are.
|
||||
return switch (side) {
|
||||
.top_left => parent.directionNext(.top_left),
|
||||
.bottom_right => parent.directionNext(.bottom_right),
|
||||
};
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn removeChildren(self: *const Split) void {
|
||||
c.gtk_paned_set_start_child(@ptrCast(self.paned), null);
|
||||
c.gtk_paned_set_end_child(@ptrCast(self.paned), null);
|
||||
}
|
@ -4,6 +4,7 @@
|
||||
const Surface = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const configpkg = @import("../../config.zig");
|
||||
const apprt = @import("../../apprt.zig");
|
||||
const font = @import("../../font/main.zig");
|
||||
@ -12,44 +13,174 @@ const terminal = @import("../../terminal/main.zig");
|
||||
const CoreSurface = @import("../../Surface.zig");
|
||||
|
||||
const App = @import("App.zig");
|
||||
const Split = @import("Split.zig");
|
||||
const Tab = @import("Tab.zig");
|
||||
const Window = @import("Window.zig");
|
||||
const ClipboardConfirmationWindow = @import("ClipboardConfirmationWindow.zig");
|
||||
const inspector = @import("inspector.zig");
|
||||
const gtk_key = @import("key.zig");
|
||||
const c = @import("c.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
const log = std.log.scoped(.gtk_surface);
|
||||
|
||||
/// This is detected by the OpenGL renderer to move to a single-threaded
|
||||
/// draw operation. This basically puts locks around our draw path.
|
||||
pub const opengl_single_threaded_draw = true;
|
||||
|
||||
pub const Options = struct {
|
||||
/// The window that this surface is attached to.
|
||||
window: *Window,
|
||||
|
||||
/// The GL area that this surface should draw to.
|
||||
gl_area: *c.GtkGLArea,
|
||||
|
||||
/// The label to use as the title of this surface. This will be
|
||||
/// modified with setTitle.
|
||||
title_label: ?*c.GtkLabel = null,
|
||||
|
||||
/// A font size to set on the surface once it is initialized.
|
||||
font_size: ?font.face.DesiredSize = null,
|
||||
|
||||
/// True if this surface has a parent. This is a bit of a hack currently
|
||||
/// to work around newConfig unconditinally inheriting the working
|
||||
/// directory. The proper long term fix is to have the working directory
|
||||
/// inherited upstream likely at the point where this field would be set,
|
||||
/// then remove this field.
|
||||
parent: bool = false,
|
||||
/// The parent surface to inherit settings such as font size, working
|
||||
/// directory, etc. from.
|
||||
parent: ?*CoreSurface = null,
|
||||
};
|
||||
|
||||
/// Where the title of this surface will go.
|
||||
const Title = union(enum) {
|
||||
/// The container that this surface is directly attached to.
|
||||
pub const Container = union(enum) {
|
||||
/// The surface is not currently attached to anything. This means
|
||||
/// that the GLArea has been created and potentially initialized
|
||||
/// but the widget is currently floating and not part of any parent.
|
||||
none: void,
|
||||
label: *c.GtkLabel,
|
||||
|
||||
/// Directly attached to a tab. (i.e. no splits)
|
||||
tab_: *Tab,
|
||||
|
||||
/// A split within a split hierarchy. The key determines the
|
||||
/// position of the split within the parent split.
|
||||
split_tl: *Elem,
|
||||
split_br: *Elem,
|
||||
|
||||
/// The side of the split.
|
||||
pub const SplitSide = enum { top_left, bottom_right };
|
||||
|
||||
/// Elem is the possible element of any container. A container can
|
||||
/// hold both a surface and a split. Any valid container should
|
||||
/// have an Elem value so that it can be properly used with
|
||||
/// splits.
|
||||
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: Elem) *c.GtkWidget {
|
||||
return switch (self) {
|
||||
.surface => |s| @ptrCast(s.gl_area),
|
||||
.split => |s| @ptrCast(@alignCast(s.paned)),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn containerPtr(self: Elem) *Container {
|
||||
return switch (self) {
|
||||
.surface => |s| &s.container,
|
||||
.split => |s| &s.container,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: Elem, alloc: Allocator) void {
|
||||
switch (self) {
|
||||
.surface => |s| s.unref(),
|
||||
.split => |s| s.destroy(alloc),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn grabFocus(self: Elem) void {
|
||||
switch (self) {
|
||||
.surface => |s| s.grabFocus(),
|
||||
.split => |s| s.grabFocus(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The last surface in this container in the direction specified.
|
||||
/// Direction must be "top_left" or "bottom_right".
|
||||
pub fn deepestSurface(self: Elem, side: SplitSide) ?*Surface {
|
||||
return switch (self) {
|
||||
.surface => |s| s,
|
||||
.split => |s| (switch (side) {
|
||||
.top_left => s.top_left,
|
||||
.bottom_right => s.bottom_right,
|
||||
}).deepestSurface(side),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// Returns the window that this surface is attached to.
|
||||
pub fn window(self: Container) ?*Window {
|
||||
return switch (self) {
|
||||
.none => null,
|
||||
.tab_ => |v| v.window,
|
||||
.split_tl, .split_br => split: {
|
||||
const s = self.split() orelse break :split null;
|
||||
break :split s.container.window();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the tab container if it exists.
|
||||
pub fn tab(self: Container) ?*Tab {
|
||||
return switch (self) {
|
||||
.none => null,
|
||||
.tab_ => |v| v,
|
||||
.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),
|
||||
};
|
||||
}
|
||||
|
||||
/// The side that we are in the split.
|
||||
pub fn splitSide(self: Container) ?SplitSide {
|
||||
return switch (self) {
|
||||
.none, .tab_ => null,
|
||||
.split_tl => .top_left,
|
||||
.split_br => .bottom_right,
|
||||
};
|
||||
}
|
||||
|
||||
/// Replace the container's element with this element. This is
|
||||
/// used by children to modify their parents to for example change
|
||||
/// from a surface to a split or a split back to a surface or
|
||||
/// a split to a nested split and so on.
|
||||
pub fn replace(self: Container, elem: Elem) void {
|
||||
// Move the element into the container
|
||||
switch (self) {
|
||||
.none => {},
|
||||
.tab_ => |t| t.replaceElem(elem),
|
||||
inline .split_tl, .split_br => |ptr| {
|
||||
const s = self.split().?;
|
||||
s.replace(ptr, elem);
|
||||
},
|
||||
}
|
||||
|
||||
// Update the reverse reference to the container
|
||||
elem.containerPtr().* = self;
|
||||
}
|
||||
|
||||
/// Remove ourselves from the container. This is used by
|
||||
/// children to effectively notify they're container that
|
||||
/// all children at this level are exiting.
|
||||
pub fn remove(self: Container) void {
|
||||
switch (self) {
|
||||
.none => {},
|
||||
.tab_ => |t| t.remove(),
|
||||
.split_tl => self.split().?.removeTopLeft(),
|
||||
.split_br => self.split().?.removeBottomRight(),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// Whether the surface has been realized or not yet. When a surface is
|
||||
@ -57,23 +188,27 @@ const Title = union(enum) {
|
||||
/// surface has been initialized.
|
||||
realized: bool = false,
|
||||
|
||||
/// See Options.parent
|
||||
parent: bool = false,
|
||||
/// True if this surface had a parent to start with.
|
||||
parent_surface: bool = false,
|
||||
|
||||
/// The GUI container that this surface has been attached to. This
|
||||
/// dictates some behaviors such as new splits, etc.
|
||||
container: Container = .{ .none = {} },
|
||||
|
||||
/// The app we're part of
|
||||
app: *App,
|
||||
|
||||
/// The window we're part of
|
||||
window: *Window,
|
||||
|
||||
/// Our GTK area
|
||||
gl_area: *c.GtkGLArea,
|
||||
|
||||
/// Any active cursor we may have
|
||||
cursor: ?*c.GdkCursor = null,
|
||||
|
||||
/// Our title label (if there is one).
|
||||
title: Title,
|
||||
/// Our title. The raw value of the title. This will be kept up to date and
|
||||
/// .title will be updated if we have focus.
|
||||
/// When set the text in this buf will be null-terminated, because we need to
|
||||
/// pass it to GTK.
|
||||
title_text: ?[:0]const u8 = null,
|
||||
|
||||
/// The core surface backing this surface
|
||||
core_surface: CoreSurface,
|
||||
@ -95,12 +230,37 @@ im_composing: bool = false,
|
||||
im_buf: [128]u8 = undefined,
|
||||
im_len: u7 = 0,
|
||||
|
||||
pub fn create(alloc: Allocator, app: *App, opts: Options) !*Surface {
|
||||
var surface = try alloc.create(Surface);
|
||||
errdefer alloc.destroy(surface);
|
||||
try surface.init(app, opts);
|
||||
return surface;
|
||||
}
|
||||
|
||||
pub fn init(self: *Surface, app: *App, opts: Options) !void {
|
||||
const widget = @as(*c.GtkWidget, @ptrCast(opts.gl_area));
|
||||
c.gtk_gl_area_set_required_version(opts.gl_area, 3, 3);
|
||||
c.gtk_gl_area_set_has_stencil_buffer(opts.gl_area, 0);
|
||||
c.gtk_gl_area_set_has_depth_buffer(opts.gl_area, 0);
|
||||
c.gtk_gl_area_set_use_es(opts.gl_area, 0);
|
||||
const widget: *c.GtkWidget = c.gtk_gl_area_new();
|
||||
const gl_area: *c.GtkGLArea = @ptrCast(widget);
|
||||
|
||||
// We grab the floating reference to GL area. This lets the
|
||||
// GL area be moved around i.e. between a split, a tab, etc.
|
||||
// without having to be really careful about ordering to
|
||||
// prevent a destroy.
|
||||
//
|
||||
// This is unref'd in the unref() method that's called by the
|
||||
// self.container through Elem.deinit.
|
||||
_ = c.g_object_ref_sink(@ptrCast(gl_area));
|
||||
errdefer c.g_object_unref(@ptrCast(gl_area));
|
||||
|
||||
// We want the gl area to expand to fill the parent container.
|
||||
c.gtk_widget_set_hexpand(widget, 1);
|
||||
c.gtk_widget_set_vexpand(widget, 1);
|
||||
|
||||
// Various other GL properties
|
||||
c.gtk_widget_set_cursor_from_name(@ptrCast(gl_area), "text");
|
||||
c.gtk_gl_area_set_required_version(gl_area, 3, 3);
|
||||
c.gtk_gl_area_set_has_stencil_buffer(gl_area, 0);
|
||||
c.gtk_gl_area_set_has_depth_buffer(gl_area, 0);
|
||||
c.gtk_gl_area_set_use_es(gl_area, 0);
|
||||
|
||||
// Key event controller will tell us about raw keypress events.
|
||||
const ec_key = c.gtk_event_controller_key_new();
|
||||
@ -150,17 +310,22 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void {
|
||||
c.gtk_widget_set_focusable(widget, 1);
|
||||
c.gtk_widget_set_focus_on_click(widget, 1);
|
||||
|
||||
// Inherit the parent's font size if we have a parent.
|
||||
const font_size: ?font.face.DesiredSize = font_size: {
|
||||
if (!app.config.@"window-inherit-font-size") break :font_size null;
|
||||
const parent = opts.parent orelse break :font_size null;
|
||||
break :font_size parent.font_size;
|
||||
};
|
||||
|
||||
// Build our result
|
||||
self.* = .{
|
||||
.app = app,
|
||||
.window = opts.window,
|
||||
.gl_area = opts.gl_area,
|
||||
.title = if (opts.title_label) |label| .{
|
||||
.label = label,
|
||||
} else .{ .none = {} },
|
||||
.container = .{ .none = {} },
|
||||
.gl_area = gl_area,
|
||||
.title_text = null,
|
||||
.core_surface = undefined,
|
||||
.font_size = opts.font_size,
|
||||
.parent = opts.parent,
|
||||
.font_size = font_size,
|
||||
.parent_surface = opts.parent != null,
|
||||
.size = .{ .width = 800, .height = 600 },
|
||||
.cursor_pos = .{ .x = 0, .y = 0 },
|
||||
.im_context = im_context,
|
||||
@ -171,11 +336,11 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void {
|
||||
try self.setMouseShape(.text);
|
||||
|
||||
// GL events
|
||||
_ = c.g_signal_connect_data(opts.gl_area, "realize", c.G_CALLBACK(>kRealize), self, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(opts.gl_area, "unrealize", c.G_CALLBACK(>kUnrealize), self, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(opts.gl_area, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(opts.gl_area, "render", c.G_CALLBACK(>kRender), self, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(opts.gl_area, "resize", c.G_CALLBACK(>kResize), self, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(gl_area, "realize", c.G_CALLBACK(>kRealize), self, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(gl_area, "unrealize", c.G_CALLBACK(>kUnrealize), self, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(gl_area, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(gl_area, "render", c.G_CALLBACK(>kRender), self, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(gl_area, "resize", c.G_CALLBACK(>kResize), self, null, c.G_CONNECT_DEFAULT);
|
||||
|
||||
_ = c.g_signal_connect_data(ec_key_press, "key-pressed", c.G_CALLBACK(>kKeyPressed), self, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(ec_key_press, "key-released", c.G_CALLBACK(>kKeyReleased), self, null, c.G_CONNECT_DEFAULT);
|
||||
@ -209,8 +374,8 @@ fn realize(self: *Surface) !void {
|
||||
// Get our new surface config
|
||||
var config = try apprt.surface.newConfig(self.app.core_app, &self.app.config);
|
||||
defer config.deinit();
|
||||
if (!self.parent) {
|
||||
// A hack, see the "parent" field for more information.
|
||||
if (!self.parent_surface) {
|
||||
// A hack, see the "parent_surface" field for more information.
|
||||
config.@"working-directory" = self.app.config.@"working-directory";
|
||||
}
|
||||
|
||||
@ -234,6 +399,8 @@ fn realize(self: *Surface) !void {
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Surface) void {
|
||||
if (self.title_text) |title| self.app.core_app.alloc.free(title);
|
||||
|
||||
// We don't allocate anything if we aren't realized.
|
||||
if (!self.realized) return;
|
||||
|
||||
@ -253,6 +420,17 @@ pub fn deinit(self: *Surface) void {
|
||||
if (self.cursor) |cursor| c.g_object_unref(cursor);
|
||||
}
|
||||
|
||||
// unref removes the long-held reference to the gl_area and kicks off the
|
||||
// deinit/destroy process for this surface.
|
||||
pub fn unref(self: *Surface) void {
|
||||
c.g_object_unref(self.gl_area);
|
||||
}
|
||||
|
||||
pub fn destroy(self: *Surface, alloc: Allocator) void {
|
||||
self.deinit();
|
||||
alloc.destroy(self);
|
||||
}
|
||||
|
||||
fn render(self: *Surface) !void {
|
||||
try self.core_surface.renderer.drawFrame(self);
|
||||
}
|
||||
@ -269,14 +447,22 @@ pub fn redraw(self: *Surface) void {
|
||||
|
||||
/// Close this surface.
|
||||
pub fn close(self: *Surface, processActive: bool) void {
|
||||
// If we're not part of a window hierarchy, we never confirm
|
||||
// so we can just directly remove ourselves and exit.
|
||||
const window = self.container.window() orelse {
|
||||
self.container.remove();
|
||||
return;
|
||||
};
|
||||
|
||||
// If we have no process active we can just exit immediately.
|
||||
if (!processActive) {
|
||||
self.window.closeSurface(self);
|
||||
self.container.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup our basic message
|
||||
const alert = c.gtk_message_dialog_new(
|
||||
self.window.window,
|
||||
window.window,
|
||||
c.GTK_DIALOG_MODAL,
|
||||
c.GTK_MESSAGE_QUESTION,
|
||||
c.GTK_BUTTONS_YES_NO,
|
||||
@ -336,27 +522,91 @@ pub fn controlInspector(self: *Surface, mode: input.InspectorMode) void {
|
||||
}
|
||||
|
||||
pub fn toggleFullscreen(self: *Surface, mac_non_native: configpkg.NonNativeFullscreen) void {
|
||||
self.window.toggleFullscreen(mac_non_native);
|
||||
const window = self.container.window() orelse {
|
||||
log.info(
|
||||
"toggleFullscreen invalid for container={s}",
|
||||
.{@tagName(self.container)},
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
window.toggleFullscreen(mac_non_native);
|
||||
}
|
||||
|
||||
pub fn getTitleLabel(self: *Surface) ?*c.GtkWidget {
|
||||
switch (self.title) {
|
||||
.none => return null,
|
||||
.label => |label| {
|
||||
const widget = @as(*c.GtkWidget, @ptrCast(@alignCast(label)));
|
||||
return widget;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn newSplit(self: *Surface, direction: input.SplitDirection) !void {
|
||||
const alloc = self.app.core_app.alloc;
|
||||
_ = try Split.create(alloc, self, direction);
|
||||
}
|
||||
|
||||
pub fn gotoSplit(self: *const Surface, direction: input.SplitFocusDirection) void {
|
||||
const s = self.container.split() orelse return;
|
||||
const map = s.directionMap(switch (self.container) {
|
||||
.split_tl => .top_left,
|
||||
.split_br => .bottom_right,
|
||||
.none, .tab_ => unreachable,
|
||||
});
|
||||
const surface_ = map.get(direction) orelse return;
|
||||
if (surface_) |surface| surface.grabFocus();
|
||||
}
|
||||
|
||||
pub fn newTab(self: *Surface) !void {
|
||||
try self.window.newTab(&self.core_surface);
|
||||
const window = self.container.window() orelse {
|
||||
log.info("surface cannot create new tab when not attached to a window", .{});
|
||||
return;
|
||||
};
|
||||
|
||||
try window.newTab(&self.core_surface);
|
||||
}
|
||||
|
||||
pub fn hasTabs(self: *const Surface) bool {
|
||||
return self.window.hasTabs();
|
||||
const window = self.container.window() orelse return false;
|
||||
return window.hasTabs();
|
||||
}
|
||||
|
||||
pub fn gotoPreviousTab(self: *Surface) void {
|
||||
self.window.gotoPreviousTab(self);
|
||||
const window = self.container.window() orelse {
|
||||
log.info(
|
||||
"gotoPreviousTab invalid for container={s}",
|
||||
.{@tagName(self.container)},
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
window.gotoPreviousTab(self);
|
||||
}
|
||||
|
||||
pub fn gotoNextTab(self: *Surface) void {
|
||||
self.window.gotoNextTab(self);
|
||||
const window = self.container.window() orelse {
|
||||
log.info(
|
||||
"gotoNextTab invalid for container={s}",
|
||||
.{@tagName(self.container)},
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
window.gotoNextTab(self);
|
||||
}
|
||||
|
||||
pub fn gotoTab(self: *Surface, n: usize) void {
|
||||
self.window.gotoTab(n);
|
||||
const window = self.container.window() orelse {
|
||||
log.info(
|
||||
"gotoTab invalid for container={s}",
|
||||
.{@tagName(self.container)},
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
window.gotoTab(n);
|
||||
}
|
||||
|
||||
pub fn setShouldClose(self: *Surface) void {
|
||||
@ -380,10 +630,13 @@ pub fn getSize(self: *const Surface) !apprt.SurfaceSize {
|
||||
}
|
||||
|
||||
pub fn setInitialWindowSize(self: *const Surface, width: u32, height: u32) !void {
|
||||
// This operation only makes sense if we're within a window view hierarchy.
|
||||
const window = self.container.window() orelse return;
|
||||
|
||||
// Note: this doesn't properly take into account the window decorations.
|
||||
// I'm not currently sure how to do that.
|
||||
c.gtk_window_set_default_size(
|
||||
@ptrCast(self.window.window),
|
||||
@ptrCast(window.window),
|
||||
@intCast(width),
|
||||
@intCast(height),
|
||||
);
|
||||
@ -401,24 +654,39 @@ pub fn setSizeLimits(self: *Surface, min: apprt.SurfaceSize, max_: ?apprt.Surfac
|
||||
_ = max_;
|
||||
}
|
||||
|
||||
pub fn setTitle(self: *Surface, slice: [:0]const u8) !void {
|
||||
switch (self.title) {
|
||||
.none => {},
|
||||
pub fn grabFocus(self: *Surface) void {
|
||||
if (self.container.tab()) |tab| tab.focus_child = self;
|
||||
|
||||
.label => |label| {
|
||||
c.gtk_label_set_text(label, slice.ptr);
|
||||
self.updateTitleLabels();
|
||||
const widget = @as(*c.GtkWidget, @ptrCast(self.gl_area));
|
||||
_ = c.gtk_widget_grab_focus(widget);
|
||||
}
|
||||
|
||||
const widget = @as(*c.GtkWidget, @ptrCast(self.gl_area));
|
||||
if (c.gtk_widget_is_focus(widget) == 1) {
|
||||
c.gtk_window_set_title(self.window.window, c.gtk_label_get_text(label));
|
||||
}
|
||||
},
|
||||
fn updateTitleLabels(self: *Surface) void {
|
||||
// If we have no title, then we have nothing to update.
|
||||
const title = self.title_text orelse return;
|
||||
|
||||
// If we have a tab, then we have to update the tab
|
||||
if (self.container.tab()) |tab| {
|
||||
c.gtk_label_set_text(tab.label_text, title.ptr);
|
||||
}
|
||||
|
||||
// const root = c.gtk_widget_get_root(@ptrCast(
|
||||
// *c.GtkWidget,
|
||||
// self.gl_area,
|
||||
// ));
|
||||
// If we have a window, then we have to update the window title.
|
||||
if (self.container.window()) |window| {
|
||||
c.gtk_window_set_title(window.window, title.ptr);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setTitle(self: *Surface, slice: [:0]const u8) !void {
|
||||
const alloc = self.app.core_app.alloc;
|
||||
const copy = try alloc.dupeZ(u8, slice);
|
||||
errdefer alloc.free(copy);
|
||||
|
||||
if (self.title_text) |old| alloc.free(old);
|
||||
self.title_text = copy;
|
||||
|
||||
const widget = @as(*c.GtkWidget, @ptrCast(self.gl_area));
|
||||
if (c.gtk_widget_is_focus(widget) == 1) self.updateTitleLabels();
|
||||
}
|
||||
|
||||
pub fn setMouseShape(
|
||||
@ -672,6 +940,7 @@ fn gtkRealize(area: *c.GtkGLArea, ud: ?*anyopaque) callconv(.C) void {
|
||||
fn gtkUnrealize(area: *c.GtkGLArea, ud: ?*anyopaque) callconv(.C) void {
|
||||
_ = area;
|
||||
|
||||
log.debug("gl surface unrealized", .{});
|
||||
const self = userdataSelf(ud.?);
|
||||
self.core_surface.renderer.displayUnrealized();
|
||||
|
||||
@ -705,8 +974,8 @@ fn gtkResize(area: *c.GtkGLArea, width: c.gint, height: c.gint, ud: ?*anyopaque)
|
||||
};
|
||||
|
||||
const window_scale_factor = scale: {
|
||||
const window = @as(*c.GtkNative, @ptrCast(self.window.window));
|
||||
const gdk_surface = c.gtk_native_get_surface(window);
|
||||
const window = self.container.window() orelse break :scale 0;
|
||||
const gdk_surface = c.gtk_native_get_surface(@ptrCast(window.window));
|
||||
break :scale c.gdk_surface_get_scale_factor(gdk_surface);
|
||||
};
|
||||
|
||||
@ -788,7 +1057,7 @@ fn gtkMouseDown(
|
||||
// If we don't have focus, grab it.
|
||||
const gl_widget = @as(*c.GtkWidget, @ptrCast(self.gl_area));
|
||||
if (c.gtk_widget_has_focus(gl_widget) == 0) {
|
||||
_ = c.gtk_widget_grab_focus(gl_widget);
|
||||
self.grabFocus();
|
||||
}
|
||||
|
||||
self.core_surface.mouseButtonCallback(.press, button, mods) catch |err| {
|
||||
@ -1249,7 +1518,7 @@ fn gtkCloseConfirmation(
|
||||
c.gtk_window_destroy(@ptrCast(alert));
|
||||
if (response == c.GTK_RESPONSE_YES) {
|
||||
const self = userdataSelf(ud.?);
|
||||
self.window.closeSurface(self);
|
||||
self.container.remove();
|
||||
}
|
||||
}
|
||||
|
||||
|
187
src/apprt/gtk/Tab.zig
Normal file
187
src/apprt/gtk/Tab.zig
Normal file
@ -0,0 +1,187 @@
|
||||
/// The state associated with a single tab in the window.
|
||||
///
|
||||
/// A tab can contain one or more terminals due to splits.
|
||||
const Tab = @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 Window = @import("Window.zig");
|
||||
const c = @import("c.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
|
||||
pub const GHOSTTY_TAB = "ghostty_tab";
|
||||
|
||||
/// The window that owns this tab.
|
||||
window: *Window,
|
||||
|
||||
/// The tab label. The tab label is the text that appears on the tab.
|
||||
label_text: *c.GtkLabel,
|
||||
|
||||
/// We'll put our children into this box instead of packing them
|
||||
/// directly, so that we can send the box into `c.g_signal_connect_data`
|
||||
/// for the close button
|
||||
box: *c.GtkBox,
|
||||
|
||||
/// The element of this tab so that we can handle splits and so on.
|
||||
elem: Surface.Container.Elem,
|
||||
|
||||
// We'll update this every time a Surface gains focus, so that we have it
|
||||
// when we switch to another Tab. Then when we switch back to this tab, we
|
||||
// can easily re-focus that terminal.
|
||||
focus_child: *Surface,
|
||||
|
||||
pub fn create(alloc: Allocator, window: *Window, parent_: ?*CoreSurface) !*Tab {
|
||||
var tab = try alloc.create(Tab);
|
||||
errdefer alloc.destroy(tab);
|
||||
try tab.init(window, parent_);
|
||||
return tab;
|
||||
}
|
||||
|
||||
/// Initialize the tab, create a surface, and add it to the window. "self"
|
||||
/// needs to be a stable pointer, since it is used for GTK events.
|
||||
pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void {
|
||||
self.* = .{
|
||||
.window = window,
|
||||
.label_text = undefined,
|
||||
.box = undefined,
|
||||
.elem = undefined,
|
||||
.focus_child = undefined,
|
||||
};
|
||||
|
||||
// Build the tab label
|
||||
const label_box_widget = c.gtk_box_new(c.GTK_ORIENTATION_HORIZONTAL, 0);
|
||||
const label_box = @as(*c.GtkBox, @ptrCast(label_box_widget));
|
||||
const label_text_widget = c.gtk_label_new("Ghostty");
|
||||
const label_text: *c.GtkLabel = @ptrCast(label_text_widget);
|
||||
c.gtk_box_append(label_box, label_text_widget);
|
||||
self.label_text = label_text;
|
||||
|
||||
// Build the close button for the tab
|
||||
const label_close_widget = c.gtk_button_new_from_icon_name("window-close");
|
||||
const label_close: *c.GtkButton = @ptrCast(label_close_widget);
|
||||
c.gtk_button_set_has_frame(label_close, 0);
|
||||
c.gtk_box_append(label_box, label_close_widget);
|
||||
|
||||
// Wide style GTK tabs
|
||||
if (window.app.config.@"gtk-wide-tabs") {
|
||||
c.gtk_widget_set_hexpand(label_box_widget, 1);
|
||||
c.gtk_widget_set_halign(label_box_widget, c.GTK_ALIGN_FILL);
|
||||
c.gtk_widget_set_hexpand(label_text_widget, 1);
|
||||
c.gtk_widget_set_halign(label_text_widget, c.GTK_ALIGN_FILL);
|
||||
|
||||
// This ensures that tabs are always equal width. If they're too
|
||||
// long, they'll be truncated with an ellipsis.
|
||||
c.gtk_label_set_max_width_chars(label_text, 1);
|
||||
c.gtk_label_set_ellipsize(label_text, c.PANGO_ELLIPSIZE_END);
|
||||
|
||||
// We need to set a minimum width so that at a certain point
|
||||
// the notebook will have an arrow button rather than shrinking tabs
|
||||
// to an unreadably small size.
|
||||
c.gtk_widget_set_size_request(label_text_widget, 100, 1);
|
||||
}
|
||||
|
||||
// Create a Box in which we'll later keep either Surface or Split.
|
||||
// Using a box makes it easier to maintain the tab contents because
|
||||
// we never need to change the root widget of the notebook page (tab).
|
||||
const box_widget = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 0);
|
||||
c.gtk_widget_set_hexpand(box_widget, 1);
|
||||
c.gtk_widget_set_vexpand(box_widget, 1);
|
||||
self.box = @ptrCast(box_widget);
|
||||
|
||||
// Create the initial surface since all tabs start as a single non-split
|
||||
var surface = try Surface.create(window.app.core_app.alloc, window.app, .{
|
||||
.parent = parent_,
|
||||
});
|
||||
errdefer surface.unref();
|
||||
surface.container = .{ .tab_ = self };
|
||||
self.elem = .{ .surface = surface };
|
||||
|
||||
// Add Surface to the Tab
|
||||
const gl_area_widget = @as(*c.GtkWidget, @ptrCast(surface.gl_area));
|
||||
c.gtk_box_append(self.box, gl_area_widget);
|
||||
|
||||
// Add the notebook page (create tab). We create the tab after our
|
||||
// current selected tab if we have one.
|
||||
const page_idx = c.gtk_notebook_insert_page(
|
||||
window.notebook,
|
||||
box_widget,
|
||||
label_box_widget,
|
||||
c.gtk_notebook_get_current_page(window.notebook) + 1,
|
||||
);
|
||||
if (page_idx < 0) {
|
||||
log.warn("failed to add page to notebook", .{});
|
||||
return error.GtkAppendPageFailed;
|
||||
}
|
||||
|
||||
// Tab settings
|
||||
c.gtk_notebook_set_tab_reorderable(window.notebook, box_widget, 1);
|
||||
c.gtk_notebook_set_tab_detachable(window.notebook, box_widget, 1);
|
||||
|
||||
// If we have multiple tabs, show the tab bar.
|
||||
if (c.gtk_notebook_get_n_pages(window.notebook) > 1) {
|
||||
c.gtk_notebook_set_show_tabs(window.notebook, 1);
|
||||
}
|
||||
|
||||
// Set the userdata of the box to point to this tab.
|
||||
c.g_object_set_data(@ptrCast(box_widget), GHOSTTY_TAB, self);
|
||||
|
||||
// Attach all events
|
||||
_ = c.g_signal_connect_data(label_close, "clicked", c.G_CALLBACK(>kTabCloseClick), self, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(box_widget, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT);
|
||||
|
||||
// Switch to the new tab
|
||||
c.gtk_notebook_set_current_page(window.notebook, page_idx);
|
||||
|
||||
// We need to grab focus after Surface and Tab is added to the window. When
|
||||
// creating a Tab we want to always focus on the widget.
|
||||
surface.grabFocus();
|
||||
}
|
||||
|
||||
/// Deinits tab by deiniting child elem.
|
||||
pub fn deinit(self: *Tab, alloc: Allocator) void {
|
||||
self.elem.deinit(alloc);
|
||||
}
|
||||
|
||||
/// Deinit and deallocate the tab.
|
||||
pub fn destroy(self: *Tab, alloc: Allocator) void {
|
||||
self.deinit(alloc);
|
||||
alloc.destroy(self);
|
||||
}
|
||||
|
||||
// TODO: move this
|
||||
/// Replace the surface element that this tab is showing.
|
||||
pub fn replaceElem(self: *Tab, elem: Surface.Container.Elem) void {
|
||||
// Remove our previous widget
|
||||
c.gtk_box_remove(self.box, self.elem.widget());
|
||||
|
||||
// Add our new one
|
||||
c.gtk_box_append(self.box, elem.widget());
|
||||
self.elem = elem;
|
||||
}
|
||||
|
||||
/// Remove this tab from the window.
|
||||
pub fn remove(self: *Tab) void {
|
||||
self.window.closeTab(self);
|
||||
}
|
||||
|
||||
fn gtkTabCloseClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void {
|
||||
const tab: *Tab = @ptrCast(@alignCast(ud));
|
||||
const window = tab.window;
|
||||
window.closeTab(tab);
|
||||
}
|
||||
|
||||
fn gtkDestroy(v: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void {
|
||||
_ = v;
|
||||
log.debug("tab box destroy", .{});
|
||||
|
||||
// When our box is destroyed, we want to destroy our tab, too.
|
||||
const tab: *Tab = @ptrCast(@alignCast(ud));
|
||||
tab.destroy(tab.window.app.core_app.alloc);
|
||||
}
|
@ -1,4 +1,8 @@
|
||||
/// A Window is a single, real GTK window.
|
||||
/// A Window is a single, real GTK window that holds terminal surfaces.
|
||||
///
|
||||
/// A Window always contains a notebook (what GTK calls a tabbed container)
|
||||
/// even while no tabs are in use, because a notebook without a tab bar has
|
||||
/// no visible UI chrome.
|
||||
const Window = @This();
|
||||
|
||||
const std = @import("std");
|
||||
@ -8,17 +12,17 @@ const Allocator = std.mem.Allocator;
|
||||
const assert = std.debug.assert;
|
||||
const configpkg = @import("../../config.zig");
|
||||
const font = @import("../../font/main.zig");
|
||||
const input = @import("../../input.zig");
|
||||
const CoreSurface = @import("../../Surface.zig");
|
||||
|
||||
const App = @import("App.zig");
|
||||
const Surface = @import("Surface.zig");
|
||||
const Tab = @import("Tab.zig");
|
||||
const icon = @import("icon.zig");
|
||||
const c = @import("c.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
|
||||
const GL_AREA_SURFACE = "gl_area_surface";
|
||||
|
||||
app: *App,
|
||||
|
||||
/// Our window
|
||||
@ -184,102 +188,25 @@ pub fn deinit(self: *Window) void {
|
||||
}
|
||||
|
||||
/// Add a new tab to this window.
|
||||
pub fn newTab(self: *Window, parent_: ?*CoreSurface) !void {
|
||||
// Grab a surface allocation we'll need it later.
|
||||
var surface = try self.app.core_app.alloc.create(Surface);
|
||||
errdefer self.app.core_app.alloc.destroy(surface);
|
||||
pub fn newTab(self: *Window, parent: ?*CoreSurface) !void {
|
||||
const alloc = self.app.core_app.alloc;
|
||||
_ = try Tab.create(alloc, self, parent);
|
||||
|
||||
// Inherit the parent's font size if we are configured to.
|
||||
const font_size: ?font.face.DesiredSize = font_size: {
|
||||
if (!self.app.config.@"window-inherit-font-size") break :font_size null;
|
||||
const parent = parent_ orelse break :font_size null;
|
||||
break :font_size parent.font_size;
|
||||
};
|
||||
|
||||
// Build our tab label
|
||||
const label_box_widget = c.gtk_box_new(c.GTK_ORIENTATION_HORIZONTAL, 0);
|
||||
const label_box = @as(*c.GtkBox, @ptrCast(label_box_widget));
|
||||
const label_text = c.gtk_label_new("Ghostty");
|
||||
c.gtk_box_append(label_box, label_text);
|
||||
|
||||
// Wide style GTK tabs
|
||||
if (self.app.config.@"gtk-wide-tabs") {
|
||||
c.gtk_widget_set_hexpand(label_box_widget, 1);
|
||||
c.gtk_widget_set_halign(label_box_widget, c.GTK_ALIGN_FILL);
|
||||
c.gtk_widget_set_hexpand(label_text, 1);
|
||||
c.gtk_widget_set_halign(label_text, c.GTK_ALIGN_FILL);
|
||||
|
||||
// This ensures that tabs are always equal width. If they're too
|
||||
// long, they'll be truncated with an ellipsis.
|
||||
c.gtk_label_set_max_width_chars(@ptrCast(label_text), 1);
|
||||
c.gtk_label_set_ellipsize(@ptrCast(label_text), c.PANGO_ELLIPSIZE_END);
|
||||
|
||||
// We need to set a minimum width so that at a certain point
|
||||
// the notebook will have an arrow button rather than shrinking tabs
|
||||
// to an unreadably small size.
|
||||
c.gtk_widget_set_size_request(@ptrCast(label_text), 100, 1);
|
||||
}
|
||||
|
||||
const label_close_widget = c.gtk_button_new_from_icon_name("window-close");
|
||||
const label_close = @as(*c.GtkButton, @ptrCast(label_close_widget));
|
||||
c.gtk_button_set_has_frame(label_close, 0);
|
||||
c.gtk_box_append(label_box, label_close_widget);
|
||||
_ = c.g_signal_connect_data(label_close, "clicked", c.G_CALLBACK(>kTabCloseClick), surface, null, c.G_CONNECT_DEFAULT);
|
||||
|
||||
// Initialize the GtkGLArea and attach it to our surface.
|
||||
// The surface starts in the "unrealized" state because we have to
|
||||
// wait for the "realize" callback from GTK to know that the OpenGL
|
||||
// context is ready. See Surface docs for more info.
|
||||
const gl_area = c.gtk_gl_area_new();
|
||||
c.gtk_widget_set_cursor_from_name(gl_area, "text");
|
||||
try surface.init(self.app, .{
|
||||
.window = self,
|
||||
.gl_area = @ptrCast(gl_area),
|
||||
.title_label = @ptrCast(label_text),
|
||||
.font_size = font_size,
|
||||
.parent = parent_ != null,
|
||||
});
|
||||
errdefer surface.deinit();
|
||||
|
||||
// Add the notebook page (create tab). We create the tab after our
|
||||
// current selected tab if we have one.
|
||||
const page_idx = c.gtk_notebook_insert_page(
|
||||
self.notebook,
|
||||
gl_area,
|
||||
label_box_widget,
|
||||
c.gtk_notebook_get_current_page(self.notebook) + 1,
|
||||
);
|
||||
if (page_idx < 0) {
|
||||
log.warn("failed to add surface to notebook", .{});
|
||||
return error.GtkAppendPageFailed;
|
||||
}
|
||||
|
||||
// Tab settings
|
||||
c.gtk_notebook_set_tab_reorderable(self.notebook, gl_area, 1);
|
||||
c.gtk_notebook_set_tab_detachable(self.notebook, gl_area, 1);
|
||||
|
||||
// If we have multiple tabs, show the tab bar.
|
||||
if (c.gtk_notebook_get_n_pages(self.notebook) > 1) {
|
||||
c.gtk_notebook_set_show_tabs(self.notebook, 1);
|
||||
}
|
||||
|
||||
// Set the userdata of the close button so it points to this page.
|
||||
c.g_object_set_data(@ptrCast(gl_area), GL_AREA_SURFACE, surface);
|
||||
|
||||
// Switch to the new tab
|
||||
c.gtk_notebook_set_current_page(self.notebook, page_idx);
|
||||
|
||||
// We need to grab focus after it is added to the window. When
|
||||
// creating a window we want to always focus on the widget.
|
||||
const widget = @as(*c.GtkWidget, @ptrCast(gl_area));
|
||||
_ = c.gtk_widget_grab_focus(widget);
|
||||
// TODO: When this is triggered through a GTK action, the new surface
|
||||
// redraws correctly. When it's triggered through keyboard shortcuts, it
|
||||
// does not (cursor doesn't blink) unless reactivated by refocusing.
|
||||
}
|
||||
|
||||
/// Close the tab for the given notebook page. This will automatically
|
||||
/// handle closing the window if there are no more tabs.
|
||||
fn closeTab(self: *Window, page: *c.GtkNotebookPage) void {
|
||||
// Remove the page
|
||||
pub fn closeTab(self: *Window, tab: *Tab) void {
|
||||
const page = c.gtk_notebook_get_page(self.notebook, @ptrCast(tab.box)) orelse return;
|
||||
|
||||
// Find page and tab which we're closing
|
||||
const page_idx = getNotebookPageIndex(page);
|
||||
|
||||
// Remove the page. This will destroy the GTK widgets in the page which
|
||||
// will trigger Tab cleanup.
|
||||
c.gtk_notebook_remove_page(self.notebook, page_idx);
|
||||
|
||||
const remaining = c.gtk_notebook_get_n_pages(self.notebook);
|
||||
@ -297,14 +224,6 @@ fn closeTab(self: *Window, page: *c.GtkNotebookPage) void {
|
||||
if (remaining > 0) self.focusCurrentTab();
|
||||
}
|
||||
|
||||
/// Close the surface. This surface must be definitely part of this window.
|
||||
pub fn closeSurface(self: *Window, surface: *Surface) void {
|
||||
assert(surface.window == self);
|
||||
|
||||
const page = c.gtk_notebook_get_page(self.notebook, @ptrCast(surface.gl_area)) orelse return;
|
||||
self.closeTab(page);
|
||||
}
|
||||
|
||||
/// Returns true if this window has any tabs.
|
||||
pub fn hasTabs(self: *const Window) bool {
|
||||
return c.gtk_notebook_get_n_pages(self.notebook) > 1;
|
||||
@ -312,7 +231,12 @@ pub fn hasTabs(self: *const Window) bool {
|
||||
|
||||
/// Go to the previous tab for a surface.
|
||||
pub fn gotoPreviousTab(self: *Window, surface: *Surface) void {
|
||||
const page = c.gtk_notebook_get_page(self.notebook, @ptrCast(surface.gl_area)) orelse return;
|
||||
const tab = surface.container.tab() orelse {
|
||||
log.info("surface is not attached to a tab bar, cannot navigate", .{});
|
||||
return;
|
||||
};
|
||||
|
||||
const page = c.gtk_notebook_get_page(self.notebook, @ptrCast(tab.box)) orelse return;
|
||||
const page_idx = getNotebookPageIndex(page);
|
||||
|
||||
// The next index is the previous or we wrap around.
|
||||
@ -330,7 +254,12 @@ pub fn gotoPreviousTab(self: *Window, surface: *Surface) void {
|
||||
|
||||
/// Go to the next tab for a surface.
|
||||
pub fn gotoNextTab(self: *Window, surface: *Surface) void {
|
||||
const page = c.gtk_notebook_get_page(self.notebook, @ptrCast(surface.gl_area)) orelse return;
|
||||
const tab = surface.container.tab() orelse {
|
||||
log.info("surface is not attached to a tab bar, cannot navigate", .{});
|
||||
return;
|
||||
};
|
||||
|
||||
const page = c.gtk_notebook_get_page(self.notebook, @ptrCast(tab.box)) orelse return;
|
||||
const page_idx = getNotebookPageIndex(page);
|
||||
const max = c.gtk_notebook_get_n_pages(self.notebook) -| 1;
|
||||
const next_idx = if (page_idx < max) page_idx + 1 else 0;
|
||||
@ -364,13 +293,12 @@ pub fn toggleFullscreen(self: *Window, _: configpkg.NonNativeFullscreen) void {
|
||||
/// Grabs focus on the currently selected tab.
|
||||
fn focusCurrentTab(self: *Window) void {
|
||||
const page_idx = c.gtk_notebook_get_current_page(self.notebook);
|
||||
const widget = c.gtk_notebook_get_nth_page(self.notebook, page_idx);
|
||||
_ = c.gtk_widget_grab_focus(widget);
|
||||
}
|
||||
|
||||
fn gtkTabCloseClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void {
|
||||
const surface: *Surface = @ptrCast(@alignCast(ud));
|
||||
surface.core_surface.close();
|
||||
const page = c.gtk_notebook_get_nth_page(self.notebook, page_idx);
|
||||
const tab: *Tab = @ptrCast(@alignCast(
|
||||
c.g_object_get_data(@ptrCast(page), Tab.GHOSTTY_TAB) orelse return,
|
||||
));
|
||||
const gl_area = @as(*c.GtkWidget, @ptrCast(tab.focus_child.gl_area));
|
||||
_ = c.gtk_widget_grab_focus(gl_area);
|
||||
}
|
||||
|
||||
// Note: we MUST NOT use the GtkButton parameter because gtkActionNewTab
|
||||
@ -423,23 +351,23 @@ fn gtkNotebookCreateWindow(
|
||||
page: *c.GtkWidget,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) ?*c.GtkNotebook {
|
||||
// The surface for the page is stored in the widget data.
|
||||
const surface: *Surface = @ptrCast(@alignCast(
|
||||
c.g_object_get_data(@ptrCast(page), GL_AREA_SURFACE) orelse return null,
|
||||
// The tab for the page is stored in the widget data.
|
||||
const tab: *Tab = @ptrCast(@alignCast(
|
||||
c.g_object_get_data(@ptrCast(page), Tab.GHOSTTY_TAB) orelse return null,
|
||||
));
|
||||
|
||||
const self = userdataSelf(ud.?);
|
||||
const alloc = self.app.core_app.alloc;
|
||||
const currentWindow = userdataSelf(ud.?);
|
||||
const alloc = currentWindow.app.core_app.alloc;
|
||||
const app = currentWindow.app;
|
||||
|
||||
// Create a new window
|
||||
const window = Window.create(alloc, self.app) catch |err| {
|
||||
const window = Window.create(alloc, app) catch |err| {
|
||||
log.warn("error creating new window error={}", .{err});
|
||||
return null;
|
||||
};
|
||||
|
||||
// We need to update our surface to point to the new window so that
|
||||
// events such as new tab go to the right window.
|
||||
surface.window = window;
|
||||
// And add it to the new window.
|
||||
tab.window = window;
|
||||
|
||||
return window.notebook;
|
||||
}
|
||||
@ -451,8 +379,9 @@ fn gtkCloseRequest(v: *c.GtkWindow, ud: ?*anyopaque) callconv(.C) bool {
|
||||
|
||||
// If none of our surfaces need confirmation, we can just exit.
|
||||
for (self.app.core_app.surfaces.items) |surface| {
|
||||
if (surface.window == self) {
|
||||
if (surface.core_surface.needsConfirmQuit()) break;
|
||||
if (surface.container.window()) |window| {
|
||||
if (window == self and
|
||||
surface.core_surface.needsConfirmQuit()) break;
|
||||
}
|
||||
} else {
|
||||
c.gtk_window_destroy(self.window);
|
||||
@ -602,10 +531,10 @@ fn gtkActionToggleInspector(
|
||||
fn actionSurface(self: *Window) ?*CoreSurface {
|
||||
const page_idx = c.gtk_notebook_get_current_page(self.notebook);
|
||||
const page = c.gtk_notebook_get_nth_page(self.notebook, page_idx);
|
||||
const surface: *Surface = @ptrCast(@alignCast(
|
||||
c.g_object_get_data(@ptrCast(page), GL_AREA_SURFACE) orelse return null,
|
||||
const tab: *Tab = @ptrCast(@alignCast(
|
||||
c.g_object_get_data(@ptrCast(page), Tab.GHOSTTY_TAB) orelse return null,
|
||||
));
|
||||
return &surface.core_surface;
|
||||
return &tab.focus_child.core_surface;
|
||||
}
|
||||
|
||||
fn userdataSelf(ud: *anyopaque) *Window {
|
||||
|
Reference in New Issue
Block a user