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 void (*ghostty_runtime_set_title_cb)(void *, const char *);
typedef const char* (*ghostty_runtime_read_clipboard_cb)(void *); typedef const char* (*ghostty_runtime_read_clipboard_cb)(void *);
typedef void (*ghostty_runtime_write_clipboard_cb)(void *, const char *); typedef void (*ghostty_runtime_write_clipboard_cb)(void *, const char *);
typedef void (*ghostty_runtime_new_split_cb)(void *, ghostty_split_direction_e);
typedef struct { typedef struct {
void *userdata; void *userdata;
@ -37,6 +38,7 @@ typedef struct {
ghostty_runtime_set_title_cb set_title_cb; ghostty_runtime_set_title_cb set_title_cb;
ghostty_runtime_read_clipboard_cb read_clipboard_cb; ghostty_runtime_read_clipboard_cb read_clipboard_cb;
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_config_s; } ghostty_runtime_config_s;
typedef struct { typedef struct {
@ -45,6 +47,11 @@ typedef struct {
double scale_factor; double scale_factor;
} ghostty_surface_config_s; } ghostty_surface_config_s;
typedef enum {
GHOSTTY_SPLIT_RIGHT,
GHOSTTY_SPLIT_DOWN
} ghostty_split_direction_e;
typedef enum { typedef enum {
GHOSTTY_MOUSE_RELEASE, GHOSTTY_MOUSE_RELEASE,
GHOSTTY_MOUSE_PRESS, GHOSTTY_MOUSE_PRESS,

View File

@ -56,7 +56,9 @@ extension Ghostty {
wakeup_cb: { userdata in AppState.wakeup(userdata) }, wakeup_cb: { userdata in AppState.wakeup(userdata) },
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 in AppState.readClipboard(userdata) }, 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. // Create the ghostty app.
guard let app = ghostty_app_new(&runtime_cfg, cfg) else { guard let app = ghostty_app_new(&runtime_cfg, cfg) else {
@ -81,6 +83,13 @@ extension Ghostty {
// MARK: Ghostty Callbacks // 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>? { static func readClipboard(_ userdata: UnsafeMutableRawPointer?) -> UnsafePointer<CChar>? {
guard let appState = self.appState(fromSurface: userdata) else { return nil } guard let appState = self.appState(fromSurface: userdata) else { return nil }
guard let str = NSPasteboard.general.string(forType: .string) 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. /// 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() let surfaceView = Unmanaged<SurfaceView>.fromOpaque(userdata!).takeUnretainedValue()
guard let surface = surfaceView.surface else { return nil } guard let surface = surfaceView.surface else { return nil }
guard let app = ghostty_surface_app(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 } guard let app_ud = ghostty_app_userdata(app) else { return nil }
return Unmanaged<AppState>.fromOpaque(app_ud).takeUnretainedValue() 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. /// that should have it.
private func fixFocus() { private func fixFocus() {
DispatchQueue.main.async { 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 // If the callback runs before the surface is attached to a view
// then the window will be nil. We just reschedule in that case. // 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() self.fixFocus()
return 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 { var body: some View {
switch (state.direction) { switch (state.direction) {
case .none: case .none:
VStack { let pub = NotificationCenter.default.publisher(for: Ghostty.Notification.ghosttyNewSplit, object: state.topLeft)
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) SurfaceWrapper(surfaceView: state.topLeft)
} .onReceive(pub) { onNewSplit(notification: $0) }
case .horizontal: case .horizontal:
VStack {
HStack {
Button("Close Left") { closeTopLeft() }
Button("Close Right") { closeBottomRight() }
}
SplitView(.horizontal, left: { SplitView(.horizontal, left: {
TerminalSplitChild(app, topLeft: state.topLeft) TerminalSplitChild(app, topLeft: state.topLeft)
}, right: { }, right: {
TerminalSplitChild(app, topLeft: state.bottomRight!) TerminalSplitChild(app, topLeft: state.bottomRight!)
}) })
}
case .vertical: case .vertical:
VStack {
HStack {
Button("Close Top") { closeTopLeft() }
Button("Close Bottom") { closeBottomRight() }
}
SplitView(.vertical, left: { SplitView(.vertical, left: {
TerminalSplitChild(app, topLeft: state.topLeft) TerminalSplitChild(app, topLeft: state.topLeft)
}, right: { }, 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 // We need to support being a first responder so that we can get input events
override var acceptsFirstResponder: Bool { return true } 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. // so we'll use that to tell ghostty to refresh.
override var wantsUpdateLayer: Bool { return true } override var wantsUpdateLayer: Bool { return true }
@ -161,6 +161,16 @@ extension Ghostty {
ghostty_surface_set_size(surface, UInt32(scaledSize.width), UInt32(scaledSize.height)) 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() { override func updateTrackingAreas() {
// To update our tracking area we just recreate it all. // To update our tracking area we just recreate it all.
trackingAreas.forEach { removeTrackingArea($0) } 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 // MARK: Surface Environment Keys
private struct GhosttySurfaceViewKey: EnvironmentKey { private struct GhosttySurfaceViewKey: EnvironmentKey {

View File

@ -44,6 +44,10 @@ pub const App = struct {
/// Write the clipboard value. /// Write the clipboard value.
write_clipboard: *const fn (SurfaceUD, [*:0]const u8) callconv(.C) void, 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, core_app: *CoreApp,
@ -148,6 +152,15 @@ pub const Surface = struct {
self.core_surface.deinit(); 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 { pub fn getContentScale(self: *const Surface) !apprt.ContentScale {
return self.content_scale; return self.content_scale;
} }

View File

@ -3,6 +3,7 @@ const std = @import("std");
pub usingnamespace @import("input/mouse.zig"); pub usingnamespace @import("input/mouse.zig");
pub usingnamespace @import("input/key.zig"); pub usingnamespace @import("input/key.zig");
pub const Binding = @import("input/Binding.zig"); pub const Binding = @import("input/Binding.zig");
pub const SplitDirection = Binding.Action.SplitDirection;
test { test {
std.testing.refAllDecls(@This()); std.testing.refAllDecls(@This());

View File

@ -195,7 +195,9 @@ pub const Action = union(enum) {
application: []const u8, 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, right,
down, down,