From 86122624e005f2798ced461970140578a21f3ade Mon Sep 17 00:00:00 2001 From: Will Pragnell Date: Fri, 1 Sep 2023 20:17:30 -0700 Subject: [PATCH 1/2] macos: add visible-menu non-native-fullscreen option --- include/ghostty.h | 8 ++++- .../Features/Primary Window/PrimaryView.swift | 2 +- macos/Sources/Ghostty/AppState.swift | 7 ++-- macos/Sources/Helpers/FullScreenHandler.swift | 32 +++++++++++++------ src/Surface.zig | 2 +- src/apprt/embedded.zig | 7 ++-- src/apprt/gtk.zig | 7 ++-- src/config.zig | 25 ++++++++++++--- 8 files changed, 63 insertions(+), 27 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index 856b260d8..df042ff95 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -70,6 +70,12 @@ typedef enum { GHOSTTY_MOUSE_MOMENTUM_MAY_BEGIN, } ghostty_input_mouse_momentum_e; +typedef enum { + GHOSTTY_NON_NATIVE_FULLSCREEN_FALSE, + GHOSTTY_NON_NATIVE_FULLSCREEN_TRUE, + GHOSTTY_NON_NATIVE_FULLSCREEN_VISIBLE_MENU, +} ghostty_non_native_fullscreen_e; + // This is a packed struct (see src/input/mouse.zig) but the C standard // afaik doesn't let us reliably define packed structs so we build it up // from scratch. @@ -257,7 +263,7 @@ typedef void (*ghostty_runtime_new_window_cb)(void *, ghostty_surface_config_s); typedef void (*ghostty_runtime_close_surface_cb)(void *, bool); typedef void (*ghostty_runtime_focus_split_cb)(void *, ghostty_split_focus_direction_e); typedef void (*ghostty_runtime_goto_tab_cb)(void *, int32_t); -typedef void (*ghostty_runtime_toggle_fullscreen_cb)(void *, bool); +typedef void (*ghostty_runtime_toggle_fullscreen_cb)(void *, ghostty_non_native_fullscreen_e); typedef struct { void *userdata; diff --git a/macos/Sources/Features/Primary Window/PrimaryView.swift b/macos/Sources/Features/Primary Window/PrimaryView.swift index b62e630b7..1aefbdd0e 100644 --- a/macos/Sources/Features/Primary Window/PrimaryView.swift +++ b/macos/Sources/Features/Primary Window/PrimaryView.swift @@ -146,7 +146,7 @@ struct PrimaryView: View { // Check whether we use non-native fullscreen guard let useNonNativeFullscreenAny = notification.userInfo?[Ghostty.Notification.NonNativeFullscreenKey] else { return } - guard let useNonNativeFullscreen = useNonNativeFullscreenAny as? Bool else { return } + guard let useNonNativeFullscreen = useNonNativeFullscreenAny as? ghostty_non_native_fullscreen_e else { return } self.fullScreen.toggleFullscreen(window: window, nonNativeFullscreen: useNonNativeFullscreen) // After toggling fullscreen we need to focus the terminal again. diff --git a/macos/Sources/Ghostty/AppState.swift b/macos/Sources/Ghostty/AppState.swift index 6367e6ad4..7149f2b4a 100644 --- a/macos/Sources/Ghostty/AppState.swift +++ b/macos/Sources/Ghostty/AppState.swift @@ -74,7 +74,7 @@ extension Ghostty { close_surface_cb: { userdata, processAlive in AppState.closeSurface(userdata, processAlive: processAlive) }, focus_split_cb: { userdata, direction in AppState.focusSplit(userdata, direction: direction) }, goto_tab_cb: { userdata, n in AppState.gotoTab(userdata, n: n) }, - toggle_fullscreen_cb: { userdata, nonNativeFullscreen in AppState.toggleFullscreen(userdata, useNonNativeFullscreen: nonNativeFullscreen) } + toggle_fullscreen_cb: { userdata, nonNativeFullscreen in AppState.toggleFullscreen(userdata, nonNativeFullscreen: nonNativeFullscreen) } ) // Create the ghostty app. @@ -276,14 +276,13 @@ extension Ghostty { } } - static func toggleFullscreen(_ userdata: UnsafeMutableRawPointer?, useNonNativeFullscreen: Bool) { - // togo: use non-native fullscreen + static func toggleFullscreen(_ userdata: UnsafeMutableRawPointer?, nonNativeFullscreen: ghostty_non_native_fullscreen_e) { guard let surface = self.surfaceUserdata(from: userdata) else { return } NotificationCenter.default.post( name: Notification.ghosttyToggleFullscreen, object: surface, userInfo: [ - Notification.NonNativeFullscreenKey: useNonNativeFullscreen, + Notification.NonNativeFullscreenKey: nonNativeFullscreen, ] ) } diff --git a/macos/Sources/Helpers/FullScreenHandler.swift b/macos/Sources/Helpers/FullScreenHandler.swift index e4f40b553..9ac49ba37 100644 --- a/macos/Sources/Helpers/FullScreenHandler.swift +++ b/macos/Sources/Helpers/FullScreenHandler.swift @@ -1,4 +1,5 @@ import SwiftUI +import GhosttyKit class FullScreenHandler { var previousTabGroup: NSWindowTabGroup? @@ -11,9 +12,10 @@ class FullScreenHandler { // and then wants to toggle it off var isInNonNativeFullscreen: Bool = false - func toggleFullscreen(window: NSWindow, nonNativeFullscreen: Bool) { + func toggleFullscreen(window: NSWindow, nonNativeFullscreen: ghostty_non_native_fullscreen_e) { + let useNonNativeFullscreen = nonNativeFullscreen != GHOSTTY_NON_NATIVE_FULLSCREEN_FALSE if isInFullscreen { - if nonNativeFullscreen || isInNonNativeFullscreen { + if useNonNativeFullscreen || isInNonNativeFullscreen { leaveFullscreen(window: window) isInNonNativeFullscreen = false } else { @@ -21,8 +23,9 @@ class FullScreenHandler { } isInFullscreen = false } else { - if nonNativeFullscreen { - enterFullscreen(window: window) + if useNonNativeFullscreen { + let hideMenu = nonNativeFullscreen != GHOSTTY_NON_NATIVE_FULLSCREEN_VISIBLE_MENU + enterFullscreen(window: window, hideMenu: hideMenu) isInNonNativeFullscreen = true } else { window.toggleFullScreen(nil) @@ -31,7 +34,7 @@ class FullScreenHandler { } } - func enterFullscreen(window: NSWindow) { + func enterFullscreen(window: NSWindow, hideMenu: Bool) { guard let screen = window.screen else { return } guard let contentView = window.contentView else { return } @@ -41,7 +44,7 @@ class FullScreenHandler { // Save previous contentViewFrame and screen previousContentFrame = window.convertToScreen(contentView.frame) - // Change presentation style to hide menu bar and dock + // Change presentation style to hide menu bar and dock if needed // It's important to do this in two calls, because setting them in a single call guarantees // that the menu bar will also be hidden on any additional displays (why? nobody knows!) // When these options are set separately, the menu bar hiding problem will only occur in @@ -52,19 +55,30 @@ class FullScreenHandler { if (shouldHideDock(screen: screen)) { NSApp.presentationOptions.insert(.autoHideDock) } - NSApp.presentationOptions.insert(.autoHideMenuBar) + if (hideMenu) { + NSApp.presentationOptions.insert(.autoHideMenuBar) + } // This is important: it gives us the full screen, including the // notch area on MacBooks. window.styleMask.remove(.titled) - // Set frame to screen size - window.setFrame(screen.frame, display: true) + // Set frame to screen size, accounting for the menu bar if needed + let frame = calculateFullscreenFrame(screenFrame: screen.frame, subtractMenu: !hideMenu) + window.setFrame(frame, display: true) // Focus window window.makeKeyAndOrderFront(nil) } + func calculateFullscreenFrame(screenFrame: NSRect, subtractMenu: Bool)->NSRect { + if (subtractMenu) { + let menuHeight = NSApp.mainMenu?.menuBarHeight ?? 0 + return NSMakeRect(screenFrame.minX, screenFrame.minY, screenFrame.width, screenFrame.height - menuHeight) + } + return screenFrame + } + func leaveFullscreen(window: NSWindow) { guard let previousFrame = previousContentFrame else { return } diff --git a/src/Surface.zig b/src/Surface.zig index 9299aa111..7fa51c268 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -137,7 +137,7 @@ const DerivedConfig = struct { copy_on_select: configpkg.CopyOnSelect, confirm_close_surface: bool, mouse_interval: u64, - macos_non_native_fullscreen: bool, + macos_non_native_fullscreen: configpkg.NonNativeFullscreen, macos_option_as_alt: configpkg.OptionAsAlt, pub fn init(alloc_gpa: Allocator, config: *const configpkg.Config) !DerivedConfig { diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 03b6409cf..75cfeeb3b 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -13,7 +13,8 @@ const apprt = @import("../apprt.zig"); const input = @import("../input.zig"); const CoreApp = @import("../App.zig"); const CoreSurface = @import("../Surface.zig"); -const Config = @import("../config.zig").Config; +const configpkg = @import("../config.zig"); +const Config = configpkg.Config; const log = std.log.scoped(.embedded_window); @@ -75,7 +76,7 @@ pub const App = struct { goto_tab: ?*const fn (SurfaceUD, usize) callconv(.C) void = null, /// Toggle fullscreen for current window. - toggle_fullscreen: ?*const fn (SurfaceUD, bool) callconv(.C) void = null, + toggle_fullscreen: ?*const fn (SurfaceUD, configpkg.NonNativeFullscreen) callconv(.C) void = null, }; core_app: *CoreApp, @@ -622,7 +623,7 @@ pub const Surface = struct { func(self.opts.userdata, n); } - pub fn toggleFullscreen(self: *Surface, nonNativeFullscreen: bool) void { + pub fn toggleFullscreen(self: *Surface, nonNativeFullscreen: configpkg.NonNativeFullscreen) void { const func = self.app.opts.toggle_fullscreen orelse { log.info("runtime embedder does not toggle_fullscreen", .{}); return; diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig index 37c3da69a..3dd09fb0d 100644 --- a/src/apprt/gtk.zig +++ b/src/apprt/gtk.zig @@ -10,7 +10,8 @@ const font = @import("../font/main.zig"); const input = @import("../input.zig"); const CoreApp = @import("../App.zig"); const CoreSurface = @import("../Surface.zig"); -const Config = @import("../config.zig").Config; +const configpkg = @import("../config.zig"); +const Config = configpkg.Config; pub const c = @cImport({ @cInclude("gtk/gtk.h"); @@ -518,7 +519,7 @@ const Window = struct { } /// Toggle fullscreen for this window. - fn toggleFullscreen(self: *Window, _: bool) void { + fn toggleFullscreen(self: *Window, _: configpkg.NonNativeFullscreen) void { const is_fullscreen = c.gtk_window_is_fullscreen(self.window); if (is_fullscreen == 0) { c.gtk_window_fullscreen(self.window); @@ -896,7 +897,7 @@ pub const Surface = struct { c.gtk_widget_show(alert); } - pub fn toggleFullscreen(self: *Surface, mac_non_native: bool) void { + pub fn toggleFullscreen(self: *Surface, mac_non_native: configpkg.NonNativeFullscreen) void { self.window.toggleFullscreen(mac_non_native); } diff --git a/src/config.zig b/src/config.zig index fea085933..bd826c5d6 100644 --- a/src/config.zig +++ b/src/config.zig @@ -278,11 +278,17 @@ pub const Config = struct { /// The default value is "detect". @"shell-integration": ShellIntegration = .detect, - /// If true, fullscreen mode on macOS will not use the native fullscreen, - /// but make the window fullscreen without animations and using a new space. - /// That's faster than the native fullscreen mode since it doesn't use - /// animations. - @"macos-non-native-fullscreen": bool = false, + /// If anything other than false, fullscreen mode on macOS will not use the + /// native fullscreen, but make the window fullscreen without animations and + /// using a new space. It's faster than the native fullscreen mode since it + /// doesn't use animations. + /// + /// Allowable values are: + /// + /// * "visible-menu" - Use non-native macOS fullscreen, keep the menu bar visible + /// * "true" - Use non-native macOS fullscreen, hide the menu bar + /// * "false" - Use native macOS fullscreeen + @"macos-non-native-fullscreen": NonNativeFullscreen = .false, /// If true, the Option key will be treated as Alt. This makes terminal /// sequences expecting Alt to work properly, but will break Unicode @@ -1075,6 +1081,15 @@ fn equal(comptime T: type, old: T, new: T) bool { } } +/// Valid values for macos-non-native-fullscreen +/// c_int because it needs to be extern compatible +/// If this is changed, you must also update ghostty.h +pub const NonNativeFullscreen = enum(c_int) { + false, + true, + @"visible-menu", +}; + /// Valid values for macos-option-as-alt. pub const OptionAsAlt = enum { false, From f6e2b507328ada3d28ed3436837f7d48013e22c6 Mon Sep 17 00:00:00 2001 From: Will Pragnell Date: Fri, 1 Sep 2023 21:28:34 -0700 Subject: [PATCH 2/2] macos: fix non-native-fullscreen menu & dock visibility bugs --- macos/Sources/Helpers/FullScreenHandler.swift | 53 ++++++++++++++++++- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/macos/Sources/Helpers/FullScreenHandler.swift b/macos/Sources/Helpers/FullScreenHandler.swift index 9ac49ba37..a85864b5b 100644 --- a/macos/Sources/Helpers/FullScreenHandler.swift +++ b/macos/Sources/Helpers/FullScreenHandler.swift @@ -53,10 +53,38 @@ class FullScreenHandler { // Furthermore, it's much easier to figure out which screen the dock is on if the menubar // has not yet been hidden, so the order matters here! if (shouldHideDock(screen: screen)) { - NSApp.presentationOptions.insert(.autoHideDock) + self.hideDock() + + // Ensure that we always hide the dock bar for this window, but not for non fullscreen ones + NotificationCenter.default.addObserver( + self, + selector: #selector(FullScreenHandler.hideDock), + name: NSWindow.didBecomeMainNotification, + object: window) + NotificationCenter.default.addObserver( + self, + selector: #selector(FullScreenHandler.unHideDock), + name: NSWindow.didResignMainNotification, + object: window) } if (hideMenu) { - NSApp.presentationOptions.insert(.autoHideMenuBar) + self.hideMenu() + + // Ensure that we always hide the menu bar for this window, but not for non fullscreen ones + // This is not the best way to do this, not least because it causes the menu to stay visible + // for a brief moment before being hidden in some cases (e.g. when switching spaces). + // If we end up adding a NSWindowDelegate to PrimaryWindow, then we may be better off + // handling this there. + NotificationCenter.default.addObserver( + self, + selector: #selector(FullScreenHandler.hideMenu), + name: NSWindow.didBecomeMainNotification, + object: window) + NotificationCenter.default.addObserver( + self, + selector: #selector(FullScreenHandler.unHideMenu), + name: NSWindow.didResignMainNotification, + object: window) } // This is important: it gives us the full screen, including the @@ -71,6 +99,22 @@ class FullScreenHandler { window.makeKeyAndOrderFront(nil) } + @objc func hideMenu() { + NSApp.presentationOptions.insert(.autoHideMenuBar) + } + + @objc func unHideMenu() { + NSApp.presentationOptions.remove(.autoHideMenuBar) + } + + @objc func hideDock() { + NSApp.presentationOptions.insert(.autoHideDock) + } + + @objc func unHideDock() { + NSApp.presentationOptions.remove(.autoHideDock) + } + func calculateFullscreenFrame(screenFrame: NSRect, subtractMenu: Bool)->NSRect { if (subtractMenu) { let menuHeight = NSApp.mainMenu?.menuBarHeight ?? 0 @@ -88,6 +132,11 @@ class FullScreenHandler { // Restore previous presentation options NSApp.presentationOptions = [] + // Stop handling any window focus notifications + // that we use to manage menu bar visibility + NotificationCenter.default.removeObserver(self, name: NSWindow.didBecomeMainNotification, object: window) + NotificationCenter.default.removeObserver(self, name: NSWindow.didResignMainNotification, object: window) + // Restore frame window.setFrame(window.frameRect(forContentRect: previousFrame), display: true)