Merge pull request #311 from mitchellh/mrn/macos-window-inherit-font-size

macOS: new windows/split can inherit font size
This commit is contained in:
Mitchell Hashimoto
2023-08-19 11:05:32 -07:00
committed by GitHub
8 changed files with 117 additions and 25 deletions

View File

@ -234,6 +234,7 @@ typedef enum {
GHOSTTY_BINDING_COPY_TO_CLIPBOARD, GHOSTTY_BINDING_COPY_TO_CLIPBOARD,
GHOSTTY_BINDING_PASTE_FROM_CLIPBOARD, GHOSTTY_BINDING_PASTE_FROM_CLIPBOARD,
GHOSTTY_BINDING_NEW_TAB, GHOSTTY_BINDING_NEW_TAB,
GHOSTTY_BINDING_NEW_WINDOW,
} ghostty_binding_action_e; } ghostty_binding_action_e;
// Fully defined types. This MUST be kept in sync with equivalent Zig // Fully defined types. This MUST be kept in sync with equivalent Zig
@ -251,8 +252,9 @@ typedef const ghostty_config_t (*ghostty_runtime_reload_config_cb)(void *);
typedef void (*ghostty_runtime_set_title_cb)(void *, const char *); typedef void (*ghostty_runtime_set_title_cb)(void *, const char *);
typedef const char* (*ghostty_runtime_read_clipboard_cb)(void *, ghostty_clipboard_e); 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_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_split_cb)(void *, ghostty_split_direction_e, ghostty_surface_config_s);
typedef void (*ghostty_runtime_new_tab_cb)(void *, ghostty_surface_config_s); typedef void (*ghostty_runtime_new_tab_cb)(void *, ghostty_surface_config_s);
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_close_surface_cb)(void *, bool);
typedef void (*ghostty_runtime_focus_split_cb)(void *, ghostty_split_focus_direction_e); 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_goto_tab_cb)(void *, int32_t);
@ -268,6 +270,7 @@ typedef struct {
ghostty_runtime_write_clipboard_cb write_clipboard_cb; ghostty_runtime_write_clipboard_cb write_clipboard_cb;
ghostty_runtime_new_split_cb new_split_cb; ghostty_runtime_new_split_cb new_split_cb;
ghostty_runtime_new_tab_cb new_tab_cb; ghostty_runtime_new_tab_cb new_tab_cb;
ghostty_runtime_new_window_cb new_window_cb;
ghostty_runtime_close_surface_cb close_surface_cb; ghostty_runtime_close_surface_cb close_surface_cb;
ghostty_runtime_focus_split_cb focus_split_cb; ghostty_runtime_focus_split_cb focus_split_cb;
ghostty_runtime_goto_tab_cb goto_tab_cb; ghostty_runtime_goto_tab_cb goto_tab_cb;

View File

@ -77,7 +77,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
} }
@IBAction func newWindow(_ sender: Any?) { @IBAction func newWindow(_ sender: Any?) {
windowManager.addNewWindow() windowManager.newWindow()
} }
@IBAction func newTab(_ sender: Any?) { @IBAction func newTab(_ sender: Any?) {

View File

@ -46,21 +46,32 @@ class PrimaryWindowManager {
init(ghostty: Ghostty.AppState) { init(ghostty: Ghostty.AppState) {
self.ghostty = ghostty self.ghostty = ghostty
// Register self as observer for the NewTab notification that // Register self as observer for the NewTab/NewWindow notifications that
// is triggered via callback from Zig code. // are triggered via callback from Zig code.
NotificationCenter.default.addObserver( let center = NotificationCenter.default;
center.addObserver(
self, self,
selector: #selector(onNewTab), selector: #selector(onNewTab),
name: Ghostty.Notification.ghosttyNewTab, name: Ghostty.Notification.ghosttyNewTab,
object: nil) object: nil)
center.addObserver(
self,
selector: #selector(onNewWindow),
name: Ghostty.Notification.ghosttyNewWindow,
object: nil)
} }
deinit { deinit {
// Clean up the observer. // Clean up the observers.
NotificationCenter.default.removeObserver( let center = NotificationCenter.default;
center.removeObserver(
self, self,
name: Ghostty.Notification.ghosttyNewTab, name: Ghostty.Notification.ghosttyNewTab,
object: nil) object: nil)
center.removeObserver(
self,
name: Ghostty.Notification.ghosttyNewWindow,
object: nil)
} }
/// Add the initial window for the application. This should only be called once from the AppDelegate. /// Add the initial window for the application. This should only be called once from the AppDelegate.
@ -73,12 +84,33 @@ class PrimaryWindowManager {
} }
} }
func addNewWindow() { func newWindow() {
guard let controller = createWindowController() else { return } if let window = mainWindow as? PrimaryWindow {
// If we already have a window, we go through Zig core code, which calls back into Swift.
self.triggerNewWindow(withParent: window)
} else {
self.addNewWindow()
}
}
func triggerNewWindow(withParent window: PrimaryWindow) {
guard let surface = window.focusedSurfaceWrapper.surface else { return }
ghostty.newWindow(surface: surface)
}
func addNewWindow(withBaseConfig config: ghostty_surface_config_s? = nil) {
guard let controller = createWindowController(withBaseConfig: config) else { return }
guard let newWindow = addManagedWindow(windowController: controller)?.window else { return } guard let newWindow = addManagedWindow(windowController: controller)?.window else { return }
newWindow.makeKeyAndOrderFront(nil) newWindow.makeKeyAndOrderFront(nil)
} }
@objc private func onNewWindow(notification: SwiftUI.Notification) {
let configAny = notification.userInfo?[Ghostty.Notification.NewSurfaceConfigKey]
let config = configAny as? ghostty_surface_config_s
self.addNewWindow(withBaseConfig: config)
}
// triggerNewTab tells the Zig core code to create a new tab, which then calls // triggerNewTab tells the Zig core code to create a new tab, which then calls
// back into Swift code. // back into Swift code.
func triggerNewTab(for window: PrimaryWindow) { func triggerNewTab(for window: PrimaryWindow) {
@ -98,7 +130,7 @@ class PrimaryWindowManager {
guard let surfaceView = notification.object as? Ghostty.SurfaceView else { return } guard let surfaceView = notification.object as? Ghostty.SurfaceView else { return }
guard let window = surfaceView.window else { return } guard let window = surfaceView.window else { return }
let configAny = notification.userInfo?[Ghostty.Notification.NewTabKey] let configAny = notification.userInfo?[Ghostty.Notification.NewSurfaceConfigKey]
let config = configAny as? ghostty_surface_config_s let config = configAny as? ghostty_surface_config_s
self.addNewTab(to: window, withBaseConfig: config) self.addNewTab(to: window, withBaseConfig: config)

View File

@ -60,8 +60,9 @@ extension Ghostty {
set_title_cb: { userdata, title in AppState.setTitle(userdata, title: title) }, set_title_cb: { userdata, title in AppState.setTitle(userdata, title: title) },
read_clipboard_cb: { userdata, loc in AppState.readClipboard(userdata, location: loc) }, 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) }, 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_split_cb: { userdata, direction, surfaceConfig in AppState.newSplit(userdata, direction: direction, config: surfaceConfig) },
new_tab_cb: { userdata, surfaceConfig in AppState.newTab(userdata, config: surfaceConfig) }, new_tab_cb: { userdata, surfaceConfig in AppState.newTab(userdata, config: surfaceConfig) },
new_window_cb: { userdata, surfaceConfig in AppState.newWindow(userdata, config: surfaceConfig) },
close_surface_cb: { userdata, processAlive in AppState.closeSurface(userdata, processAlive: processAlive) }, close_surface_cb: { userdata, processAlive in AppState.closeSurface(userdata, processAlive: processAlive) },
focus_split_cb: { userdata, direction in AppState.focusSplit(userdata, direction: direction) }, focus_split_cb: { userdata, direction in AppState.focusSplit(userdata, direction: direction) },
goto_tab_cb: { userdata, n in AppState.gotoTab(userdata, n: n) }, goto_tab_cb: { userdata, n in AppState.gotoTab(userdata, n: n) },
@ -142,6 +143,10 @@ extension Ghostty {
ghostty_surface_binding_action(surface, GHOSTTY_BINDING_NEW_TAB, nil) ghostty_surface_binding_action(surface, GHOSTTY_BINDING_NEW_TAB, nil)
} }
func newWindow(surface: ghostty_surface_t) {
ghostty_surface_binding_action(surface, GHOSTTY_BINDING_NEW_WINDOW, nil)
}
func split(surface: ghostty_surface_t, direction: ghostty_split_direction_e) { func split(surface: ghostty_surface_t, direction: ghostty_split_direction_e) {
ghostty_surface_split(surface, direction) ghostty_surface_split(surface, direction)
} }
@ -159,10 +164,11 @@ extension Ghostty {
// MARK: Ghostty Callbacks // MARK: Ghostty Callbacks
static func newSplit(_ userdata: UnsafeMutableRawPointer?, direction: ghostty_split_direction_e) { static func newSplit(_ userdata: UnsafeMutableRawPointer?, direction: ghostty_split_direction_e, config: ghostty_surface_config_s) {
guard let surface = self.surfaceUserdata(from: userdata) else { return } guard let surface = self.surfaceUserdata(from: userdata) else { return }
NotificationCenter.default.post(name: Notification.ghosttyNewSplit, object: surface, userInfo: [ NotificationCenter.default.post(name: Notification.ghosttyNewSplit, object: surface, userInfo: [
"direction": direction, "direction": direction,
Notification.NewSurfaceConfigKey: config,
]) ])
} }
@ -266,13 +272,24 @@ extension Ghostty {
static func newTab(_ userdata: UnsafeMutableRawPointer?, config: ghostty_surface_config_s) { static func newTab(_ userdata: UnsafeMutableRawPointer?, config: ghostty_surface_config_s) {
guard let surface = self.surfaceUserdata(from: userdata) else { return } guard let surface = self.surfaceUserdata(from: userdata) else { return }
var userInfo: [AnyHashable : Any] = [:];
userInfo[Notification.NewTabKey] = config;
NotificationCenter.default.post( NotificationCenter.default.post(
name: Notification.ghosttyNewTab, name: Notification.ghosttyNewTab,
object: surface, object: surface,
userInfo: userInfo userInfo: [
Notification.NewSurfaceConfigKey: config
]
)
}
static func newWindow(_ userdata: UnsafeMutableRawPointer?, config: ghostty_surface_config_s) {
guard let surface = self.surfaceUserdata(from: userdata) else { return }
NotificationCenter.default.post(
name: Notification.ghosttyNewWindow,
object: surface,
userInfo: [
Notification.NewSurfaceConfigKey: config
]
) )
} }

View File

@ -82,13 +82,13 @@ extension Ghostty {
/// A container is always initialized from some prior leaf because a split has to originate /// A container is always initialized from some prior leaf because a split has to originate
/// from a non-split value. When initializing, we inherit the leaf's surface and then /// from a non-split value. When initializing, we inherit the leaf's surface and then
/// initialize a new surface for the new pane. /// initialize a new surface for the new pane.
init(from: Leaf) { init(from: Leaf, baseConfig: ghostty_surface_config_s? = nil) {
self.app = from.app self.app = from.app
// Initially, both topLeft and bottomRight are in the "nosplit" // Initially, both topLeft and bottomRight are in the "nosplit"
// state since this is a new split. // state since this is a new split.
self.topLeft = .noSplit(from) self.topLeft = .noSplit(from)
self.bottomRight = .noSplit(.init(app, nil)) self.bottomRight = .noSplit(.init(app, baseConfig))
} }
} }
@ -254,6 +254,9 @@ extension Ghostty {
} }
private func onNewSplit(notification: SwiftUI.Notification) { private func onNewSplit(notification: SwiftUI.Notification) {
let configAny = notification.userInfo?[Ghostty.Notification.NewSurfaceConfigKey]
let config = configAny as? ghostty_surface_config_s
// Determine our desired direction // Determine our desired direction
guard let directionAny = notification.userInfo?["direction"] else { return } guard let directionAny = notification.userInfo?["direction"] else { return }
guard let direction = directionAny as? ghostty_split_direction_e else { return } guard let direction = directionAny as? ghostty_split_direction_e else { return }
@ -270,7 +273,7 @@ extension Ghostty {
} }
// Setup our new container since we are now split // Setup our new container since we are now split
let container = SplitNode.Container(from: leaf) let container = SplitNode.Container(from: leaf, baseConfig: config)
// Depending on the direction, change the parent node. This will trigger // Depending on the direction, change the parent node. This will trigger
// the parent to relayout our views. // the parent to relayout our views.

View File

@ -64,6 +64,9 @@ extension Ghostty {
} }
extension Ghostty.Notification { extension Ghostty.Notification {
/// Used to pass a configuration along when creating a new tab/window/split.
static let NewSurfaceConfigKey = "com.mitchellh.ghostty.newSurfaceConfig"
/// Posted when a new split is requested. The sending object will be the surface that had focus. The /// Posted when a new split is requested. The sending object will be the surface that had focus. The
/// userdata has one key "direction" with the direction to split to. /// userdata has one key "direction" with the direction to split to.
static let ghosttyNewSplit = Notification.Name("com.mitchellh.ghostty.newSplit") static let ghosttyNewSplit = Notification.Name("com.mitchellh.ghostty.newSplit")
@ -79,9 +82,11 @@ extension Ghostty.Notification {
static let ghosttyGotoTab = Notification.Name("com.mitchellh.ghostty.gotoTab") static let ghosttyGotoTab = Notification.Name("com.mitchellh.ghostty.gotoTab")
static let GotoTabKey = ghosttyGotoTab.rawValue static let GotoTabKey = ghosttyGotoTab.rawValue
/// New tab. Has base surface config requestesd in userinfo. /// New tab. Has base surface config requested in userinfo.
static let ghosttyNewTab = Notification.Name("com.mitchellh.ghostty.newTab") static let ghosttyNewTab = Notification.Name("com.mitchellh.ghostty.newTab")
static let NewTabKey = ghosttyNewTab.rawValue
/// New window. Has base surface config requested in userinfo.
static let ghosttyNewWindow = Notification.Name("com.mitchellh.ghostty.newWindow")
/// Toggle fullscreen of current window /// Toggle fullscreen of current window
static let ghosttyToggleFullscreen = Notification.Name("com.mitchellh.ghostty.toggleFullscreen") static let ghosttyToggleFullscreen = Notification.Name("com.mitchellh.ghostty.toggleFullscreen")

View File

@ -57,11 +57,14 @@ pub const App = struct {
/// Create a new split view. If the embedder doesn't support split /// Create a new split view. If the embedder doesn't support split
/// views then this can be null. /// views then this can be null.
new_split: ?*const fn (SurfaceUD, input.SplitDirection) callconv(.C) void = null, new_split: ?*const fn (SurfaceUD, input.SplitDirection, apprt.Surface.Options) callconv(.C) void = null,
/// New tab with options. /// New tab with options.
new_tab: ?*const fn (SurfaceUD, apprt.Surface.Options) callconv(.C) void = null, new_tab: ?*const fn (SurfaceUD, apprt.Surface.Options) callconv(.C) void = null,
/// New window with options.
new_window: ?*const fn (SurfaceUD, apprt.Surface.Options) callconv(.C) void = null,
/// Close the current surface given by this function. /// Close the current surface given by this function.
close_surface: ?*const fn (SurfaceUD, bool) callconv(.C) void = null, close_surface: ?*const fn (SurfaceUD, bool) callconv(.C) void = null,
@ -148,6 +151,17 @@ pub const App = struct {
_ = surface; _ = surface;
// No-op, we use a threaded interface so we're constantly drawing. // No-op, we use a threaded interface so we're constantly drawing.
} }
pub fn newWindow(self: *App, parent: ?*CoreSurface) !void {
_ = self;
// Right now we only support creating a new window with a parent
// through this code.
// The other case is handled by the embedding runtime.
if (parent) |surface| {
try surface.rt_surface.newWindow();
}
}
}; };
pub const Surface = struct { pub const Surface = struct {
@ -233,7 +247,8 @@ pub const Surface = struct {
return; return;
}; };
func(self.opts.userdata, direction); const options = self.newSurfaceOptions();
func(self.opts.userdata, direction, options);
} }
pub fn close(self: *const Surface, process_alive: bool) void { pub fn close(self: *const Surface, process_alive: bool) void {
@ -615,14 +630,29 @@ pub const Surface = struct {
return; return;
}; };
const options = self.newSurfaceOptions();
func(self.opts.userdata, options);
}
pub fn newWindow(self: *const Surface) !void {
const func = self.app.opts.new_window orelse {
log.info("runtime embedder does not support new_window", .{});
return;
};
const options = self.newSurfaceOptions();
func(self.opts.userdata, options);
}
fn newSurfaceOptions(self: *const Surface) apprt.Surface.Options {
const font_size: u16 = font_size: { const font_size: u16 = font_size: {
if (!self.app.config.@"window-inherit-font-size") break :font_size 0; if (!self.app.config.@"window-inherit-font-size") break :font_size 0;
break :font_size self.core_surface.font_size.points; break :font_size self.core_surface.font_size.points;
}; };
func(self.opts.userdata, .{ return .{
.font_size = font_size, .font_size = font_size,
}); };
} }
/// The cursor position from the host directly is in screen coordinates but /// The cursor position from the host directly is in screen coordinates but
@ -855,6 +885,7 @@ pub const CAPI = struct {
.copy_to_clipboard => .{ .copy_to_clipboard = {} }, .copy_to_clipboard => .{ .copy_to_clipboard = {} },
.paste_from_clipboard => .{ .paste_from_clipboard = {} }, .paste_from_clipboard => .{ .paste_from_clipboard = {} },
.new_tab => .{ .new_tab = {} }, .new_tab => .{ .new_tab = {} },
.new_window => .{ .new_window = {} },
}; };
ptr.core_surface.performBindingAction(action) catch |err| { ptr.core_surface.performBindingAction(action) catch |err| {

View File

@ -277,6 +277,7 @@ pub const Key = enum(c_int) {
copy_to_clipboard, copy_to_clipboard,
paste_from_clipboard, paste_from_clipboard,
new_tab, new_tab,
new_window,
}; };
/// Trigger is the associated key state that can trigger an action. /// Trigger is the associated key state that can trigger an action.