apprt/gtk-ng: surface has correct initial size (#8104)

Ensure the surface has a correct initial size when created. This avoids
a rapid resize event and also the pty reports the correct size for
startup scripts.

This is a departure from macOS and legacy GTK. This has been an issue in
Ghostty for awhile so this is the proper path forward.

This works by deferring Surface initialization until the first resize
event. This MIGHT result in a frame or two not rendering but I haven't
noticed anything visually and having the correct size is far more
important.
This commit is contained in:
Mitchell Hashimoto
2025-07-30 08:54:51 -07:00
committed by GitHub
2 changed files with 77 additions and 62 deletions

View File

@ -556,6 +556,13 @@ pub const Application = extern struct {
.toggle_quick_terminal, .toggle_quick_terminal,
.toggle_command_palette, .toggle_command_palette,
.open_url, .open_url,
=> {
log.warn("unimplemented action={}", .{action});
return false;
},
// Unimplemented
.secure_input,
.close_all_windows, .close_all_windows,
.float_window, .float_window,
.toggle_visibility, .toggle_visibility,
@ -572,13 +579,6 @@ pub const Application = extern struct {
log.warn("unimplemented action={}", .{action}); log.warn("unimplemented action={}", .{action});
return false; return false;
}, },
// Unimplemented
.secure_input,
=> {
log.warn("unimplemented action={}", .{action});
return false;
},
} }
// Assume it was handled. The unhandled case must be explicit // Assume it was handled. The unhandled case must be explicit

View File

@ -975,7 +975,13 @@ pub const Surface = extern struct {
} }
pub fn getSize(self: *Self) apprt.SurfaceSize { pub fn getSize(self: *Self) apprt.SurfaceSize {
return self.private().size; const priv = self.private();
// By the time this is called, we should be in a widget tree.
// This should not be called before that. We ensure this by initializing
// the surface in `glareaResize`. This is VERY important because it
// avoids the pty having an incorrect initial size.
assert(priv.size.width >= 0 and priv.size.height >= 0);
return priv.size;
} }
pub fn getCursorPos(self: *Self) apprt.CursorPos { pub fn getCursorPos(self: *Self) apprt.CursorPos {
@ -1072,12 +1078,7 @@ pub const Surface = extern struct {
priv.mouse_shape = .text; priv.mouse_shape = .text;
priv.mouse_hidden = false; priv.mouse_hidden = false;
priv.focused = true; priv.focused = true;
priv.size = .{ priv.size = .{ .width = 0, .height = 0 };
// Funky numbers on purpose so they stand out if for some reason
// our size doesn't get properly set.
.width = 111,
.height = 111,
};
// If our configuration is null then we get the configuration // If our configuration is null then we get the configuration
// from the application. // from the application.
@ -1839,15 +1840,32 @@ pub const Surface = extern struct {
) callconv(.c) void { ) callconv(.c) void {
log.debug("realize", .{}); log.debug("realize", .{});
// Setup our core surface // If we already have an initialized surface then we notify it.
self.realizeSurface() catch |err| { // If we don't, we'll initialize it on the first resize so we have
log.warn("surface failed to realize err={}", .{err}); // our proper initial dimensions.
}; const priv = self.private();
if (priv.core_surface) |v| realize: {
// We need to make the context current so we can call GL functions.
// This is required for all surface operations.
priv.gl_area.makeCurrent();
if (priv.gl_area.getError()) |err| {
log.warn("failed to make GL context current: {s}", .{err.f_message orelse "(no message)"});
log.warn("this error is usually due to a driver or gtk bug", .{});
log.warn("this is a common cause of this issue: https://gitlab.gnome.org/GNOME/gtk/-/issues/4950", .{});
break :realize;
}
v.renderer.displayRealized() catch |err| {
log.warn("core displayRealized failed err={}", .{err});
break :realize;
};
self.redraw();
}
// Setup our input method. We do this here because this will // Setup our input method. We do this here because this will
// create a strong reference back to ourself and we want to be // create a strong reference back to ourself and we want to be
// able to release that in unrealize. // able to release that in unrealize.
const priv = self.private();
priv.im_context.as(gtk.IMContext).setClientWidget(self.as(gtk.Widget)); priv.im_context.as(gtk.IMContext).setClientWidget(self.as(gtk.Widget));
} }
@ -1949,49 +1967,24 @@ pub const Surface = extern struct {
// Setup our resize overlay if configured // Setup our resize overlay if configured
self.resizeOverlaySchedule(); self.resizeOverlaySchedule();
}
}
fn resizeOverlaySchedule(self: *Self) void { return;
const priv = self.private();
const surface = priv.core_surface orelse return;
// Only show the resize overlay if its enabled
const config = if (priv.config) |c| c.get() else return;
switch (config.@"resize-overlay") {
.always, .@"after-first" => {},
.never => return,
} }
// If we have resize overlays enabled, setup an idler // If we don't have a surface, then we initialize it.
// to show that. We do this in an idle tick because doing it self.initSurface() catch |err| {
// during the resize results in flickering. log.warn("surface failed to initialize err={}", .{err});
var buf: [32]u8 = undefined; };
priv.resize_overlay.setLabel(text: {
const grid_size = surface.size.grid();
break :text std.fmt.bufPrintZ(
&buf,
"{d} x {d}",
.{
grid_size.columns,
grid_size.rows,
},
) catch |err| err: {
log.warn("unable to format text: {}", .{err});
break :err "";
};
});
priv.resize_overlay.schedule();
} }
const RealizeError = Allocator.Error || error{ const InitError = Allocator.Error || error{
GLAreaError, GLAreaError,
RendererError,
SurfaceError, SurfaceError,
}; };
fn realizeSurface(self: *Self) RealizeError!void { fn initSurface(self: *Self) InitError!void {
const priv = self.private(); const priv = self.private();
assert(priv.core_surface == null);
const gl_area = priv.gl_area; const gl_area = priv.gl_area;
// We need to make the context current so we can call GL functions. // We need to make the context current so we can call GL functions.
@ -2004,16 +1997,6 @@ pub const Surface = extern struct {
return error.GLAreaError; return error.GLAreaError;
} }
// If we already have an initialized surface then we just notify.
if (priv.core_surface) |v| {
v.renderer.displayRealized() catch |err| {
log.warn("core displayRealized failed err={}", .{err});
return error.RendererError;
};
self.redraw();
return;
}
const app = Application.default(); const app = Application.default();
const alloc = app.allocator(); const alloc = app.allocator();
@ -2057,6 +2040,38 @@ pub const Surface = extern struct {
priv.core_surface = surface; priv.core_surface = surface;
} }
fn resizeOverlaySchedule(self: *Self) void {
const priv = self.private();
const surface = priv.core_surface orelse return;
// Only show the resize overlay if its enabled
const config = if (priv.config) |c| c.get() else return;
switch (config.@"resize-overlay") {
.always, .@"after-first" => {},
.never => return,
}
// If we have resize overlays enabled, setup an idler
// to show that. We do this in an idle tick because doing it
// during the resize results in flickering.
var buf: [32]u8 = undefined;
priv.resize_overlay.setLabel(text: {
const grid_size = surface.size.grid();
break :text std.fmt.bufPrintZ(
&buf,
"{d} x {d}",
.{
grid_size.columns,
grid_size.rows,
},
) catch |err| err: {
log.warn("unable to format text: {}", .{err});
break :err "";
};
});
priv.resize_overlay.schedule();
}
fn ecUrlMouseEnter( fn ecUrlMouseEnter(
_: *gtk.EventControllerMotion, _: *gtk.EventControllerMotion,
_: f64, _: f64,