From 780d4af8bc397958b7ead39c2b9d830f51ed0ea7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 31 Jul 2025 21:25:51 -0700 Subject: [PATCH] apprt/gtk-ng: size_limit apprt action This ports the same behavior from GTK, mostly. This also fixes a bug where the limits would be enforced on reload. Instead, we should only enforce them on the first surface ever. --- src/apprt/gtk-ng/class/application.zig | 31 +++++++-- src/apprt/gtk-ng/class/surface.zig | 93 +++++++++++++++++++++++--- src/apprt/gtk-ng/class/window.zig | 65 +++++++++++++----- 3 files changed, 158 insertions(+), 31 deletions(-) diff --git a/src/apprt/gtk-ng/class/application.zig b/src/apprt/gtk-ng/class/application.zig index 0ce086450..707a997f8 100644 --- a/src/apprt/gtk-ng/class/application.zig +++ b/src/apprt/gtk-ng/class/application.zig @@ -545,12 +545,13 @@ pub const Application = extern struct { .show_gtk_inspector => Action.showGtkInspector(), + .size_limit => return Action.sizeLimit(target, value), + .toggle_maximize => Action.toggleMaximize(target), .toggle_fullscreen => Action.toggleFullscreen(target), .toggle_tab_overview => return Action.toggleTabOverview(target), // Unimplemented but todo on gtk-ng branch - .size_limit, .prompt_title, .toggle_command_palette, .inspector, @@ -1355,10 +1356,10 @@ const Action = struct { .app => return false, .surface => |core| { const surface = core.rt_surface.surface; - surface.setDefaultSize( - value.width, - value.height, - ); + surface.setDefaultSize(.{ + .width = value.width, + .height = value.height, + }); return true; }, } @@ -1666,6 +1667,26 @@ const Action = struct { gtk.Window.setInteractiveDebugging(@intFromBool(true)); } + pub fn sizeLimit( + target: apprt.Target, + value: apprt.action.SizeLimit, + ) bool { + switch (target) { + .app => return false, + .surface => |core| { + // Note: we ignore the max size currently because we have + // no mechanism to enforce it. + const surface = core.rt_surface.surface; + surface.setMinSize(.{ + .width = value.min_width, + .height = value.min_height, + }); + + return true; + }, + } + } + pub fn toggleFullscreen(target: apprt.Target) void { switch (target) { .app => {}, diff --git a/src/apprt/gtk-ng/class/surface.zig b/src/apprt/gtk-ng/class/surface.zig index c6f698054..5f7de8443 100644 --- a/src/apprt/gtk-ng/class/surface.zig +++ b/src/apprt/gtk-ng/class/surface.zig @@ -81,7 +81,7 @@ pub const Surface = extern struct { const impl = gobject.ext.defineProperty( name, Self, - ?*apprt.action.InitialSize, + ?*Size, .{ .nick = "Default Size", .blurb = "The default size of the window for this surface.", @@ -124,6 +124,20 @@ pub const Surface = extern struct { ); }; + pub const @"min-size" = struct { + pub const name = "min-size"; + const impl = gobject.ext.defineProperty( + name, + Self, + ?*Size, + .{ + .nick = "Minimum Size", + .blurb = "The minimum size of the surface.", + .accessor = C.privateBoxedFieldAccessor("min_size"), + }, + ); + }; + pub const @"mouse-hidden" = struct { pub const name = "mouse-hidden"; const impl = gobject.ext.defineProperty( @@ -300,6 +314,18 @@ pub const Surface = extern struct { ); }; + /// Emitted after the surface is initialized. + pub const init = struct { + pub const name = "init"; + pub const connect = impl.connect; + const impl = gobject.ext.defineSignal( + name, + Self, + &.{}, + void, + ); + }; + /// Emitted when the focus wants to be brought to the top and /// focused. pub const @"present-request" = struct { @@ -349,7 +375,11 @@ pub const Surface = extern struct { cgroup_path: ?[]const u8 = null, /// The default size for a window that embeds this surface. - default_size: ?*apprt.action.InitialSize = null, + default_size: ?*Size = null, + + /// The minimum size for this surface. Embedders enforce this, + /// not the surface itself. + min_size: ?*Size = null, /// The requested font size. This only applies to initialization /// and has no effect later. @@ -1201,13 +1231,17 @@ pub const Surface = extern struct { priv.mouse_hover_url = null; } if (priv.default_size) |v| { - ext.boxedFree(apprt.action.InitialSize, v); + ext.boxedFree(Size, v); priv.default_size = null; } if (priv.font_size_request) |v| { glib.ext.destroy(v); priv.font_size_request = null; } + if (priv.min_size) |v| { + ext.boxedFree(Size, v); + priv.min_size = null; + } if (priv.pwd) |v| { glib.free(@constCast(@ptrCast(v))); priv.pwd = null; @@ -1246,7 +1280,7 @@ pub const Surface = extern struct { } /// Return the default size, if set. - pub fn getDefaultSize(self: *Self) ?*apprt.action.InitialSize { + pub fn getDefaultSize(self: *Self) ?*Size { const priv = self.private(); return priv.default_size; } @@ -1254,19 +1288,41 @@ pub const Surface = extern struct { /// 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 { + pub fn setDefaultSize(self: *Self, size: Size) void { const priv = self.private(); if (priv.default_size) |v| ext.boxedFree( - apprt.action.InitialSize, + Size, v, ); priv.default_size = ext.boxedCopy( - apprt.action.InitialSize, - &.{ .width = width, .height = height }, + Size, + &size, ); self.as(gobject.Object).notifyByPspec(properties.@"default-size".impl.param_spec); } + /// Return the min size, if set. + pub fn getMinSize(self: *Self) ?*Size { + const priv = self.private(); + return priv.min_size; + } + + /// Set the min 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 setMinSize(self: *Self, size: Size) void { + const priv = self.private(); + if (priv.min_size) |v| ext.boxedFree( + Size, + v, + ); + priv.min_size = ext.boxedCopy( + Size, + &size, + ); + self.as(gobject.Object).notifyByPspec(properties.@"min-size".impl.param_spec); + } + fn propConfig( self: *Self, _: *gobject.ParamSpec, @@ -2106,6 +2162,14 @@ pub const Surface = extern struct { // Store it! priv.core_surface = surface; + + // Emit the signal that we initialized the surface. + Surface.signals.init.impl.emit( + self, + null, + .{}, + null, + ); } fn resizeOverlaySchedule(self: *Self) void { @@ -2229,6 +2293,7 @@ pub const Surface = extern struct { properties.@"default-size".impl, properties.@"font-size-request".impl, properties.focused.impl, + properties.@"min-size".impl, properties.@"mouse-shape".impl, properties.@"mouse-hidden".impl, properties.@"mouse-hover-url".impl, @@ -2242,6 +2307,7 @@ pub const Surface = extern struct { signals.bell.impl.register(.{}); signals.@"clipboard-read".impl.register(.{}); signals.@"clipboard-write".impl.register(.{}); + signals.init.impl.register(.{}); signals.@"present-request".impl.register(.{}); signals.@"toggle-fullscreen".impl.register(.{}); signals.@"toggle-maximize".impl.register(.{}); @@ -2274,6 +2340,17 @@ pub const Surface = extern struct { .{ .name = "GhosttySurfaceCloseScope" }, ); }; + + /// Simple dimensions struct for the surface used by various properties. + pub const Size = extern struct { + width: u32, + height: u32, + + pub const getGObjectType = gobject.ext.defineBoxed( + Size, + .{ .name = "GhosttySurfaceSize" }, + ); + }; }; /// The state of the key event while we're doing IM composition. diff --git a/src/apprt/gtk-ng/class/window.zig b/src/apprt/gtk-ng/class/window.zig index 4b3a20047..7757ca50e 100644 --- a/src/apprt/gtk-ng/class/window.zig +++ b/src/apprt/gtk-ng/class/window.zig @@ -211,6 +211,18 @@ pub const Window = extern struct { /// The configuration that this surface is using. config: ?*Config = null, + /// Kind of hacky to have this but this lets us know if we've + /// initialized any single surface yet. We need this because we + /// gate default size on this so that we don't resize the window + /// after surfaces already exist. + /// + /// I think long term we can probably get rid of this by implementing + /// a property or method that gets us all the surfaces in all the + /// tabs and checking if we have zero or one that isn't initialized. + /// + /// For now, this logic is more similar to our legacy GTK side. + surface_init: bool = false, + /// See tabOverviewOpen for why we have this. tab_overview_focus_timer: ?c_uint = null, @@ -913,6 +925,8 @@ pub const Window = extern struct { _: c_int, self: *Self, ) callconv(.c) void { + const priv = self.private(); + // Get the attached page which must be a Tab object. const child = page.getChild(); const tab = gobject.ext.cast(Tab, child) orelse return; @@ -983,13 +997,20 @@ pub const Window = extern struct { self, .{}, ); - _ = gobject.Object.signals.notify.connect( - surface, - *Self, - surfaceDefaultSize, - self, - .{ .detail = "default-size" }, - ); + + // If we've never had a surface initialize yet, then we register + // this signal. Its theoretically possible to launch multiple surfaces + // before init so we could register this on multiple and that is not + // a problem because we'll check the flag again in each handler. + if (!priv.surface_init) { + _ = Surface.signals.init.connect( + surface, + *Self, + surfaceInit, + self, + .{}, + ); + } } fn tabViewPageDetached( @@ -1179,21 +1200,29 @@ pub const Window = extern struct { // We react to the changes in the propMaximized callback } - fn surfaceDefaultSize( + fn surfaceInit( surface: *Surface, - _: *gobject.ParamSpec, self: *Self, ) callconv(.c) void { - const size = surface.getDefaultSize() orelse return; + const priv = self.private(); - // 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), - ); + // Make sure we init only once + if (priv.surface_init) return; + priv.surface_init = true; + + // Setup our default and minimum size. + if (surface.getDefaultSize()) |size| { + self.as(gtk.Window).setDefaultSize( + @intCast(size.width), + @intCast(size.height), + ); + } + if (surface.getMinSize()) |size| { + self.as(gtk.Widget).setSizeRequest( + @intCast(size.width), + @intCast(size.height), + ); + } } fn actionAbout(