apprt/gtk-ng: show error widget if GLArea fails to initialize

If GTK can't acquire an OpenGL context, this shows a message.
Previously, we would only log a warning which was difficult to find. The
GUI previously was the default GTK view which showed "Failed to acquire
EGL display" which was equally confusing.
pull/8390/head
Mitchell Hashimoto 2025-08-25 09:38:44 -07:00
parent 3fb17dc802
commit 4d6269a859
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
3 changed files with 256 additions and 141 deletions

View File

@ -116,6 +116,11 @@ pub const Application = extern struct {
/// and initialization was successful. /// and initialization was successful.
transient_cgroup_base: ?[]const u8 = null, transient_cgroup_base: ?[]const u8 = null,
/// This is set to true so long as we request a window exactly
/// once. This prevents quitting the app before we've shown one
/// window.
requested_window: bool = false,
/// This is set to false internally when the event loop /// This is set to false internally when the event loop
/// should exit and the application should quit. This must /// should exit and the application should quit. This must
/// only be set by the main loop thread. /// only be set by the main loop thread.
@ -461,7 +466,13 @@ pub const Application = extern struct {
// If the quit timer has expired, quit. // If the quit timer has expired, quit.
if (priv.quit_timer == .expired) break :q true; if (priv.quit_timer == .expired) break :q true;
// There's no quit timer running, or it hasn't expired, don't quit. // If we have no windows attached to our app, also quit.
if (priv.requested_window and @as(
?*glib.List,
self.as(gtk.Application).getWindows(),
) == null) break :q true;
// No quit conditions met
break :q false; break :q false;
}; };
@ -1858,6 +1869,13 @@ const Action = struct {
self: *Application, self: *Application,
parent: ?*CoreSurface, parent: ?*CoreSurface,
) !void { ) !void {
// Note that we've requested a window at least once. This is used
// to trigger quit on no windows. Note I'm not sure if this is REALLY
// necessary, but I don't want to risk a bug where on a slow machine
// or something we quit immediately after starting up because there
// was a delay in the event loop before we created a Window.
self.private().requested_window = true;
const win = Window.new(self); const win = Window.new(self);
initAndShowWindow(self, win, parent); initAndShowWindow(self, win, parent);
} }

View File

