apprt/gtk-ng: introduce a basic surface

This commit is contained in:
Mitchell Hashimoto
2025-07-18 07:01:52 -07:00
parent 40818d4f7c
commit 9f2ff0cb9c
8 changed files with 343 additions and 15 deletions

View File

@ -31,6 +31,7 @@ pub const icon_sizes: []const comptime_int = &.{ 16, 32, 128, 256, 512, 1024 };
/// These will be asserted to exist at runtime.
pub const blueprints: []const Blueprint = &.{
.{ .major = 1, .minor = 2, .name = "config-errors-dialog" },
.{ .major = 1, .minor = 2, .name = "surface" },
.{ .major = 1, .minor = 5, .name = "config-errors-dialog" },
.{ .major = 1, .minor = 5, .name = "window" },
};

View File

@ -3,10 +3,12 @@
const glib = @import("glib");
const gobject = @import("gobject");
const gtk = @import("gtk");
pub const Application = @import("class/application.zig").Application;
pub const Window = @import("class/window.zig").Window;
pub const Config = @import("class/config.zig").Config;
pub const Surface = @import("class/surface.zig").Surface;
/// Unrefs the given GObject on the next event loop tick.
///
@ -60,6 +62,30 @@ pub fn Common(
);
}
}).private else {};
/// Common class functions.
pub const Class = struct {
pub fn as(class: *Self.Class, comptime T: type) *T {
return gobject.ext.as(T, class);
}
/// Bind a template child to a private entry in the class.
pub const bindTemplateChildPrivate = if (Private) |P| (struct {
pub fn bindTemplateChildPrivate(
class: *Self.Class,
comptime name: [:0]const u8,
comptime options: gtk.ext.BindTemplateChildOptions,
) void {
gtk.ext.impl_helpers.bindTemplateChildPrivate(
class,
name,
P,
P.offset,
options,
);
}
}).bindTemplateChildPrivate else {};
};
};
}

View File

