From bac1780c3c7dbafb4f7748638e7fdfaa0fc860d9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 6 Oct 2024 09:54:07 -1000 Subject: [PATCH 1/4] core: add app focused state, make App.keyEvent focus aware --- src/App.zig | 62 +++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 56 insertions(+), 6 deletions(-) diff --git a/src/App.zig b/src/App.zig index 7e82bf007..b557aaa08 100644 --- a/src/App.zig +++ b/src/App.zig @@ -30,6 +30,21 @@ alloc: Allocator, /// The list of surfaces that are currently active. surfaces: SurfaceList, +/// This is true if the app that Ghostty is in is focused. This may +/// mean that no surfaces (terminals) are focused but the app is still +/// focused, i.e. may an about window. On macOS, this concept is known +/// as the "active" app while focused windows are known as the +/// "main" window. +/// +/// This is used to determine if keyboard shortcuts that are non-global +/// should be processed. If the app is not focused, then we don't want +/// to process keyboard shortcuts that are not global. +/// +/// This defaults to true since we assume that the app is focused when +/// Ghostty is initialized but a well behaved apprt should call +/// focusEvent to set this to the correct value right away. +focused: bool = true, + /// The last focused surface. This surface may not be valid; /// you must always call hasSurface to validate it. focused_surface: ?*Surface = null, @@ -54,6 +69,9 @@ last_notification_digest: u64 = 0, /// Initialize the main app instance. This creates the main window, sets /// up the renderer state, compiles the shaders, etc. This is the primary /// "startup" logic. +/// +/// After calling this function, well behaved apprts should then call +/// `focusEvent` to set the initial focus state of the app. pub fn create( alloc: Allocator, ) !*App { @@ -265,9 +283,21 @@ pub fn setQuit(self: *App) !void { self.quit = true; } +/// Handle an app-level focus event. This should be called whenever +/// the focus state of the entire app containing Ghostty changes. +/// This is separate from surface focus events. See the `focused` +/// field for more information. +pub fn focusEvent(self: *App, focused: bool) void { + self.focused = focused; +} + /// Handle a key event at the app-scope. If this key event is used, /// this will return true and the caller shouldn't continue processing /// the event. If the event is not used, this will return false. +/// +/// If the app currently has focus then all key events are processed. +/// If the app does not have focus then only global key events are +/// processed. pub fn keyEvent( self: *App, rt_app: *apprt.App, @@ -294,13 +324,33 @@ pub fn keyEvent( .leaf => |leaf| leaf, }; - // We only care about global keybinds - if (!leaf.flags.global) return false; + // If we aren't focused, then we only process global keybinds. + if (!self.focused and !leaf.flags.global) return false; - // Perform the action - self.performAllAction(rt_app, leaf.action) catch |err| { - log.warn("error performing global keybind action action={s} err={}", .{ - @tagName(leaf.action), + // Global keybinds are done using performAll so that they + // can target all surfaces too. + if (leaf.flags.global) { + self.performAllAction(rt_app, leaf.action) catch |err| { + log.warn("error performing global keybind action action={s} err={}", .{ + @tagName(leaf.action), + err, + }); + }; + + return true; + } + + // Must be focused to process non-global keybinds + assert(self.focused); + assert(!leaf.flags.global); + + // If we are focused, then we process keybinds only if they are + // app-scoped. Otherwise, we do nothing. Surface-scoped should + // be processed by Surface.keyEvent. + const app_action = leaf.action.scoped(.app) orelse return false; + self.performAction(rt_app, app_action) catch |err| { + log.warn("error performing app keybind action action={s} err={}", .{ + @tagName(app_action), err, }); }; From 8dc4ebb4f7dbc297c2a79c414a38ff4fc1752c3a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 6 Oct 2024 09:54:07 -1000 Subject: [PATCH 2/4] apprt/embedded: add ghostty_app_set_focus --- include/ghostty.h | 1 + src/apprt/embedded.zig | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/include/ghostty.h b/include/ghostty.h index b0e5c3fd6..354da33d0 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -615,6 +615,7 @@ ghostty_app_t ghostty_app_new(const ghostty_runtime_config_s*, void ghostty_app_free(ghostty_app_t); bool ghostty_app_tick(ghostty_app_t); void* ghostty_app_userdata(ghostty_app_t); +void ghostty_app_set_focus(ghostty_app_t, bool); bool ghostty_app_key(ghostty_app_t, ghostty_input_key_s); void ghostty_app_keyboard_changed(ghostty_app_t); void ghostty_app_open_config(ghostty_app_t); diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index dc6006caf..4d45166aa 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -132,6 +132,11 @@ pub const App = struct { surface: *Surface, }; + /// See CoreApp.focusEvent + pub fn focusEvent(self: *App, focused: bool) void { + self.core_app.focusEvent(focused); + } + /// See CoreApp.keyEvent. pub fn keyEvent( self: *App, @@ -1296,6 +1301,14 @@ pub const CAPI = struct { core_app.destroy(); } + /// Update the focused state of the app. + export fn ghostty_app_set_focus( + app: *App, + focused: bool, + ) void { + app.focusEvent(focused); + } + /// Notify the app of a global keypress capture. This will return /// true if the key was captured by the app, in which case the caller /// should not process the key. From e56cfbdc8b8a5550889452f9da0d71d382938bc1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 6 Oct 2024 10:06:07 -1000 Subject: [PATCH 3/4] macos: set the proper app focus state --- macos/Sources/Ghostty/Ghostty.App.swift | 46 ++++++++++++++++++------- src/App.zig | 1 + 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index 3ed082d87..5716e9801 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -83,14 +83,27 @@ extension Ghostty { } self.app = app - #if os(macOS) - // Subscribe to notifications for keyboard layout change so that we can update Ghostty. - NotificationCenter.default.addObserver( +#if os(macOS) + // Set our initial focus state + ghostty_app_set_focus(app, NSApp.isActive) + + let center = NotificationCenter.default + center.addObserver( self, selector: #selector(keyboardSelectionDidChange(notification:)), name: NSTextInputContext.keyboardSelectionDidChangeNotification, object: nil) - #endif + center.addObserver( + self, + selector: #selector(applicationDidBecomeActive(notification:)), + name: NSApplication.didBecomeActiveNotification, + object: nil) + center.addObserver( + self, + selector: #selector(applicationDidResignActive(notification:)), + name: NSApplication.didResignActiveNotification, + object: nil) +#endif self.readiness = .ready } @@ -98,14 +111,10 @@ extension Ghostty { deinit { // This will force the didSet callbacks to run which free. self.app = nil - - #if os(macOS) - // Remove our observer - NotificationCenter.default.removeObserver( - self, - name: NSTextInputContext.keyboardSelectionDidChangeNotification, - object: nil) - #endif + +#if os(macOS) + NotificationCenter.default.removeObserver(self) +#endif } // MARK: App Operations @@ -266,6 +275,19 @@ extension Ghostty { ghostty_app_keyboard_changed(app) } + // Called when the app becomes active. + @objc private func applicationDidBecomeActive(notification: NSNotification) { + guard let app = self.app else { return } + ghostty_app_set_focus(app, true) + } + + // Called when the app becomes inactive. + @objc private func applicationDidResignActive(notification: NSNotification) { + guard let app = self.app else { return } + ghostty_app_set_focus(app, false) + } + + // MARK: Ghostty Callbacks (macOS) static func closeSurface(_ userdata: UnsafeMutableRawPointer?, processAlive: Bool) { diff --git a/src/App.zig b/src/App.zig index b557aaa08..6a4a7a546 100644 --- a/src/App.zig +++ b/src/App.zig @@ -288,6 +288,7 @@ pub fn setQuit(self: *App) !void { /// This is separate from surface focus events. See the `focused` /// field for more information. pub fn focusEvent(self: *App, focused: bool) void { + log.debug("focus event focused={}", .{focused}); self.focused = focused; } From 494fedca2f33bf1959c03dfd5c6cce1d854699e3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 6 Oct 2024 13:30:53 -0700 Subject: [PATCH 4/4] apprt/gtk: report proper app focus state --- src/App.zig | 3 ++ src/apprt/gtk/App.zig | 86 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/src/App.zig b/src/App.zig index 6a4a7a546..599265a44 100644 --- a/src/App.zig +++ b/src/App.zig @@ -288,6 +288,9 @@ pub fn setQuit(self: *App) !void { /// This is separate from surface focus events. See the `focused` /// field for more information. pub fn focusEvent(self: *App, focused: bool) void { + // Prevent redundant focus events + if (self.focused == focused) return; + log.debug("focus event focused={}", .{focused}); self.focused = focused; } diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 2e0bbad84..3bed756d0 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -97,7 +97,7 @@ pub fn init(core_app: *CoreApp, opts: Options) !App { c.gtk_get_minor_version(), c.gtk_get_micro_version(), }); - + if (version.atLeast(4, 16, 0)) { // From gtk 4.16, GDK_DEBUG is split into GDK_DEBUG and GDK_DISABLE _ = internal_os.setenv("GDK_DISABLE", "gles-api"); @@ -235,6 +235,24 @@ pub fn init(core_app: *CoreApp, opts: Options) !App { c.G_CONNECT_DEFAULT, ); + // Other signals + _ = c.g_signal_connect_data( + app, + "window-added", + c.G_CALLBACK(>kWindowAdded), + core_app, + null, + c.G_CONNECT_DEFAULT, + ); + _ = c.g_signal_connect_data( + app, + "window-removed", + c.G_CALLBACK(>kWindowRemoved), + core_app, + null, + c.G_CONNECT_DEFAULT, + ); + // We don't use g_application_run, we want to manually control the // loop so we have to do the same things the run function does: // https://github.com/GNOME/glib/blob/a8e8b742e7926e33eb635a8edceac74cf239d6ed/gio/gapplication.c#L2533 @@ -1065,6 +1083,72 @@ fn gtkActivate(app: *c.GtkApplication, ud: ?*anyopaque) callconv(.C) void { }, .{ .forever = {} }); } +fn gtkWindowAdded( + _: *c.GtkApplication, + window: *c.GtkWindow, + ud: ?*anyopaque, +) callconv(.C) void { + const core_app: *CoreApp = @ptrCast(@alignCast(ud orelse return)); + + // Request the is-active property change so we can detect + // when our app loses focus. + _ = c.g_signal_connect_data( + window, + "notify::is-active", + c.G_CALLBACK(>kWindowIsActive), + core_app, + null, + c.G_CONNECT_DEFAULT, + ); +} + +fn gtkWindowRemoved( + _: *c.GtkApplication, + _: *c.GtkWindow, + ud: ?*anyopaque, +) callconv(.C) void { + const core_app: *CoreApp = @ptrCast(@alignCast(ud orelse return)); + + // Recheck if we are focused + gtkWindowIsActive(null, undefined, core_app); +} + +fn gtkWindowIsActive( + window: ?*c.GtkWindow, + _: *c.GParamSpec, + ud: ?*anyopaque, +) callconv(.C) void { + const core_app: *CoreApp = @ptrCast(@alignCast(ud orelse return)); + + // If our window is active, then we can tell the app + // that we are focused. + if (window) |w| { + if (c.gtk_window_is_active(w) == 1) { + core_app.focusEvent(true); + return; + } + } + + // If the window becomes inactive, we need to check if any + // other windows are active. If not, then we are no longer + // focused. + if (c.gtk_window_list_toplevels()) |list| { + defer c.g_list_free(list); + var current: ?*c.GList = list; + while (current) |elem| : (current = elem.next) { + // If the window is active then we are still focused. + // This is another window since we did our check above. + // That window should trigger its own is-active + // callback so we don't need to call it here. + const w: *c.GtkWindow = @alignCast(@ptrCast(elem.data)); + if (c.gtk_window_is_active(w) == 1) return; + } + } + + // We are not focused + core_app.focusEvent(false); +} + /// Call a D-Bus method to determine the current color scheme. If there /// is any error at any point we'll log the error and return "light" pub fn getColorScheme(self: *App) apprt.ColorScheme {