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 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,
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
// 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 {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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());
|
||||||
|
@ -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,
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user