@ -16,6 +16,7 @@ const configpkg = @import("../../../config.zig");
const internal_os = @import("../../../os/main.zig");
const xev = @import("../../../global.zig").xev;
const CoreConfig = configpkg.Config;
const CoreSurface = @import("../../../Surface.zig");
const adw_version = @import("../adw_version.zig");
const gtk_version = @import("../gtk_version.zig");
@ -58,6 +59,25 @@ pub const Application = extern struct {
.private = .{ .Type = Private, .offset = &Private.offset },
});
pub const properties = struct {
pub const config = struct {
pub const name = "config";
const impl = gobject.ext.defineProperty(
"config",
Self,
?*Config,
.{
.nick = "Config",
.blurb = "The current active configuration for the application.",
.default = null,
.accessor = .{
.getter = Self.getPropConfig,
},
},
);
};
};
const Private = struct {
/// The apprt App. This is annoying that we need this it'd be
/// nicer to just make THIS the apprt app but the current libghostty
@ -88,6 +108,17 @@ pub const Application = extern struct {
pub var offset: c_int = 0;
};
/// Get this application as the default, allowing access to its
/// properties globally.
///
/// This asserts that there is a default application and that the
/// default application is a GhosttyApplication. The program would have
/// to be in a very bad state for this to be violated.
pub fn default() *Self {
const app = gio.Application.getDefault().?;
return gobject.ext.cast(Self, app).?;
}
/// Creates a new Application instance.
///
/// This does a lot more work than a typical class instantiation,
@ -121,14 +152,14 @@ pub const Application = extern struct {
// the error in the diagnostics so it can be shown to the user.
// We can still load a default which only fails for OOM, allowing
// us to startup.
var default: CoreConfig = try .default(alloc);
errdefer default.deinit();
try default.addDiagnosticFmt(
var def: CoreConfig = try .default(alloc);
errdefer def.deinit();
try def.addDiagnosticFmt(
"error loading user configuration: {}",
.{err},
);
break :err default;
break :err def;
};
defer config.deinit();
@ -223,6 +254,13 @@ pub const Application = extern struct {
if (priv.transient_cgroup_base) |base| alloc.free(base);
}
/// The global allocator that all other classes should use by
/// calling `Application.default().allocator()`. Zig code should prefer
/// this wherever possible so we get leak detection in debug/tests.
pub fn allocator(self: *Self) std.mem.Allocator {
return self.private().core_app.alloc;
}
/// Run the application. This is a replacement for `gio.Application.run`
/// because we want more tight control over our event loop so we can
/// integrate it with libghostty.
@ -332,9 +370,16 @@ pub const Application = extern struct {
value.config,
),
.new_window => try Action.newWindow(
self,
switch (target) {
.app => null,
.surface => |v| v,
},
),
// Unimplemented
.quit,
.new_window,
.close_window,
.toggle_maximize,
.toggle_fullscreen,
@ -410,6 +455,27 @@ pub const Application = extern struct {
try priv.core_app.updateConfig(priv.rt_app, &config);
}
/// Returns the configuration for this application.
///
/// The reference count is increased.
pub fn getConfig(self: *Self) *Config {
var value = gobject.ext.Value.zero;
gobject.Object.getProperty(
self.as(gobject.Object),
properties.config.name,
&value,
);
const obj = value.getObject().?;
return gobject.ext.cast(Config, obj).?;
}
fn getPropConfig(self: *Self) *Config {
// Property return must not increase reference count since
// the gobject getter handles this automatically.
return self.private().config;
}
//---------------------------------------------------------------
// Virtual Methods
@ -421,6 +487,9 @@ pub const Application = extern struct {
self.as(Parent),
);
// Set ourselves as the default application.
gio.Application.setDefault(self.as(gio.Application));
// Setup our event loop
self.startupXev();
@ -581,14 +650,17 @@ pub const Application = extern struct {
fn activate(self: *Self) callconv(.C) void {
log.debug("activate", .{});
// Queue a new window
const priv = self.private();
_ = priv.core_app.mailbox.push(.{
.new_window = .{},
}, .{ .forever = {} });
// Call the parent activate method.
gio.Application.virtual_methods.activate.call(
Class.parent,
self.as(Parent),
);
// const win = Window.new(self);
// gtk.Window.present(win.as(gtk.Window));
}
fn dispose(self: *Self) callconv(.C) void {
@ -697,10 +769,6 @@ pub const Application = extern struct {
//----------------------------------------------------------------
// Boilerplate/Noise
fn allocator(self: *Self) std.mem.Allocator {
return self.private().core_app.alloc;
}
const C = Common(Self, Private);
pub const as = C.as;
pub const ref = C.ref;
@ -729,6 +797,11 @@ pub const Application = extern struct {
}
}
// Properties
gobject.ext.registerProperties(class, &.{
properties.config.impl,
});
// Virtual methods
gio.Application.virtual_methods.activate.implement(class, &activate);
gio.Application.virtual_methods.startup.implement(class, &startup);
@ -765,6 +838,16 @@ const Action = struct {
},
}
}
pub fn newWindow(
self: *Application,
parent: ?*CoreSurface,
) !void {
_ = parent;
const win = Window.new(self);
gtk.Window.present(win.as(gtk.Window));
}
};
/// This sets various GTK-related environment variables as necessary

View File

@ -0,0 +1,166 @@
const std = @import("std");
const adw = @import("adw");
const gobject = @import("gobject");
const gtk = @import("gtk");
const renderer = @import("../../../renderer.zig");
const gresource = @import("../build/gresource.zig");
const adw_version = @import("../adw_version.zig");
const Common = @import("../class.zig").Common;
const Application = @import("application.zig").Application;
const Config = @import("config.zig").Config;
const log = std.log.scoped(.gtk_ghostty_surface);
pub const Surface = extern struct {
const Self = @This();
parent_instance: Parent,
pub const Parent = adw.Bin;
pub const getGObjectType = gobject.ext.defineClass(Self, .{
.name = "GhosttySurface",
.instanceInit = &init,
.classInit = &Class.init,
.parent_class = &Class.parent,
.private = .{ .Type = Private, .offset = &Private.offset },
});
pub const properties = struct {
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.",
.default = null,
.accessor = gobject.ext.privateFieldAccessor(
Self,
Private,
&Private.offset,
"config",
),
},
);
};
};
const Private = struct {
/// The configuration that this surface is using.
config: ?*Config = null,
/// 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,
pub var offset: c_int = 0;
};
pub fn new() *Self {
return gobject.ext.newInstance(Self, .{});
}
fn init(self: *Self, _: *Class) callconv(.C) void {
gtk.Widget.initTemplate(self.as(gtk.Widget));
const priv = self.private();
// If our configuration is null then we get the configuration
// from the application.
if (priv.config == null) {
const app = Application.default();
priv.config = app.getConfig();
}
// Initialize our GLArea. We could do a lot of this in
// the Blueprint file but I think its cleaner to separate
// the "UI" part of the blueprint file from the internal logic/config
// part.
const gl_area = priv.gl_area;
gl_area.setRequiredVersion(
renderer.OpenGL.MIN_VERSION_MAJOR,
renderer.OpenGL.MIN_VERSION_MINOR,
);
gl_area.setHasStencilBuffer(0);
gl_area.setHasDepthBuffer(0);
gl_area.setUseEs(0);
}
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(),
);
gobject.Object.virtual_methods.dispose.call(
Class.parent,
self.as(Parent),
);
}
fn realize(self: *Self) callconv(.C) void {
log.debug("realize", .{});
// Call the parent class's realize method.
gtk.Widget.virtual_methods.realize.call(
Class.parent,
self.as(Parent),
);
}
fn unrealize(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),
);
}
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 {
gtk.Widget.Class.setTemplateFromResource(
class.as(gtk.Widget.Class),
comptime gresource.blueprint(.{
.major = 1,
.minor = 2,
.name = "surface",
}),
);
// Bindings
class.bindTemplateChildPrivate("gl_area", .{});
// Properties
gobject.ext.registerProperties(class, &.{
properties.config.impl,
});
// 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);
}
pub const as = C.Class.as;
pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate;
};
};

