From 4d6269a8596f72d86aa609cedf3ffff89bb41be1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 25 Aug 2025 09:38:44 -0700 Subject: [PATCH] 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. --- src/apprt/gtk-ng/class/application.zig | 20 +- src/apprt/gtk-ng/class/surface.zig | 101 +++++++-- src/apprt/gtk-ng/ui/1.2/surface.blp | 276 ++++++++++++++----------- 3 files changed, 256 insertions(+), 141 deletions(-) diff --git a/src/apprt/gtk-ng/class/application.zig b/src/apprt/gtk-ng/class/application.zig index 984eda15e..05b24f064 100644 --- a/src/apprt/gtk-ng/class/application.zig +++ b/src/apprt/gtk-ng/class/application.zig @@ -116,6 +116,11 @@ pub const Application = extern struct { /// and initialization was successful. 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 /// should exit and the application should quit. This must /// 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 (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; }; @@ -1858,6 +1869,13 @@ const Action = struct { self: *Application, parent: ?*CoreSurface, ) !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); initAndShowWindow(self, win, parent); } diff --git a/src/apprt/gtk-ng/class/surface.zig b/src/apprt/gtk-ng/class/surface.zig index 2debff93b..25ee1f94f 100644 --- a/src/apprt/gtk-ng/class/surface.zig +++ b/src/apprt/gtk-ng/class/surface.zig @@ -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 name = "font-size-request"; const impl = gobject.ext.defineProperty( @@ -472,6 +490,12 @@ pub const Surface = extern struct { // false by a parent widget. 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. inspector: ?*InspectorWindow = null, @@ -571,6 +595,17 @@ pub const Surface = extern struct { 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 { signals.@"toggle-fullscreen".impl.emit( self, @@ -1540,6 +1575,12 @@ pub const Surface = extern struct { 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( self: *Self, _: *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( self: *Self, _: *gobject.ParamSpec, @@ -1942,8 +2005,11 @@ pub const Surface = extern struct { // Bell stops ringing if any mouse button is pressed. 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 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); if (gl_area_widget.hasFocus() == 0) { _ = gl_area_widget.grabFocus(); @@ -1951,10 +2017,10 @@ pub const Surface = extern struct { // Report the event 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 mods = gtk_key.translateMods(gtk_mods); - break :consumed surface.mouseButtonCallback( + break :consumed core_surface.mouseButtonCallback( .press, button, mods, @@ -1962,7 +2028,7 @@ pub const Surface = extern struct { log.warn("error in key callback err={}", .{err}); break :err false; }; - } else false; + }; // If a right click isn't consumed, mouseButtonCallback selects the hovered // 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 { 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 don't, we'll initialize it on the first resize so we have // 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; @@ -2662,11 +2730,13 @@ pub const Surface = extern struct { class.bindTemplateCallback("child_exited_close", &childExitedClose); class.bindTemplateCallback("context_menu_closed", &contextMenuClosed); class.bindTemplateCallback("notify_config", &propConfig); + class.bindTemplateCallback("notify_error", &propError); class.bindTemplateCallback("notify_mouse_hover_url", &propMouseHoverUrl); class.bindTemplateCallback("notify_mouse_hidden", &propMouseHidden); class.bindTemplateCallback("notify_mouse_shape", &propMouseShape); class.bindTemplateCallback("notify_bell_ringing", &propBellRinging); class.bindTemplateCallback("should_border_be_shown", &closureShouldBorderBeShown); + class.bindTemplateCallback("stack_child_name", &closureStackChildName); // Properties gobject.ext.registerProperties(class, &.{ @@ -2674,6 +2744,7 @@ pub const Surface = extern struct { properties.config.impl, properties.@"child-exited".impl, properties.@"default-size".impl, + properties.@"error".impl, properties.@"font-size-request".impl, properties.focused.impl, properties.@"min-size".impl, diff --git a/src/apprt/gtk-ng/ui/1.2/surface.blp b/src/apprt/gtk-ng/ui/1.2/surface.blp index 9989e9c10..39c88ff33 100644 --- a/src/apprt/gtk-ng/ui/1.2/surface.blp +++ b/src/apprt/gtk-ng/ui/1.2/surface.blp @@ -8,146 +8,172 @@ template $GhosttySurface: Adw.Bin { notify::bell-ringing => $notify_bell_ringing(); notify::config => $notify_config(); + notify::error => $notify_error(); notify::mouse-hover-url => $notify_mouse_hover_url(); notify::mouse-hidden => $notify_mouse_hidden(); notify::mouse-shape => $notify_mouse_shape(); - Overlay { - focusable: false; - focus-on-click: false; + Stack { + StackPage { + name: "terminal"; - child: Box { - hexpand: true; - vexpand: true; + child: Overlay { + focusable: false; + focus-on-click: false; - GLArea gl_area { - realize => $gl_realize(); - unrealize => $gl_unrealize(); - 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; - } + child: Box { + hexpand: true; + vexpand: true; - PopoverMenu context_menu { - closed => $context_menu_closed(); - menu-model: context_menu_model; - flags: nested; - halign: start; - has-arrow: false; - } - }; + GLArea gl_area { + realize => $gl_realize(); + unrealize => $gl_unrealize(); + 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; + } - [overlay] - ProgressBar progress_bar_overlay { - styles [ - "osd", - ] + PopoverMenu context_menu { + closed => $context_menu_closed(); + menu-model: context_menu_model; + flags: nested; + halign: start; + has-arrow: false; + } + }; - visible: false; - halign: fill; - valign: start; + [overlay] + ProgressBar progress_bar_overlay { + 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 ; + 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] - // 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 ; - transition-type: crossfade; - transition-duration: 500; + StackPage { + name: "error"; - Box bell_overlay { - styles [ - "bell-overlay", - ] + child: Adw.StatusPage { + icon-name: "computer-fail-symbolic"; + title: _("Oh, no."); + description: _("Unable to acquire an OpenGL context for rendering."); - halign: fill; - valign: fill; - } + child: LinkButton { + label: "https://ghostty.org/docs/help/gtk-opengl-context"; + uri: "https://ghostty.org/docs/help/gtk-opengl-context"; + }; + }; } - [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; + // The order matters here: we can only set this after the stack + // pages above have been created. + visible-child-name: bind $stack_child_name(template.error) as ; } }