@ -105,6 +105,24 @@ pub const Surface = extern struct {
); );
}; };
pub const @"error" = struct {
pub const name = "error";
const impl = gobject.ext.defineProperty(
name,
Self,
bool,
.{
.default = false,
.accessor = gobject.ext.privateFieldAccessor(
Self,
Private,
&Private.offset,
"error",
),
},
);
};
pub const @"font-size-request" = struct { pub const @"font-size-request" = struct {
pub const name = "font-size-request"; pub const name = "font-size-request";
const impl = gobject.ext.defineProperty( const impl = gobject.ext.defineProperty(
@ -472,6 +490,12 @@ pub const Surface = extern struct {
// false by a parent widget. // false by a parent widget.
bell_ringing: bool = false, bell_ringing: bool = false,
/// True if this surface is in an error state. This is currently
/// a simple boolean with no additional information on WHAT the
/// error state is, because we don't yet need it or use it. For now,
/// if this is true, then it means the terminal is non-functional.
@"error": bool = false,
/// A weak reference to an inspector window. /// A weak reference to an inspector window.
inspector: ?*InspectorWindow = null, inspector: ?*InspectorWindow = null,
@ -571,6 +595,17 @@ pub const Surface = extern struct {
return @intFromBool(config.@"bell-features".border); return @intFromBool(config.@"bell-features".border);
} }
fn closureStackChildName(
_: *Self,
error_: c_int,
) callconv(.c) ?[*:0]const u8 {
const err = error_ != 0;
return if (err)
glib.ext.dupeZ(u8, "error")
else
glib.ext.dupeZ(u8, "terminal");
}
pub fn toggleFullscreen(self: *Self) void { pub fn toggleFullscreen(self: *Self) void {
signals.@"toggle-fullscreen".impl.emit( signals.@"toggle-fullscreen".impl.emit(
self, self,
@ -1540,6 +1575,12 @@ pub const Surface = extern struct {
self.as(gobject.Object).notifyByPspec(properties.@"bell-ringing".impl.param_spec); self.as(gobject.Object).notifyByPspec(properties.@"bell-ringing".impl.param_spec);
} }
pub fn setError(self: *Self, v: bool) void {
const priv = self.private();
priv.@"error" = v;
self.as(gobject.Object).notifyByPspec(properties.@"error".impl.param_spec);
}
fn propConfig( fn propConfig(
self: *Self, self: *Self,
_: *gobject.ParamSpec, _: *gobject.ParamSpec,
@ -1592,6 +1633,28 @@ pub const Surface = extern struct {
} }
} }
fn propError(
self: *Self,
_: *gobject.ParamSpec,
_: ?*anyopaque,
) callconv(.c) void {
const priv = self.private();
if (priv.@"error") {
// Ensure we have an opaque background. The window will NOT set
// this if we have transparency set and we need an opaque
// background for the error message to be readable.
self.as(gtk.Widget).addCssClass("background");
} else {
// Regardless of transparency setting, we remove the background
// CSS class from this widget. Parent widgets will set it
// appropriately (see window.zig for example).
self.as(gtk.Widget).removeCssClass("background");
}
// Note above: in both cases setting our error view is handled by
// a Gtk.Stack visible-child-name binding.
}
fn propMouseHoverUrl( fn propMouseHoverUrl(
self: *Self, self: *Self,
_: *gobject.ParamSpec, _: *gobject.ParamSpec,
@ -1942,8 +2005,11 @@ pub const Surface = extern struct {
// Bell stops ringing if any mouse button is pressed. // Bell stops ringing if any mouse button is pressed.
self.setBellRinging(false); self.setBellRinging(false);
// If we don't have focus, grab it. // Get our surface. If we don't have one, ignore this.
const priv = self.private(); const priv = self.private();
const core_surface = priv.core_surface orelse return;
// If we don't have focus, grab it.
const gl_area_widget = priv.gl_area.as(gtk.Widget); const gl_area_widget = priv.gl_area.as(gtk.Widget);
if (gl_area_widget.hasFocus() == 0) { if (gl_area_widget.hasFocus() == 0) {
_ = gl_area_widget.grabFocus(); _ = gl_area_widget.grabFocus();
@ -1951,10 +2017,10 @@ pub const Surface = extern struct {
// Report the event // Report the event
const button = translateMouseButton(gesture.as(gtk.GestureSingle).getCurrentButton()); const button = translateMouseButton(gesture.as(gtk.GestureSingle).getCurrentButton());
const consumed = if (priv.core_surface) |surface| consumed: { const consumed = consumed: {
const gtk_mods = event.getModifierState(); const gtk_mods = event.getModifierState();
const mods = gtk_key.translateMods(gtk_mods); const mods = gtk_key.translateMods(gtk_mods);
break :consumed surface.mouseButtonCallback( break :consumed core_surface.mouseButtonCallback(
.press, .press,
button, button,
mods, mods,
@ -1962,7 +2028,7 @@ pub const Surface = extern struct {
log.warn("error in key callback err={}", .{err}); log.warn("error in key callback err={}", .{err});
break :err false; break :err false;
}; };
} else false; };
// If a right click isn't consumed, mouseButtonCallback selects the hovered // If a right click isn't consumed, mouseButtonCallback selects the hovered
// word and returns false. We can use this to handle the context menu // word and returns false. We can use this to handle the context menu
@ -2303,21 +2369,23 @@ pub const Surface = extern struct {
) callconv(.c) void { ) callconv(.c) void {
log.debug("realize", .{}); log.debug("realize", .{});
// Make the GL area current so we can detect any OpenGL errors. If
// we have errors here we can't render and we switch to the error
// state.
const priv = self.private();
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 almost always due to a library, driver, or GTK issue", .{});
log.warn("this is a common cause of this issue: https://ghostty.org/docs/help/gtk-opengl-context", .{});
self.setError(true);
return;
}
// If we already have an initialized surface then we notify it. // If we already have an initialized surface then we notify it.
// If we don't, we'll initialize it on the first resize so we have // If we don't, we'll initialize it on the first resize so we have
// our proper initial dimensions. // our proper initial dimensions.
const priv = self.private();
if (priv.core_surface) |v| realize: { 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| { v.renderer.displayRealized() catch |err| {
log.warn("core displayRealized failed err={}", .{err}); log.warn("core displayRealized failed err={}", .{err});
break :realize; break :realize;
@ -2662,11 +2730,13 @@ pub const Surface = extern struct {
class.bindTemplateCallback("child_exited_close", &childExitedClose); class.bindTemplateCallback("child_exited_close", &childExitedClose);
class.bindTemplateCallback("context_menu_closed", &contextMenuClosed); class.bindTemplateCallback("context_menu_closed", &contextMenuClosed);
class.bindTemplateCallback("notify_config", &propConfig); class.bindTemplateCallback("notify_config", &propConfig);
class.bindTemplateCallback("notify_error", &propError);
class.bindTemplateCallback("notify_mouse_hover_url", &propMouseHoverUrl); class.bindTemplateCallback("notify_mouse_hover_url", &propMouseHoverUrl);
class.bindTemplateCallback("notify_mouse_hidden", &propMouseHidden); class.bindTemplateCallback("notify_mouse_hidden", &propMouseHidden);
class.bindTemplateCallback("notify_mouse_shape", &propMouseShape); class.bindTemplateCallback("notify_mouse_shape", &propMouseShape);
class.bindTemplateCallback("notify_bell_ringing", &propBellRinging); class.bindTemplateCallback("notify_bell_ringing", &propBellRinging);
class.bindTemplateCallback("should_border_be_shown", &closureShouldBorderBeShown); class.bindTemplateCallback("should_border_be_shown", &closureShouldBorderBeShown);
class.bindTemplateCallback("stack_child_name", &closureStackChildName);
// Properties // Properties
gobject.ext.registerProperties(class, &.{ gobject.ext.registerProperties(class, &.{
@ -2674,6 +2744,7 @@ pub const Surface = extern struct {
properties.config.impl, properties.config.impl,
properties.@"child-exited".impl, properties.@"child-exited".impl,
properties.@"default-size".impl, properties.@"default-size".impl,
properties.@"error".impl,
properties.@"font-size-request".impl, properties.@"font-size-request".impl,
properties.focused.impl, properties.focused.impl,
properties.@"min-size".impl, properties.@"min-size".impl,

View File

@ -8,146 +8,172 @@ template $GhosttySurface: Adw.Bin {
notify::bell-ringing => $notify_bell_ringing(); notify::bell-ringing => $notify_bell_ringing();
notify::config => $notify_config(); notify::config => $notify_config();
notify::error => $notify_error();
notify::mouse-hover-url => $notify_mouse_hover_url(); notify::mouse-hover-url => $notify_mouse_hover_url();
notify::mouse-hidden => $notify_mouse_hidden(); notify::mouse-hidden => $notify_mouse_hidden();
notify::mouse-shape => $notify_mouse_shape(); notify::mouse-shape => $notify_mouse_shape();
Overlay { Stack {
focusable: false; StackPage {
focus-on-click: false; name: "terminal";
child: Box { child: Overlay {
hexpand: true; focusable: false;
vexpand: true; focus-on-click: false;
GLArea gl_area { child: Box {
realize => $gl_realize(); hexpand: true;
unrealize => $gl_unrealize(); vexpand: true;
render => $gl_render();
resize => $gl_resize();
hexpand: true;
vexpand: true;
focusable: true;
focus-on-click: true;
has-stencil-buffer: false;
has-depth-buffer: false;
allowed-apis: gl;
}
PopoverMenu context_menu { GLArea gl_area {
closed => $context_menu_closed(); realize => $gl_realize();
menu-model: context_menu_model; unrealize => $gl_unrealize();
flags: nested; render => $gl_render();
halign: start; resize => $gl_resize();
has-arrow: false; hexpand: true;
} vexpand: true;
}; focusable: true;
focus-on-click: true;
has-stencil-buffer: false;
has-depth-buffer: false;
allowed-apis: gl;
}
[overlay] PopoverMenu context_menu {
ProgressBar progress_bar_overlay { closed => $context_menu_closed();
styles [ menu-model: context_menu_model;
"osd", flags: nested;
] halign: start;
has-arrow: false;
}
};
visible: false; [overlay]
halign: fill; ProgressBar progress_bar_overlay {
valign: start; styles [
"osd",
]
visible: false;
halign: fill;
valign: start;
}
[overlay]
// The "border" bell feature is implemented here as an overlay rather than
// just adding a border to the GLArea or other widget for two reasons.
// First, adding a border to an existing widget causes a resize of the
// widget which undesirable side effects. Second, we can make it reactive
// here in the blueprint with relatively little code.
Revealer {
reveal-child: bind $should_border_be_shown(template.config, template.bell-ringing) as <bool>;
transition-type: crossfade;
transition-duration: 500;
Box bell_overlay {
styles [
"bell-overlay",
]
halign: fill;
valign: fill;
}
}
[overlay]
$GhosttySurfaceChildExited child_exited_overlay {
visible: bind template.child-exited;
close-request => $child_exited_close();
}
[overlay]
$GhosttyResizeOverlay resize_overlay {}
[overlay]
Label url_left {
styles [
"background",
"url-overlay",
]
visible: false;
halign: start;
valign: end;
label: bind template.mouse-hover-url;
EventControllerMotion url_ec_motion {
enter => $url_mouse_enter();
leave => $url_mouse_leave();
}
}
[overlay]
Label url_right {
styles [
"background",
"url-overlay",
]
visible: false;
halign: end;
valign: end;
label: bind template.mouse-hover-url;
}
// Event controllers for interactivity
EventControllerFocus {
enter => $focus_enter();
leave => $focus_leave();
}
EventControllerKey {
key-pressed => $key_pressed();
key-released => $key_released();
}
EventControllerMotion {
motion => $mouse_motion();
leave => $mouse_leave();
}
EventControllerScroll {
scroll => $scroll();
scroll-begin => $scroll_begin();
scroll-end => $scroll_end();
flags: both_axes;
}
GestureClick {
pressed => $mouse_down();
released => $mouse_up();
button: 0;
}
DropTarget drop_target {
drop => $drop();
actions: copy;
}
};
} }
[overlay] StackPage {
// The "border" bell feature is implemented here as an overlay rather than name: "error";
// just adding a border to the GLArea or other widget for two reasons.
// First, adding a border to an existing widget causes a resize of the
// widget which undesirable side effects. Second, we can make it reactive
// here in the blueprint with relatively little code.
Revealer {
reveal-child: bind $should_border_be_shown(template.config, template.bell-ringing) as <bool>;
transition-type: crossfade;
transition-duration: 500;
Box bell_overlay { child: Adw.StatusPage {
styles [ icon-name: "computer-fail-symbolic";
"bell-overlay", title: _("Oh, no.");
] description: _("Unable to acquire an OpenGL context for rendering.");
halign: fill; child: LinkButton {
valign: fill; label: "https://ghostty.org/docs/help/gtk-opengl-context";
} uri: "https://ghostty.org/docs/help/gtk-opengl-context";
};
};
} }
[overlay] // The order matters here: we can only set this after the stack
$GhosttySurfaceChildExited child_exited_overlay { // pages above have been created.
visible: bind template.child-exited; visible-child-name: bind $stack_child_name(template.error) as <string>;
close-request => $child_exited_close();
}
[overlay]
$GhosttyResizeOverlay resize_overlay {}
[overlay]
Label url_left {
styles [
"background",
"url-overlay",
]
visible: false;
halign: start;
valign: end;
label: bind template.mouse-hover-url;
EventControllerMotion url_ec_motion {
enter => $url_mouse_enter();
leave => $url_mouse_leave();
}
}
[overlay]
Label url_right {
styles [
"background",
"url-overlay",
]
visible: false;
halign: end;
valign: end;
label: bind template.mouse-hover-url;
}
}
// Event controllers for interactivity
EventControllerFocus {
enter => $focus_enter();
leave => $focus_leave();
}
EventControllerKey {
key-pressed => $key_pressed();
key-released => $key_released();
}
EventControllerMotion {
motion => $mouse_motion();
leave => $mouse_leave();
}
EventControllerScroll {
scroll => $scroll();
scroll-begin => $scroll_begin();
scroll-end => $scroll_end();
flags: both_axes;
}
GestureClick {
pressed => $mouse_down();
released => $mouse_up();
button: 0;
}
DropTarget drop_target {
drop => $drop();
actions: copy;
} }
} }