apprt/embedded: newSplit callback

This commit is contained in:
Mitchell Hashimoto
2023-03-08 14:56:50 -08:00
parent 8ce6f349f8
commit fa9ee0815f
8 changed files with 99 additions and 41 deletions

View File

@ -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,

View File

@ -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<CChar>? {
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<SurfaceView>.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<AppState>.fromOpaque(app_ud).takeUnretainedValue()
}
/// Returns the surface view from the userdata.
static private func surfaceUserdata(from userdata: UnsafeMutableRawPointer?) -> SurfaceView? {
return Unmanaged<SurfaceView>.fromOpaque(userdata!).takeUnretainedValue()
}
}
}

View File

@ -103,50 +103,51 @@ 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])
}
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!)
})
}
case .vertical:
VStack {
HStack {
Button("Close Top") { closeTopLeft() }
Button("Close Bottom") { closeBottomRight() }
}
SplitView(.vertical, left: {
TerminalSplitChild(app, topLeft: state.topLeft)
}, right: {
@ -155,6 +156,5 @@ extension Ghostty {
}
}
}
}
}

View File

@ -1 +1,4 @@
struct Ghostty {}
struct Ghostty {
// All the notifications that will be emitted will be put here.
struct Notification {}
}

View File

@ -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 {

View File

@ -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;
}

View File

@ -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());

View File

@ -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,