From fa9ee0815f124b61a0589677ecb793e2492b7a26 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 8 Mar 2023 14:56:50 -0800 Subject: [PATCH] apprt/embedded: newSplit callback --- include/ghostty.h | 7 ++ macos/Sources/Ghostty/AppState.swift | 18 ++++- macos/Sources/Ghostty/Ghostty.SplitView.swift | 72 +++++++++---------- macos/Sources/Ghostty/Package.swift | 5 +- macos/Sources/Ghostty/SurfaceView.swift | 20 +++++- src/apprt/embedded.zig | 13 ++++ src/input.zig | 1 + src/input/Binding.zig | 4 +- 8 files changed, 99 insertions(+), 41 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index 3302db522..b93f44ea4 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -30,6 +30,7 @@ typedef void (*ghostty_runtime_wakeup_cb)(void *); typedef void (*ghostty_runtime_set_title_cb)(void *, const char *); typedef const char* (*ghostty_runtime_read_clipboard_cb)(void *); typedef void (*ghostty_runtime_write_clipboard_cb)(void *, const char *); +typedef void (*ghostty_runtime_new_split_cb)(void *, ghostty_split_direction_e); typedef struct { void *userdata; @@ -37,6 +38,7 @@ typedef struct { ghostty_runtime_set_title_cb set_title_cb; 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_config_s; typedef struct { @@ -45,6 +47,11 @@ typedef struct { double scale_factor; } ghostty_surface_config_s; +typedef enum { + GHOSTTY_SPLIT_RIGHT, + GHOSTTY_SPLIT_DOWN +} ghostty_split_direction_e; + typedef enum { GHOSTTY_MOUSE_RELEASE, GHOSTTY_MOUSE_PRESS, diff --git a/macos/Sources/Ghostty/AppState.swift b/macos/Sources/Ghostty/AppState.swift index bc2c8eaa7..2e40e3aef 100644 --- a/macos/Sources/Ghostty/AppState.swift +++ b/macos/Sources/Ghostty/AppState.swift @@ -56,7 +56,9 @@ extension Ghostty { wakeup_cb: { userdata in AppState.wakeup(userdata) }, set_title_cb: { userdata, title in AppState.setTitle(userdata, title: title) }, read_clipboard_cb: { userdata in AppState.readClipboard(userdata) }, - write_clipboard_cb: { userdata, str in AppState.writeClipboard(userdata, string: str) }) + write_clipboard_cb: { userdata, str in AppState.writeClipboard(userdata, string: str) }, + new_split_cb: { userdata, direction in AppState.newSplit(userdata, direction: ghostty_split_direction_e(UInt32(direction))) } + ) // Create the ghostty app. guard let app = ghostty_app_new(&runtime_cfg, cfg) else { @@ -81,6 +83,13 @@ extension Ghostty { // MARK: Ghostty Callbacks + static func newSplit(_ userdata: UnsafeMutableRawPointer?, direction: ghostty_split_direction_e) { + guard let surface = self.surfaceUserdata(from: userdata) else { return } + NotificationCenter.default.post(name: Notification.ghosttyNewSplit, object: surface, userInfo: [ + "direction": direction, + ]) + } + static func readClipboard(_ userdata: UnsafeMutableRawPointer?) -> UnsafePointer? { guard let appState = self.appState(fromSurface: userdata) else { return nil } guard let str = NSPasteboard.general.string(forType: .string) else { return nil } @@ -117,13 +126,18 @@ extension Ghostty { } /// Returns the GhosttyState from the given userdata value. - static func appState(fromSurface userdata: UnsafeMutableRawPointer?) -> AppState? { + static private func appState(fromSurface userdata: UnsafeMutableRawPointer?) -> AppState? { let surfaceView = Unmanaged.fromOpaque(userdata!).takeUnretainedValue() guard let surface = surfaceView.surface else { return nil } guard let app = ghostty_surface_app(surface) else { return nil } guard let app_ud = ghostty_app_userdata(app) else { return nil } return Unmanaged.fromOpaque(app_ud).takeUnretainedValue() } + + /// Returns the surface view from the userdata. + static private func surfaceUserdata(from userdata: UnsafeMutableRawPointer?) -> SurfaceView? { + return Unmanaged.fromOpaque(userdata!).takeUnretainedValue() + } } } diff --git a/macos/Sources/Ghostty/Ghostty.SplitView.swift b/macos/Sources/Ghostty/Ghostty.SplitView.swift index 3e6397f7e..edc580ba4 100644 --- a/macos/Sources/Ghostty/Ghostty.SplitView.swift +++ b/macos/Sources/Ghostty/Ghostty.SplitView.swift @@ -103,56 +103,56 @@ extension Ghostty { /// that should have it. private func fixFocus() { DispatchQueue.main.async { + // The view we want to focus + var view = state.topLeft + if let right = state.bottomRight { view = right } + // If the callback runs before the surface is attached to a view // then the window will be nil. We just reschedule in that case. - guard let window = state.topLeft.window else { + guard let window = view.window else { self.fixFocus() return } - window.makeFirstResponder(state.topLeft) + _ = state.topLeft.resignFirstResponder() + _ = state.bottomRight?.resignFirstResponder() + window.makeFirstResponder(view) + } + } + + private func onNewSplit(notification: SwiftUI.Notification) { + guard let directionAny = notification.userInfo?["direction"] else { return } + guard let direction = directionAny as? ghostty_split_direction_e else { return } + switch (direction) { + case GHOSTTY_SPLIT_RIGHT: + split(to: .horizontal) + + case GHOSTTY_SPLIT_DOWN: + split(to: .vertical) + + default: + break } } var body: some View { switch (state.direction) { case .none: - VStack { - HStack { - Button("Split Horizontal") { split(to: .horizontal) } - .keyboardShortcut("d", modifiers: .command) - Button("Split Vertical") { split(to: .vertical) } - .keyboardShortcut("d", modifiers: [.command, .shift]) - } - - SurfaceWrapper(surfaceView: state.topLeft) - } + let pub = NotificationCenter.default.publisher(for: Ghostty.Notification.ghosttyNewSplit, object: state.topLeft) + SurfaceWrapper(surfaceView: state.topLeft) + .onReceive(pub) { onNewSplit(notification: $0) } case .horizontal: - VStack { - HStack { - Button("Close Left") { closeTopLeft() } - Button("Close Right") { closeBottomRight() } - } - - SplitView(.horizontal, left: { - TerminalSplitChild(app, topLeft: state.topLeft) - }, right: { - TerminalSplitChild(app, topLeft: state.bottomRight!) - }) - } + SplitView(.horizontal, left: { + TerminalSplitChild(app, topLeft: state.topLeft) + }, right: { + TerminalSplitChild(app, topLeft: state.bottomRight!) + }) case .vertical: - VStack { - HStack { - Button("Close Top") { closeTopLeft() } - Button("Close Bottom") { closeBottomRight() } - } - - SplitView(.vertical, left: { - TerminalSplitChild(app, topLeft: state.topLeft) - }, right: { - TerminalSplitChild(app, topLeft: state.bottomRight!) - }) - } + SplitView(.vertical, left: { + TerminalSplitChild(app, topLeft: state.topLeft) + }, right: { + TerminalSplitChild(app, topLeft: state.bottomRight!) + }) } } } diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index f170e9eb6..b673549e5 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -1 +1,4 @@ -struct Ghostty {} +struct Ghostty { + // All the notifications that will be emitted will be put here. + struct Notification {} +} diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index 4c3def0c2..2d7584d69 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -107,7 +107,7 @@ extension Ghostty { // We need to support being a first responder so that we can get input events override var acceptsFirstResponder: Bool { return true } - // I don't thikn we need this but this lets us know we should redraw our layer + // I don't think we need this but this lets us know we should redraw our layer // so we'll use that to tell ghostty to refresh. override var wantsUpdateLayer: Bool { return true } @@ -161,6 +161,16 @@ extension Ghostty { ghostty_surface_set_size(surface, UInt32(scaledSize.width), UInt32(scaledSize.height)) } + override func resignFirstResponder() -> Bool { + let result = super.resignFirstResponder() + + // We sometimes call this manually (see SplitView) as a way to force us to + // yield our focus state. + if (result) { focusDidChange(false) } + + return result + } + override func updateTrackingAreas() { // To update our tracking area we just recreate it all. trackingAreas.forEach { removeTrackingArea($0) } @@ -494,6 +504,14 @@ extension Ghostty { } +// MARK: Surface Notifications + +extension Ghostty.Notification { + /// 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. + static let ghosttyNewSplit = Notification.Name("com.mitchellh.ghostty.newSplit") +} + // MARK: Surface Environment Keys private struct GhosttySurfaceViewKey: EnvironmentKey { diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 0a21504e7..946fad24f 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -44,6 +44,10 @@ pub const App = struct { /// Write the clipboard value. write_clipboard: *const fn (SurfaceUD, [*:0]const u8) callconv(.C) void, + + /// Create a new split view. If the embedder doesn't support split + /// views then this can be null. + new_split: ?*const fn (SurfaceUD, input.Binding.Action.SplitDirection) callconv(.C) void = null, }; core_app: *CoreApp, @@ -148,6 +152,15 @@ pub const Surface = struct { self.core_surface.deinit(); } + pub fn newSplit(self: *const Surface, direction: input.SplitDirection) !void { + const func = self.app.opts.new_split orelse { + log.info("runtime embedder does not support splits", .{}); + return; + }; + + func(self.opts.userdata, direction); + } + pub fn getContentScale(self: *const Surface) !apprt.ContentScale { return self.content_scale; } diff --git a/src/input.zig b/src/input.zig index da121890f..2bdcbe86a 100644 --- a/src/input.zig +++ b/src/input.zig @@ -3,6 +3,7 @@ const std = @import("std"); pub usingnamespace @import("input/mouse.zig"); pub usingnamespace @import("input/key.zig"); pub const Binding = @import("input/Binding.zig"); +pub const SplitDirection = Binding.Action.SplitDirection; test { std.testing.refAllDecls(@This()); diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 2fb9f0534..ecae7495b 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -195,7 +195,9 @@ pub const Action = union(enum) { application: []const u8, }; - pub const SplitDirection = enum { + // This is made extern (c_int) to make interop easier with our embedded + // runtime. The small size cost doesn't make a difference in our union. + pub const SplitDirection = enum(c_int) { right, down,