mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
apprt/gtk-ng: window headerbar, maximize, fullscreen (#8071)
This ports over a basic headerbar, maximize, and fullscreen. The headerbar only has the main menu button for now since we have no tabbing or splits. The main menu has the full main menu that mainline GTK has but most of it is disabled since we don't implement the actions yet. I didn't use anything from your branch @tristan957 so I didn't add coauthor but I want to note that @tristan957 worked on this as well and I suspect there's overlap.
This commit is contained in:
@ -32,7 +32,7 @@ pub fn rtApp(self: *Self) *ApprtApp {
|
||||
}
|
||||
|
||||
pub fn close(self: *Self, process_active: bool) void {
|
||||
self.surface.close(process_active);
|
||||
self.surface.close(.{ .surface = process_active });
|
||||
}
|
||||
|
||||
pub fn cgroup(self: *Self) ?[]const u8 {
|
||||
|
@ -10,6 +10,7 @@ const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const build_config = @import("../../../build_config.zig");
|
||||
const i18n = @import("../../../os/main.zig").i18n;
|
||||
const apprt = @import("../../../apprt.zig");
|
||||
const cgroup = @import("../cgroup.zig");
|
||||
const CoreApp = @import("../../../App.zig");
|
||||
@ -28,6 +29,7 @@ const ApprtApp = @import("../App.zig");
|
||||
const Common = @import("../class.zig").Common;
|
||||
const WeakRef = @import("../weak_ref.zig").WeakRef;
|
||||
const Config = @import("config.zig").Config;
|
||||
const Surface = @import("surface.zig").Surface;
|
||||
const Window = @import("window.zig").Window;
|
||||
const CloseConfirmationDialog = @import("close_confirmation_dialog.zig").CloseConfirmationDialog;
|
||||
const ConfigErrorsDialog = @import("config_errors_dialog.zig").ConfigErrorsDialog;
|
||||
@ -420,10 +422,16 @@ pub const Application = extern struct {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the parent for our dialog
|
||||
const parent: ?*gtk.Widget = parent: {
|
||||
const list = gtk.Window.listToplevels();
|
||||
defer list.free();
|
||||
const focused = list.findCustom(null, findActiveWindow);
|
||||
break :parent @ptrCast(@alignCast(focused.f_data));
|
||||
};
|
||||
|
||||
// Show a confirmation dialog
|
||||
const dialog: *CloseConfirmationDialog = .new(.app);
|
||||
|
||||
// Connect to the reload signal so we know to reload our config.
|
||||
_ = CloseConfirmationDialog.signals.@"close-request".connect(
|
||||
dialog,
|
||||
*Application,
|
||||
@ -433,7 +441,7 @@ pub const Application = extern struct {
|
||||
);
|
||||
|
||||
// Show it
|
||||
dialog.present();
|
||||
dialog.present(parent);
|
||||
}
|
||||
|
||||
fn quitNow(self: *Self) void {
|
||||
@ -472,6 +480,9 @@ pub const Application = extern struct {
|
||||
value: apprt.Action.Value(action),
|
||||
) !bool {
|
||||
switch (action) {
|
||||
.close_tab => Action.close(target, .tab),
|
||||
.close_window => Action.close(target, .window),
|
||||
|
||||
.config_change => try Action.configChange(
|
||||
self,
|
||||
target,
|
||||
@ -498,7 +509,7 @@ pub const Application = extern struct {
|
||||
|
||||
.progress_report => return Action.progressReport(target, value),
|
||||
|
||||
.render => Action.render(self, target),
|
||||
.render => Action.render(target),
|
||||
|
||||
.ring_bell => Action.ringBell(target),
|
||||
|
||||
@ -508,12 +519,11 @@ pub const Application = extern struct {
|
||||
|
||||
.show_gtk_inspector => Action.showGtkInspector(),
|
||||
|
||||
.toggle_maximize => Action.toggleMaximize(target),
|
||||
.toggle_fullscreen => Action.toggleFullscreen(target),
|
||||
|
||||
// Unimplemented but todo on gtk-ng branch
|
||||
.close_window,
|
||||
.toggle_maximize,
|
||||
.toggle_fullscreen,
|
||||
.new_tab,
|
||||
.close_tab,
|
||||
.goto_tab,
|
||||
.move_tab,
|
||||
.new_split,
|
||||
@ -681,6 +691,9 @@ pub const Application = extern struct {
|
||||
// Setup our style manager (light/dark mode)
|
||||
self.startupStyleManager();
|
||||
|
||||
// Setup our action map
|
||||
self.startupActionMap();
|
||||
|
||||
// Setup our cgroup for the application.
|
||||
self.startupCgroup() catch |err| {
|
||||
log.warn("cgroup initialization failed err={}", .{err});
|
||||
@ -766,6 +779,30 @@ pub const Application = extern struct {
|
||||
);
|
||||
}
|
||||
|
||||
/// Setup our action map.
|
||||
fn startupActionMap(self: *Self) void {
|
||||
const actions = .{
|
||||
.{ "quit", actionQuit, null },
|
||||
};
|
||||
|
||||
const action_map = self.as(gio.ActionMap);
|
||||
inline for (actions) |entry| {
|
||||
const action = gio.SimpleAction.new(
|
||||
entry[0],
|
||||
entry[2],
|
||||
);
|
||||
defer action.unref();
|
||||
_ = gio.SimpleAction.signals.activate.connect(
|
||||
action,
|
||||
*Self,
|
||||
entry[1],
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
action_map.addAction(action.as(gio.Action));
|
||||
}
|
||||
}
|
||||
|
||||
const CgroupError = error{
|
||||
DbusConnectionFailed,
|
||||
CgroupInitFailed,
|
||||
@ -965,6 +1002,17 @@ pub const Application = extern struct {
|
||||
dialog.present(null);
|
||||
}
|
||||
|
||||
fn actionQuit(
|
||||
_: *gio.SimpleAction,
|
||||
_: ?*glib.Variant,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
const priv = self.private();
|
||||
priv.core_app.performAction(self.rt(), .quit) catch |err| {
|
||||
log.warn("error quitting err={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------
|
||||
// Boilerplate/Noise
|
||||
|
||||
@ -1012,6 +1060,16 @@ pub const Application = extern struct {
|
||||
|
||||
/// All apprt action handlers
|
||||
const Action = struct {
|
||||
pub fn close(
|
||||
target: apprt.Target,
|
||||
scope: Surface.CloseScope,
|
||||
) void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |v| v.rt_surface.surface.close(scope),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn configChange(
|
||||
self: *Application,
|
||||
target: apprt.Target,
|
||||
@ -1145,7 +1203,7 @@ const Action = struct {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn render(_: *Application, target: apprt.Target) void {
|
||||
pub fn render(target: apprt.Target) void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |v| v.rt_surface.surface.redraw(),
|
||||
@ -1190,6 +1248,20 @@ const Action = struct {
|
||||
pub fn showGtkInspector() void {
|
||||
gtk.Window.setInteractiveDebugging(@intFromBool(true));
|
||||
}
|
||||
|
||||
pub fn toggleFullscreen(target: apprt.Target) void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |v| v.rt_surface.surface.toggleFullscreen(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggleMaximize(target: apprt.Target) void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |v| v.rt_surface.surface.toggleMaximize(),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// This sets various GTK-related environment variables as necessary
|
||||
@ -1297,3 +1369,12 @@ fn setGtkEnv(config: *const CoreConfig) error{NoSpaceLeft}!void {
|
||||
_ = internal_os.setenv("GDK_DISABLE", value[0 .. value.len - 1 :0]);
|
||||
}
|
||||
}
|
||||
|
||||
fn findActiveWindow(data: ?*const anyopaque, _: ?*const anyopaque) callconv(.c) c_int {
|
||||
const window: *gtk.Window = @ptrCast(@alignCast(@constCast(data orelse return -1)));
|
||||
|
||||
// Confusingly, `isActive` returns 1 when active,
|
||||
// but we want to return 0 to indicate equality.
|
||||
// Abusing integers to be enums and booleans is a terrible idea, C.
|
||||
return if (window.isActive() != 0) 0 else -1;
|
||||
}
|
||||
|
@ -79,9 +79,8 @@ pub const CloseConfirmationDialog = extern struct {
|
||||
self.as(Dialog.Parent).setBody(priv.target.body());
|
||||
}
|
||||
|
||||
pub fn present(self: *Self) void {
|
||||
const priv = self.private();
|
||||
self.as(Dialog).present(priv.target.dialogParent());
|
||||
pub fn present(self: *Self, parent: ?*gtk.Widget) void {
|
||||
self.as(Dialog).present(parent);
|
||||
}
|
||||
|
||||
pub fn close(self: *Self) void {
|
||||
@ -159,28 +158,19 @@ pub const CloseConfirmationDialog = extern struct {
|
||||
/// together into one struct that is the sole source of truth.
|
||||
pub const Target = enum(c_int) {
|
||||
app,
|
||||
window,
|
||||
|
||||
pub fn title(self: Target) [*:0]const u8 {
|
||||
return switch (self) {
|
||||
.app => i18n._("Quit Ghostty?"),
|
||||
.window => i18n._("Close Window?"),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn body(self: Target) [*:0]const u8 {
|
||||
return switch (self) {
|
||||
.app => i18n._("All terminal sessions will be terminated."),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn dialogParent(self: Target) ?*gtk.Widget {
|
||||
return switch (self) {
|
||||
.app => {
|
||||
// Find the currently focused window.
|
||||
const list = gtk.Window.listToplevels();
|
||||
defer list.free();
|
||||
const focused = list.findCustom(null, findActiveWindow);
|
||||
return @ptrCast(@alignCast(focused.f_data));
|
||||
},
|
||||
.window => i18n._("All terminal sessions in this window will be terminated."),
|
||||
};
|
||||
}
|
||||
|
||||
@ -189,12 +179,3 @@ pub const Target = enum(c_int) {
|
||||
.{ .name = "GhosttyCloseConfirmationDialogTarget" },
|
||||
);
|
||||
};
|
||||
|
||||
fn findActiveWindow(data: ?*const anyopaque, _: ?*const anyopaque) callconv(.c) c_int {
|
||||
const window: *gtk.Window = @ptrCast(@alignCast(@constCast(data orelse return -1)));
|
||||
|
||||
// Confusingly, `isActive` returns 1 when active,
|
||||
// but we want to return 0 to indicate equality.
|
||||
// Abusing integers to be enums and booleans is a terrible idea, C.
|
||||
return if (window.isActive() != 0) 0 else -1;
|
||||
}
|
||||
|
@ -209,6 +209,26 @@ pub const Surface = extern struct {
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
pub const zoom = struct {
|
||||
pub const name = "zoom";
|
||||
const impl = gobject.ext.defineProperty(
|
||||
name,
|
||||
Self,
|
||||
bool,
|
||||
.{
|
||||
.nick = "Zoom",
|
||||
.blurb = "Whether the surface should be zoomed.",
|
||||
.default = false,
|
||||
.accessor = gobject.ext.privateFieldAccessor(
|
||||
Self,
|
||||
Private,
|
||||
&Private.offset,
|
||||
"zoom",
|
||||
),
|
||||
},
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
pub const signals = struct {
|
||||
@ -228,7 +248,7 @@ pub const Surface = extern struct {
|
||||
const impl = gobject.ext.defineSignal(
|
||||
name,
|
||||
Self,
|
||||
&.{bool},
|
||||
&.{*const CloseScope},
|
||||
void,
|
||||
);
|
||||
};
|
||||
@ -271,6 +291,32 @@ pub const Surface = extern struct {
|
||||
void,
|
||||
);
|
||||
};
|
||||
|
||||
/// Emitted when this surface requests its container to toggle its
|
||||
/// fullscreen state.
|
||||
pub const @"toggle-fullscreen" = struct {
|
||||
pub const name = "toggle-fullscreen";
|
||||
pub const connect = impl.connect;
|
||||
const impl = gobject.ext.defineSignal(
|
||||
name,
|
||||
Self,
|
||||
&.{},
|
||||
void,
|
||||
);
|
||||
};
|
||||
|
||||
/// Emitted when this surface requests its container to toggle its
|
||||
/// maximized state.
|
||||
pub const @"toggle-maximize" = struct {
|
||||
pub const name = "toggle-maximize";
|
||||
pub const connect = impl.connect;
|
||||
const impl = gobject.ext.defineSignal(
|
||||
name,
|
||||
Self,
|
||||
&.{},
|
||||
void,
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
const Private = struct {
|
||||
@ -310,6 +356,10 @@ pub const Surface = extern struct {
|
||||
/// focus events.
|
||||
focused: bool = true,
|
||||
|
||||
/// Whether this surface is "zoomed" or not. A zoomed surface
|
||||
/// shows up taking the full bounds of a split view.
|
||||
zoom: bool = false,
|
||||
|
||||
/// The GLAarea that renders the actual surface. This is a binding
|
||||
/// to the template so it doesn't have to be unrefed manually.
|
||||
gl_area: *gtk.GLArea,
|
||||
@ -430,6 +480,24 @@ pub const Surface = extern struct {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn toggleFullscreen(self: *Self) void {
|
||||
signals.@"toggle-fullscreen".impl.emit(
|
||||
self,
|
||||
null,
|
||||
.{},
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn toggleMaximize(self: *Self) void {
|
||||
signals.@"toggle-maximize".impl.emit(
|
||||
self,
|
||||
null,
|
||||
.{},
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
/// Set the current progress report state.
|
||||
pub fn setProgressReport(
|
||||
self: *Self,
|
||||
@ -829,11 +897,11 @@ pub const Surface = extern struct {
|
||||
//---------------------------------------------------------------
|
||||
// Libghostty Callbacks
|
||||
|
||||
pub fn close(self: *Self, process_active: bool) void {
|
||||
pub fn close(self: *Self, scope: CloseScope) void {
|
||||
signals.@"close-request".impl.emit(
|
||||
self,
|
||||
null,
|
||||
.{process_active},
|
||||
.{&scope},
|
||||
null,
|
||||
);
|
||||
}
|
||||
@ -1262,7 +1330,7 @@ pub const Surface = extern struct {
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
// This closes the surface with no confirmation.
|
||||
self.close(false);
|
||||
self.close(.{ .surface = false });
|
||||
}
|
||||
|
||||
fn dtDrop(
|
||||
@ -2076,6 +2144,7 @@ pub const Surface = extern struct {
|
||||
properties.@"mouse-hover-url".impl,
|
||||
properties.pwd.impl,
|
||||
properties.title.impl,
|
||||
properties.zoom.impl,
|
||||
});
|
||||
|
||||
// Signals
|
||||
@ -2083,6 +2152,8 @@ pub const Surface = extern struct {
|
||||
signals.bell.impl.register(.{});
|
||||
signals.@"clipboard-read".impl.register(.{});
|
||||
signals.@"clipboard-write".impl.register(.{});
|
||||
signals.@"toggle-fullscreen".impl.register(.{});
|
||||
signals.@"toggle-maximize".impl.register(.{});
|
||||
|
||||
// Virtual methods
|
||||
gobject.Object.virtual_methods.dispose.implement(class, &dispose);
|
||||
@ -2093,6 +2164,25 @@ pub const Surface = extern struct {
|
||||
pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate;
|
||||
pub const bindTemplateCallback = C.Class.bindTemplateCallback;
|
||||
};
|
||||
|
||||
/// The scope of a close request.
|
||||
pub const CloseScope = union(enum) {
|
||||
/// Close the surface. The boolean determines if there is a
|
||||
/// process active.
|
||||
surface: bool,
|
||||
|
||||
/// Close the tab. We can't know if there are processes active
|
||||
/// for the entire tab scope so listeners must query the app.
|
||||
tab,
|
||||
|
||||
/// Close the window.
|
||||
window,
|
||||
|
||||
pub const getGObjectType = gobject.ext.defineBoxed(
|
||||
CloseScope,
|
||||
.{ .name = "GhosttySurfaceCloseScope" },
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
/// The state of the key event while we're doing IM composition.
|
||||
|
@ -2,13 +2,21 @@ 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 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 DebugWarning = @import("debug_warning.zig").DebugWarning;
|
||||
|
||||
@ -27,6 +35,49 @@ pub const Window = extern struct {
|
||||
});
|
||||
|
||||
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 = gobject.ext.privateFieldAccessor(
|
||||
Self,
|
||||
Private,
|
||||
&Private.offset,
|
||||
"config",
|
||||
),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
pub const debug = struct {
|
||||
pub const name = "debug";
|
||||
const impl = gobject.ext.defineProperty(
|
||||
@ -47,10 +98,30 @@ pub const Window = extern struct {
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
pub const @"headerbar-visible" = struct {
|
||||
pub const name = "headerbar-visible";
|
||||
const impl = gobject.ext.defineProperty(
|
||||
name,
|
||||
Self,
|
||||
bool,
|
||||
.{
|
||||
.nick = "Headerbar Visible",
|
||||
.blurb = "True if the headerbar is visible.",
|
||||
.default = true,
|
||||
.accessor = gobject.ext.typedAccessor(Self, bool, .{
|
||||
.getter = Self.getHeaderbarVisible,
|
||||
}),
|
||||
},
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
const Private = struct {
|
||||
/// The surface in the view.
|
||||
/// The configuration that this surface is using.
|
||||
config: ?*Config = null,
|
||||
|
||||
// Template bindings
|
||||
surface: *Surface = undefined,
|
||||
|
||||
pub var offset: c_int = 0;
|
||||
@ -72,14 +143,187 @@ pub const Window = extern struct {
|
||||
fn init(self: *Self, _: *Class) callconv(.C) void {
|
||||
gtk.Widget.initTemplate(self.as(gtk.Widget));
|
||||
|
||||
if (comptime build_config.is_debug)
|
||||
// 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();
|
||||
}
|
||||
|
||||
// Add our dev CSS class if we're in debug mode.
|
||||
if (comptime build_config.is_debug) {
|
||||
self.as(gtk.Widget).addCssClass("devel");
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
// Initialize our actions
|
||||
self.initActionMap();
|
||||
|
||||
// We always sync our appearance at the end because loading our
|
||||
// config and such can affect our bindings which ar setup initially
|
||||
// in initTemplate.
|
||||
self.syncAppearance();
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
/// Setup our action map.
|
||||
fn initActionMap(self: *Self) void {
|
||||
const actions = .{
|
||||
.{ "about", actionAbout, null },
|
||||
.{ "close", actionClose, null },
|
||||
.{ "copy", actionCopy, null },
|
||||
.{ "paste", actionPaste, null },
|
||||
.{ "reset", actionReset, null },
|
||||
.{ "clear", actionClear, null },
|
||||
};
|
||||
|
||||
const action_map = self.as(gio.ActionMap);
|
||||
inline for (actions) |entry| {
|
||||
const action = gio.SimpleAction.new(
|
||||
entry[0],
|
||||
entry[2],
|
||||
);
|
||||
defer action.unref();
|
||||
_ = gio.SimpleAction.signals.activate.connect(
|
||||
action,
|
||||
*Self,
|
||||
entry[1],
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
action_map.addAction(action.as(gio.Action));
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
// TODO: CSD/SSD
|
||||
|
||||
// Trigger our headerbar visibility to refresh
|
||||
self.as(gobject.Object).notifyByPspec(properties.@"headerbar-visible".impl.param_spec);
|
||||
}
|
||||
|
||||
/// Perform a binding action on the window's active surface.
|
||||
fn performBindingAction(
|
||||
self: *Window,
|
||||
action: input.Binding.Action,
|
||||
) void {
|
||||
const surface = self.getActiveSurface() orelse return;
|
||||
const core_surface = surface.core() orelse return;
|
||||
_ = core_surface.performBindingAction(action) catch |err| {
|
||||
log.warn("error performing binding action error={}", .{err});
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Properties
|
||||
|
||||
/// Get the currently active surface. See the "active-surface" property.
|
||||
/// This does not ref the value.
|
||||
fn getActiveSurface(self: *Self) ?*Surface {
|
||||
const priv = self.private();
|
||||
return priv.surface;
|
||||
}
|
||||
|
||||
fn getHeaderbarVisible(self: *Self) bool {
|
||||
// TODO: CSD/SSD
|
||||
// TODO: QuickTerminal
|
||||
|
||||
// If we're fullscreen we never show the header bar.
|
||||
if (self.as(gtk.Window).isFullscreen() != 0) return false;
|
||||
|
||||
// The remainder needs a config
|
||||
const config_obj = self.private().config orelse return true;
|
||||
const config = config_obj.get();
|
||||
|
||||
// *Conditionally* disable the header bar when maximized,
|
||||
// and gtk-titlebar-hide-when-maximized is set
|
||||
if (self.as(gtk.Window).isMaximized() != 0 and
|
||||
config.@"gtk-titlebar-hide-when-maximized")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return config.@"gtk-titlebar";
|
||||
}
|
||||
|
||||
fn propConfig(
|
||||
_: *adw.ApplicationWindow,
|
||||
_: *gobject.ParamSpec,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
self.syncAppearance();
|
||||
}
|
||||
|
||||
fn propFullscreened(
|
||||
_: *adw.ApplicationWindow,
|
||||
_: *gobject.ParamSpec,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
self.syncAppearance();
|
||||
}
|
||||
|
||||
fn propMaximized(
|
||||
_: *adw.ApplicationWindow,
|
||||
_: *gobject.ParamSpec,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
self.syncAppearance();
|
||||
}
|
||||
|
||||
fn propMenuActive(
|
||||
button: *gtk.MenuButton,
|
||||
_: *gobject.ParamSpec,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
// Debian 12 is stuck on GTK 4.8
|
||||
if (!gtk_version.atLeast(4, 10, 0)) return;
|
||||
|
||||
// We only care if we're activating. If we're activating then
|
||||
// we need to check the validity of our menu items.
|
||||
const active = button.getActive() != 0;
|
||||
if (!active) return;
|
||||
|
||||
const has_selection = selection: {
|
||||
const surface = self.getActiveSurface() orelse
|
||||
break :selection false;
|
||||
const core_surface = surface.core() orelse
|
||||
break :selection false;
|
||||
break :selection core_surface.hasSelection();
|
||||
};
|
||||
|
||||
const action_map: *gio.ActionMap = gobject.ext.cast(
|
||||
gio.ActionMap,
|
||||
self,
|
||||
) orelse return;
|
||||
const action: *gio.SimpleAction = gobject.ext.cast(
|
||||
gio.SimpleAction,
|
||||
action_map.lookupAction("copy") orelse return,
|
||||
) orelse return;
|
||||
action.setEnabled(@intFromBool(has_selection));
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Virtual methods
|
||||
|
||||
fn dispose(self: *Self) callconv(.C) void {
|
||||
const priv = self.private();
|
||||
if (priv.config) |v| {
|
||||
v.unref();
|
||||
priv.config = null;
|
||||
}
|
||||
|
||||
gtk.Widget.disposeTemplate(
|
||||
self.as(gtk.Widget),
|
||||
getGObjectType(),
|
||||
@ -94,18 +338,168 @@ pub const Window = extern struct {
|
||||
//---------------------------------------------------------------
|
||||
// Signal handlers
|
||||
|
||||
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;
|
||||
|
||||
// Show a confirmation dialog
|
||||
const dialog: *CloseConfirmationDialog = .new(.app);
|
||||
_ = CloseConfirmationDialog.signals.@"close-request".connect(
|
||||
dialog,
|
||||
*Self,
|
||||
closeConfirmationClose,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
|
||||
// Show it
|
||||
dialog.present(self.as(gtk.Widget));
|
||||
return @intFromBool(true);
|
||||
}
|
||||
|
||||
self.as(gtk.Window).destroy();
|
||||
return @intFromBool(false);
|
||||
}
|
||||
|
||||
fn closeConfirmationClose(
|
||||
_: *CloseConfirmationDialog,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
self.as(gtk.Window).destroy();
|
||||
}
|
||||
|
||||
fn surfaceCloseRequest(
|
||||
surface: *Surface,
|
||||
process_active: bool,
|
||||
scope: *const Surface.CloseScope,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
// Todo
|
||||
_ = process_active;
|
||||
_ = scope;
|
||||
|
||||
assert(surface == self.private().surface);
|
||||
self.as(gtk.Window).close();
|
||||
}
|
||||
|
||||
fn surfaceToggleFullscreen(
|
||||
surface: *Surface,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
_ = surface;
|
||||
if (self.as(gtk.Window).isFullscreen() != 0) {
|
||||
self.as(gtk.Window).unfullscreen();
|
||||
} else {
|
||||
self.as(gtk.Window).fullscreen();
|
||||
}
|
||||
|
||||
// We react to the changes in the propFullscreen callback
|
||||
}
|
||||
|
||||
fn surfaceToggleMaximize(
|
||||
surface: *Surface,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
_ = surface;
|
||||
if (self.as(gtk.Window).isMaximized() != 0) {
|
||||
self.as(gtk.Window).unmaximize();
|
||||
} else {
|
||||
self.as(gtk.Window).maximize();
|
||||
}
|
||||
|
||||
// We react to the changes in the propMaximized callback
|
||||
}
|
||||
|
||||
fn actionAbout(
|
||||
_: *gio.SimpleAction,
|
||||
_: ?*glib.Variant,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
const name = "Ghostty";
|
||||
const icon = "com.mitchellh.ghostty";
|
||||
const website = "https://ghostty.org";
|
||||
|
||||
if (adw_version.supportsDialogs()) {
|
||||
adw.showAboutDialog(
|
||||
self.as(gtk.Widget),
|
||||
"application-name",
|
||||
name,
|
||||
"developer-name",
|
||||
i18n._("Ghostty Developers"),
|
||||
"application-icon",
|
||||
icon,
|
||||
"version",
|
||||
build_config.version_string.ptr,
|
||||
"issue-url",
|
||||
"https://github.com/ghostty-org/ghostty/issues",
|
||||
"website",
|
||||
website,
|
||||
@as(?*anyopaque, null),
|
||||
);
|
||||
} else {
|
||||
gtk.showAboutDialog(
|
||||
self.as(gtk.Window),
|
||||
"program-name",
|
||||
name,
|
||||
"logo-icon-name",
|
||||
icon,
|
||||
"title",
|
||||
i18n._("About Ghostty"),
|
||||
"version",
|
||||
build_config.version_string.ptr,
|
||||
"website",
|
||||
website,
|
||||
@as(?*anyopaque, null),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn actionClose(
|
||||
_: *gio.SimpleAction,
|
||||
_: ?*glib.Variant,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
self.as(gtk.Window).close();
|
||||
}
|
||||
|
||||
fn actionCopy(
|
||||
_: *gio.SimpleAction,
|
||||
_: ?*glib.Variant,
|
||||
self: *Window,
|
||||
) callconv(.c) void {
|
||||
self.performBindingAction(.copy_to_clipboard);
|
||||
}
|
||||
|
||||
fn actionPaste(
|
||||
_: *gio.SimpleAction,
|
||||
_: ?*glib.Variant,
|
||||
self: *Window,
|
||||
) callconv(.c) void {
|
||||
self.performBindingAction(.paste_from_clipboard);
|
||||
}
|
||||
|
||||
fn actionReset(
|
||||
_: *gio.SimpleAction,
|
||||
_: ?*glib.Variant,
|
||||
self: *Window,
|
||||
) callconv(.c) void {
|
||||
self.performBindingAction(.reset);
|
||||
}
|
||||
|
||||
fn actionClear(
|
||||
_: *gio.SimpleAction,
|
||||
_: ?*glib.Variant,
|
||||
self: *Window,
|
||||
) callconv(.c) void {
|
||||
self.performBindingAction(.clear_screen);
|
||||
}
|
||||
|
||||
const C = Common(Self, Private);
|
||||
pub const as = C.as;
|
||||
pub const ref = C.ref;
|
||||
@ -131,14 +525,24 @@ pub const Window = extern struct {
|
||||
|
||||
// Properties
|
||||
gobject.ext.registerProperties(class, &.{
|
||||
properties.@"active-surface".impl,
|
||||
properties.config.impl,
|
||||
properties.debug.impl,
|
||||
properties.@"headerbar-visible".impl,
|
||||
});
|
||||
|
||||
// Bindings
|
||||
class.bindTemplateChildPrivate("surface", .{});
|
||||
|
||||
// Template Callbacks
|
||||
class.bindTemplateCallback("close_request", &windowCloseRequest);
|
||||
class.bindTemplateCallback("surface_close_request", &surfaceCloseRequest);
|
||||
class.bindTemplateCallback("surface_toggle_fullscreen", &surfaceToggleFullscreen);
|
||||
class.bindTemplateCallback("surface_toggle_maximize", &surfaceToggleMaximize);
|
||||
class.bindTemplateCallback("notify_config", &propConfig);
|
||||
class.bindTemplateCallback("notify_fullscreened", &propFullscreened);
|
||||
class.bindTemplateCallback("notify_maximized", &propMaximized);
|
||||
class.bindTemplateCallback("notify_menu_active", &propMenuActive);
|
||||
|
||||
// Virtual methods
|
||||
gobject.Object.virtual_methods.dispose.implement(class, &dispose);
|
||||
|
@ -6,12 +6,38 @@ template $GhosttyWindow: Adw.ApplicationWindow {
|
||||
"window",
|
||||
]
|
||||
|
||||
close-request => $close_request();
|
||||
notify::config => $notify_config();
|
||||
notify::fullscreened => $notify_fullscreened();
|
||||
notify::maximized => $notify_maximized();
|
||||
default-width: 800;
|
||||
default-height: 600;
|
||||
// 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: Gtk.Box {
|
||||
content: Box {
|
||||
orientation: vertical;
|
||||
spacing: 0;
|
||||
|
||||
Adw.HeaderBar {
|
||||
visible: bind template.headerbar-visible;
|
||||
|
||||
title-widget: Adw.WindowTitle {
|
||||
title: bind (template.active-surface as <$GhosttySurface>).title;
|
||||
};
|
||||
|
||||
[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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$GhosttyDebugWarning {
|
||||
visible: bind template.debug;
|
||||
@ -19,6 +45,145 @@ template $GhosttyWindow: Adw.ApplicationWindow {
|
||||
|
||||
$GhosttySurface surface {
|
||||
close-request => $surface_close_request();
|
||||
toggle-fullscreen => $surface_toggle_fullscreen();
|
||||
toggle-maximize => $surface_toggle_maximize();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
menu split_menu {
|
||||
item {
|
||||
label: _("Split Up");
|
||||
action: "win.split-up";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Split Down");
|
||||
action: "win.split-down";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Split Left");
|
||||
action: "win.split-left";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Split Right");
|
||||
action: "win.split-right";
|
||||
}
|
||||
}
|
||||
|
||||
menu main_menu {
|
||||
section {
|
||||
item {
|
||||
label: _("Copy");
|
||||
action: "win.copy";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Paste");
|
||||
action: "win.paste";
|
||||
}
|
||||
}
|
||||
|
||||
section {
|
||||
item {
|
||||
label: _("New Window");
|
||||
action: "win.new-window";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Close Window");
|
||||
action: "win.close";
|
||||
}
|
||||
}
|
||||
|
||||
section {
|
||||
item {
|
||||
label: _("New Tab");
|
||||
action: "win.new-tab";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Close Tab");
|
||||
action: "win.close-tab";
|
||||
}
|
||||
}
|
||||
|
||||
section {
|
||||
submenu {
|
||||
label: _("Split");
|
||||
|
||||
item {
|
||||
label: _("Change Title…");
|
||||
action: "win.prompt-title";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Split Up");
|
||||
action: "win.split-up";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Split Down");
|
||||
action: "win.split-down";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Split Left");
|
||||
action: "win.split-left";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Split Right");
|
||||
action: "win.split-right";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section {
|
||||
item {
|
||||
label: _("Clear");
|
||||
action: "win.clear";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Reset");
|
||||
action: "win.reset";
|
||||
}
|
||||
}
|
||||
|
||||
section {
|
||||
item {
|
||||
label: _("Command Palette");
|
||||
action: "win.toggle-command-palette";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Terminal Inspector");
|
||||
action: "win.toggle-inspector";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Open Configuration");
|
||||
action: "app.open-config";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Reload Configuration");
|
||||
action: "app.reload-config";
|
||||
}
|
||||
}
|
||||
|
||||
section {
|
||||
item {
|
||||
label: _("About Ghostty");
|
||||
action: "win.about";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Quit");
|
||||
action: "app.quit";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
122
valgrind.supp
122
valgrind.supp
@ -13,6 +13,34 @@
|
||||
# You must gracefully exit Ghostty (do not SIGINT) by closing all windows
|
||||
# and quitting. Otherwise, we leave a number of GTK resources around.
|
||||
|
||||
{
|
||||
GDK SVG Loading Leaks
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: possible
|
||||
fun:malloc
|
||||
...
|
||||
fun:rsvg_*
|
||||
...
|
||||
fun:gdk_pixbuf_loader_close
|
||||
fun:load_from_stream
|
||||
fun:gdk_pixbuf_new_from_stream_at_scale
|
||||
fun:gtk_make_symbolic_pixbuf_from_data
|
||||
fun:gdk_texture_new_*
|
||||
...
|
||||
}
|
||||
|
||||
{
|
||||
GDK SVG Loading Touches Undefined Memory
|
||||
Memcheck:Cond
|
||||
...
|
||||
fun:rsvg_handle_get_pixbuf_and_error
|
||||
...
|
||||
fun:gdk_pixbuf_loader_close
|
||||
fun:load_from_stream
|
||||
fun:gdk_pixbuf_new_from_stream_at_scale
|
||||
...
|
||||
}
|
||||
|
||||
{
|
||||
GDK Drag and Drop Leaks Data
|
||||
Memcheck:Leak
|
||||
@ -136,7 +164,7 @@
|
||||
fun:signal_emit_valist_unlocked
|
||||
fun:g_signal_emit_valist
|
||||
fun:g_signal_emit
|
||||
fun:gdk_frame_clock_paint_idle
|
||||
fun:gdk_frame_clock_*
|
||||
...
|
||||
fun:g_timeout_dispatch
|
||||
fun:g_main_context_dispatch_unlocked
|
||||
@ -144,6 +172,36 @@
|
||||
fun:g_main_context_iteration
|
||||
...
|
||||
}
|
||||
{
|
||||
GSK GPU Rendering Another Form
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: possible
|
||||
...
|
||||
fun:gsk_gpu_render_pass_op_gl_command
|
||||
fun:gsk_gl_frame_submit
|
||||
fun:gsk_gpu_renderer_render
|
||||
fun:gsk_renderer_render
|
||||
fun:gtk_widget_render
|
||||
fun:surface_render
|
||||
fun:_gdk_marshal_BOOLEAN__BOXEDv
|
||||
fun:_g_closure_invoke_va
|
||||
fun:signal_emit_valist_unlocked
|
||||
fun:g_signal_emit_valist
|
||||
fun:g_signal_emit
|
||||
fun:gdk_surface_paint_on_clock
|
||||
fun:g_closure_invoke
|
||||
fun:signal_emit_unlocked_R.isra.0
|
||||
fun:signal_emit_valist_unlocked
|
||||
fun:g_signal_emit_valist
|
||||
fun:g_signal_emit
|
||||
fun:gdk_frame_clock_*
|
||||
...
|
||||
fun:g_timeout_dispatch
|
||||
fun:g_main_context_dispatch_unlocked
|
||||
fun:g_main_context_iterate_unlocked.isra.0
|
||||
fun:g_main_context_iteration
|
||||
...
|
||||
}
|
||||
{
|
||||
GTK Shader Selector
|
||||
Memcheck:Leak
|
||||
@ -178,6 +236,31 @@
|
||||
...
|
||||
}
|
||||
|
||||
{
|
||||
Another tooltip
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: possible
|
||||
...
|
||||
fun:gtk_string_accessible_value_new
|
||||
fun:gtk_accessible_update_property
|
||||
fun:gtk_label_set_text_internal
|
||||
fun:gtk_label_set_markup_internal
|
||||
fun:gtk_label_recalculate
|
||||
fun:gtk_label_set_markup
|
||||
fun:gtk_tooltip_window_set_label_markup
|
||||
fun:gtk_widget_real_query_tooltip
|
||||
fun:_gtk_marshal_BOOLEAN__INT_INT_BOOLEAN_OBJECTv
|
||||
fun:g_type_class_meta_marshalv
|
||||
fun:_g_closure_invoke_va
|
||||
fun:signal_emit_valist_unlocked
|
||||
fun:g_signal_emit_valist
|
||||
fun:g_signal_emit
|
||||
fun:gtk_widget_query_tooltip
|
||||
fun:gtk_tooltip_run_requery
|
||||
fun:gtk_tooltip_handle_event_internal
|
||||
fun:_gtk_tooltip_handle_event
|
||||
...
|
||||
}
|
||||
{
|
||||
Not sure about this one, I can't figure it out.
|
||||
Memcheck:Leak
|
||||
@ -239,6 +322,32 @@
|
||||
...
|
||||
}
|
||||
|
||||
{
|
||||
GTK FontConfig
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: possible
|
||||
fun:malloc
|
||||
fun:FcFontSet*
|
||||
fun:FcFontSet*
|
||||
fun:sort_in_thread.isra.0
|
||||
fun:fc_thread_func
|
||||
fun:g_thread_proxy
|
||||
fun:start_thread
|
||||
fun:thread_start
|
||||
}
|
||||
|
||||
{
|
||||
GTK FontConfig
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: possible
|
||||
fun:malloc
|
||||
fun:strdup
|
||||
fun:FcValueSave
|
||||
fun:FcConfigEvaluate
|
||||
fun:FcConfigValues
|
||||
...
|
||||
}
|
||||
|
||||
{
|
||||
GTK init
|
||||
Memcheck:Leak
|
||||
@ -300,6 +409,17 @@
|
||||
...
|
||||
}
|
||||
|
||||
{
|
||||
pango and fontconfig
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: definite
|
||||
fun:*alloc
|
||||
...
|
||||
fun:FcChar*
|
||||
fun:pango_fc_coverage_real_get
|
||||
...
|
||||
}
|
||||
|
||||
{
|
||||
pango font map
|
||||
Memcheck:Leak
|
||||
|
Reference in New Issue
Block a user