View File

@ -6,6 +6,7 @@ const gtk = @import("gtk");
const gresource = @import("../build/gresource.zig");
const Common = @import("../class.zig").Common;
const Application = @import("application.zig").Application;
const Surface = @import("surface.zig").Surface;
const log = std.log.scoped(.gtk_ghostty_window);
@ -58,6 +59,8 @@ pub const Window = extern struct {
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(.{

View File

@ -0,0 +1,18 @@
using Gtk 4.0;
using Adw 1;
template $GhosttySurface: Adw.Bin {
Box {
orientation: vertical;
hexpand: true;
Label {
label: "Hello";
}
GLArea gl_area {
hexpand: true;
vexpand: true;
}
}
}

View File

@ -2,7 +2,5 @@ using Gtk 4.0;
using Adw 1;
template $GhosttyWindow: Adw.ApplicationWindow {
content: Label {
label: "Hello, Ghostty!";
};
content: $GhosttySurface {};
}

View File

@ -13,6 +13,39 @@
# You must gracefully exit Ghostty (do not SIGINT) by closing all windows
# and quitting. Otherwise, we leave a number of GTK resources around.
{
GSK Renderer GPU Stuff
Memcheck:Leak
match-leak-kinds: possible
...
fun:gsk_gpu_image_toggle_ref_texture
fun:gsk_gl_image_new_for_texture
fun:gsk_gl_frame_upload_texture
fun:gsk_gpu_frame_do_upload_texture
fun:gsk_gpu_lookup_texture
...
fun:gsk_gpu_node_processor_add_first_node
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
...
}
{
GTK Shader Selector
Memcheck:Leak
match-leak-kinds: possible
...
fun:_ZL29si_init_shader_selector_asyncPvS_i
fun:util_queue_thread_func
fun:impl_thrd_routine
fun:start_thread
fun:clone
}
# Weird gtk_tooltip_init leak I can't figure out
{
Non-builder tooltip create