mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
apprt/embedded: newSplit callback
This commit is contained in:
@ -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,
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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!)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1 +1,4 @@
|
||||
struct Ghostty {}
|
||||
struct Ghostty {
|
||||
// All the notifications that will be emitted will be put here.
|
||||
struct Notification {}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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());
|
||||
|
@ -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,
|
||||
|
||||
|
Reference in New Issue
Block a user