mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
apprt/gtk-ng: tabs are back! (#8098)
This brings back all tabbing behaviors. I ran through create/close tabs and windows with Valgrind and everything ran clean. TODO: - [x] goto tab keybinding - [x] move tab - [x] toggle tab overview (binding only, UI works!) - [x] create window action to pull tab out into a window
This commit is contained in:
@ -37,12 +37,13 @@ pub const blueprints: []const Blueprint = &.{
|
||||
.{ .major = 1, .minor = 4, .name = "clipboard-confirmation-dialog" },
|
||||
.{ .major = 1, .minor = 2, .name = "close-confirmation-dialog" },
|
||||
.{ .major = 1, .minor = 2, .name = "config-errors-dialog" },
|
||||
.{ .major = 1, .minor = 2, .name = "debug-warning" },
|
||||
.{ .major = 1, .minor = 3, .name = "debug-warning" },
|
||||
.{ .major = 1, .minor = 2, .name = "resize-overlay" },
|
||||
.{ .major = 1, .minor = 2, .name = "surface" },
|
||||
.{ .major = 1, .minor = 3, .name = "surface-child-exited" },
|
||||
.{ .major = 1, .minor = 5, .name = "tab" },
|
||||
.{ .major = 1, .minor = 5, .name = "window" },
|
||||
.{ .major = 1, .minor = 2, .name = "debug-warning" },
|
||||
.{ .major = 1, .minor = 3, .name = "debug-warning" },
|
||||
};
|
||||
|
||||
/// CSS files in css_path
|
||||
|
@ -22,6 +22,7 @@ const xev = @import("../../../global.zig").xev;
|
||||
const CoreConfig = configpkg.Config;
|
||||
const CoreSurface = @import("../../../Surface.zig");
|
||||
|
||||
const ext = @import("../ext.zig");
|
||||
const adw_version = @import("../adw_version.zig");
|
||||
const gtk_version = @import("../gtk_version.zig");
|
||||
const winprotopkg = @import("../winproto.zig");
|
||||
@ -496,10 +497,16 @@ pub const Application = extern struct {
|
||||
value.config,
|
||||
),
|
||||
|
||||
.goto_tab => return Action.gotoTab(target, value),
|
||||
|
||||
.mouse_over_link => Action.mouseOverLink(target, value),
|
||||
.mouse_shape => Action.mouseShape(target, value),
|
||||
.mouse_visibility => Action.mouseVisibility(target, value),
|
||||
|
||||
.move_tab => return Action.moveTab(target, value),
|
||||
|
||||
.new_tab => return Action.newTab(target),
|
||||
|
||||
.new_window => try Action.newWindow(
|
||||
self,
|
||||
switch (target) {
|
||||
@ -530,11 +537,9 @@ pub const Application = extern struct {
|
||||
|
||||
.toggle_maximize => Action.toggleMaximize(target),
|
||||
.toggle_fullscreen => Action.toggleFullscreen(target),
|
||||
.toggle_tab_overview => return Action.toggleTabOverview(target),
|
||||
|
||||
// Unimplemented but todo on gtk-ng branch
|
||||
.new_tab,
|
||||
.goto_tab,
|
||||
.move_tab,
|
||||
.new_split,
|
||||
.resize_split,
|
||||
.equalize_splits,
|
||||
@ -545,7 +550,6 @@ pub const Application = extern struct {
|
||||
.present_terminal,
|
||||
.initial_size,
|
||||
.size_limit,
|
||||
.toggle_tab_overview,
|
||||
.toggle_split_zoom,
|
||||
.toggle_window_decorations,
|
||||
.prompt_title,
|
||||
@ -1214,6 +1218,32 @@ const Action = struct {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn gotoTab(
|
||||
target: apprt.Target,
|
||||
tab: apprt.action.GotoTab,
|
||||
) bool {
|
||||
switch (target) {
|
||||
.app => return false,
|
||||
.surface => |core| {
|
||||
const surface = core.rt_surface.surface;
|
||||
const window = ext.getAncestor(
|
||||
Window,
|
||||
surface.as(gtk.Widget),
|
||||
) orelse {
|
||||
log.warn("surface is not in a window, ignoring new_tab", .{});
|
||||
return false;
|
||||
};
|
||||
|
||||
return window.selectTab(switch (tab) {
|
||||
.previous => .previous,
|
||||
.next => .next,
|
||||
.last => .last,
|
||||
else => .{ .n = @intCast(@intFromEnum(tab)) },
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mouseOverLink(
|
||||
target: apprt.Target,
|
||||
value: apprt.action.MouseOverLink,
|
||||
@ -1272,11 +1302,60 @@ const Action = struct {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn moveTab(
|
||||
target: apprt.Target,
|
||||
value: apprt.action.MoveTab,
|
||||
) bool {
|
||||
switch (target) {
|
||||
.app => return false,
|
||||
.surface => |core| {
|
||||
const surface = core.rt_surface.surface;
|
||||
const window = ext.getAncestor(
|
||||
Window,
|
||||
surface.as(gtk.Widget),
|
||||
) orelse {
|
||||
log.warn("surface is not in a window, ignoring new_tab", .{});
|
||||
return false;
|
||||
};
|
||||
|
||||
return window.moveTab(
|
||||
surface,
|
||||
@intCast(value.amount),
|
||||
);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn newTab(target: apprt.Target) bool {
|
||||
switch (target) {
|
||||
.app => {
|
||||
log.warn("new tab to app is unexpected", .{});
|
||||
return false;
|
||||
},
|
||||
|
||||
.surface => |core| {
|
||||
// Get the window ancestor of the surface. Surfaces shouldn't
|
||||
// be aware they might be in windows but at the app level we
|
||||
// can do this.
|
||||
const surface = core.rt_surface.surface;
|
||||
const window = ext.getAncestor(
|
||||
Window,
|
||||
surface.as(gtk.Widget),
|
||||
) orelse {
|
||||
log.warn("surface is not in a window, ignoring new_tab", .{});
|
||||
return false;
|
||||
};
|
||||
window.newTab(core);
|
||||
return true;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn newWindow(
|
||||
self: *Application,
|
||||
parent: ?*CoreSurface,
|
||||
) !void {
|
||||
const win = Window.new(self, parent);
|
||||
const win = Window.new(self);
|
||||
|
||||
// Setup a binding so that whenever our config changes so does the
|
||||
// window. There's never a time when the window config should be out
|
||||
@ -1289,6 +1368,9 @@ const Action = struct {
|
||||
.{},
|
||||
);
|
||||
|
||||
// Create a new tab
|
||||
win.newTab(parent);
|
||||
|
||||
// Show the window
|
||||
gtk.Window.present(win.as(gtk.Window));
|
||||
}
|
||||
@ -1434,6 +1516,25 @@ const Action = struct {
|
||||
.surface => |v| v.rt_surface.surface.toggleMaximize(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggleTabOverview(target: apprt.Target) bool {
|
||||
switch (target) {
|
||||
.app => return false,
|
||||
.surface => |core| {
|
||||
const surface = core.rt_surface.surface;
|
||||
const window = ext.getAncestor(
|
||||
Window,
|
||||
surface.as(gtk.Widget),
|
||||
) orelse {
|
||||
log.warn("surface is not in a window, ignoring new_tab", .{});
|
||||
return false;
|
||||
};
|
||||
|
||||
window.toggleTabOverview();
|
||||
return true;
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// This sets various GTK-related environment variables as necessary
|
||||
|
@ -57,6 +57,17 @@ pub const CloseConfirmationDialog = extern struct {
|
||||
void,
|
||||
);
|
||||
};
|
||||
|
||||
pub const cancel = struct {
|
||||
pub const name = "cancel";
|
||||
pub const connect = impl.connect;
|
||||
const impl = gobject.ext.defineSignal(
|
||||
name,
|
||||
Self,
|
||||
&.{},
|
||||
void,
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
const Private = struct {
|
||||
@ -72,14 +83,15 @@ pub const CloseConfirmationDialog = extern struct {
|
||||
|
||||
fn init(self: *Self, _: *Class) callconv(.C) void {
|
||||
gtk.Widget.initTemplate(self.as(gtk.Widget));
|
||||
}
|
||||
|
||||
pub fn present(self: *Self, parent: ?*gtk.Widget) void {
|
||||
// Setup our title/body text.
|
||||
const priv = self.private();
|
||||
self.as(Dialog.Parent).setHeading(priv.target.title());
|
||||
self.as(Dialog.Parent).setBody(priv.target.body());
|
||||
}
|
||||
|
||||
pub fn present(self: *Self, parent: ?*gtk.Widget) void {
|
||||
// Show it
|
||||
self.as(Dialog).present(parent);
|
||||
}
|
||||
|
||||
@ -91,13 +103,21 @@ pub const CloseConfirmationDialog = extern struct {
|
||||
self: *Self,
|
||||
response_id: [*:0]const u8,
|
||||
) callconv(.C) void {
|
||||
if (std.mem.orderZ(u8, response_id, "close") != .eq) return;
|
||||
signals.@"close-request".impl.emit(
|
||||
self,
|
||||
null,
|
||||
.{},
|
||||
null,
|
||||
);
|
||||
if (std.mem.orderZ(u8, response_id, "close") == .eq) {
|
||||
signals.@"close-request".impl.emit(
|
||||
self,
|
||||
null,
|
||||
.{},
|
||||
null,
|
||||
);
|
||||
} else {
|
||||
signals.cancel.impl.emit(
|
||||
self,
|
||||
null,
|
||||
.{},
|
||||
null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn dispose(self: *Self) callconv(.C) void {
|
||||
@ -141,6 +161,7 @@ pub const CloseConfirmationDialog = extern struct {
|
||||
|
||||
// Signals
|
||||
signals.@"close-request".impl.register(.{});
|
||||
signals.cancel.impl.register(.{});
|
||||
|
||||
// Virtual methods
|
||||
gobject.Object.virtual_methods.dispose.implement(class, &dispose);
|
||||
@ -158,11 +179,13 @@ pub const CloseConfirmationDialog = extern struct {
|
||||
/// together into one struct that is the sole source of truth.
|
||||
pub const Target = enum(c_int) {
|
||||
app,
|
||||
tab,
|
||||
window,
|
||||
|
||||
pub fn title(self: Target) [*:0]const u8 {
|
||||
return switch (self) {
|
||||
.app => i18n._("Quit Ghostty?"),
|
||||
.tab => i18n._("Close Tab?"),
|
||||
.window => i18n._("Close Window?"),
|
||||
};
|
||||
}
|
||||
@ -170,6 +193,7 @@ pub const Target = enum(c_int) {
|
||||
pub fn body(self: Target) [*:0]const u8 {
|
||||
return switch (self) {
|
||||
.app => i18n._("All terminal sessions will be terminated."),
|
||||
.tab => i18n._("All terminal sessions in this tab will be terminated."),
|
||||
.window => i18n._("All terminal sessions in this window will be terminated."),
|
||||
};
|
||||
}
|
||||
|
@ -1050,6 +1050,13 @@ pub const Surface = extern struct {
|
||||
);
|
||||
}
|
||||
|
||||
/// Focus this surface. This properly focuses the input part of
|
||||
/// our surface.
|
||||
pub fn grabFocus(self: *Self) void {
|
||||
const priv = self.private();
|
||||
_ = priv.gl_area.as(gtk.Widget).grabFocus();
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Virtual Methods
|
||||
|
||||
|
289
src/apprt/gtk-ng/class/tab.zig
Normal file
289
src/apprt/gtk-ng/class/tab.zig
Normal file
@ -0,0 +1,289 @@
|
||||
const std = @import("std");
|
||||
const build_config = @import("../../../build_config.zig");
|
||||
const assert = std.debug.assert;
|
||||
const adw = @import("adw");
|
||||
const gio = @import("gio");
|
||||
const glib = @import("glib");
|
||||
const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const i18n = @import("../../../os/main.zig").i18n;
|
||||
const apprt = @import("../../../apprt.zig");
|
||||
const input = @import("../../../input.zig");
|
||||
const CoreSurface = @import("../../../Surface.zig");
|
||||
const gtk_version = @import("../gtk_version.zig");
|
||||
const adw_version = @import("../adw_version.zig");
|
||||
const gresource = @import("../build/gresource.zig");
|
||||
const Common = @import("../class.zig").Common;
|
||||
const Config = @import("config.zig").Config;
|
||||
const Application = @import("application.zig").Application;
|
||||
const CloseConfirmationDialog = @import("close_confirmation_dialog.zig").CloseConfirmationDialog;
|
||||
const Surface = @import("surface.zig").Surface;
|
||||
|
||||
const log = std.log.scoped(.gtk_ghostty_window);
|
||||
|
||||
pub const Tab = extern struct {
|
||||
const Self = @This();
|
||||
parent_instance: Parent,
|
||||
pub const Parent = gtk.Box;
|
||||
pub const getGObjectType = gobject.ext.defineClass(Self, .{
|
||||
.name = "GhosttyTab",
|
||||
.instanceInit = &init,
|
||||
.classInit = &Class.init,
|
||||
.parent_class = &Class.parent,
|
||||
.private = .{ .Type = Private, .offset = &Private.offset },
|
||||
});
|
||||
|
||||
pub const properties = struct {
|
||||
/// The active surface is the focus that should be receiving all
|
||||
/// surface-targeted actions. This is usually the focused surface,
|
||||
/// but may also not be focused if the user has selected a non-surface
|
||||
/// widget.
|
||||
pub const @"active-surface" = struct {
|
||||
pub const name = "active-surface";
|
||||
const impl = gobject.ext.defineProperty(
|
||||
name,
|
||||
Self,
|
||||
?*Surface,
|
||||
.{
|
||||
.nick = "Active Surface",
|
||||
.blurb = "The currently active surface.",
|
||||
.accessor = gobject.ext.typedAccessor(
|
||||
Self,
|
||||
?*Surface,
|
||||
.{
|
||||
.getter = Self.getActiveSurface,
|
||||
},
|
||||
),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
pub const config = struct {
|
||||
pub const name = "config";
|
||||
const impl = gobject.ext.defineProperty(
|
||||
name,
|
||||
Self,
|
||||
?*Config,
|
||||
.{
|
||||
.nick = "Config",
|
||||
.blurb = "The configuration that this surface is using.",
|
||||
.accessor = C.privateObjFieldAccessor("config"),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
pub const title = struct {
|
||||
pub const name = "title";
|
||||
pub const get = impl.get;
|
||||
pub const set = impl.set;
|
||||
const impl = gobject.ext.defineProperty(
|
||||
name,
|
||||
Self,
|
||||
?[:0]const u8,
|
||||
.{
|
||||
.nick = "Title",
|
||||
.blurb = "The title of the active surface.",
|
||||
.default = null,
|
||||
.accessor = C.privateStringFieldAccessor("title"),
|
||||
},
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
pub const signals = struct {
|
||||
/// Emitted whenever the tab would like to be closed.
|
||||
pub const @"close-request" = struct {
|
||||
pub const name = "close-request";
|
||||
pub const connect = impl.connect;
|
||||
const impl = gobject.ext.defineSignal(
|
||||
name,
|
||||
Self,
|
||||
&.{},
|
||||
void,
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
const Private = struct {
|
||||
/// The configuration that this surface is using.
|
||||
config: ?*Config = null,
|
||||
|
||||
/// The title to show for this tab. This is usually set to a binding
|
||||
/// with the active surface but can be manually set to anything.
|
||||
title: ?[:0]const u8 = null,
|
||||
|
||||
/// The binding groups for the current active surface.
|
||||
surface_bindings: *gobject.BindingGroup,
|
||||
|
||||
// Template bindings
|
||||
surface: *Surface,
|
||||
|
||||
pub var offset: c_int = 0;
|
||||
};
|
||||
|
||||
/// Set the parent of this tab page. This only affects the first surface
|
||||
/// ever created for a tab. If a surface was already created this does
|
||||
/// nothing.
|
||||
pub fn setParent(
|
||||
self: *Self,
|
||||
parent: *CoreSurface,
|
||||
) void {
|
||||
const priv = self.private();
|
||||
priv.surface.setParent(parent);
|
||||
}
|
||||
|
||||
fn init(self: *Self, _: *Class) callconv(.C) void {
|
||||
gtk.Widget.initTemplate(self.as(gtk.Widget));
|
||||
|
||||
// If our configuration is null then we get the configuration
|
||||
// from the application.
|
||||
const priv = self.private();
|
||||
if (priv.config == null) {
|
||||
const app = Application.default();
|
||||
priv.config = app.getConfig();
|
||||
}
|
||||
|
||||
// Setup binding groups for surface properties
|
||||
priv.surface_bindings = gobject.BindingGroup.new();
|
||||
priv.surface_bindings.bind(
|
||||
"title",
|
||||
self.as(gobject.Object),
|
||||
"title",
|
||||
.{},
|
||||
);
|
||||
|
||||
// TODO: Eventually this should be set dynamically based on the
|
||||
// current active surface.
|
||||
priv.surface_bindings.setSource(priv.surface.as(gobject.Object));
|
||||
|
||||
// We need to do this so that the title initializes properly,
|
||||
// I think because its a dynamic getter.
|
||||
self.as(gobject.Object).notifyByPspec(properties.@"active-surface".impl.param_spec);
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Properties
|
||||
|
||||
/// Get the currently active surface. See the "active-surface" property.
|
||||
/// This does not ref the value.
|
||||
pub fn getActiveSurface(self: *Self) *Surface {
|
||||
const priv = self.private();
|
||||
return priv.surface;
|
||||
}
|
||||
|
||||
/// Returns true if this tab needs confirmation before quitting based
|
||||
/// on the various Ghostty configurations.
|
||||
pub fn getNeedsConfirmQuit(self: *Self) bool {
|
||||
const surface = self.getActiveSurface();
|
||||
const core_surface = surface.core() orelse return false;
|
||||
return core_surface.needsConfirmQuit();
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Virtual methods
|
||||
|
||||
fn dispose(self: *Self) callconv(.C) void {
|
||||
const priv = self.private();
|
||||
if (priv.config) |v| {
|
||||
v.unref();
|
||||
priv.config = null;
|
||||
}
|
||||
priv.surface_bindings.setSource(null);
|
||||
|
||||
gtk.Widget.disposeTemplate(
|
||||
self.as(gtk.Widget),
|
||||
getGObjectType(),
|
||||
);
|
||||
|
||||
gobject.Object.virtual_methods.dispose.call(
|
||||
Class.parent,
|
||||
self.as(Parent),
|
||||
);
|
||||
}
|
||||
|
||||
fn finalize(self: *Self) callconv(.C) void {
|
||||
const priv = self.private();
|
||||
if (priv.title) |v| {
|
||||
glib.free(@constCast(@ptrCast(v)));
|
||||
priv.title = null;
|
||||
}
|
||||
priv.surface_bindings.unref();
|
||||
|
||||
gobject.Object.virtual_methods.finalize.call(
|
||||
Class.parent,
|
||||
self.as(Parent),
|
||||
);
|
||||
}
|
||||
//---------------------------------------------------------------
|
||||
// Signal handlers
|
||||
|
||||
fn surfaceCloseRequest(
|
||||
_: *Surface,
|
||||
scope: *const Surface.CloseScope,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
switch (scope.*) {
|
||||
// Handled upstream... we don't control our window close.
|
||||
.window => return,
|
||||
|
||||
// Presently both the same, results in the tab closing.
|
||||
.surface, .tab => {
|
||||
signals.@"close-request".impl.emit(
|
||||
self,
|
||||
null,
|
||||
.{},
|
||||
null,
|
||||
);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const C = Common(Self, Private);
|
||||
pub const as = C.as;
|
||||
pub const ref = C.ref;
|
||||
pub const unref = C.unref;
|
||||
const private = C.private;
|
||||
|
||||
pub const Class = extern struct {
|
||||
parent_class: Parent.Class,
|
||||
var parent: *Parent.Class = undefined;
|
||||
pub const Instance = Self;
|
||||
|
||||
fn init(class: *Class) callconv(.C) void {
|
||||
gobject.ext.ensureType(Surface);
|
||||
gtk.Widget.Class.setTemplateFromResource(
|
||||
class.as(gtk.Widget.Class),
|
||||
comptime gresource.blueprint(.{
|
||||
.major = 1,
|
||||
.minor = 5,
|
||||
.name = "tab",
|
||||
}),
|
||||
);
|
||||
|
||||
// Properties
|
||||
gobject.ext.registerProperties(class, &.{
|
||||
properties.@"active-surface".impl,
|
||||
properties.config.impl,
|
||||
properties.title.impl,
|
||||
});
|
||||
|
||||
// Bindings
|
||||
class.bindTemplateChildPrivate("surface", .{});
|
||||
|
||||
// Template Callbacks
|
||||
class.bindTemplateCallback("surface_close_request", &surfaceCloseRequest);
|
||||
|
||||
// Signals
|
||||
signals.@"close-request".impl.register(.{});
|
||||
|
||||
// Virtual methods
|
||||
gobject.Object.virtual_methods.dispose.implement(class, &dispose);
|
||||
gobject.Object.virtual_methods.finalize.implement(class, &finalize);
|
||||
}
|
||||
|
||||
pub const as = C.Class.as;
|
||||
pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate;
|
||||
pub const bindTemplateCallback = C.Class.bindTemplateCallback;
|
||||
};
|
||||
};
|
@ -11,6 +11,7 @@ const i18n = @import("../../../os/main.zig").i18n;
|
||||
const apprt = @import("../../../apprt.zig");
|
||||
const input = @import("../../../input.zig");
|
||||
const CoreSurface = @import("../../../Surface.zig");
|
||||
const ext = @import("../ext.zig");
|
||||
const gtk_version = @import("../gtk_version.zig");
|
||||
const adw_version = @import("../adw_version.zig");
|
||||
const gresource = @import("../build/gresource.zig");
|
||||
@ -19,6 +20,7 @@ const Config = @import("config.zig").Config;
|
||||
const Application = @import("application.zig").Application;
|
||||
const CloseConfirmationDialog = @import("close_confirmation_dialog.zig").CloseConfirmationDialog;
|
||||
const Surface = @import("surface.zig").Surface;
|
||||
const Tab = @import("tab.zig").Tab;
|
||||
const DebugWarning = @import("debug_warning.zig").DebugWarning;
|
||||
|
||||
const log = std.log.scoped(.gtk_ghostty_window);
|
||||
@ -86,7 +88,7 @@ pub const Window = extern struct {
|
||||
.default = build_config.is_debug,
|
||||
.accessor = gobject.ext.typedAccessor(Self, bool, .{
|
||||
.getter = struct {
|
||||
pub fn getter(_: *Window) bool {
|
||||
pub fn getter(_: *Self) bool {
|
||||
return build_config.is_debug;
|
||||
}
|
||||
}.getter,
|
||||
@ -128,30 +130,104 @@ pub const Window = extern struct {
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
pub const @"tabs-autohide" = struct {
|
||||
pub const name = "tabs-autohide";
|
||||
const impl = gobject.ext.defineProperty(
|
||||
name,
|
||||
Self,
|
||||
bool,
|
||||
.{
|
||||
.nick = "Autohide Tab Bar",
|
||||
.blurb = "If true, tab bar should autohide.",
|
||||
.default = true,
|
||||
.accessor = gobject.ext.typedAccessor(Self, bool, .{
|
||||
.getter = Self.getTabsAutohide,
|
||||
}),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
pub const @"tabs-wide" = struct {
|
||||
pub const name = "tabs-wide";
|
||||
const impl = gobject.ext.defineProperty(
|
||||
name,
|
||||
Self,
|
||||
bool,
|
||||
.{
|
||||
.nick = "Wide Tabs",
|
||||
.blurb = "If true, tabs will be in the wide expanded style.",
|
||||
.default = true,
|
||||
.accessor = gobject.ext.typedAccessor(Self, bool, .{
|
||||
.getter = Self.getTabsWide,
|
||||
}),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
pub const @"tabs-visible" = struct {
|
||||
pub const name = "tabs-visible";
|
||||
const impl = gobject.ext.defineProperty(
|
||||
name,
|
||||
Self,
|
||||
bool,
|
||||
.{
|
||||
.nick = "Tab Bar Visibility",
|
||||
.blurb = "If true, tab bar should be visible.",
|
||||
.default = true,
|
||||
.accessor = gobject.ext.typedAccessor(Self, bool, .{
|
||||
.getter = Self.getTabsVisible,
|
||||
}),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
pub const @"toolbar-style" = struct {
|
||||
pub const name = "toolbar-style";
|
||||
const impl = gobject.ext.defineProperty(
|
||||
name,
|
||||
Self,
|
||||
adw.ToolbarStyle,
|
||||
.{
|
||||
.nick = "Toolbar Style",
|
||||
.blurb = "The style for the toolbar top/bottom bars.",
|
||||
.default = .raised,
|
||||
.accessor = gobject.ext.typedAccessor(
|
||||
Self,
|
||||
adw.ToolbarStyle,
|
||||
.{
|
||||
.getter = Self.getToolbarStyle,
|
||||
},
|
||||
),
|
||||
},
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
const Private = struct {
|
||||
/// Binding group for our active tab.
|
||||
tab_bindings: *gobject.BindingGroup,
|
||||
|
||||
/// The configuration that this surface is using.
|
||||
config: ?*Config = null,
|
||||
|
||||
/// See tabOverviewOpen for why we have this.
|
||||
tab_overview_focus_timer: ?c_uint = null,
|
||||
|
||||
// Template bindings
|
||||
surface: *Surface,
|
||||
tab_overview: *adw.TabOverview,
|
||||
tab_bar: *adw.TabBar,
|
||||
tab_view: *adw.TabView,
|
||||
toolbar: *adw.ToolbarView,
|
||||
toast_overlay: *adw.ToastOverlay,
|
||||
|
||||
pub var offset: c_int = 0;
|
||||
};
|
||||
|
||||
pub fn new(app: *Application, parent_: ?*CoreSurface) *Self {
|
||||
const self = gobject.ext.newInstance(Self, .{
|
||||
pub fn new(app: *Application) *Self {
|
||||
return gobject.ext.newInstance(Self, .{
|
||||
.application = app,
|
||||
});
|
||||
|
||||
if (parent_) |parent| {
|
||||
const priv = self.private();
|
||||
priv.surface.setParent(parent);
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
fn init(self: *Self, _: *Class) callconv(.C) void {
|
||||
@ -170,6 +246,11 @@ pub const Window = extern struct {
|
||||
self.as(gtk.Widget).addCssClass("devel");
|
||||
}
|
||||
|
||||
// Setup our tab binding group. This ensures certain properties
|
||||
// are only synced from the currently active tab.
|
||||
priv.tab_bindings = gobject.BindingGroup.new();
|
||||
priv.tab_bindings.bind("title", self.as(gobject.Object), "title", .{});
|
||||
|
||||
// Set our window icon. We can't set this in the blueprint file
|
||||
// because its dependent on the build config.
|
||||
self.as(gtk.Window).setIconName(build_config.bundle_id);
|
||||
@ -192,6 +273,8 @@ pub const Window = extern struct {
|
||||
const actions = .{
|
||||
.{ "about", actionAbout, null },
|
||||
.{ "close", actionClose, null },
|
||||
.{ "close-tab", actionCloseTab, null },
|
||||
.{ "new-tab", actionNewTab, null },
|
||||
.{ "new-window", actionNewWindow, null },
|
||||
.{ "copy", actionCopy, null },
|
||||
.{ "paste", actionPaste, null },
|
||||
@ -217,20 +300,194 @@ pub const Window = extern struct {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new tab with the given parent. The tab will be inserted
|
||||
/// at the position dictated by the `window-new-tab-position` config.
|
||||
/// The new tab will be selected.
|
||||
pub fn newTab(self: *Self, parent_: ?*CoreSurface) void {
|
||||
_ = self.newTabPage(parent_);
|
||||
}
|
||||
|
||||
fn newTabPage(self: *Self, parent_: ?*CoreSurface) *adw.TabPage {
|
||||
const priv = self.private();
|
||||
const tab_view = priv.tab_view;
|
||||
|
||||
// Create our new tab object
|
||||
const tab = gobject.ext.newInstance(Tab, .{
|
||||
.config = priv.config,
|
||||
});
|
||||
if (parent_) |p| tab.setParent(p);
|
||||
|
||||
// Get the position that we should insert the new tab at.
|
||||
const config = if (priv.config) |v| v.get() else {
|
||||
// If we don't have a config we just append it at the end.
|
||||
// This should never happen.
|
||||
return tab_view.append(tab.as(gtk.Widget));
|
||||
};
|
||||
const position = switch (config.@"window-new-tab-position") {
|
||||
.current => current: {
|
||||
const selected = tab_view.getSelectedPage() orelse
|
||||
break :current tab_view.getNPages();
|
||||
const current = tab_view.getPagePosition(selected);
|
||||
break :current current + 1;
|
||||
},
|
||||
|
||||
.end => tab_view.getNPages(),
|
||||
};
|
||||
|
||||
// Add the page and select it
|
||||
const page = tab_view.insert(tab.as(gtk.Widget), position);
|
||||
tab_view.setSelectedPage(page);
|
||||
|
||||
// Create some property bindings
|
||||
_ = tab.as(gobject.Object).bindProperty(
|
||||
"title",
|
||||
page.as(gobject.Object),
|
||||
"title",
|
||||
.{ .sync_create = true },
|
||||
);
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
pub const SelectTab = union(enum) {
|
||||
previous,
|
||||
next,
|
||||
last,
|
||||
n: usize,
|
||||
};
|
||||
|
||||
/// Select the tab as requested. Returns true if the tab selection
|
||||
/// changed.
|
||||
pub fn selectTab(self: *Self, n: SelectTab) bool {
|
||||
const priv = self.private();
|
||||
const tab_view = priv.tab_view;
|
||||
|
||||
// Get our current tab numeric position
|
||||
const selected = tab_view.getSelectedPage() orelse return false;
|
||||
const current = tab_view.getPagePosition(selected);
|
||||
|
||||
// Get our total
|
||||
const total = tab_view.getNPages();
|
||||
|
||||
const goto: c_int = switch (n) {
|
||||
.previous => if (current > 0)
|
||||
current - 1
|
||||
else
|
||||
total - 1,
|
||||
|
||||
.next => if (current < total - 1)
|
||||
current + 1
|
||||
else
|
||||
0,
|
||||
|
||||
.last => total - 1,
|
||||
|
||||
.n => |v| n: {
|
||||
// 1-indexed
|
||||
if (v == 0) return false;
|
||||
|
||||
const n_int = std.math.cast(
|
||||
c_int,
|
||||
v,
|
||||
) orelse return false;
|
||||
break :n @min(n_int - 1, total - 1);
|
||||
},
|
||||
};
|
||||
assert(goto >= 0);
|
||||
assert(goto < total);
|
||||
|
||||
// If our target is the same as our current then we do nothing.
|
||||
if (goto == current) return false;
|
||||
|
||||
// Add the page and select it
|
||||
const page = tab_view.getNthPage(goto);
|
||||
tab_view.setSelectedPage(page);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Move the tab containing the given surface by the given amount.
|
||||
/// Returns if this affected any tab positioning.
|
||||
pub fn moveTab(
|
||||
self: *Self,
|
||||
surface: *Surface,
|
||||
amount: isize,
|
||||
) bool {
|
||||
const priv = self.private();
|
||||
const tab_view = priv.tab_view;
|
||||
|
||||
// If we have one tab we never move.
|
||||
const total = tab_view.getNPages();
|
||||
if (total == 1) return false;
|
||||
|
||||
// Get the tab that contains the given surface.
|
||||
const tab = ext.getAncestor(
|
||||
Tab,
|
||||
surface.as(gtk.Widget),
|
||||
) orelse return false;
|
||||
|
||||
// Get the page position that contains the tab.
|
||||
const page = tab_view.getPage(tab.as(gtk.Widget));
|
||||
const pos = tab_view.getPagePosition(page);
|
||||
|
||||
// Move it
|
||||
const desired_pos: c_int = desired: {
|
||||
const initial: c_int = @intCast(pos + amount);
|
||||
const max = total - 1;
|
||||
break :desired if (initial < 0)
|
||||
max + initial + 1
|
||||
else if (initial > max)
|
||||
initial - max - 1
|
||||
else
|
||||
initial;
|
||||
};
|
||||
assert(desired_pos >= 0);
|
||||
assert(desired_pos < total);
|
||||
|
||||
return tab_view.reorderPage(page, desired_pos) != 0;
|
||||
}
|
||||
|
||||
pub fn toggleTabOverview(self: *Self) void {
|
||||
const priv = self.private();
|
||||
const tab_overview = priv.tab_overview;
|
||||
const is_open = tab_overview.getOpen() != 0;
|
||||
tab_overview.setOpen(@intFromBool(!is_open));
|
||||
}
|
||||
|
||||
/// Updates various appearance properties. This should always be safe
|
||||
/// to call multiple times. This should be called whenever a change
|
||||
/// happens that might affect how the window appears (config change,
|
||||
/// fullscreen, etc.).
|
||||
fn syncAppearance(self: *Window) void {
|
||||
fn syncAppearance(self: *Self) void {
|
||||
// TODO: CSD/SSD
|
||||
|
||||
// Trigger our headerbar visibility to refresh
|
||||
self.as(gobject.Object).notifyByPspec(properties.@"headerbar-visible".impl.param_spec);
|
||||
// Trigger background opacity to refresh
|
||||
self.as(gobject.Object).notifyByPspec(properties.@"background-opaque".impl.param_spec);
|
||||
// Trigger all our dynamic properties that depend on the config.
|
||||
inline for (&.{
|
||||
"background-opaque",
|
||||
"headerbar-visible",
|
||||
"tabs-autohide",
|
||||
"tabs-visible",
|
||||
"tabs-wide",
|
||||
"toolbar-style",
|
||||
}) |key| {
|
||||
self.as(gobject.Object).notifyByPspec(
|
||||
@field(properties, key).impl.param_spec,
|
||||
);
|
||||
}
|
||||
|
||||
// Remainder uses the config
|
||||
const priv = self.private();
|
||||
const config = if (priv.config) |v| v.get() else return;
|
||||
|
||||
// Move the tab bar to the proper location.
|
||||
priv.toolbar.remove(priv.tab_bar.as(gtk.Widget));
|
||||
switch (config.@"gtk-tabs-location") {
|
||||
.top => priv.toolbar.addTopBar(priv.tab_bar.as(gtk.Widget)),
|
||||
.bottom => priv.toolbar.addBottomBar(priv.tab_bar.as(gtk.Widget)),
|
||||
}
|
||||
}
|
||||
|
||||
fn toggleCssClass(self: *Window, class: [:0]const u8, value: bool) void {
|
||||
fn toggleCssClass(self: *Self, class: [:0]const u8, value: bool) void {
|
||||
const widget = self.as(gtk.Widget);
|
||||
if (value)
|
||||
widget.addCssClass(class.ptr)
|
||||
@ -240,7 +497,7 @@ pub const Window = extern struct {
|
||||
|
||||
/// Perform a binding action on the window's active surface.
|
||||
fn performBindingAction(
|
||||
self: *Window,
|
||||
self: *Self,
|
||||
action: input.Binding.Action,
|
||||
) void {
|
||||
const surface = self.getActiveSurface() orelse return;
|
||||
@ -257,7 +514,7 @@ pub const Window = extern struct {
|
||||
// This is not `pub` because we should be using signals emitted by
|
||||
// other widgets to trigger our toasts. Other objects should not
|
||||
// trigger toasts directly.
|
||||
fn addToast(self: *Window, title: [*:0]const u8) void {
|
||||
fn addToast(self: *Self, title: [*:0]const u8) void {
|
||||
const toast = adw.Toast.new(title);
|
||||
toast.setTimeout(3);
|
||||
self.private().toast_overlay.addToast(toast);
|
||||
@ -269,8 +526,36 @@ pub const Window = extern struct {
|
||||
/// Get the currently active surface. See the "active-surface" property.
|
||||
/// This does not ref the value.
|
||||
fn getActiveSurface(self: *Self) ?*Surface {
|
||||
const tab = self.getSelectedTab() orelse return null;
|
||||
return tab.getActiveSurface();
|
||||
}
|
||||
|
||||
/// Get the currently selected tab as a Tab object.
|
||||
fn getSelectedTab(self: *Self) ?*Tab {
|
||||
const priv = self.private();
|
||||
return priv.surface;
|
||||
const page = priv.tab_view.getSelectedPage() orelse return null;
|
||||
const child = page.getChild();
|
||||
assert(gobject.ext.isA(child, Tab));
|
||||
return gobject.ext.cast(Tab, child);
|
||||
}
|
||||
|
||||
/// Returns true if this window needs confirmation before quitting.
|
||||
fn getNeedsConfirmQuit(self: *Self) bool {
|
||||
const priv = self.private();
|
||||
const n = priv.tab_view.getNPages();
|
||||
assert(n >= 0);
|
||||
|
||||
for (0..@intCast(n)) |i| {
|
||||
const page = priv.tab_view.getNthPage(@intCast(i));
|
||||
const child = page.getChild();
|
||||
const tab = gobject.ext.cast(Tab, child) orelse {
|
||||
log.warn("unexpected non-Tab child in tab view", .{});
|
||||
continue;
|
||||
};
|
||||
if (tab.getNeedsConfirmQuit()) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
fn getHeaderbarVisible(self: *Self) bool {
|
||||
@ -301,6 +586,47 @@ pub const Window = extern struct {
|
||||
return config.@"background-opacity" >= 1.0;
|
||||
}
|
||||
|
||||
fn getTabsAutohide(self: *Self) bool {
|
||||
const priv = self.private();
|
||||
const config = if (priv.config) |v| v.get() else return true;
|
||||
return switch (config.@"window-show-tab-bar") {
|
||||
// Auto we always autohide... obviously.
|
||||
.auto => true,
|
||||
|
||||
// Always we never autohide because we always show the tab bar.
|
||||
.always => false,
|
||||
|
||||
// Never we autohide because it doesn't actually matter,
|
||||
// since getTabsVisible will return false.
|
||||
.never => true,
|
||||
};
|
||||
}
|
||||
|
||||
fn getTabsVisible(self: *Self) bool {
|
||||
const priv = self.private();
|
||||
const config = if (priv.config) |v| v.get() else return true;
|
||||
return switch (config.@"window-show-tab-bar") {
|
||||
.always, .auto => true,
|
||||
.never => false,
|
||||
};
|
||||
}
|
||||
|
||||
fn getTabsWide(self: *Self) bool {
|
||||
const priv = self.private();
|
||||
const config = if (priv.config) |v| v.get() else return true;
|
||||
return config.@"gtk-wide-tabs";
|
||||
}
|
||||
|
||||
fn getToolbarStyle(self: *Self) adw.ToolbarStyle {
|
||||
const priv = self.private();
|
||||
const config = if (priv.config) |v| v.get() else return .raised;
|
||||
return switch (config.@"gtk-toolbar-style") {
|
||||
.flat => .flat,
|
||||
.raised => .raised,
|
||||
.@"raised-border" => .raised_border,
|
||||
};
|
||||
}
|
||||
|
||||
fn propConfig(
|
||||
_: *adw.ApplicationWindow,
|
||||
_: *gobject.ParamSpec,
|
||||
@ -377,6 +703,7 @@ pub const Window = extern struct {
|
||||
v.unref();
|
||||
priv.config = null;
|
||||
}
|
||||
priv.tab_bindings.setSource(null);
|
||||
|
||||
gtk.Widget.disposeTemplate(
|
||||
self.as(gtk.Widget),
|
||||
@ -389,23 +716,86 @@ pub const Window = extern struct {
|
||||
);
|
||||
}
|
||||
|
||||
fn finalize(self: *Self) callconv(.C) void {
|
||||
const priv = self.private();
|
||||
priv.tab_bindings.unref();
|
||||
|
||||
gobject.Object.virtual_methods.finalize.call(
|
||||
Class.parent,
|
||||
self.as(Parent),
|
||||
);
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Signal handlers
|
||||
|
||||
fn btnNewTab(_: *adw.SplitButton, self: *Self) callconv(.c) void {
|
||||
self.performBindingAction(.new_tab);
|
||||
}
|
||||
|
||||
fn tabOverviewCreateTab(
|
||||
_: *adw.TabOverview,
|
||||
self: *Self,
|
||||
) callconv(.c) *adw.TabPage {
|
||||
return self.newTabPage(if (self.getActiveSurface()) |v| v.core() else null);
|
||||
}
|
||||
|
||||
fn tabOverviewOpen(
|
||||
tab_overview: *adw.TabOverview,
|
||||
_: *gobject.ParamSpec,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
// We only care about when the tab overview is closed.
|
||||
if (tab_overview.getOpen() != 0) return;
|
||||
|
||||
// On tab overview close, focus is sometimes lost. This is an
|
||||
// upstream issue in libadwaita[1]. When this is resolved we
|
||||
// can put a runtime version check here to avoid this workaround.
|
||||
//
|
||||
// Our workaround is to start a timer after 500ms to refocus
|
||||
// the currently selected tab. We choose 500ms because the adw
|
||||
// animation is 400ms.
|
||||
//
|
||||
// [1]: https://gitlab.gnome.org/GNOME/libadwaita/-/issues/670
|
||||
|
||||
// If we have an old timer remove it
|
||||
const priv = self.private();
|
||||
if (priv.tab_overview_focus_timer) |timer| {
|
||||
_ = glib.Source.remove(timer);
|
||||
}
|
||||
|
||||
// Restart our timer
|
||||
priv.tab_overview_focus_timer = glib.timeoutAdd(
|
||||
500,
|
||||
tabOverviewFocusTimer,
|
||||
self,
|
||||
);
|
||||
}
|
||||
|
||||
fn tabOverviewFocusTimer(
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.c) c_int {
|
||||
const self: *Self = @ptrCast(@alignCast(ud orelse return 0));
|
||||
|
||||
// Always note our timer is removed
|
||||
self.private().tab_overview_focus_timer = null;
|
||||
|
||||
// Get our currently active surface which should respect the newly
|
||||
// selected tab. Grab focus.
|
||||
const surface = self.getActiveSurface() orelse return 0;
|
||||
surface.grabFocus();
|
||||
|
||||
// Remove the timer
|
||||
return 0;
|
||||
}
|
||||
|
||||
fn windowCloseRequest(
|
||||
_: *gtk.Window,
|
||||
self: *Self,
|
||||
) callconv(.c) c_int {
|
||||
// If our surface needs confirmation then we show confirmation.
|
||||
// This will have to be expanded to a list when we have tabs
|
||||
// or splits.
|
||||
confirm: {
|
||||
const surface = self.getActiveSurface() orelse break :confirm;
|
||||
const core_surface = surface.core() orelse break :confirm;
|
||||
if (!core_surface.needsConfirmQuit()) break :confirm;
|
||||
|
||||
if (self.getNeedsConfirmQuit()) {
|
||||
// Show a confirmation dialog
|
||||
const dialog: *CloseConfirmationDialog = .new(.app);
|
||||
const dialog: *CloseConfirmationDialog = .new(.window);
|
||||
_ = CloseConfirmationDialog.signals.@"close-request".connect(
|
||||
dialog,
|
||||
*Self,
|
||||
@ -430,6 +820,237 @@ pub const Window = extern struct {
|
||||
self.as(gtk.Window).destroy();
|
||||
}
|
||||
|
||||
fn closeConfirmationCloseTab(
|
||||
_: *CloseConfirmationDialog,
|
||||
page: *adw.TabPage,
|
||||
) callconv(.c) void {
|
||||
const tab_view = ext.getAncestor(
|
||||
adw.TabView,
|
||||
page.getChild().as(gtk.Widget),
|
||||
) orelse {
|
||||
log.warn("close confirmation called for non-existent page", .{});
|
||||
return;
|
||||
};
|
||||
tab_view.closePageFinish(page, @intFromBool(true));
|
||||
}
|
||||
|
||||
fn closeConfirmationCancelTab(
|
||||
_: *CloseConfirmationDialog,
|
||||
page: *adw.TabPage,
|
||||
) callconv(.c) void {
|
||||
const tab_view = ext.getAncestor(
|
||||
adw.TabView,
|
||||
page.getChild().as(gtk.Widget),
|
||||
) orelse {
|
||||
log.warn("close confirmation called for non-existent page", .{});
|
||||
return;
|
||||
};
|
||||
tab_view.closePageFinish(page, @intFromBool(false));
|
||||
}
|
||||
|
||||
fn tabViewClosePage(
|
||||
_: *adw.TabView,
|
||||
page: *adw.TabPage,
|
||||
self: *Self,
|
||||
) callconv(.c) c_int {
|
||||
const priv = self.private();
|
||||
const child = page.getChild();
|
||||
const tab = gobject.ext.cast(Tab, child) orelse
|
||||
return @intFromBool(false);
|
||||
|
||||
// If the tab says it doesn't need confirmation then we go ahead
|
||||
// and close immediately.
|
||||
if (!tab.getNeedsConfirmQuit()) {
|
||||
priv.tab_view.closePageFinish(page, @intFromBool(true));
|
||||
return @intFromBool(true);
|
||||
}
|
||||
|
||||
// Show a confirmation dialog
|
||||
const dialog: *CloseConfirmationDialog = .new(.tab);
|
||||
_ = CloseConfirmationDialog.signals.@"close-request".connect(
|
||||
dialog,
|
||||
*adw.TabPage,
|
||||
closeConfirmationCloseTab,
|
||||
page,
|
||||
.{},
|
||||
);
|
||||
_ = CloseConfirmationDialog.signals.cancel.connect(
|
||||
dialog,
|
||||
*adw.TabPage,
|
||||
closeConfirmationCancelTab,
|
||||
page,
|
||||
.{},
|
||||
);
|
||||
|
||||
// Show it
|
||||
dialog.present(child);
|
||||
return @intFromBool(true);
|
||||
}
|
||||
|
||||
fn tabViewSelectedPage(
|
||||
_: *adw.TabView,
|
||||
_: *gobject.ParamSpec,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
const priv = self.private();
|
||||
|
||||
// Always reset our binding source in case we have no pages.
|
||||
priv.tab_bindings.setSource(null);
|
||||
|
||||
// Get our current page which MUST be a Tab object.
|
||||
const page = priv.tab_view.getSelectedPage() orelse return;
|
||||
const child = page.getChild();
|
||||
assert(gobject.ext.isA(child, Tab));
|
||||
|
||||
// Setup our binding group. This ensures things like the title
|
||||
// are synced from the active tab.
|
||||
priv.tab_bindings.setSource(child.as(gobject.Object));
|
||||
}
|
||||
|
||||
fn tabViewPageAttached(
|
||||
_: *adw.TabView,
|
||||
page: *adw.TabPage,
|
||||
_: c_int,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
// Get the attached page which must be a Tab object.
|
||||
const child = page.getChild();
|
||||
const tab = gobject.ext.cast(Tab, child) orelse return;
|
||||
|
||||
// Attach listeners for the tab.
|
||||
_ = Tab.signals.@"close-request".connect(
|
||||
tab,
|
||||
*Self,
|
||||
tabCloseRequest,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
|
||||
// Attach listeners for the surface.
|
||||
//
|
||||
// Interesting behavior here that was previously undocumented but
|
||||
// I'm going to make it explicit here: we accept all the signals here
|
||||
// (like toggle-fullscreen) regardless of whether the surface or tab
|
||||
// is focused. At the time of writing this we have no API that could
|
||||
// really trigger these that way but its theoretically possible.
|
||||
//
|
||||
// What is DEFINITELY possible is something like OSC52 triggering
|
||||
// a clipboard-write signal on an unfocused tab/surface. We definitely
|
||||
// want to show the user a notification about that but our notification
|
||||
// right now is a toast that doesn't make it clear WHO used the
|
||||
// clipboard. We probably want to change that in the future.
|
||||
//
|
||||
// I'm not sure how desirable all the above is, and we probably
|
||||
// should be thoughtful about future signals here. But all of this
|
||||
// behavior is consistent with macOS and the previous GTK apprt,
|
||||
// but that behavior was all implicit and not documented, so here
|
||||
// I am.
|
||||
//
|
||||
// TODO: When we have a split tree we'll want to attach to that.
|
||||
const surface = tab.getActiveSurface();
|
||||
_ = Surface.signals.@"close-request".connect(
|
||||
surface,
|
||||
*Self,
|
||||
surfaceCloseRequest,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
_ = Surface.signals.@"clipboard-write".connect(
|
||||
surface,
|
||||
*Self,
|
||||
surfaceClipboardWrite,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
_ = Surface.signals.@"toggle-fullscreen".connect(
|
||||
surface,
|
||||
*Self,
|
||||
surfaceToggleFullscreen,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
_ = Surface.signals.@"toggle-maximize".connect(
|
||||
surface,
|
||||
*Self,
|
||||
surfaceToggleMaximize,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
}
|
||||
|
||||
fn tabViewPageDetached(
|
||||
_: *adw.TabView,
|
||||
page: *adw.TabPage,
|
||||
_: c_int,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
// We need to get the tab to disconnect the signals.
|
||||
const child = page.getChild();
|
||||
const tab = gobject.ext.cast(Tab, child) orelse return;
|
||||
_ = gobject.signalHandlersDisconnectMatched(
|
||||
tab.as(gobject.Object),
|
||||
.{ .data = true },
|
||||
0,
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
self,
|
||||
);
|
||||
|
||||
// Remove all the signals that have this window as the userdata.
|
||||
const surface = tab.getActiveSurface();
|
||||
_ = gobject.signalHandlersDisconnectMatched(
|
||||
surface.as(gobject.Object),
|
||||
.{ .data = true },
|
||||
0,
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
self,
|
||||
);
|
||||
}
|
||||
|
||||
fn tabViewCreateWindow(
|
||||
_: *adw.TabView,
|
||||
_: *Self,
|
||||
) callconv(.c) *adw.TabView {
|
||||
// Create a new window without creating a new tab.
|
||||
const win = gobject.ext.newInstance(
|
||||
Self,
|
||||
.{
|
||||
.application = Application.default(),
|
||||
},
|
||||
);
|
||||
|
||||
// We have to show it otherwise it'll just be hidden.
|
||||
gtk.Window.present(win.as(gtk.Window));
|
||||
|
||||
// Get our tab view
|
||||
return win.private().tab_view;
|
||||
}
|
||||
|
||||
fn tabCloseRequest(
|
||||
tab: *Tab,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
const priv = self.private();
|
||||
const page = priv.tab_view.getPage(tab.as(gtk.Widget));
|
||||
// TODO: connect close page handler to tab to check for confirmation
|
||||
priv.tab_view.closePage(page);
|
||||
}
|
||||
|
||||
fn tabViewNPages(
|
||||
_: *adw.TabView,
|
||||
_: *gobject.ParamSpec,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
const priv = self.private();
|
||||
if (priv.tab_view.getNPages() == 0) {
|
||||
// If we have no pages left then we want to close window.
|
||||
self.as(gtk.Window).close();
|
||||
}
|
||||
}
|
||||
|
||||
fn surfaceClipboardWrite(
|
||||
_: *Surface,
|
||||
clipboard_type: apprt.Clipboard,
|
||||
@ -454,15 +1075,22 @@ pub const Window = extern struct {
|
||||
}
|
||||
|
||||
fn surfaceCloseRequest(
|
||||
surface: *Surface,
|
||||
_: *Surface,
|
||||
scope: *const Surface.CloseScope,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
// Todo
|
||||
_ = scope;
|
||||
switch (scope.*) {
|
||||
// Handled directly by the tab. If the surface is the last
|
||||
// surface then the tab will emit its own signal to request
|
||||
// closing itself.
|
||||
.surface => return,
|
||||
|
||||
assert(surface == self.private().surface);
|
||||
self.as(gtk.Window).close();
|
||||
// Also handled directly by the tab.
|
||||
.tab => return,
|
||||
|
||||
// The only one we care about!
|
||||
.window => self.as(gtk.Window).close(),
|
||||
}
|
||||
}
|
||||
|
||||
fn surfaceToggleFullscreen(
|
||||
@ -545,6 +1173,14 @@ pub const Window = extern struct {
|
||||
self.as(gtk.Window).close();
|
||||
}
|
||||
|
||||
fn actionCloseTab(
|
||||
_: *gio.SimpleAction,
|
||||
_: ?*glib.Variant,
|
||||
self: *Window,
|
||||
) callconv(.c) void {
|
||||
self.performBindingAction(.close_tab);
|
||||
}
|
||||
|
||||
fn actionNewWindow(
|
||||
_: *gio.SimpleAction,
|
||||
_: ?*glib.Variant,
|
||||
@ -553,6 +1189,14 @@ pub const Window = extern struct {
|
||||
self.performBindingAction(.new_window);
|
||||
}
|
||||
|
||||
fn actionNewTab(
|
||||
_: *gio.SimpleAction,
|
||||
_: ?*glib.Variant,
|
||||
self: *Window,
|
||||
) callconv(.c) void {
|
||||
self.performBindingAction(.new_tab);
|
||||
}
|
||||
|
||||
fn actionCopy(
|
||||
_: *gio.SimpleAction,
|
||||
_: ?*glib.Variant,
|
||||
@ -597,7 +1241,6 @@ pub const Window = extern struct {
|
||||
pub const Instance = Self;
|
||||
|
||||
fn init(class: *Class) callconv(.C) void {
|
||||
gobject.ext.ensureType(Surface);
|
||||
gobject.ext.ensureType(DebugWarning);
|
||||
gtk.Widget.Class.setTemplateFromResource(
|
||||
class.as(gtk.Widget.Class),
|
||||
@ -611,22 +1254,34 @@ pub const Window = extern struct {
|
||||
// Properties
|
||||
gobject.ext.registerProperties(class, &.{
|
||||
properties.@"active-surface".impl,
|
||||
properties.@"background-opaque".impl,
|
||||
properties.config.impl,
|
||||
properties.debug.impl,
|
||||
properties.@"headerbar-visible".impl,
|
||||
properties.@"background-opaque".impl,
|
||||
properties.@"tabs-autohide".impl,
|
||||
properties.@"tabs-visible".impl,
|
||||
properties.@"tabs-wide".impl,
|
||||
properties.@"toolbar-style".impl,
|
||||
});
|
||||
|
||||
// Bindings
|
||||
class.bindTemplateChildPrivate("surface", .{});
|
||||
class.bindTemplateChildPrivate("tab_overview", .{});
|
||||
class.bindTemplateChildPrivate("tab_bar", .{});
|
||||
class.bindTemplateChildPrivate("tab_view", .{});
|
||||
class.bindTemplateChildPrivate("toolbar", .{});
|
||||
class.bindTemplateChildPrivate("toast_overlay", .{});
|
||||
|
||||
// Template Callbacks
|
||||
class.bindTemplateCallback("new_tab", &btnNewTab);
|
||||
class.bindTemplateCallback("overview_create_tab", &tabOverviewCreateTab);
|
||||
class.bindTemplateCallback("overview_notify_open", &tabOverviewOpen);
|
||||
class.bindTemplateCallback("close_request", &windowCloseRequest);
|
||||
class.bindTemplateCallback("surface_clipboard_write", &surfaceClipboardWrite);
|
||||
class.bindTemplateCallback("surface_close_request", &surfaceCloseRequest);
|
||||
class.bindTemplateCallback("surface_toggle_fullscreen", &surfaceToggleFullscreen);
|
||||
class.bindTemplateCallback("surface_toggle_maximize", &surfaceToggleMaximize);
|
||||
class.bindTemplateCallback("close_page", &tabViewClosePage);
|
||||
class.bindTemplateCallback("page_attached", &tabViewPageAttached);
|
||||
class.bindTemplateCallback("page_detached", &tabViewPageDetached);
|
||||
class.bindTemplateCallback("tab_create_window", &tabViewCreateWindow);
|
||||
class.bindTemplateCallback("notify_n_pages", &tabViewNPages);
|
||||
class.bindTemplateCallback("notify_selected_page", &tabViewSelectedPage);
|
||||
class.bindTemplateCallback("notify_config", &propConfig);
|
||||
class.bindTemplateCallback("notify_fullscreened", &propFullscreened);
|
||||
class.bindTemplateCallback("notify_maximized", &propMaximized);
|
||||
@ -635,6 +1290,7 @@ pub const Window = extern struct {
|
||||
|
||||
// Virtual methods
|
||||
gobject.Object.virtual_methods.dispose.implement(class, &dispose);
|
||||
gobject.Object.virtual_methods.finalize.implement(class, &finalize);
|
||||
}
|
||||
|
||||
pub const as = C.Class.as;
|
||||
|
19
src/apprt/gtk-ng/ext.zig
Normal file
19
src/apprt/gtk-ng/ext.zig
Normal file
@ -0,0 +1,19 @@
|
||||
//! Extensions/helpers for GTK objects, following a similar naming
|
||||
//! style to zig-gobject. These should, wherever possible, be Zig-friendly
|
||||
//! wrappers around existing GTK functionality, rather than complex new
|
||||
//! helpers.
|
||||
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
|
||||
const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
/// Wrapper around `gtk.Widget.getAncestor` to get the widget ancestor
|
||||
/// of the given type `T`, or null if it doesn't exist.
|
||||
pub fn getAncestor(comptime T: type, widget: *gtk.Widget) ?*T {
|
||||
const ancestor_ = widget.getAncestor(gobject.ext.typeFor(T));
|
||||
const ancestor = ancestor_ orelse return null;
|
||||
// We can assert the unwrap because getAncestor above
|
||||
return gobject.ext.cast(T, ancestor).?;
|
||||
}
|
15
src/apprt/gtk-ng/ui/1.5/tab.blp
Normal file
15
src/apprt/gtk-ng/ui/1.5/tab.blp
Normal file
@ -0,0 +1,15 @@
|
||||
using Gtk 4.0;
|
||||
|
||||
template $GhosttyTab: Box {
|
||||
styles [
|
||||
"tab",
|
||||
]
|
||||
|
||||
hexpand: true;
|
||||
vexpand: true;
|
||||
// A tab currently just contains a surface directly. When we introduce
|
||||
// splits we probably want to replace this with the split widget type.
|
||||
$GhosttySurface surface {
|
||||
close-request => $surface_close_request();
|
||||
}
|
||||
}
|
@ -16,40 +16,80 @@ template $GhosttyWindow: Adw.ApplicationWindow {
|
||||
// GTK4 grabs F10 input by default to focus the menubar icon. We want
|
||||
// to disable this so that terminal programs can capture F10 (such as htop)
|
||||
handle-menubar-accel: false;
|
||||
title: bind (template.active-surface as <$GhosttySurface>).title;
|
||||
|
||||
content: Box {
|
||||
orientation: vertical;
|
||||
content: Adw.TabOverview tab_overview {
|
||||
create-tab => $overview_create_tab();
|
||||
notify::open => $overview_notify_open();
|
||||
enable-new-tab: true;
|
||||
view: tab_view;
|
||||
|
||||
Adw.HeaderBar {
|
||||
visible: bind template.headerbar-visible;
|
||||
Adw.ToolbarView toolbar {
|
||||
top-bar-style: bind template.toolbar-style;
|
||||
bottom-bar-style: bind template.toolbar-style;
|
||||
|
||||
title-widget: Adw.WindowTitle {
|
||||
title: bind (template.active-surface as <$GhosttySurface>).title;
|
||||
};
|
||||
[top]
|
||||
Adw.HeaderBar {
|
||||
visible: bind template.headerbar-visible;
|
||||
|
||||
[end]
|
||||
Gtk.Box {
|
||||
Gtk.MenuButton {
|
||||
notify::active => $notify_menu_active();
|
||||
icon-name: "open-menu-symbolic";
|
||||
menu-model: main_menu;
|
||||
tooltip-text: _("Main Menu");
|
||||
can-focus: false;
|
||||
title-widget: Adw.WindowTitle {
|
||||
title: bind template.title;
|
||||
};
|
||||
|
||||
[start]
|
||||
Adw.SplitButton {
|
||||
clicked => $new_tab();
|
||||
icon-name: "tab-new-symbolic";
|
||||
tooltip-text: _("New Tab");
|
||||
dropdown-tooltip: _("New Split");
|
||||
menu-model: split_menu;
|
||||
}
|
||||
|
||||
[end]
|
||||
Gtk.Box {
|
||||
Gtk.ToggleButton {
|
||||
icon-name: "view-grid-symbolic";
|
||||
tooltip-text: _("View Open Tabs");
|
||||
active: bind tab_overview.open bidirectional;
|
||||
can-focus: false;
|
||||
focus-on-click: false;
|
||||
}
|
||||
|
||||
Gtk.MenuButton {
|
||||
notify::active => $notify_menu_active();
|
||||
icon-name: "open-menu-symbolic";
|
||||
menu-model: main_menu;
|
||||
tooltip-text: _("Main Menu");
|
||||
can-focus: false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$GhosttyDebugWarning {
|
||||
visible: bind template.debug;
|
||||
}
|
||||
[top]
|
||||
Adw.TabBar tab_bar {
|
||||
autohide: bind template.tabs-autohide;
|
||||
expand-tabs: bind template.tabs-wide;
|
||||
view: tab_view;
|
||||
visible: bind template.tabs-visible;
|
||||
}
|
||||
|
||||
Adw.ToastOverlay toast_overlay {
|
||||
$GhosttySurface surface {
|
||||
close-request => $surface_close_request();
|
||||
clipboard-write => $surface_clipboard_write();
|
||||
toggle-fullscreen => $surface_toggle_fullscreen();
|
||||
toggle-maximize => $surface_toggle_maximize();
|
||||
Box {
|
||||
orientation: vertical;
|
||||
|
||||
$GhosttyDebugWarning {
|
||||
visible: bind template.debug;
|
||||
}
|
||||
|
||||
Adw.ToastOverlay toast_overlay {
|
||||
Adw.TabView tab_view {
|
||||
notify::n-pages => $notify_n_pages();
|
||||
notify::selected-page => $notify_selected_page();
|
||||
close-page => $close_page();
|
||||
page-attached => $page_attached();
|
||||
page-detached => $page_detached();
|
||||
create-window => $tab_create_window();
|
||||
shortcuts: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -135,13 +135,34 @@
|
||||
...
|
||||
fun:gsk_gpu_node_processor_process
|
||||
fun:gsk_gpu_frame_render
|
||||
fun:gsk_gpu_renderer_render
|
||||
fun:gsk_renderer_render
|
||||
fun:gtk_widget_render
|
||||
fun:surface_render
|
||||
...
|
||||
}
|
||||
|
||||
{
|
||||
GDK GLArea
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: possible
|
||||
fun:*alloc
|
||||
...
|
||||
fun:gdk_memory_texture_from_texture
|
||||
fun:gdk_gl_texture_release
|
||||
fun:delete_one_texture
|
||||
fun:g_list_foreach
|
||||
fun:g_list_free_full
|
||||
fun:gtk_gl_area_unrealize
|
||||
...
|
||||
}
|
||||
|
||||
{
|
||||
GDK GLArea Snapshot
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: definite
|
||||
fun:*alloc
|
||||
...
|
||||
fun:gtk_gl_area_snapshot
|
||||
...
|
||||
}
|
||||
|
||||
{
|
||||
GSK GPU Rendering
|
||||
Memcheck:Leak
|
||||
@ -351,6 +372,7 @@
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: possible
|
||||
fun:*alloc
|
||||
...
|
||||
fun:FcFontSet*
|
||||
...
|
||||
fun:fc_thread_func
|
||||
|
Reference in New Issue
Block a user