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 {
// Go through and update all of the surface configurations.
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
@ -190,7 +190,7 @@ pub fn addSurface(
// 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
// should be canceled.
_ = rt_surface.app.performAction(
_ = rt_surface.rtApp().performAction(
.app,
.quit_timer,
.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
// after free and giving false positives, so we must clear it.
if (self.focused_surface) |focused| {
if (focused == &rt_surface.core_surface) {
if (focused == rt_surface.core()) {
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
// 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,
.quit_timer,
.start,
@ -245,7 +245,7 @@ pub fn focusedSurface(self: *const App) ?*Surface {
/// the apprt to call this.
pub fn needsConfirmQuit(self: *const App) bool {
for (self.surfaces.items) |v| {
if (v.core_surface.needsConfirmQuit()) return true;
if (v.core().needsConfirmQuit()) return true;
}
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 {
if (!self.hasSurface(&surface.core_surface)) return;
if (!self.hasRtSurface(surface)) return;
rt_app.redrawSurface(surface);
}
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);
}
@ -482,7 +482,7 @@ pub fn performAllAction(
// Surface-scoped actions are performed on all surfaces. Errors
// are logged but processing continues.
.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={}", .{
@intFromPtr(surface),
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 {
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;

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
/// the pwd can change at any point from termio. If we are calling from the IO
/// 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();
defer self.renderer_state.mutex.unlock();
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 {
const func = self.app.opts.close_surface orelse {
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);
/// 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
app: *Application,
@ -48,6 +53,11 @@ pub fn terminate(self: *App) void {
self.app.deinit();
}
/// Called by CoreApp to wake up the event loop.
pub fn wakeup(self: *App) void {
self.app.wakeup();
}
pub fn performAction(
self: *App,
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 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;
}
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;
_ = process_active;
}
pub fn shouldClose(self: *Surface) bool {
pub fn shouldClose(self: *Self) bool {
_ = self;
return false;
}
pub fn getTitle(self: *Surface) ?[:0]const u8 {
pub fn getTitle(self: *Self) ?[:0]const u8 {
_ = self;
return null;
}
pub fn getContentScale(self: *const Surface) !apprt.ContentScale {
pub fn getContentScale(self: *const Self) !apprt.ContentScale {
_ = self;
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;
return .{ .x = 0, .y = 0 };
}
pub fn clipboardRequest(
self: *Surface,
self: *Self,
clipboard_type: apprt.Clipboard,
state: apprt.ClipboardRequest,
) !void {
@ -45,7 +66,7 @@ pub fn clipboardRequest(
}
pub fn setClipboardString(
self: *Surface,
self: *Self,
val: [:0]const u8,
clipboard_type: apprt.Clipboard,
confirm: bool,
@ -55,3 +76,7 @@ pub fn setClipboardString(
_ = clipboard_type;
_ = 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;
}
/// 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

View File

@ -1,11 +1,16 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const adw = @import("adw");
const gobject = @import("gobject");
const gtk = @import("gtk");
const apprt = @import("../../../apprt.zig");
const internal_os = @import("../../../os/main.zig");
const renderer = @import("../../../renderer.zig");
const CoreSurface = @import("../../../Surface.zig");
const gresource = @import("../build/gresource.zig");
const adw_version = @import("../adw_version.zig");
const ApprtSurface = @import("../Surface.zig");
const Common = @import("../class.zig").Common;
const Application = @import("application.zig").Application;
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.
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;
};
@ -61,11 +80,81 @@ pub const Surface = extern struct {
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 {
gtk.Widget.initTemplate(self.as(gtk.Widget));
const priv = self.private();
// Initialize our apprt surface.
priv.rt_surface = .{ .surface = self };
// If our configuration is null then we get the configuration
// from the application.
if (priv.config == null) {
@ -85,6 +174,20 @@ pub const Surface = extern struct {
gl_area.setHasStencilBuffer(0);
gl_area.setHasDepthBuffer(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 {
@ -105,24 +208,135 @@ pub const Surface = extern struct {
);
}
fn realize(self: *Self) callconv(.C) void {
log.debug("realize", .{});
fn finalize(self: *Self) callconv(.C) void {
const priv = self.private();
if (priv.core_surface) |v| {
priv.core_surface = null;
// Call the parent class's realize method.
gtk.Widget.virtual_methods.realize.call(
// Remove ourselves from the list of known surfaces in the app.
// 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,
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", .{});
// Call the parent class's unrealize method.
gtk.Widget.virtual_methods.unrealize.call(
Class.parent,
self.as(Parent),
// Get our surface. If we don't have one, there's no work we
// need to do here.
const priv = self.private();
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);
@ -156,8 +370,7 @@ pub const Surface = extern struct {
// Virtual methods
gobject.Object.virtual_methods.dispose.implement(class, &dispose);
gtk.Widget.virtual_methods.realize.implement(class, &realize);
gtk.Widget.virtual_methods.unrealize.implement(class, &unrealize);
gobject.Object.virtual_methods.finalize.implement(class, &finalize);
}
pub const as = C.Class.as;

View File

@ -781,6 +781,14 @@ pub fn deinit(self: *Surface) void {
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.
pub fn updateConfig(self: *Surface, config: *const configpkg.Config) !void {
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 App = @import("../App.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
/// used for any new surfaces. The resulting config should be deinitialized
/// 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
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"),
// 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 => {
// 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) {
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
// tell, so we use the renderer thread to setup all the state
// 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) {
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
// be sharing the global bindings with other windows.
},
@ -236,7 +238,7 @@ pub fn displayRealized(self: *const OpenGL) void {
_ = self;
switch (apprt.runtime) {
apprt.gtk => prepareContext(null) catch |err| {
apprt.gtk, apprt.gtk_ng => prepareContext(null) catch |err| {
log.warn(
"Error preparing GL context in displayRealized, err={}",
.{err},