From 0b9130aba9cb8ab5a192f1c33b7ac9df413d60ec Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 31 Jul 2025 13:05:11 -0700 Subject: [PATCH] apprt/gtk-ng: initial size apprt action (window-width/height) --- src/apprt/action.zig | 11 +++++++ src/apprt/gtk-ng/class/application.zig | 20 +++++++++++- src/apprt/gtk-ng/class/surface.zig | 45 ++++++++++++++++++++++++++ src/apprt/gtk-ng/class/window.zig | 24 ++++++++++++++ src/apprt/gtk-ng/ext.zig | 14 ++++++++ 5 files changed, 113 insertions(+), 1 deletion(-) diff --git a/src/apprt/action.zig b/src/apprt/action.zig index 6f745c080..0f2b68087 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const build_config = @import("../build_config.zig"); const assert = std.debug.assert; const apprt = @import("../apprt.zig"); const configpkg = @import("../config.zig"); @@ -533,6 +534,16 @@ pub const SizeLimit = extern struct { pub const InitialSize = extern struct { width: u32, height: u32, + + /// Make this a valid gobject if we're in a GTK environment. + pub const getGObjectType = switch (build_config.app_runtime) { + .gtk, .@"gtk-ng" => @import("gobject").ext.defineBoxed( + InitialSize, + .{ .name = "GhosttyApprtInitialSize" }, + ), + + .none => void, + }; }; pub const CellSize = extern struct { diff --git a/src/apprt/gtk-ng/class/application.zig b/src/apprt/gtk-ng/class/application.zig index 8cbe9fd18..0ce086450 100644 --- a/src/apprt/gtk-ng/class/application.zig +++ b/src/apprt/gtk-ng/class/application.zig @@ -501,6 +501,8 @@ pub const Application = extern struct { .goto_tab => return Action.gotoTab(target, value), + .initial_size => return Action.initialSize(target, value), + .mouse_over_link => Action.mouseOverLink(target, value), .mouse_shape => Action.mouseShape(target, value), .mouse_visibility => Action.mouseVisibility(target, value), @@ -548,7 +550,6 @@ pub const Application = extern struct { .toggle_tab_overview => return Action.toggleTabOverview(target), // Unimplemented but todo on gtk-ng branch - .initial_size, .size_limit, .prompt_title, .toggle_command_palette, @@ -1346,6 +1347,23 @@ const Action = struct { } } + pub fn initialSize( + target: apprt.Target, + value: apprt.action.InitialSize, + ) bool { + switch (target) { + .app => return false, + .surface => |core| { + const surface = core.rt_surface.surface; + surface.setDefaultSize( + value.width, + value.height, + ); + return true; + }, + } + } + pub fn mouseOverLink( target: apprt.Target, value: apprt.action.MouseOverLink, diff --git a/src/apprt/gtk-ng/class/surface.zig b/src/apprt/gtk-ng/class/surface.zig index 90a1d9e40..c6f698054 100644 --- a/src/apprt/gtk-ng/class/surface.zig +++ b/src/apprt/gtk-ng/class/surface.zig @@ -16,6 +16,7 @@ const renderer = @import("../../../renderer.zig"); const terminal = @import("../../../terminal/main.zig"); const CoreSurface = @import("../../../Surface.zig"); const gresource = @import("../build/gresource.zig"); +const ext = @import("../ext.zig"); const adw_version = @import("../adw_version.zig"); const gtk_key = @import("../key.zig"); const ApprtSurface = @import("../Surface.zig"); @@ -75,6 +76,20 @@ pub const Surface = extern struct { ); }; + pub const @"default-size" = struct { + pub const name = "default-size"; + const impl = gobject.ext.defineProperty( + name, + Self, + ?*apprt.action.InitialSize, + .{ + .nick = "Default Size", + .blurb = "The default size of the window for this surface.", + .accessor = C.privateBoxedFieldAccessor("default_size"), + }, + ); + }; + pub const @"font-size-request" = struct { pub const name = "font-size-request"; const impl = gobject.ext.defineProperty( @@ -333,6 +348,9 @@ pub const Surface = extern struct { /// if `Application.transient_cgroup_base` is set. cgroup_path: ?[]const u8 = null, + /// The default size for a window that embeds this surface. + default_size: ?*apprt.action.InitialSize = null, + /// The requested font size. This only applies to initialization /// and has no effect later. font_size_request: ?*font.face.DesiredSize = null, @@ -1182,6 +1200,10 @@ pub const Surface = extern struct { glib.free(@constCast(@ptrCast(v))); priv.mouse_hover_url = null; } + if (priv.default_size) |v| { + ext.boxedFree(apprt.action.InitialSize, v); + priv.default_size = null; + } if (priv.font_size_request) |v| { glib.ext.destroy(v); priv.font_size_request = null; @@ -1223,6 +1245,28 @@ pub const Surface = extern struct { self.as(gobject.Object).notifyByPspec(properties.config.impl.param_spec); } + /// Return the default size, if set. + pub fn getDefaultSize(self: *Self) ?*apprt.action.InitialSize { + const priv = self.private(); + return priv.default_size; + } + + /// Set the default size for a window that contains this surface. + /// This is up to the embedding widget to respect this. Generally, only + /// the first surface in a window respects this. + pub fn setDefaultSize(self: *Self, width: u32, height: u32) void { + const priv = self.private(); + if (priv.default_size) |v| ext.boxedFree( + apprt.action.InitialSize, + v, + ); + priv.default_size = ext.boxedCopy( + apprt.action.InitialSize, + &.{ .width = width, .height = height }, + ); + self.as(gobject.Object).notifyByPspec(properties.@"default-size".impl.param_spec); + } + fn propConfig( self: *Self, _: *gobject.ParamSpec, @@ -2182,6 +2226,7 @@ pub const Surface = extern struct { gobject.ext.registerProperties(class, &.{ properties.config.impl, properties.@"child-exited".impl, + properties.@"default-size".impl, properties.@"font-size-request".impl, properties.focused.impl, properties.@"mouse-shape".impl, diff --git a/src/apprt/gtk-ng/class/window.zig b/src/apprt/gtk-ng/class/window.zig index 426b57797..4b3a20047 100644 --- a/src/apprt/gtk-ng/class/window.zig +++ b/src/apprt/gtk-ng/class/window.zig @@ -983,6 +983,13 @@ pub const Window = extern struct { self, .{}, ); + _ = gobject.Object.signals.notify.connect( + surface, + *Self, + surfaceDefaultSize, + self, + .{ .detail = "default-size" }, + ); } fn tabViewPageDetached( @@ -1172,6 +1179,23 @@ pub const Window = extern struct { // We react to the changes in the propMaximized callback } + fn surfaceDefaultSize( + surface: *Surface, + _: *gobject.ParamSpec, + self: *Self, + ) callconv(.c) void { + const size = surface.getDefaultSize() orelse return; + + // We previously gated this on whether this was called before but + // its useful to always set this to whatever the expected value is + // so we can do a "return to default size" later. This call only + // affects the window on first load. It won't resize it again later. + self.as(gtk.Window).setDefaultSize( + @intCast(size.width), + @intCast(size.height), + ); + } + fn actionAbout( _: *gio.SimpleAction, _: ?*glib.Variant, diff --git a/src/apprt/gtk-ng/ext.zig b/src/apprt/gtk-ng/ext.zig index 2a6ac8f6d..551d54b23 100644 --- a/src/apprt/gtk-ng/ext.zig +++ b/src/apprt/gtk-ng/ext.zig @@ -9,6 +9,20 @@ const assert = std.debug.assert; const gobject = @import("gobject"); const gtk = @import("gtk"); +/// Wrapper around `gobject.boxedCopy` to copy a boxed type `T`. +pub fn boxedCopy(comptime T: type, ptr: *const T) *T { + const copy = gobject.boxedCopy(T.getGObjectType(), ptr); + return @ptrCast(@alignCast(copy)); +} + +/// Wrapper around `gobject.boxedFree` to free a boxed type `T`. +pub fn boxedFree(comptime T: type, ptr: ?*T) void { + if (ptr) |p| gobject.boxedFree( + T.getGObjectType(), + p, + ); +} + /// Wrapper around `gtk.Widget.getAncestor` to get the widget ancestor /// of the given type `T`, or null if it doesn't exist. pub fn getAncestor(comptime T: type, widget: *gtk.Widget) ?*T {