apprt/gtk-ng: implement Surface.close (#7994)

A small, simple change. This implements the `Surface.close` apprt
required function. After this PR, a process exiting within the terminal
will close the window properly (unless `wait-after-command` is set of
course!).

This doesn't yet show close confirmation. I'm working on some dialog
refactors on the side to see if we can simplify our Adw 1.2 vs. 1.5
dialogs and didn't want to include it here.

Close now works by the `GhosttySurface` class emitting the
`close-request` signal (similar to a `gtk.Window`) and the parent
container is responsible for closing it. This will let us reuse the
surface within different contexts: tabs, splits, etc.

This also remove the unused `shouldClose`, `setShouldClose` apprt APIs
which are a holdover from the glfw days!
This commit is contained in:
Mitchell Hashimoto
2025-07-20 07:05:12 -07:00
committed by GitHub
7 changed files with 74 additions and 43 deletions

View File

@ -132,18 +132,6 @@ pub fn destroy(self: *App) void {
/// events. This should be called by the application runtime on every loop
/// tick.
pub fn tick(self: *App, rt_app: *apprt.App) !void {
// If any surfaces are closing, destroy them
var i: usize = 0;
while (i < self.surfaces.items.len) {
const surface = self.surfaces.items[i];
if (surface.shouldClose()) {
surface.close(false);
continue;
}
i += 1;
}
// Drain our mailbox
try self.drainMailbox(rt_app);
}

View File

@ -705,15 +705,6 @@ pub const Surface = struct {
);
}
pub fn setShouldClose(self: *Surface) void {
_ = self;
}
pub fn shouldClose(self: *const Surface) bool {
_ = self;
return false;
}
pub fn getCursorPos(self: *const Surface) !apprt.CursorPos {
return self.cursor_pos;
}

View File

@ -26,13 +26,7 @@ pub fn rtApp(self: *Self) *ApprtApp {
}
pub fn close(self: *Self, process_active: bool) void {
_ = self;
_ = process_active;
}
pub fn shouldClose(self: *Self) bool {
_ = self;
return false;
self.surface.close(process_active);
}
pub fn getTitle(self: *Self) ?[:0]const u8 {

View File

@ -54,6 +54,29 @@ pub const Surface = extern struct {
};
};
pub const signals = struct {
/// Emitted whenever the surface would like to be closed for any
/// reason.
///
/// The surface view does NOT handle its own close confirmation.
/// If there is a process alive then the boolean parameter will
/// specify it and the parent widget should handle this request.
///
/// This signal lets the containing widget decide how closure works.
/// This lets this Surface widget be used as a split, tab, etc.
/// without it having to be aware of its own semantics.
pub const @"close-request" = struct {
pub const name = "close-request";
pub const connect = impl.connect;
const impl = gobject.ext.defineSignal(
name,
Self,
&.{bool},
void,
);
};
};
const Private = struct {
/// The configuration that this surface is using.
config: ?*Config = null,
@ -374,6 +397,15 @@ pub const Surface = extern struct {
//---------------------------------------------------------------
// Libghostty Callbacks
pub fn close(self: *Self, process_active: bool) void {
signals.@"close-request".impl.emit(
self,
null,
.{process_active},
null,
);
}
pub fn getContentScale(self: *Self) apprt.ContentScale {
const priv = self.private();
const gl_area = priv.gl_area;
@ -1341,6 +1373,9 @@ pub const Surface = extern struct {
properties.config.impl,
});
// Signals
signals.@"close-request".impl.register(.{});
// Virtual methods
gobject.Object.virtual_methods.dispose.implement(class, &dispose);
gobject.Object.virtual_methods.finalize.implement(class, &finalize);

View File

@ -1,4 +1,5 @@
const std = @import("std");
const assert = std.debug.assert;
const adw = @import("adw");
const gobject = @import("gobject");
const gtk = @import("gtk");
@ -23,7 +24,9 @@ pub const Window = extern struct {
});
const Private = struct {
_todo: u8,
/// The surface in the view.
surface: *Surface = undefined,
pub var offset: c_int = 0;
};
@ -33,8 +36,20 @@ pub const Window = extern struct {
fn init(self: *Self, _: *Class) callconv(.C) void {
gtk.Widget.initTemplate(self.as(gtk.Widget));
const surface = self.private().surface;
_ = Surface.signals.@"close-request".connect(
surface,
*Self,
surfaceCloseRequest,
self,
.{},
);
}
//---------------------------------------------------------------
// Virtual methods
fn dispose(self: *Self) callconv(.C) void {
gtk.Widget.disposeTemplate(
self.as(gtk.Widget),
@ -47,6 +62,21 @@ pub const Window = extern struct {
);
}
//---------------------------------------------------------------
// Signal handlers
fn surfaceCloseRequest(
surface: *Surface,
process_active: bool,
self: *Self,
) callconv(.c) void {
// Todo
_ = process_active;
assert(surface == self.private().surface);
self.as(gtk.Window).close();
}
const C = Common(Self, Private);
pub const as = C.as;
pub const ref = C.ref;
@ -60,7 +90,6 @@ pub const Window = extern struct {
fn init(class: *Class) callconv(.C) void {
gobject.ext.ensureType(Surface);
gtk.Widget.Class.setTemplateFromResource(
class.as(gtk.Widget.Class),
comptime gresource.blueprint(.{
@ -70,11 +99,14 @@ pub const Window = extern struct {
}),
);
// Bindings
class.bindTemplateChildPrivate("surface", .{});
// Virtual methods
gobject.Object.virtual_methods.dispose.implement(class, &dispose);
}
pub fn as(class: *Class, comptime T: type) *T {
return gobject.ext.as(T, class);
}
pub const as = C.Class.as;
pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate;
};
};

View File

@ -2,5 +2,5 @@ using Gtk 4.0;
using Adw 1;
template $GhosttyWindow: Adw.ApplicationWindow {
content: $GhosttySurface {};
content: $GhosttySurface surface {};
}

View File

@ -877,15 +877,6 @@ pub fn controlInspector(
};
}
pub fn setShouldClose(self: *Surface) void {
_ = self;
}
pub fn shouldClose(self: *const Surface) bool {
_ = self;
return false;
}
pub fn getContentScale(self: *const Surface) !apprt.ContentScale {
const gtk_scale: f32 = scale: {
const widget = self.gl_area.as(gtk.Widget);