From 9f2ff0cb9c38693ffa37350f58db6045c4be0d9a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 18 Jul 2025 07:01:52 -0700 Subject: [PATCH] apprt/gtk-ng: introduce a basic surface --- src/apprt/gtk-ng/build/gresource.zig | 1 + src/apprt/gtk-ng/class.zig | 26 ++++ src/apprt/gtk-ng/class/application.zig | 107 ++++++++++++++-- src/apprt/gtk-ng/class/surface.zig | 166 +++++++++++++++++++++++++ src/apprt/gtk-ng/class/window.zig | 3 + src/apprt/gtk-ng/ui/1.2/surface.blp | 18 +++ src/apprt/gtk-ng/ui/1.5/window.blp | 4 +- valgrind.supp | 33 +++++ 8 files changed, 343 insertions(+), 15 deletions(-) create mode 100644 src/apprt/gtk-ng/class/surface.zig create mode 100644 src/apprt/gtk-ng/ui/1.2/surface.blp diff --git a/src/apprt/gtk-ng/build/gresource.zig b/src/apprt/gtk-ng/build/gresource.zig index f38e73b01..ca3f762cc 100644 --- a/src/apprt/gtk-ng/build/gresource.zig +++ b/src/apprt/gtk-ng/build/gresource.zig @@ -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" }, }; diff --git a/src/apprt/gtk-ng/class.zig b/src/apprt/gtk-ng/class.zig index db403610e..2c3626a16 100644 --- a/src/apprt/gtk-ng/class.zig +++ b/src/apprt/gtk-ng/class.zig @@ -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 {}; + }; }; } diff --git a/src/apprt/gtk-ng/class/application.zig b/src/apprt/gtk-ng/class/application.zig index df86c13b0..ee96ed9ea 100644 --- a/src/apprt/gtk-ng/class/application.zig +++ b/src/apprt/gtk-ng/class/application.zig @@ -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 diff --git a/src/apprt/gtk-ng/class/surface.zig b/src/apprt/gtk-ng/class/surface.zig new file mode 100644 index 000000000..a473931e7 --- /dev/null +++ b/src/apprt/gtk-ng/class/surface.zig @@ -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; + }; +}; diff --git a/src/apprt/gtk-ng/class/window.zig b/src/apprt/gtk-ng/class/window.zig index 0eab18ee1..81ea376fa 100644 --- a/src/apprt/gtk-ng/class/window.zig +++ b/src/apprt/gtk-ng/class/window.zig @@ -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(.{ diff --git a/src/apprt/gtk-ng/ui/1.2/surface.blp b/src/apprt/gtk-ng/ui/1.2/surface.blp new file mode 100644 index 000000000..3fde1d1c0 --- /dev/null +++ b/src/apprt/gtk-ng/ui/1.2/surface.blp @@ -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; + } + } +} diff --git a/src/apprt/gtk-ng/ui/1.5/window.blp b/src/apprt/gtk-ng/ui/1.5/window.blp index d6321537e..9f3a0c45f 100644 --- a/src/apprt/gtk-ng/ui/1.5/window.blp +++ b/src/apprt/gtk-ng/ui/1.5/window.blp @@ -2,7 +2,5 @@ using Gtk 4.0; using Adw 1; template $GhosttyWindow: Adw.ApplicationWindow { - content: Label { - label: "Hello, Ghostty!"; - }; + content: $GhosttySurface {}; } diff --git a/valgrind.supp b/valgrind.supp index 0e1a90f6a..4c5d6b4a0 100644 --- a/valgrind.supp +++ b/valgrind.supp @@ -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