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(),
.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 => {},

View File

@ -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.

View File

@ -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(