mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-21 19:26:09 +03:00
Merge pull request #735 from gpanders/resize-increments
macos: set window resizeIncrements when cell size changes
This commit is contained in:
@ -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;
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
|
@ -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 = "<group>"; };
|
||||
A5FEB2FF2ABB69450068369E /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
|
||||
A5FECBD629D1FC3900022361 /* PrimaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryView.swift; sourceTree = "<group>"; };
|
||||
A5FECBD829D2010400022361 /* WindowAccessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowAccessor.swift; sourceTree = "<group>"; };
|
||||
/* 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 */,
|
||||
|
@ -14,9 +14,10 @@ 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.
|
||||
@State private var window: NSWindow?
|
||||
// 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
|
||||
@State private var fullScreen = FullScreenHandler()
|
||||
@ -27,6 +28,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 +70,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 +80,12 @@ 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
|
||||
if !ghostty.windowStepResize { return }
|
||||
guard let size = newValue else { return }
|
||||
self.window.contentResizeIncrements = size
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -95,8 +100,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 +139,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 }
|
||||
|
@ -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
|
||||
|
@ -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 }
|
||||
@ -143,7 +152,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 +478,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 }
|
||||
|
||||
|
@ -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,13 @@ 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 = "👻"
|
||||
|
||||
|
||||
// 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.
|
||||
var initialSize: NSSize? = nil
|
||||
@ -930,3 +937,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
|
||||
}
|
||||
}
|
||||
|
@ -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) {}
|
||||
}
|
@ -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.
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -390,6 +390,11 @@ keybind: Keybinds = .{},
|
||||
@"window-height": u32 = 0,
|
||||
@"window-width": u32 = 0,
|
||||
|
||||
/// 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
|
||||
/// disallow clipboard reading but allow writing.
|
||||
|
Reference in New Issue
Block a user