apprt/gtk-ng: size_limit apprt action (#8116)

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.
This commit is contained in:
Mitchell Hashimoto
2025-08-01 08:06:40 -07:00
committed by GitHub
3 changed files with 158 additions and 31 deletions

View File

@ -545,12 +545,13 @@ pub const Application = extern struct {
.show_gtk_inspector => Action.showGtkInspector(), .show_gtk_inspector => Action.showGtkInspector(),
.size_limit => return Action.sizeLimit(target, value),
.toggle_maximize => Action.toggleMaximize(target), .toggle_maximize => Action.toggleMaximize(target),
.toggle_fullscreen => Action.toggleFullscreen(target), .toggle_fullscreen => Action.toggleFullscreen(target),
.toggle_tab_overview => return Action.toggleTabOverview(target), .toggle_tab_overview => return Action.toggleTabOverview(target),
// Unimplemented but todo on gtk-ng branch // Unimplemented but todo on gtk-ng branch
.size_limit,
.prompt_title, .prompt_title,
.toggle_command_palette, .toggle_command_palette,
.inspector, .inspector,
@ -1355,10 +1356,10 @@ const Action = struct {
.app => return false, .app => return false,
.surface => |core| { .surface => |core| {
const surface = core.rt_surface.surface; const surface = core.rt_surface.surface;
surface.setDefaultSize( surface.setDefaultSize(.{
value.width, .width = value.width,
value.height, .height = value.height,
); });
return true; return true;
}, },
} }
@ -1666,6 +1667,26 @@ const Action = struct {
gtk.Window.setInteractiveDebugging(@intFromBool(true)); 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 { pub fn toggleFullscreen(target: apprt.Target) void {
switch (target) { switch (target) {
.app => {}, .app => {},

View File

@ -81,7 +81,7 @@ pub const Surface = extern struct {
const impl = gobject.ext.defineProperty( const impl = gobject.ext.defineProperty(
name, name,
Self, Self,
?*apprt.action.InitialSize, ?*Size,
.{ .{
.nick = "Default Size", .nick = "Default Size",
.blurb = "The default size of the window for this surface.", .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 @"mouse-hidden" = struct {
pub const name = "mouse-hidden"; pub const name = "mouse-hidden";
const impl = gobject.ext.defineProperty( 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 /// Emitted when the focus wants to be brought to the top and
/// focused. /// focused.
pub const @"present-request" = struct { pub const @"present-request" = struct {
@ -349,7 +375,11 @@ pub const Surface = extern struct {
cgroup_path: ?[]const u8 = null, cgroup_path: ?[]const u8 = null,
/// The default size for a window that embeds this surface. /// 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 /// The requested font size. This only applies to initialization
/// and has no effect later. /// and has no effect later.
@ -1201,13 +1231,17 @@ pub const Surface = extern struct {
priv.mouse_hover_url = null; priv.mouse_hover_url = null;
} }
if (priv.default_size) |v| { if (priv.default_size) |v| {
ext.boxedFree(apprt.action.InitialSize, v); ext.boxedFree(Size, v);
priv.default_size = null; priv.default_size = null;
} }
if (priv.font_size_request) |v| { if (priv.font_size_request) |v| {
glib.ext.destroy(v); glib.ext.destroy(v);
priv.font_size_request = null; priv.font_size_request = null;
} }
if (priv.min_size) |v| {
ext.boxedFree(Size, v);
priv.min_size = null;
}
if (priv.pwd) |v| { if (priv.pwd) |v| {
glib.free(@constCast(@ptrCast(v))); glib.free(@constCast(@ptrCast(v)));
priv.pwd = null; priv.pwd = null;
@ -1246,7 +1280,7 @@ pub const Surface = extern struct {
} }
/// Return the default size, if set. /// Return the default size, if set.
pub fn getDefaultSize(self: *Self) ?*apprt.action.InitialSize { pub fn getDefaultSize(self: *Self) ?*Size {
const priv = self.private(); const priv = self.private();
return priv.default_size; return priv.default_size;
} }
@ -1254,19 +1288,41 @@ pub const Surface = extern struct {
/// Set the default size for a window that contains this surface. /// Set the default size for a window that contains this surface.
/// This is up to the embedding widget to respect this. Generally, only /// This is up to the embedding widget to respect this. Generally, only
/// the first surface in a window respects this. /// 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(); const priv = self.private();
if (priv.default_size) |v| ext.boxedFree( if (priv.default_size) |v| ext.boxedFree(
apprt.action.InitialSize, Size,
v, v,
); );
priv.default_size = ext.boxedCopy( priv.default_size = ext.boxedCopy(
apprt.action.InitialSize, Size,
&.{ .width = width, .height = height }, &size,
); );
self.as(gobject.Object).notifyByPspec(properties.@"default-size".impl.param_spec); 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( fn propConfig(
self: *Self, self: *Self,
_: *gobject.ParamSpec, _: *gobject.ParamSpec,
@ -2106,6 +2162,14 @@ pub const Surface = extern struct {
// Store it! // Store it!
priv.core_surface = surface; 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 { fn resizeOverlaySchedule(self: *Self) void {
@ -2229,6 +2293,7 @@ pub const Surface = extern struct {
properties.@"default-size".impl, properties.@"default-size".impl,
properties.@"font-size-request".impl, properties.@"font-size-request".impl,
properties.focused.impl, properties.focused.impl,
properties.@"min-size".impl,
properties.@"mouse-shape".impl, properties.@"mouse-shape".impl,
properties.@"mouse-hidden".impl, properties.@"mouse-hidden".impl,
properties.@"mouse-hover-url".impl, properties.@"mouse-hover-url".impl,
@ -2242,6 +2307,7 @@ pub const Surface = extern struct {
signals.bell.impl.register(.{}); signals.bell.impl.register(.{});
signals.@"clipboard-read".impl.register(.{}); signals.@"clipboard-read".impl.register(.{});
signals.@"clipboard-write".impl.register(.{}); signals.@"clipboard-write".impl.register(.{});
signals.init.impl.register(.{});
signals.@"present-request".impl.register(.{}); signals.@"present-request".impl.register(.{});
signals.@"toggle-fullscreen".impl.register(.{}); signals.@"toggle-fullscreen".impl.register(.{});
signals.@"toggle-maximize".impl.register(.{}); signals.@"toggle-maximize".impl.register(.{});
@ -2274,6 +2340,17 @@ pub const Surface = extern struct {
.{ .name = "GhosttySurfaceCloseScope" }, .{ .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. /// The state of the key event while we're doing IM composition.

View File

@ -211,6 +211,18 @@ pub const Window = extern struct {
/// The configuration that this surface is using. /// The configuration that this surface is using.
config: ?*Config = null, 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. /// See tabOverviewOpen for why we have this.
tab_overview_focus_timer: ?c_uint = null, tab_overview_focus_timer: ?c_uint = null,
@ -913,6 +925,8 @@ pub const Window = extern struct {
_: c_int, _: c_int,
self: *Self, self: *Self,
) callconv(.c) void { ) callconv(.c) void {
const priv = self.private();
// Get the attached page which must be a Tab object. // Get the attached page which must be a Tab object.
const child = page.getChild(); const child = page.getChild();
const tab = gobject.ext.cast(Tab, child) orelse return; const tab = gobject.ext.cast(Tab, child) orelse return;
@ -983,14 +997,21 @@ pub const Window = extern struct {
self, self,
.{}, .{},
); );
_ = gobject.Object.signals.notify.connect(
// 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, surface,
*Self, *Self,
surfaceDefaultSize, surfaceInit,
self, self,
.{ .detail = "default-size" }, .{},
); );
} }
}
fn tabViewPageDetached( fn tabViewPageDetached(
_: *adw.TabView, _: *adw.TabView,
@ -1179,22 +1200,30 @@ pub const Window = extern struct {
// We react to the changes in the propMaximized callback // We react to the changes in the propMaximized callback
} }
fn surfaceDefaultSize( fn surfaceInit(
surface: *Surface, surface: *Surface,
_: *gobject.ParamSpec,
self: *Self, self: *Self,
) callconv(.c) void { ) callconv(.c) void {
const size = surface.getDefaultSize() orelse return; const priv = self.private();
// We previously gated this on whether this was called before but // Make sure we init only once
// its useful to always set this to whatever the expected value is if (priv.surface_init) return;
// so we can do a "return to default size" later. This call only priv.surface_init = true;
// affects the window on first load. It won't resize it again later.
// Setup our default and minimum size.
if (surface.getDefaultSize()) |size| {
self.as(gtk.Window).setDefaultSize( self.as(gtk.Window).setDefaultSize(
@intCast(size.width), @intCast(size.width),
@intCast(size.height), @intCast(size.height),
); );
} }
if (surface.getMinSize()) |size| {
self.as(gtk.Widget).setSizeRequest(
@intCast(size.width),
@intCast(size.height),
);
}
}
fn actionAbout( fn actionAbout(
_: *gio.SimpleAction, _: *gio.SimpleAction,