apprt/gtk-ng: hook up surface initialization

This commit is contained in:
Mitchell Hashimoto
2025-07-18 10:42:23 -07:00
parent 9f2ff0cb9c
commit 7c9e913ca9
10 changed files with 338 additions and 36 deletions

View File

@ -154,7 +154,7 @@ pub fn tick(self: *App, rt_app: *apprt.App) !void {
pub fn updateConfig(self: *App, rt_app: *apprt.App, config: *const Config) !void { pub fn updateConfig(self: *App, rt_app: *apprt.App, config: *const Config) !void {
// Go through and update all of the surface configurations. // Go through and update all of the surface configurations.
for (self.surfaces.items) |surface| { for (self.surfaces.items) |surface| {
try surface.core_surface.handleMessage(.{ .change_config = config }); try surface.core().handleMessage(.{ .change_config = config });
} }
// Apply our conditional state. If we fail to apply the conditional state // Apply our conditional state. If we fail to apply the conditional state
@ -190,7 +190,7 @@ pub fn addSurface(
// Since we have non-zero surfaces, we can cancel the quit timer. // Since we have non-zero surfaces, we can cancel the quit timer.
// It is up to the apprt if there is a quit timer at all and if it // It is up to the apprt if there is a quit timer at all and if it
// should be canceled. // should be canceled.
_ = rt_surface.app.performAction( _ = rt_surface.rtApp().performAction(
.app, .app,
.quit_timer, .quit_timer,
.stop, .stop,
@ -207,7 +207,7 @@ pub fn deleteSurface(self: *App, rt_surface: *apprt.Surface) void {
// just let focused surface be but the allocator was reusing addresses // just let focused surface be but the allocator was reusing addresses
// after free and giving false positives, so we must clear it. // after free and giving false positives, so we must clear it.
if (self.focused_surface) |focused| { if (self.focused_surface) |focused| {
if (focused == &rt_surface.core_surface) { if (focused == rt_surface.core()) {
self.focused_surface = null; self.focused_surface = null;
} }
} }
@ -224,7 +224,7 @@ pub fn deleteSurface(self: *App, rt_surface: *apprt.Surface) void {
// If we have no surfaces, we can start the quit timer. It is up to the // If we have no surfaces, we can start the quit timer. It is up to the
// apprt to determine if this is necessary. // apprt to determine if this is necessary.
if (self.surfaces.items.len == 0) _ = rt_surface.app.performAction( if (self.surfaces.items.len == 0) _ = rt_surface.rtApp().performAction(
.app, .app,
.quit_timer, .quit_timer,
.start, .start,
@ -245,7 +245,7 @@ pub fn focusedSurface(self: *const App) ?*Surface {
/// the apprt to call this. /// the apprt to call this.
pub fn needsConfirmQuit(self: *const App) bool { pub fn needsConfirmQuit(self: *const App) bool {
for (self.surfaces.items) |v| { for (self.surfaces.items) |v| {
if (v.core_surface.needsConfirmQuit()) return true; if (v.core().needsConfirmQuit()) return true;
} }
return false; return false;
@ -287,12 +287,12 @@ pub fn focusSurface(self: *App, surface: *Surface) void {
} }
fn redrawSurface(self: *App, rt_app: *apprt.App, surface: *apprt.Surface) void { fn redrawSurface(self: *App, rt_app: *apprt.App, surface: *apprt.Surface) void {
if (!self.hasSurface(&surface.core_surface)) return; if (!self.hasRtSurface(surface)) return;
rt_app.redrawSurface(surface); rt_app.redrawSurface(surface);
} }
fn redrawInspector(self: *App, rt_app: *apprt.App, surface: *apprt.Surface) void { fn redrawInspector(self: *App, rt_app: *apprt.App, surface: *apprt.Surface) void {
if (!self.hasSurface(&surface.core_surface)) return; if (!self.hasRtSurface(surface)) return;
rt_app.redrawInspector(surface); rt_app.redrawInspector(surface);
} }
@ -482,7 +482,7 @@ pub fn performAllAction(
// Surface-scoped actions are performed on all surfaces. Errors // Surface-scoped actions are performed on all surfaces. Errors
// are logged but processing continues. // are logged but processing continues.
.surface => for (self.surfaces.items) |surface| { .surface => for (self.surfaces.items) |surface| {
_ = surface.core_surface.performBindingAction(action) catch |err| { _ = surface.core().performBindingAction(action) catch |err| {
log.warn("error performing binding action on surface ptr={X} err={}", .{ log.warn("error performing binding action on surface ptr={X} err={}", .{
@intFromPtr(surface), @intFromPtr(surface),
err, err,
@ -507,7 +507,15 @@ fn surfaceMessage(self: *App, surface: *Surface, msg: apprt.surface.Message) !vo
fn hasSurface(self: *const App, surface: *const Surface) bool { fn hasSurface(self: *const App, surface: *const Surface) bool {
for (self.surfaces.items) |v| { for (self.surfaces.items) |v| {
if (&v.core_surface == surface) return true; if (v.core() == surface) return true;
}
return false;
}
fn hasRtSurface(self: *const App, surface: *apprt.Surface) bool {
for (self.surfaces.items) |v| {
if (v == surface) return true;
} }
return false; return false;

View File

@ -1738,7 +1738,10 @@ pub fn selectionInfo(self: *const Surface) ?apprt.Selection {
/// Returns the pwd of the terminal, if any. This is always copied because /// Returns the pwd of the terminal, if any. This is always copied because
/// the pwd can change at any point from termio. If we are calling from the IO /// the pwd can change at any point from termio. If we are calling from the IO
/// thread you should just check the terminal directly. /// thread you should just check the terminal directly.
pub fn pwd(self: *const Surface, alloc: Allocator) !?[]const u8 { pub fn pwd(
self: *const Surface,
alloc: Allocator,
) Allocator.Error!?[]const u8 {
self.renderer_state.mutex.lock(); self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock(); defer self.renderer_state.mutex.unlock();
const terminal_pwd = self.io.terminal.getPwd() orelse return null; const terminal_pwd = self.io.terminal.getPwd() orelse return null;

View File

@ -601,6 +601,14 @@ pub const Surface = struct {
} }
} }
pub fn core(self: *Surface) *CoreSurface {
return &self.core_surface;
}
pub fn rtApp(self: *const Surface) *App {
return self.app;
}
pub fn close(self: *const Surface, process_alive: bool) void { pub fn close(self: *const Surface, process_alive: bool) void {
const func = self.app.opts.close_surface orelse { const func = self.app.opts.close_surface orelse {
log.info("runtime embedder does not support closing a surface", .{}); log.info("runtime embedder does not support closing a surface", .{});

View File

@ -19,6 +19,11 @@ const adw_version = @import("adw_version.zig");
const log = std.log.scoped(.gtk); const log = std.log.scoped(.gtk);
/// This is detected by the Renderer, in which case it sends a `redraw_surface`
/// message so that we can call `drawFrame` ourselves from the app thread,
/// because GTK's `GLArea` does not support drawing from a different thread.
pub const must_draw_from_app_thread = true;
/// The GObject Application instance /// The GObject Application instance
app: *Application, app: *Application,
@ -48,6 +53,11 @@ pub fn terminate(self: *App) void {
self.app.deinit(); self.app.deinit();
} }
/// Called by CoreApp to wake up the event loop.
pub fn wakeup(self: *App) void {
self.app.wakeup();
}
pub fn performAction( pub fn performAction(
self: *App, self: *App,
target: apprt.Target, target: apprt.Target,

View File

@ -1,41 +1,62 @@
const Surface = @This(); const Self = @This();
const std = @import("std");
const apprt = @import("../../apprt.zig"); const apprt = @import("../../apprt.zig");
const CoreSurface = @import("../../Surface.zig"); const CoreSurface = @import("../../Surface.zig");
const ApprtApp = @import("App.zig");
const Application = @import("class/application.zig").Application;
const Surface = @import("class/surface.zig").Surface;
core_surface: CoreSurface, /// The GObject Surface
surface: *Surface,
pub fn deinit(self: *Surface) void { pub fn deinit(self: *Self) void {
_ = self; _ = self;
} }
pub fn close(self: *Surface, process_active: bool) void { pub fn core(self: *Self) *CoreSurface {
// This asserts the non-optional because libghostty should only
// be calling this for initialized surfaces.
return self.surface.core().?;
}
pub fn rtApp(self: *Self) *ApprtApp {
_ = self;
return Application.default().rt();
}
pub fn close(self: *Self, process_active: bool) void {
_ = self; _ = self;
_ = process_active; _ = process_active;
} }
pub fn shouldClose(self: *Surface) bool { pub fn shouldClose(self: *Self) bool {
_ = self; _ = self;
return false; return false;
} }
pub fn getTitle(self: *Surface) ?[:0]const u8 { pub fn getTitle(self: *Self) ?[:0]const u8 {
_ = self; _ = self;
return null; return null;
} }
pub fn getContentScale(self: *const Surface) !apprt.ContentScale { pub fn getContentScale(self: *const Self) !apprt.ContentScale {
_ = self; _ = self;
return .{ .x = 1, .y = 1 }; return .{ .x = 1, .y = 1 };
} }
pub fn getCursorPos(self: *const Surface) !apprt.CursorPos { pub fn getSize(self: *const Self) !apprt.SurfaceSize {
_ = self;
return .{ .width = 800, .height = 600 };
}
pub fn getCursorPos(self: *const Self) !apprt.CursorPos {
_ = self; _ = self;
return .{ .x = 0, .y = 0 }; return .{ .x = 0, .y = 0 };
} }
pub fn clipboardRequest( pub fn clipboardRequest(
self: *Surface, self: *Self,
clipboard_type: apprt.Clipboard, clipboard_type: apprt.Clipboard,
state: apprt.ClipboardRequest, state: apprt.ClipboardRequest,
) !void { ) !void {
@ -45,7 +66,7 @@ pub fn clipboardRequest(
} }
pub fn setClipboardString( pub fn setClipboardString(
self: *Surface, self: *Self,
val: [:0]const u8, val: [:0]const u8,
clipboard_type: apprt.Clipboard, clipboard_type: apprt.Clipboard,
confirm: bool, confirm: bool,
@ -55,3 +76,7 @@ pub fn setClipboardString(
_ = clipboard_type; _ = clipboard_type;
_ = confirm; _ = confirm;
} }
pub fn defaultTermioEnv(self: *Self) !std.process.EnvMap {
return try self.surface.defaultTermioEnv();
}

View File

@ -476,6 +476,25 @@ pub const Application = extern struct {
return self.private().config; return self.private().config;
} }
/// Returns the core app associated with this application. This is
/// not a reference-counted type so you should not store this.
pub fn core(self: *Self) *CoreApp {
return self.private().core_app;
}
/// Returns the apprt application associated with this application.
pub fn rt(self: *Self) *ApprtApp {
return self.private().rt_app;
}
//---------------------------------------------------------------
// Libghostty Callbacks
pub fn wakeup(self: *Self) void {
_ = self;
glib.MainContext.wakeup(null);
}
//--------------------------------------------------------------- //---------------------------------------------------------------
// Virtual Methods // Virtual Methods

View File

@ -1,11 +1,16 @@
const std = @import("std"); const std = @import("std");
const Allocator = std.mem.Allocator;
const adw = @import("adw"); const adw = @import("adw");
const gobject = @import("gobject"); const gobject = @import("gobject");
const gtk = @import("gtk"); const gtk = @import("gtk");
const apprt = @import("../../../apprt.zig");
const internal_os = @import("../../../os/main.zig");
const renderer = @import("../../../renderer.zig"); const renderer = @import("../../../renderer.zig");
const CoreSurface = @import("../../../Surface.zig");
const gresource = @import("../build/gresource.zig"); const gresource = @import("../build/gresource.zig");
const adw_version = @import("../adw_version.zig"); const adw_version = @import("../adw_version.zig");
const ApprtSurface = @import("../Surface.zig");
const Common = @import("../class.zig").Common; const Common = @import("../class.zig").Common;
const Application = @import("application.zig").Application; const Application = @import("application.zig").Application;
const Config = @import("config.zig").Config; const Config = @import("config.zig").Config;
@ -54,6 +59,20 @@ pub const Surface = extern struct {
/// to the template so it doesn't have to be unrefed manually. /// to the template so it doesn't have to be unrefed manually.
gl_area: *gtk.GLArea, gl_area: *gtk.GLArea,
/// The apprt Surface.
rt_surface: ApprtSurface,
/// The core surface backing this GTK surface. This starts out
/// null because it can't be initialized until there is an available
/// GLArea that is realized.
//
// NOTE(mitchellh): This is a limitation we should definitely remove
// at some point by modifying our OpenGL renderer for GTK to
// start in an unrealized state. There are other benefits to being
// able to initialize the surface early so we should aim for that,
// eventually.
core_surface: ?*CoreSurface = null,
pub var offset: c_int = 0; pub var offset: c_int = 0;
}; };
@ -61,11 +80,81 @@ pub const Surface = extern struct {
return gobject.ext.newInstance(Self, .{}); return gobject.ext.newInstance(Self, .{});
} }
pub fn core(self: *Self) ?*CoreSurface {
const priv = self.private();
return priv.core_surface;
}
pub fn rt(self: *Self) *ApprtSurface {
const priv = self.private();
return &priv.rt_surface;
}
/// Force the surface to redraw itself. Ghostty often will only redraw
/// the terminal in reaction to internal changes. If there are external
/// events that invalidate the surface, such as the widget moving parents,
/// then we should force a redraw.
fn redraw(self: *Self) void {
const priv = self.private();
priv.gl_area.queueRender();
}
//---------------------------------------------------------------
// Libghostty Callbacks
pub fn defaultTermioEnv(self: *Self) !std.process.EnvMap {
_ = self;
const alloc = Application.default().allocator();
var env = try internal_os.getEnvMap(alloc);
errdefer env.deinit();
// Don't leak these GTK environment variables to child processes.
env.remove("GDK_DEBUG");
env.remove("GDK_DISABLE");
env.remove("GSK_RENDERER");
// Remove some environment variables that are set when Ghostty is launched
// from a `.desktop` file, by D-Bus activation, or systemd.
env.remove("GIO_LAUNCHED_DESKTOP_FILE");
env.remove("GIO_LAUNCHED_DESKTOP_FILE_PID");
env.remove("DBUS_STARTER_ADDRESS");
env.remove("DBUS_STARTER_BUS_TYPE");
env.remove("INVOCATION_ID");
env.remove("JOURNAL_STREAM");
env.remove("NOTIFY_SOCKET");
// Unset environment varies set by snaps if we're running in a snap.
// This allows Ghostty to further launch additional snaps.
if (env.get("SNAP")) |_| {
env.remove("SNAP");
env.remove("DRIRC_CONFIGDIR");
env.remove("__EGL_EXTERNAL_PLATFORM_CONFIG_DIRS");
env.remove("__EGL_VENDOR_LIBRARY_DIRS");
env.remove("LD_LIBRARY_PATH");
env.remove("LIBGL_DRIVERS_PATH");
env.remove("LIBVA_DRIVERS_PATH");
env.remove("VK_LAYER_PATH");
env.remove("XLOCALEDIR");
env.remove("GDK_PIXBUF_MODULEDIR");
env.remove("GDK_PIXBUF_MODULE_FILE");
env.remove("GTK_PATH");
}
return env;
}
//---------------------------------------------------------------
// Virtual Methods
fn init(self: *Self, _: *Class) callconv(.C) void { fn init(self: *Self, _: *Class) callconv(.C) void {
gtk.Widget.initTemplate(self.as(gtk.Widget)); gtk.Widget.initTemplate(self.as(gtk.Widget));
const priv = self.private(); const priv = self.private();
// Initialize our apprt surface.
priv.rt_surface = .{ .surface = self };
// If our configuration is null then we get the configuration // If our configuration is null then we get the configuration
// from the application. // from the application.
if (priv.config == null) { if (priv.config == null) {
@ -85,6 +174,20 @@ pub const Surface = extern struct {
gl_area.setHasStencilBuffer(0); gl_area.setHasStencilBuffer(0);
gl_area.setHasDepthBuffer(0); gl_area.setHasDepthBuffer(0);
gl_area.setUseEs(0); gl_area.setUseEs(0);
_ = gtk.Widget.signals.realize.connect(
gl_area,
*Self,
glareaRealize,
self,
.{},
);
_ = gtk.Widget.signals.unrealize.connect(
gl_area,
*Self,
glareaUnrealize,
self,
.{},
);
} }
fn dispose(self: *Self) callconv(.C) void { fn dispose(self: *Self) callconv(.C) void {
@ -105,24 +208,135 @@ pub const Surface = extern struct {
); );
} }
fn realize(self: *Self) callconv(.C) void { fn finalize(self: *Self) callconv(.C) void {
log.debug("realize", .{}); const priv = self.private();
if (priv.core_surface) |v| {
priv.core_surface = null;
// Call the parent class's realize method. // Remove ourselves from the list of known surfaces in the app.
gtk.Widget.virtual_methods.realize.call( // We do this before deinit in case a callback triggers
// searching for this surface.
Application.default().core().deleteSurface(self.rt());
// Deinit the surface
v.deinit();
}
gobject.Object.virtual_methods.finalize.call(
Class.parent, Class.parent,
self.as(Parent), self.as(Parent),
); );
} }
fn unrealize(self: *Self) callconv(.C) void { //---------------------------------------------------------------
// Signal Handlers
fn glareaRealize(
_: *gtk.GLArea,
self: *Self,
) callconv(.c) void {
log.debug("realize", .{});
self.realizeSurface() catch |err| {
log.warn("surface failed to realize err={}", .{err});
return;
};
}
fn glareaUnrealize(
gl_area: *gtk.GLArea,
self: *Self,
) callconv(.c) void {
log.debug("unrealize", .{}); log.debug("unrealize", .{});
// Call the parent class's unrealize method. // Get our surface. If we don't have one, there's no work we
gtk.Widget.virtual_methods.unrealize.call( // need to do here.
Class.parent, const priv = self.private();
self.as(Parent), const surface = priv.core_surface orelse return;
// There is no guarantee that our GLArea context is current
// when unrealize is emitted, so we need to make it current.
gl_area.makeCurrent();
if (gl_area.getError()) |err| {
// I don't know a scenario this can happen, but it means
// we probably leaked memory because displayUnrealized
// below frees resources that aren't specifically OpenGL
// related. I didn't make the OpenGL renderer handle this
// scenario because I don't know if its even possible
// under valid circumstances, so let's log.
log.warn(
"gl_area_make_current failed in unrealize msg={s}",
.{err.f_message orelse "(no message)"},
);
log.warn("OpenGL resources and memory likely leaked", .{});
return;
}
surface.renderer.displayUnrealized();
}
const RealizeError = Allocator.Error || error{
GLAreaError,
RendererError,
SurfaceError,
};
fn realizeSurface(self: *Self) RealizeError!void {
const priv = self.private();
const gl_area = priv.gl_area;
// We need to make the context current so we can call GL functions.
// This is required for all surface operations.
gl_area.makeCurrent();
if (gl_area.getError()) |err| {
log.warn("failed to make GL context current: {s}", .{err.f_message orelse "(no message)"});
log.warn("this error is usually due to a driver or gtk bug", .{});
log.warn("this is a common cause of this issue: https://gitlab.gnome.org/GNOME/gtk/-/issues/4950", .{});
return error.GLAreaError;
}
// If we already have an initialized surface then we just notify.
if (priv.core_surface) |v| {
v.renderer.displayRealized() catch |err| {
log.warn("core displayRealized failed err={}", .{err});
return error.RendererError;
};
self.redraw();
return;
}
// Make our pointer to store our surface
const app = Application.default();
const alloc = app.allocator();
const surface = try alloc.create(CoreSurface);
errdefer alloc.destroy(surface);
// Add ourselves to the list of surfaces on the app.
try app.core().addSurface(self.rt());
errdefer app.core().deleteSurface(self.rt());
// Initialize our surface configuration.
var config = try apprt.surface.newConfig(
app.core(),
priv.config.?.get(),
); );
defer config.deinit();
// Initialize the surface
surface.init(
alloc,
&config,
app.core(),
app.rt(),
&priv.rt_surface,
) catch |err| {
log.warn("failed to initialize surface err={}", .{err});
return error.SurfaceError;
};
errdefer surface.deinit();
// Store it!
priv.core_surface = surface;
} }
const C = Common(Self, Private); const C = Common(Self, Private);
@ -156,8 +370,7 @@ pub const Surface = extern struct {
// Virtual methods // Virtual methods
gobject.Object.virtual_methods.dispose.implement(class, &dispose); gobject.Object.virtual_methods.dispose.implement(class, &dispose);
gtk.Widget.virtual_methods.realize.implement(class, &realize); gobject.Object.virtual_methods.finalize.implement(class, &finalize);
gtk.Widget.virtual_methods.unrealize.implement(class, &unrealize);
} }
pub const as = C.Class.as; pub const as = C.Class.as;

View File

@ -781,6 +781,14 @@ pub fn deinit(self: *Surface) void {
self.resize_overlay.deinit(); self.resize_overlay.deinit();
} }
pub fn core(self: *Surface) *CoreSurface {
return &self.core_surface;
}
pub fn rtApp(self: *const Surface) *App {
return self.app;
}
/// Update our local copy of any configuration that we use. /// Update our local copy of any configuration that we use.
pub fn updateConfig(self: *Surface, config: *const configpkg.Config) !void { pub fn updateConfig(self: *Surface, config: *const configpkg.Config) !void {
self.resize_overlay.updateConfig(config); self.resize_overlay.updateConfig(config);

View File

@ -1,3 +1,6 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const apprt = @import("../apprt.zig"); const apprt = @import("../apprt.zig");
const App = @import("../App.zig"); const App = @import("../App.zig");
const Surface = @import("../Surface.zig"); const Surface = @import("../Surface.zig");
@ -133,7 +136,10 @@ pub const Mailbox = struct {
/// Returns a new config for a surface for the given app that should be /// Returns a new config for a surface for the given app that should be
/// used for any new surfaces. The resulting config should be deinitialized /// used for any new surfaces. The resulting config should be deinitialized
/// after the surface is initialized. /// after the surface is initialized.
pub fn newConfig(app: *const App, config: *const Config) !Config { pub fn newConfig(
app: *const App,
config: *const Config,
) Allocator.Error!Config {
// Create a shallow clone // Create a shallow clone
var copy = config.shallowClone(app.alloc); var copy = config.shallowClone(app.alloc);

View File

@ -165,7 +165,9 @@ pub fn surfaceInit(surface: *apprt.Surface) !void {
else => @compileError("unsupported app runtime for OpenGL"), else => @compileError("unsupported app runtime for OpenGL"),
// GTK uses global OpenGL context so we load from null. // GTK uses global OpenGL context so we load from null.
apprt.gtk => try prepareContext(null), apprt.gtk,
apprt.gtk_ng,
=> try prepareContext(null),
apprt.embedded => { apprt.embedded => {
// TODO(mitchellh): this does nothing today to allow libghostty // TODO(mitchellh): this does nothing today to allow libghostty
@ -199,7 +201,7 @@ pub fn threadEnter(self: *const OpenGL, surface: *apprt.Surface) !void {
switch (apprt.runtime) { switch (apprt.runtime) {
else => @compileError("unsupported app runtime for OpenGL"), else => @compileError("unsupported app runtime for OpenGL"),
apprt.gtk => { apprt.gtk, apprt.gtk_ng => {
// GTK doesn't support threaded OpenGL operations as far as I can // GTK doesn't support threaded OpenGL operations as far as I can
// tell, so we use the renderer thread to setup all the state // tell, so we use the renderer thread to setup all the state
// but then do the actual draws and texture syncs and all that // but then do the actual draws and texture syncs and all that
@ -221,7 +223,7 @@ pub fn threadExit(self: *const OpenGL) void {
switch (apprt.runtime) { switch (apprt.runtime) {
else => @compileError("unsupported app runtime for OpenGL"), else => @compileError("unsupported app runtime for OpenGL"),
apprt.gtk => { apprt.gtk, apprt.gtk_ng => {
// We don't need to do any unloading for GTK because we may // We don't need to do any unloading for GTK because we may
// be sharing the global bindings with other windows. // be sharing the global bindings with other windows.
}, },
@ -236,7 +238,7 @@ pub fn displayRealized(self: *const OpenGL) void {
_ = self; _ = self;
switch (apprt.runtime) { switch (apprt.runtime) {
apprt.gtk => prepareContext(null) catch |err| { apprt.gtk, apprt.gtk_ng => prepareContext(null) catch |err| {
log.warn( log.warn(
"Error preparing GL context in displayRealized, err={}", "Error preparing GL context in displayRealized, err={}",
.{err}, .{err},