From 2ee80a52df2fdec3d08df69b01e4c6c7b0071ccd Mon Sep 17 00:00:00 2001 From: Gregory Anders Date: Tue, 24 Oct 2023 21:18:40 -0500 Subject: [PATCH 1/5] macos: set window resizeIncrements when cell size changes The resizeIncrements property is only modified when the cell size of the focused window changes. If two splits have the same cell size then the property is not modified when focusing between the two splits. --- include/ghostty.h | 2 ++ macos/Ghostty.xcodeproj/project.pbxproj | 4 ---- .../Features/Primary Window/PrimaryView.swift | 17 +++++++++-------- .../Features/Primary Window/PrimaryWindow.swift | 3 ++- macos/Sources/Ghostty/AppState.swift | 9 ++++++++- macos/Sources/Ghostty/SurfaceView.swift | 16 +++++++++++++++- macos/Sources/Helpers/WindowAccessor.swift | 16 ---------------- src/Surface.zig | 6 ++++++ src/apprt/embedded.zig | 12 ++++++++++++ src/apprt/glfw.zig | 7 +++++++ src/apprt/gtk/Surface.zig | 6 ++++++ 11 files changed, 67 insertions(+), 31 deletions(-) delete mode 100644 macos/Sources/Helpers/WindowAccessor.swift diff --git a/include/ghostty.h b/include/ghostty.h index 78c6dfb89..d28dcd095 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -337,6 +337,7 @@ typedef void (*ghostty_runtime_goto_tab_cb)(void *, int32_t); typedef void (*ghostty_runtime_toggle_fullscreen_cb)(void *, ghostty_non_native_fullscreen_e); typedef void (*ghostty_runtime_set_initial_window_size_cb)(void *, uint32_t, uint32_t); typedef void (*ghostty_runtime_render_inspector_cb)(void *); +typedef void (*ghostty_runtime_set_cell_size_cb)(void *, uint32_t, uint32_t); typedef struct { void *userdata; @@ -359,6 +360,7 @@ typedef struct { ghostty_runtime_toggle_fullscreen_cb toggle_fullscreen_cb; ghostty_runtime_set_initial_window_size_cb set_initial_window_size_cb; ghostty_runtime_render_inspector_cb render_inspector_cb; + ghostty_runtime_set_cell_size_cb set_cell_size_cb; } ghostty_runtime_config_s; //------------------------------------------------------------------- diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index f88f47cda..f493b2072 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -38,7 +38,6 @@ A5CEAFFF29C2410700646FDA /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFFE29C2410700646FDA /* Backport.swift */; }; A5FEB3002ABB69450068369E /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5FEB2FF2ABB69450068369E /* main.swift */; }; A5FECBD729D1FC3900022361 /* PrimaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5FECBD629D1FC3900022361 /* PrimaryView.swift */; }; - A5FECBD929D2010400022361 /* WindowAccessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5FECBD829D2010400022361 /* WindowAccessor.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -76,7 +75,6 @@ A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = GhosttyKit.xcframework; sourceTree = ""; }; A5FEB2FF2ABB69450068369E /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; A5FECBD629D1FC3900022361 /* PrimaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryView.swift; sourceTree = ""; }; - A5FECBD829D2010400022361 /* WindowAccessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowAccessor.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -120,7 +118,6 @@ A5CEAFFE29C2410700646FDA /* Backport.swift */, 8503D7C62A549C66006CFF3D /* FullScreenHandler.swift */, A59FB5D02AE0DEA7009128F3 /* MetalView.swift */, - A5FECBD829D2010400022361 /* WindowAccessor.swift */, A5CEAFDA29B8005900646FDA /* SplitView */, ); path = Helpers; @@ -313,7 +310,6 @@ A5CDF1932AAF9E0800513312 /* ConfigurationErrorsController.swift in Sources */, A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */, A55685E029A03A9F004303CE /* AppError.swift in Sources */, - A5FECBD929D2010400022361 /* WindowAccessor.swift in Sources */, A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */, 85102A1C2A6E32890084AB3E /* PrimaryWindowController.swift in Sources */, A5CEAFFF29C2410700646FDA /* Backport.swift in Sources */, diff --git a/macos/Sources/Features/Primary Window/PrimaryView.swift b/macos/Sources/Features/Primary Window/PrimaryView.swift index a0047e4f8..6269adfcf 100644 --- a/macos/Sources/Features/Primary Window/PrimaryView.swift +++ b/macos/Sources/Features/Primary Window/PrimaryView.swift @@ -16,7 +16,7 @@ struct PrimaryView: View { // We need access to our window to know if we're the key window to determine // if we show the quit confirmation or not. - @State private var window: NSWindow? + var window: NSWindow // This handles non-native fullscreen @State private var fullScreen = FullScreenHandler() @@ -27,6 +27,7 @@ struct PrimaryView: View { @FocusedValue(\.ghosttySurfaceView) private var focusedSurface @FocusedValue(\.ghosttySurfaceTitle) private var surfaceTitle @FocusedValue(\.ghosttySurfaceZoomed) private var zoomedSplit + @FocusedValue(\.ghosttySurfaceCellSize) private var cellSize // The title for our window private var title: String { @@ -68,7 +69,6 @@ struct PrimaryView: View { Ghostty.TerminalSplit(onClose: Self.closeWindow, baseConfig: self.baseConfig) .ghosttyApp(ghostty.app!) .ghosttyConfig(ghostty.config!) - .background(WindowAccessor(window: $window)) .onReceive(gotoTab) { onGotoTab(notification: $0) } .onReceive(toggleFullscreen) { onToggleFullscreen(notification: $0) } .focused($focused) @@ -79,8 +79,11 @@ struct PrimaryView: View { .onChange(of: title) { newValue in // We need to handle this manually because we are using AppKit lifecycle // so navigationTitle no longer works. - guard let window = self.window else { return } - window.title = newValue + self.window.title = newValue + } + .onChange(of: cellSize) { newValue in + guard let size = newValue else { return } + self.window.contentResizeIncrements = size } } } @@ -95,8 +98,7 @@ struct PrimaryView: View { // Notification center indiscriminately sends to every subscriber (makes sense) // but we only want to process this once. In order to process it once lets only // handle it if we're the focused window. - guard let window = self.window else { return } - guard window.isKeyWindow else { return } + guard self.window.isKeyWindow else { return } // Get the tab index from the notification guard let tabIndexAny = notification.userInfo?[Ghostty.Notification.GotoTabKey] else { return } @@ -135,8 +137,7 @@ struct PrimaryView: View { // Just like in `onGotoTab`, we might receive this multiple times. But // it's fine, because `toggleFullscreen` should only apply to the // currently focused window. - guard let window = self.window else { return } - guard window.isKeyWindow else { return } + guard self.window.isKeyWindow else { return } // Check whether we use non-native fullscreen guard let useNonNativeFullscreenAny = notification.userInfo?[Ghostty.Notification.NonNativeFullscreenKey] else { return } diff --git a/macos/Sources/Features/Primary Window/PrimaryWindow.swift b/macos/Sources/Features/Primary Window/PrimaryWindow.swift index 1d7f53b26..7a8be5471 100644 --- a/macos/Sources/Features/Primary Window/PrimaryWindow.swift +++ b/macos/Sources/Features/Primary Window/PrimaryWindow.swift @@ -41,7 +41,8 @@ class PrimaryWindow: NSWindow { ghostty: ghostty, appDelegate: appDelegate, focusedSurfaceWrapper: window.focusedSurfaceWrapper, - baseConfig: baseConfig + baseConfig: baseConfig, + window: window )) // We do want to cascade when new windows are created diff --git a/macos/Sources/Ghostty/AppState.swift b/macos/Sources/Ghostty/AppState.swift index f81131b32..8ab25a2c0 100644 --- a/macos/Sources/Ghostty/AppState.swift +++ b/macos/Sources/Ghostty/AppState.swift @@ -143,7 +143,8 @@ extension Ghostty { goto_tab_cb: { userdata, n in AppState.gotoTab(userdata, n: n) }, toggle_fullscreen_cb: { userdata, nonNativeFullscreen in AppState.toggleFullscreen(userdata, nonNativeFullscreen: nonNativeFullscreen) }, set_initial_window_size_cb: { userdata, width, height in AppState.setInitialWindowSize(userdata, width: width, height: height) }, - render_inspector_cb: { userdata in AppState.renderInspector(userdata) } + render_inspector_cb: { userdata in AppState.renderInspector(userdata) }, + set_cell_size_cb: { userdata, width, height in AppState.setCellSize(userdata, width: width, height: height) } ) // Create the ghostty app. @@ -468,6 +469,12 @@ extension Ghostty { surfaceView.initialSize = NSMakeSize(Double(width), Double(height)) } + static func setCellSize(_ userdata: UnsafeMutableRawPointer?, width: UInt32, height: UInt32) { + guard let surfaceView = self.surfaceUserdata(from: userdata) else { return } + let backingSize = NSSize(width: Double(width), height: Double(height)) + surfaceView.cellSize = surfaceView.convertFromBacking(backingSize) + } + static func newTab(_ userdata: UnsafeMutableRawPointer?, config: ghostty_surface_config_s) { guard let surface = self.surfaceUserdata(from: userdata) else { return } diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index db8251bdd..c786afd3b 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -78,6 +78,7 @@ extension Ghostty { .focused($surfaceFocus) .focusedValue(\.ghosttySurfaceTitle, surfaceView.title) .focusedValue(\.ghosttySurfaceView, surfaceView) + .focusedValue(\.ghosttySurfaceCellSize, surfaceView.cellSize) .onReceive(pubBecomeKey) { notification in guard let window = notification.object as? NSWindow else { return } guard let surfaceWindow = surfaceView.window else { return } @@ -240,7 +241,9 @@ extension Ghostty { // changed with escape codes. This is public because the callbacks go // to the app level and it is set from there. @Published var title: String = "👻" - + + @Published var cellSize: NSSize = .init() + // An initial size to request for a window. This will only affect // then the view is moved to a new window. var initialSize: NSSize? = nil @@ -930,3 +933,14 @@ extension FocusedValues { typealias Value = Bool } } + +extension FocusedValues { + var ghosttySurfaceCellSize: NSSize? { + get { self[FocusedGhosttySurfaceCellSize.self] } + set { self[FocusedGhosttySurfaceCellSize.self] = newValue } + } + + struct FocusedGhosttySurfaceCellSize: FocusedValueKey { + typealias Value = NSSize + } +} diff --git a/macos/Sources/Helpers/WindowAccessor.swift b/macos/Sources/Helpers/WindowAccessor.swift deleted file mode 100644 index f12e841bd..000000000 --- a/macos/Sources/Helpers/WindowAccessor.swift +++ /dev/null @@ -1,16 +0,0 @@ -import SwiftUI - -/// Allows accessing the window that this view is a part of. -struct WindowAccessor: NSViewRepresentable { - @Binding var window: NSWindow? - - func makeNSView(context: Context) -> NSView { - let view = NSView() - DispatchQueue.main.async { - self.window = view.window - } - return view - } - - func updateNSView(_ nsView: NSView, context: Context) {} -} diff --git a/src/Surface.zig b/src/Surface.zig index 2864c5781..509ff3ce1 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -460,6 +460,9 @@ pub fn init( .config = try DerivedConfig.init(alloc, config), }; + // Report initial cell size on surface creation + try rt_surface.setCellSize(cell_size.width, cell_size.height); + // Set a minimum size that is cols=10 h=4. This matches Mac's Terminal.app // but is otherwise somewhat arbitrary. try rt_surface.setSizeLimits(.{ @@ -896,6 +899,9 @@ fn setCellSize(self: *Surface, size: renderer.CellSize) !void { }, }, .{ .forever = {} }); self.io_thread.wakeup.notify() catch {}; + + // Notify the window + try self.rt_surface.setCellSize(size.width, size.height); } /// Change the font size. diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index bb5826594..d66682528 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -98,6 +98,9 @@ pub const App = struct { /// Render the inspector for the given surface. render_inspector: ?*const fn (SurfaceUD) callconv(.C) void = null, + + /// Called when the cell size changes. + set_cell_size: ?*const fn (SurfaceUD, u32, u32) callconv(.C) void = null, }; /// Special values for the goto_tab callback. @@ -818,6 +821,15 @@ pub const Surface = struct { func(self.opts.userdata); } + pub fn setCellSize(self: *const Surface, width: u32, height: u32) !void { + const func = self.app.opts.set_cell_size orelse { + log.info("runtime embedder does not support set_cell_size", .{}); + return; + }; + + func(self.opts.userdata, width, height); + } + fn newSurfaceOptions(self: *const Surface) apprt.Surface.Options { const font_size: u16 = font_size: { if (!self.app.config.@"window-inherit-font-size") break :font_size 0; diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 36367e20b..bf96e1e6c 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -461,6 +461,13 @@ pub const Surface = struct { self.window.setSize(.{ .width = width, .height = height }); } + /// Set the cell size. Unused by GLFW. + pub fn setCellSize(self: *const Surface, width: u32, height: u32) !void { + _ = self; + _ = width; + _ = height; + } + /// Set the size limits of the window. /// Note: this interface is not good, we should redo it if we plan /// to use this more. i.e. you can't set max width but no max height, diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index c6403a0d5..86a6c816e 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -374,6 +374,12 @@ pub fn setInitialWindowSize(self: *const Surface, width: u32, height: u32) !void ); } +pub fn setCellSize(self: *const Surface, width: u32, height: u32) !void { + _ = self; + _ = width; + _ = height; +} + pub fn setSizeLimits(self: *Surface, min: apprt.SurfaceSize, max_: ?apprt.SurfaceSize) !void { _ = self; _ = min; From 0f73bf32e615f71f5b3e21379aa109b758563608 Mon Sep 17 00:00:00 2001 From: Gregory Anders Date: Fri, 27 Oct 2023 09:23:17 -0500 Subject: [PATCH 2/5] macos: document SurfaceView.cellSize property --- macos/Sources/Ghostty/SurfaceView.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index c786afd3b..0252b5a42 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -242,7 +242,11 @@ extension Ghostty { // to the app level and it is set from there. @Published var title: String = "👻" - @Published var cellSize: NSSize = .init() + // The cell size of this surface. This is set by the core when the + // surface is first created and any time the cell size changes (i.e. + // when the font size changes). This is used to allow windows to be + // resized in discrete steps of a single cell. + @Published var cellSize: NSSize = .zero // An initial size to request for a window. This will only affect // then the view is moved to a new window. From add2b675bd46092a243eb243e7da5b4a53ce1d39 Mon Sep 17 00:00:00 2001 From: Gregory Anders Date: Fri, 27 Oct 2023 09:30:15 -0500 Subject: [PATCH 3/5] config: add window-step-resize option --- macos/Sources/Features/Primary Window/PrimaryView.swift | 1 + macos/Sources/Ghostty/AppState.swift | 9 +++++++++ src/config/Config.zig | 4 ++++ 3 files changed, 14 insertions(+) diff --git a/macos/Sources/Features/Primary Window/PrimaryView.swift b/macos/Sources/Features/Primary Window/PrimaryView.swift index 6269adfcf..9744ed234 100644 --- a/macos/Sources/Features/Primary Window/PrimaryView.swift +++ b/macos/Sources/Features/Primary Window/PrimaryView.swift @@ -82,6 +82,7 @@ struct PrimaryView: View { self.window.title = newValue } .onChange(of: cellSize) { newValue in + if !ghostty.windowStepResize { return } guard let size = newValue else { return } self.window.contentResizeIncrements = size } diff --git a/macos/Sources/Ghostty/AppState.swift b/macos/Sources/Ghostty/AppState.swift index 8ab25a2c0..4f637e3a9 100644 --- a/macos/Sources/Ghostty/AppState.swift +++ b/macos/Sources/Ghostty/AppState.swift @@ -97,6 +97,15 @@ extension Ghostty { return String(cString: ptr) } + /// Whether to resize windows in discrete steps or use "fluid" resizing + var windowStepResize: Bool { + guard let config = self.config else { return true } + var v = false + let key = "window-step-resize" + _ = ghostty_config_get(config, &v, key, UInt(key.count)) + return v + } + /// The background opacity. var backgroundOpacity: Double { guard let config = self.config else { return 1 } diff --git a/src/config/Config.zig b/src/config/Config.zig index 0f42ba743..754884ffc 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -390,6 +390,10 @@ keybind: Keybinds = .{}, @"window-height": u32 = 0, @"window-width": u32 = 0, +/// Whether to resize the window in discrete increments of the focused surface's +/// cell size. Currently only supported on macOS. +@"window-step-resize": bool = false, + /// Whether to allow programs running in the terminal to read/write to /// the system clipboard (OSC 52, for googling). The default is to /// disallow clipboard reading but allow writing. From 9d2d9ca7a39225ae5844ce8ce4c4ca9533113f78 Mon Sep 17 00:00:00 2001 From: Gregory Anders Date: Fri, 27 Oct 2023 10:07:15 -0500 Subject: [PATCH 4/5] maacos: update doc comment for PrimaryView.window --- macos/Sources/Features/Primary Window/PrimaryView.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/macos/Sources/Features/Primary Window/PrimaryView.swift b/macos/Sources/Features/Primary Window/PrimaryView.swift index 9744ed234..9cff1c93f 100644 --- a/macos/Sources/Features/Primary Window/PrimaryView.swift +++ b/macos/Sources/Features/Primary Window/PrimaryView.swift @@ -14,8 +14,9 @@ struct PrimaryView: View { // If this is set, this is the base configuration that we build our surface out of. let baseConfig: Ghostty.SurfaceConfiguration? - // We need access to our window to know if we're the key window to determine - // if we show the quit confirmation or not. + // We need access to our window to know if we're the key window and to + // modify window properties in response to events from the surface (e.g. + // updating the window title) var window: NSWindow // This handles non-native fullscreen From 9b63da2ea7acb62ba906bb39842d7965412e0e7d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 27 Oct 2023 08:08:17 -0700 Subject: [PATCH 5/5] config: slighty copy editing --- src/config/Config.zig | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 754884ffc..a5a343e3c 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -390,9 +390,10 @@ keybind: Keybinds = .{}, @"window-height": u32 = 0, @"window-width": u32 = 0, -/// Whether to resize the window in discrete increments of the focused surface's -/// cell size. Currently only supported on macOS. -@"window-step-resize": bool = false, +/// Resize the window in discrete increments of the focused surface's +/// cell size. If this is disabled, surfaces are resized in pixel increments. +/// Currently only supported on macOS. +@"window-step-resize": bool = true, /// Whether to allow programs running in the terminal to read/write to /// the system clipboard (OSC 52, for googling). The default is to