mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-06-01 13:08:38 +03:00
Merge pull request #296 from mitchellh/mrn/macos-inherit-font-size
macOS: inherit font size when creating new tab
This commit is contained in:
@ -233,6 +233,7 @@ typedef enum {
|
||||
typedef enum {
|
||||
GHOSTTY_BINDING_COPY_TO_CLIPBOARD,
|
||||
GHOSTTY_BINDING_PASTE_FROM_CLIPBOARD,
|
||||
GHOSTTY_BINDING_NEW_TAB,
|
||||
} ghostty_binding_action_e;
|
||||
|
||||
// Fully defined types. This MUST be kept in sync with equivalent Zig
|
||||
@ -242,6 +243,7 @@ typedef struct {
|
||||
void *userdata;
|
||||
void *nsview;
|
||||
double scale_factor;
|
||||
uint16_t font_size;
|
||||
} ghostty_surface_config_s;
|
||||
|
||||
typedef void (*ghostty_runtime_wakeup_cb)(void *);
|
||||
@ -250,6 +252,7 @@ typedef void (*ghostty_runtime_set_title_cb)(void *, const char *);
|
||||
typedef const char* (*ghostty_runtime_read_clipboard_cb)(void *, ghostty_clipboard_e);
|
||||
typedef void (*ghostty_runtime_write_clipboard_cb)(void *, const char *, ghostty_clipboard_e);
|
||||
typedef void (*ghostty_runtime_new_split_cb)(void *, ghostty_split_direction_e);
|
||||
typedef void (*ghostty_runtime_new_tab_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);
|
||||
@ -264,6 +267,7 @@ typedef struct {
|
||||
ghostty_runtime_read_clipboard_cb read_clipboard_cb;
|
||||
ghostty_runtime_write_clipboard_cb write_clipboard_cb;
|
||||
ghostty_runtime_new_split_cb new_split_cb;
|
||||
ghostty_runtime_new_tab_cb new_tab_cb;
|
||||
ghostty_runtime_close_surface_cb close_surface_cb;
|
||||
ghostty_runtime_focus_split_cb focus_split_cb;
|
||||
ghostty_runtime_goto_tab_cb goto_tab_cb;
|
||||
@ -289,6 +293,8 @@ bool ghostty_app_tick(ghostty_app_t);
|
||||
void *ghostty_app_userdata(ghostty_app_t);
|
||||
void ghostty_app_keyboard_changed(ghostty_app_t);
|
||||
|
||||
ghostty_surface_config_s ghostty_surface_config_new();
|
||||
|
||||
ghostty_surface_t ghostty_surface_new(ghostty_app_t, ghostty_surface_config_s*);
|
||||
void ghostty_surface_free(ghostty_surface_t);
|
||||
ghostty_app_t ghostty_surface_app(ghostty_surface_t);
|
||||
|
@ -81,11 +81,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
|
||||
}
|
||||
|
||||
@IBAction func newTab(_ sender: Any?) {
|
||||
if let existingWindow = windowManager.mainWindow {
|
||||
windowManager.addNewTab(to: existingWindow)
|
||||
} else {
|
||||
windowManager.addNewWindow()
|
||||
}
|
||||
windowManager.newTab()
|
||||
}
|
||||
|
||||
@IBAction func closeWindow(_ sender: Any) {
|
||||
|
@ -11,6 +11,9 @@ struct PrimaryView: View {
|
||||
// We need this to report back up the app controller which surface in this view is focused.
|
||||
let focusedSurfaceWrapper: FocusedSurfaceWrapper
|
||||
|
||||
// If this is set, this is the base configuration that we build our surface out of.
|
||||
let baseConfig: ghostty_surface_config_s?
|
||||
|
||||
// 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?
|
||||
@ -71,7 +74,7 @@ struct PrimaryView: View {
|
||||
self.appDelegate.confirmQuit = $0
|
||||
})
|
||||
|
||||
Ghostty.TerminalSplit(onClose: Self.closeWindow)
|
||||
Ghostty.TerminalSplit(onClose: Self.closeWindow, baseConfig: self.baseConfig)
|
||||
.ghosttyApp(ghostty.app!)
|
||||
.background(WindowAccessor(window: $window))
|
||||
.onReceive(gotoTab) { onGotoTab(notification: $0) }
|
||||
|
@ -16,7 +16,7 @@ class FocusedSurfaceWrapper {
|
||||
class PrimaryWindow: NSWindow {
|
||||
var focusedSurfaceWrapper: FocusedSurfaceWrapper = FocusedSurfaceWrapper()
|
||||
|
||||
static func create(ghostty: Ghostty.AppState, appDelegate: AppDelegate) -> PrimaryWindow {
|
||||
static func create(ghostty: Ghostty.AppState, appDelegate: AppDelegate, baseConfig: ghostty_surface_config_s? = nil) -> PrimaryWindow {
|
||||
let window = PrimaryWindow(
|
||||
contentRect: NSRect(x: 0, y: 0, width: 800, height: 600),
|
||||
styleMask: [.titled, .closable, .miniaturizable, .resizable],
|
||||
@ -27,7 +27,9 @@ class PrimaryWindow: NSWindow {
|
||||
window.contentView = NSHostingView(rootView: PrimaryView(
|
||||
ghostty: ghostty,
|
||||
appDelegate: appDelegate,
|
||||
focusedSurfaceWrapper: window.focusedSurfaceWrapper))
|
||||
focusedSurfaceWrapper: window.focusedSurfaceWrapper,
|
||||
baseConfig: baseConfig
|
||||
))
|
||||
|
||||
// We do want to cascade when new windows are created
|
||||
window.windowController?.shouldCascadeWindows = true
|
||||
|
@ -7,8 +7,8 @@ class PrimaryWindowController: NSWindowController {
|
||||
// This is required for the "+" button to show up in the tab bar to add a
|
||||
// new tab.
|
||||
override func newWindowForTab(_ sender: Any?) {
|
||||
guard let window = self.window else { preconditionFailure("Expected window to be loaded") }
|
||||
guard let window = self.window as? PrimaryWindow else { preconditionFailure("Expected window to be loaded") }
|
||||
guard let manager = self.windowManager else { return }
|
||||
manager.addNewTab(to: window)
|
||||
manager.triggerNewTab(for: window)
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
import Cocoa
|
||||
import Combine
|
||||
import GhosttyKit
|
||||
import SwiftUI
|
||||
|
||||
// PrimaryWindowManager manages the windows and tabs in the primary window
|
||||
// of the application. It keeps references to windows and cleans them up when
|
||||
@ -43,6 +45,22 @@ class PrimaryWindowManager {
|
||||
|
||||
init(ghostty: Ghostty.AppState) {
|
||||
self.ghostty = ghostty
|
||||
|
||||
// Register self as observer for the NewTab notification that
|
||||
// is triggered via callback from Zig code.
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(onNewTab),
|
||||
name: Ghostty.Notification.ghosttyNewTab,
|
||||
object: nil)
|
||||
}
|
||||
|
||||
deinit {
|
||||
// Clean up the observer.
|
||||
NotificationCenter.default.removeObserver(
|
||||
self,
|
||||
name: Ghostty.Notification.ghosttyNewTab,
|
||||
object: nil)
|
||||
}
|
||||
|
||||
/// Add the initial window for the application. This should only be called once from the AppDelegate.
|
||||
@ -61,16 +79,41 @@ class PrimaryWindowManager {
|
||||
newWindow.makeKeyAndOrderFront(nil)
|
||||
}
|
||||
|
||||
func addNewTab(to window: NSWindow) {
|
||||
guard let controller = createWindowController() else { return }
|
||||
// triggerNewTab tells the Zig core code to create a new tab, which then calls
|
||||
// back into Swift code.
|
||||
func triggerNewTab(for window: PrimaryWindow) {
|
||||
guard let surface = window.focusedSurfaceWrapper.surface else { return }
|
||||
ghostty.newTab(surface: surface)
|
||||
}
|
||||
|
||||
func newTab() {
|
||||
if let window = mainWindow as? PrimaryWindow {
|
||||
self.triggerNewTab(for: window)
|
||||
} else {
|
||||
self.addNewWindow()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func onNewTab(notification: SwiftUI.Notification) {
|
||||
guard let surfaceView = notification.object as? Ghostty.SurfaceView else { return }
|
||||
guard let window = surfaceView.window else { return }
|
||||
|
||||
let configAny = notification.userInfo?[Ghostty.Notification.NewTabKey]
|
||||
let config = configAny as? ghostty_surface_config_s
|
||||
|
||||
self.addNewTab(to: window, withBaseConfig: config)
|
||||
}
|
||||
|
||||
private func addNewTab(to window: NSWindow, withBaseConfig config: ghostty_surface_config_s? = nil) {
|
||||
guard let controller = createWindowController(withBaseConfig: config) else { return }
|
||||
guard let newWindow = addManagedWindow(windowController: controller)?.window else { return }
|
||||
window.addTabbedWindow(newWindow, ordered: .above)
|
||||
newWindow.makeKeyAndOrderFront(nil)
|
||||
}
|
||||
|
||||
private func createWindowController() -> PrimaryWindowController? {
|
||||
private func createWindowController(withBaseConfig config: ghostty_surface_config_s? = nil) -> PrimaryWindowController? {
|
||||
guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else { return nil }
|
||||
let window = PrimaryWindow.create(ghostty: ghostty, appDelegate: appDelegate)
|
||||
let window = PrimaryWindow.create(ghostty: ghostty, appDelegate: appDelegate, baseConfig: config)
|
||||
Self.lastCascadePoint = window.cascadeTopLeft(from: Self.lastCascadePoint)
|
||||
let controller = PrimaryWindowController(window: window)
|
||||
controller.windowManager = self
|
||||
|
@ -61,6 +61,7 @@ extension Ghostty {
|
||||
read_clipboard_cb: { userdata, loc in AppState.readClipboard(userdata, location: loc) },
|
||||
write_clipboard_cb: { userdata, str, loc in AppState.writeClipboard(userdata, string: str, location: loc) },
|
||||
new_split_cb: { userdata, direction in AppState.newSplit(userdata, direction: direction) },
|
||||
new_tab_cb: { userdata, surfaceConfig in AppState.newTab(userdata, config: surfaceConfig) },
|
||||
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) },
|
||||
@ -137,6 +138,10 @@ extension Ghostty {
|
||||
ghostty_surface_request_close(surface)
|
||||
}
|
||||
|
||||
func newTab(surface: ghostty_surface_t) {
|
||||
ghostty_surface_binding_action(surface, GHOSTTY_BINDING_NEW_TAB, nil)
|
||||
}
|
||||
|
||||
func split(surface: ghostty_surface_t, direction: ghostty_split_direction_e) {
|
||||
ghostty_surface_split(surface, direction)
|
||||
}
|
||||
@ -258,6 +263,19 @@ extension Ghostty {
|
||||
)
|
||||
}
|
||||
|
||||
static func newTab(_ userdata: UnsafeMutableRawPointer?, config: ghostty_surface_config_s) {
|
||||
guard let surface = self.surfaceUserdata(from: userdata) else { return }
|
||||
|
||||
var userInfo: [AnyHashable : Any] = [:];
|
||||
userInfo[Notification.NewTabKey] = config;
|
||||
|
||||
NotificationCenter.default.post(
|
||||
name: Notification.ghosttyNewTab,
|
||||
object: surface,
|
||||
userInfo: userInfo
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns the GhosttyState from the given userdata value.
|
||||
static private func appState(fromSurface userdata: UnsafeMutableRawPointer?) -> AppState? {
|
||||
let surfaceView = Unmanaged<SurfaceView>.fromOpaque(userdata!).takeUnretainedValue()
|
||||
|
@ -8,10 +8,11 @@ extension Ghostty {
|
||||
struct TerminalSplit: View {
|
||||
@Environment(\.ghosttyApp) private var app
|
||||
let onClose: (() -> Void)?
|
||||
let baseConfig: ghostty_surface_config_s?
|
||||
|
||||
var body: some View {
|
||||
if let app = app {
|
||||
TerminalSplitRoot(app: app, onClose: onClose)
|
||||
TerminalSplitRoot(app: app, onClose: onClose, baseConfig: baseConfig)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -67,9 +68,9 @@ extension Ghostty {
|
||||
@Published var surface: SurfaceView
|
||||
|
||||
/// Initialize a new leaf which creates a new terminal surface.
|
||||
init(_ app: ghostty_app_t) {
|
||||
init(_ app: ghostty_app_t, _ baseConfig: ghostty_surface_config_s?) {
|
||||
self.app = app
|
||||
self.surface = SurfaceView(app)
|
||||
self.surface = SurfaceView(app, baseConfig)
|
||||
}
|
||||
}
|
||||
|
||||
@ -87,7 +88,7 @@ extension Ghostty {
|
||||
// Initially, both topLeft and bottomRight are in the "nosplit"
|
||||
// state since this is a new split.
|
||||
self.topLeft = .noSplit(from)
|
||||
self.bottomRight = .noSplit(.init(app))
|
||||
self.bottomRight = .noSplit(.init(app, nil))
|
||||
}
|
||||
}
|
||||
|
||||
@ -141,12 +142,14 @@ extension Ghostty {
|
||||
@State private var node: SplitNode
|
||||
@State private var requestClose: Bool = false
|
||||
let onClose: (() -> Void)?
|
||||
let baseConfig: ghostty_surface_config_s?
|
||||
|
||||
@FocusedValue(\.ghosttySurfaceTitle) private var surfaceTitle: String?
|
||||
|
||||
init(app: ghostty_app_t, onClose: (() ->Void)? = nil) {
|
||||
init(app: ghostty_app_t, onClose: (() ->Void)? = nil, baseConfig: ghostty_surface_config_s? = nil) {
|
||||
self.onClose = onClose
|
||||
_node = State(wrappedValue: SplitNode.noSplit(.init(app)))
|
||||
self.baseConfig = baseConfig
|
||||
_node = State(wrappedValue: SplitNode.noSplit(.init(app, baseConfig)))
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
|
@ -78,6 +78,10 @@ extension Ghostty.Notification {
|
||||
/// Goto tab. Has tab index in the userinfo.
|
||||
static let ghosttyGotoTab = Notification.Name("com.mitchellh.ghostty.gotoTab")
|
||||
static let GotoTabKey = ghosttyGotoTab.rawValue
|
||||
|
||||
/// New tab. Has base surface config requestesd in userinfo.
|
||||
static let ghosttyNewTab = Notification.Name("com.mitchellh.ghostty.newTab")
|
||||
static let NewTabKey = ghosttyNewTab.rawValue
|
||||
|
||||
/// Toggle fullscreen of current window
|
||||
static let ghosttyToggleFullscreen = Notification.Name("com.mitchellh.ghostty.toggleFullscreen")
|
||||
|
@ -24,7 +24,7 @@ extension Ghostty {
|
||||
@StateObject private var surfaceView: SurfaceView
|
||||
|
||||
init(_ app: ghostty_app_t, @ViewBuilder content: @escaping ((SurfaceView) -> Content)) {
|
||||
_surfaceView = StateObject(wrappedValue: SurfaceView(app))
|
||||
_surfaceView = StateObject(wrappedValue: SurfaceView(app, nil))
|
||||
self.content = content
|
||||
}
|
||||
|
||||
@ -137,7 +137,7 @@ extension Ghostty {
|
||||
// so we'll use that to tell ghostty to refresh.
|
||||
override var wantsUpdateLayer: Bool { return true }
|
||||
|
||||
init(_ app: ghostty_app_t) {
|
||||
init(_ app: ghostty_app_t, _ baseConfig: ghostty_surface_config_s?) {
|
||||
self.markedText = NSMutableAttributedString()
|
||||
|
||||
// Initialize with some default frame size. The important thing is that this
|
||||
@ -146,10 +146,11 @@ extension Ghostty {
|
||||
super.init(frame: NSMakeRect(0, 0, 800, 600))
|
||||
|
||||
// Setup our surface. This will also initialize all the terminal IO.
|
||||
var surface_cfg = ghostty_surface_config_s(
|
||||
userdata: Unmanaged.passUnretained(self).toOpaque(),
|
||||
nsview: Unmanaged.passUnretained(self).toOpaque(),
|
||||
scale_factor: NSScreen.main!.backingScaleFactor)
|
||||
var surface_cfg = baseConfig ?? ghostty_surface_config_new()
|
||||
surface_cfg.userdata = Unmanaged.passUnretained(self).toOpaque()
|
||||
surface_cfg.nsview = Unmanaged.passUnretained(self).toOpaque()
|
||||
surface_cfg.scale_factor = NSScreen.main!.backingScaleFactor
|
||||
|
||||
guard let surface = ghostty_surface_new(app, &surface_cfg) else {
|
||||
self.error = AppError.surfaceCreateError
|
||||
return
|
||||
|
@ -59,6 +59,9 @@ pub const App = struct {
|
||||
/// views then this can be null.
|
||||
new_split: ?*const fn (SurfaceUD, input.SplitDirection) callconv(.C) void = null,
|
||||
|
||||
/// New tab with options.
|
||||
new_tab: ?*const fn (SurfaceUD, apprt.Surface.Options) callconv(.C) void = null,
|
||||
|
||||
/// Close the current surface given by this function.
|
||||
close_surface: ?*const fn (SurfaceUD, bool) callconv(.C) void = null,
|
||||
|
||||
@ -162,17 +165,23 @@ pub const Surface = struct {
|
||||
userdata: ?*anyopaque = null,
|
||||
|
||||
/// The pointer to the backing NSView for the surface.
|
||||
nsview: *anyopaque = undefined,
|
||||
nsview: ?*anyopaque = null,
|
||||
|
||||
/// The scale factor of the screen.
|
||||
scale_factor: f64 = 1,
|
||||
|
||||
/// The font size to inherit. If 0, default font size will be used.
|
||||
font_size: u16 = 0,
|
||||
};
|
||||
|
||||
pub fn init(self: *Surface, app: *App, opts: Options) !void {
|
||||
const nsview = objc.Object.fromId(opts.nsview orelse
|
||||
return error.NSViewMustBeSet);
|
||||
|
||||
self.* = .{
|
||||
.app = app,
|
||||
.core_surface = undefined,
|
||||
.nsview = objc.Object.fromId(opts.nsview),
|
||||
.nsview = nsview,
|
||||
.content_scale = .{
|
||||
.x = @floatCast(opts.scale_factor),
|
||||
.y = @floatCast(opts.scale_factor),
|
||||
@ -201,6 +210,13 @@ pub const Surface = struct {
|
||||
self,
|
||||
);
|
||||
errdefer self.core_surface.deinit();
|
||||
|
||||
// If our options requested a specific font-size, set that.
|
||||
if (opts.font_size != 0) {
|
||||
var font_size = self.core_surface.font_size;
|
||||
font_size.points = opts.font_size;
|
||||
self.core_surface.setFontSize(font_size);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Surface) void {
|
||||
@ -593,6 +609,22 @@ pub const Surface = struct {
|
||||
func(self.opts.userdata, nonNativeFullscreen);
|
||||
}
|
||||
|
||||
pub fn newTab(self: *const Surface) !void {
|
||||
const func = self.app.opts.new_tab orelse {
|
||||
log.info("runtime embedder does not support new_tab", .{});
|
||||
return;
|
||||
};
|
||||
|
||||
const font_size: u16 = font_size: {
|
||||
if (!self.app.config.@"window-inherit-font-size") break :font_size 0;
|
||||
break :font_size self.core_surface.font_size.points;
|
||||
};
|
||||
|
||||
func(self.opts.userdata, .{
|
||||
.font_size = font_size,
|
||||
});
|
||||
}
|
||||
|
||||
/// The cursor position from the host directly is in screen coordinates but
|
||||
/// all our interface works in pixels.
|
||||
fn cursorPosToPixels(self: *const Surface, pos: apprt.CursorPos) !apprt.CursorPos {
|
||||
@ -662,6 +694,11 @@ pub const CAPI = struct {
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns initial surface options.
|
||||
export fn ghostty_surface_config_new() apprt.Surface.Options {
|
||||
return .{};
|
||||
}
|
||||
|
||||
/// Create a new surface as part of an app.
|
||||
export fn ghostty_surface_new(
|
||||
app: *App,
|
||||
@ -817,6 +854,7 @@ pub const CAPI = struct {
|
||||
const action: input.Binding.Action = switch (key) {
|
||||
.copy_to_clipboard => .{ .copy_to_clipboard = {} },
|
||||
.paste_from_clipboard => .{ .paste_from_clipboard = {} },
|
||||
.new_tab => .{ .new_tab = {} },
|
||||
};
|
||||
|
||||
ptr.core_surface.performBindingAction(action) catch |err| {
|
||||
|
@ -276,6 +276,7 @@ pub const Action = union(enum) {
|
||||
pub const Key = enum(c_int) {
|
||||
copy_to_clipboard,
|
||||
paste_from_clipboard,
|
||||
new_tab,
|
||||
};
|
||||
|
||||
/// Trigger is the associated key state that can trigger an action.
|
||||
|
Reference in New Issue
Block a user