mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
macos: support resizing splits
This commit is contained in:
@ -49,6 +49,13 @@ typedef enum {
|
|||||||
GHOSTTY_SPLIT_FOCUS_RIGHT,
|
GHOSTTY_SPLIT_FOCUS_RIGHT,
|
||||||
} ghostty_split_focus_direction_e;
|
} ghostty_split_focus_direction_e;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
GHOSTTY_SPLIT_RESIZE_UP,
|
||||||
|
GHOSTTY_SPLIT_RESIZE_DOWN,
|
||||||
|
GHOSTTY_SPLIT_RESIZE_LEFT,
|
||||||
|
GHOSTTY_SPLIT_RESIZE_RIGHT,
|
||||||
|
} ghostty_split_resize_direction_e;
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
GHOSTTY_INSPECTOR_TOGGLE,
|
GHOSTTY_INSPECTOR_TOGGLE,
|
||||||
GHOSTTY_INSPECTOR_SHOW,
|
GHOSTTY_INSPECTOR_SHOW,
|
||||||
@ -333,6 +340,7 @@ typedef void (*ghostty_runtime_new_window_cb)(void *, ghostty_surface_config_s);
|
|||||||
typedef void (*ghostty_runtime_control_inspector_cb)(void *, ghostty_inspector_mode_e);
|
typedef void (*ghostty_runtime_control_inspector_cb)(void *, ghostty_inspector_mode_e);
|
||||||
typedef void (*ghostty_runtime_close_surface_cb)(void *, bool);
|
typedef void (*ghostty_runtime_close_surface_cb)(void *, bool);
|
||||||
typedef void (*ghostty_runtime_focus_split_cb)(void *, ghostty_split_focus_direction_e);
|
typedef void (*ghostty_runtime_focus_split_cb)(void *, ghostty_split_focus_direction_e);
|
||||||
|
typedef void (*ghostty_runtime_resize_split_cb)(void *, ghostty_split_resize_direction_e, uint16_t);
|
||||||
typedef void (*ghostty_runtime_toggle_split_zoom_cb)(void *);
|
typedef void (*ghostty_runtime_toggle_split_zoom_cb)(void *);
|
||||||
typedef void (*ghostty_runtime_goto_tab_cb)(void *, int32_t);
|
typedef void (*ghostty_runtime_goto_tab_cb)(void *, int32_t);
|
||||||
typedef void (*ghostty_runtime_toggle_fullscreen_cb)(void *, ghostty_non_native_fullscreen_e);
|
typedef void (*ghostty_runtime_toggle_fullscreen_cb)(void *, ghostty_non_native_fullscreen_e);
|
||||||
@ -357,6 +365,7 @@ typedef struct {
|
|||||||
ghostty_runtime_control_inspector_cb control_inspector_cb;
|
ghostty_runtime_control_inspector_cb control_inspector_cb;
|
||||||
ghostty_runtime_close_surface_cb close_surface_cb;
|
ghostty_runtime_close_surface_cb close_surface_cb;
|
||||||
ghostty_runtime_focus_split_cb focus_split_cb;
|
ghostty_runtime_focus_split_cb focus_split_cb;
|
||||||
|
ghostty_runtime_resize_split_cb resize_split_cb;
|
||||||
ghostty_runtime_toggle_split_zoom_cb toggle_split_zoom_cb;
|
ghostty_runtime_toggle_split_zoom_cb toggle_split_zoom_cb;
|
||||||
ghostty_runtime_goto_tab_cb goto_tab_cb;
|
ghostty_runtime_goto_tab_cb goto_tab_cb;
|
||||||
ghostty_runtime_toggle_fullscreen_cb toggle_fullscreen_cb;
|
ghostty_runtime_toggle_fullscreen_cb toggle_fullscreen_cb;
|
||||||
@ -412,6 +421,7 @@ void ghostty_surface_ime_point(ghostty_surface_t, double *, double *);
|
|||||||
void ghostty_surface_request_close(ghostty_surface_t);
|
void ghostty_surface_request_close(ghostty_surface_t);
|
||||||
void ghostty_surface_split(ghostty_surface_t, ghostty_split_direction_e);
|
void ghostty_surface_split(ghostty_surface_t, ghostty_split_direction_e);
|
||||||
void ghostty_surface_split_focus(ghostty_surface_t, ghostty_split_focus_direction_e);
|
void ghostty_surface_split_focus(ghostty_surface_t, ghostty_split_focus_direction_e);
|
||||||
|
void ghostty_surface_split_resize(ghostty_surface_t, ghostty_split_resize_direction_e, uint16_t);
|
||||||
bool ghostty_surface_binding_action(ghostty_surface_t, const char *, uintptr_t);
|
bool ghostty_surface_binding_action(ghostty_surface_t, const char *, uintptr_t);
|
||||||
void ghostty_surface_complete_clipboard_request(ghostty_surface_t, const char *, void *, bool);
|
void ghostty_surface_complete_clipboard_request(ghostty_surface_t, const char *, void *, bool);
|
||||||
|
|
||||||
|
@ -38,6 +38,11 @@ class AppDelegate: NSObject, ObservableObject, NSApplicationDelegate, GhosttyApp
|
|||||||
@IBOutlet private var menuResetFontSize: NSMenuItem?
|
@IBOutlet private var menuResetFontSize: NSMenuItem?
|
||||||
@IBOutlet private var menuTerminalInspector: NSMenuItem?
|
@IBOutlet private var menuTerminalInspector: NSMenuItem?
|
||||||
|
|
||||||
|
@IBOutlet private var menuMoveSplitDividerUp: NSMenuItem?
|
||||||
|
@IBOutlet private var menuMoveSplitDividerDown: NSMenuItem?
|
||||||
|
@IBOutlet private var menuMoveSplitDividerLeft: NSMenuItem?
|
||||||
|
@IBOutlet private var menuMoveSplitDividerRight: NSMenuItem?
|
||||||
|
|
||||||
/// The dock menu
|
/// The dock menu
|
||||||
private var dockMenu: NSMenu = NSMenu()
|
private var dockMenu: NSMenu = NSMenu()
|
||||||
|
|
||||||
@ -206,6 +211,10 @@ class AppDelegate: NSObject, ObservableObject, NSApplicationDelegate, GhosttyApp
|
|||||||
syncMenuShortcut(action: "goto_split:bottom", menuItem: self.menuSelectSplitBelow)
|
syncMenuShortcut(action: "goto_split:bottom", menuItem: self.menuSelectSplitBelow)
|
||||||
syncMenuShortcut(action: "goto_split:left", menuItem: self.menuSelectSplitLeft)
|
syncMenuShortcut(action: "goto_split:left", menuItem: self.menuSelectSplitLeft)
|
||||||
syncMenuShortcut(action: "goto_split:right", menuItem: self.menuSelectSplitRight)
|
syncMenuShortcut(action: "goto_split:right", menuItem: self.menuSelectSplitRight)
|
||||||
|
syncMenuShortcut(action: "resize_split:up,10", menuItem: self.menuMoveSplitDividerUp)
|
||||||
|
syncMenuShortcut(action: "resize_split:down,10", menuItem: self.menuMoveSplitDividerDown)
|
||||||
|
syncMenuShortcut(action: "resize_split:right,10", menuItem: self.menuMoveSplitDividerRight)
|
||||||
|
syncMenuShortcut(action: "resize_split:left,10", menuItem: self.menuMoveSplitDividerLeft)
|
||||||
|
|
||||||
syncMenuShortcut(action: "increase_font_size:1", menuItem: self.menuIncreaseFontSize)
|
syncMenuShortcut(action: "increase_font_size:1", menuItem: self.menuIncreaseFontSize)
|
||||||
syncMenuShortcut(action: "decrease_font_size:1", menuItem: self.menuDecreaseFontSize)
|
syncMenuShortcut(action: "decrease_font_size:1", menuItem: self.menuDecreaseFontSize)
|
||||||
|
@ -270,7 +270,27 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
|||||||
@IBAction func splitMoveFocusRight(_ sender: Any) {
|
@IBAction func splitMoveFocusRight(_ sender: Any) {
|
||||||
splitMoveFocus(direction: .right)
|
splitMoveFocus(direction: .right)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@IBAction func moveSplitDividerUp(_ sender: Any) {
|
||||||
|
guard let surface = focusedSurface?.surface else { return }
|
||||||
|
ghostty.splitResize(surface: surface, direction: .up, amount: 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func moveSplitDividerDown(_ sender: Any) {
|
||||||
|
guard let surface = focusedSurface?.surface else { return }
|
||||||
|
ghostty.splitResize(surface: surface, direction: .down, amount: 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func moveSplitDividerLeft(_ sender: Any) {
|
||||||
|
guard let surface = focusedSurface?.surface else { return }
|
||||||
|
ghostty.splitResize(surface: surface, direction: .left, amount: 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func moveSplitDividerRight(_ sender: Any) {
|
||||||
|
guard let surface = focusedSurface?.surface else { return }
|
||||||
|
ghostty.splitResize(surface: surface, direction: .right, amount: 10)
|
||||||
|
}
|
||||||
|
|
||||||
private func splitMoveFocus(direction: Ghostty.SplitFocusDirection) {
|
private func splitMoveFocus(direction: Ghostty.SplitFocusDirection) {
|
||||||
guard let surface = focusedSurface?.surface else { return }
|
guard let surface = focusedSurface?.surface else { return }
|
||||||
ghostty.splitMoveFocus(surface: surface, direction: direction)
|
ghostty.splitMoveFocus(surface: surface, direction: direction)
|
||||||
|
@ -84,6 +84,7 @@ struct TerminalView<ViewModel: TerminalViewModel>: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Ghostty.TerminalSplit(node: $viewModel.surfaceTree)
|
Ghostty.TerminalSplit(node: $viewModel.surfaceTree)
|
||||||
|
.environmentObject(ghostty)
|
||||||
.ghosttyApp(ghostty.app!)
|
.ghosttyApp(ghostty.app!)
|
||||||
.ghosttyConfig(ghostty.config!)
|
.ghosttyConfig(ghostty.config!)
|
||||||
.focused($focused)
|
.focused($focused)
|
||||||
|
@ -149,6 +149,8 @@ extension Ghostty {
|
|||||||
control_inspector_cb: { userdata, mode in AppState.controlInspector(userdata, mode: mode) },
|
control_inspector_cb: { userdata, mode in AppState.controlInspector(userdata, mode: mode) },
|
||||||
close_surface_cb: { userdata, processAlive in AppState.closeSurface(userdata, processAlive: processAlive) },
|
close_surface_cb: { userdata, processAlive in AppState.closeSurface(userdata, processAlive: processAlive) },
|
||||||
focus_split_cb: { userdata, direction in AppState.focusSplit(userdata, direction: direction) },
|
focus_split_cb: { userdata, direction in AppState.focusSplit(userdata, direction: direction) },
|
||||||
|
resize_split_cb: { userdata, direction, amount in
|
||||||
|
AppState.resizeSplit(userdata, direction: direction, amount: amount) },
|
||||||
toggle_split_zoom_cb: { userdata in AppState.toggleSplitZoom(userdata) },
|
toggle_split_zoom_cb: { userdata in AppState.toggleSplitZoom(userdata) },
|
||||||
goto_tab_cb: { userdata, n in AppState.gotoTab(userdata, n: n) },
|
goto_tab_cb: { userdata, n in AppState.gotoTab(userdata, n: n) },
|
||||||
toggle_fullscreen_cb: { userdata, nonNativeFullscreen in AppState.toggleFullscreen(userdata, nonNativeFullscreen: nonNativeFullscreen) },
|
toggle_fullscreen_cb: { userdata, nonNativeFullscreen in AppState.toggleFullscreen(userdata, nonNativeFullscreen: nonNativeFullscreen) },
|
||||||
@ -283,6 +285,10 @@ extension Ghostty {
|
|||||||
ghostty_surface_split_focus(surface, direction.toNative())
|
ghostty_surface_split_focus(surface, direction.toNative())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func splitResize(surface: ghostty_surface_t, direction: SplitResizeDirection, amount: UInt16) {
|
||||||
|
ghostty_surface_split_resize(surface, direction.toNative(), amount)
|
||||||
|
}
|
||||||
|
|
||||||
func splitToggleZoom(surface: ghostty_surface_t) {
|
func splitToggleZoom(surface: ghostty_surface_t) {
|
||||||
let action = "toggle_split_zoom"
|
let action = "toggle_split_zoom"
|
||||||
if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) {
|
if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) {
|
||||||
@ -355,6 +361,19 @@ extension Ghostty {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func resizeSplit(_ userdata: UnsafeMutableRawPointer?, direction: ghostty_split_resize_direction_e, amount: UInt16) {
|
||||||
|
guard let surface = self.surfaceUserdata(from: userdata) else { return }
|
||||||
|
guard let resizeDirection = SplitResizeDirection.from(direction: direction) else { return }
|
||||||
|
NotificationCenter.default.post(
|
||||||
|
name: Notification.didResizeSplit,
|
||||||
|
object: surface,
|
||||||
|
userInfo: [
|
||||||
|
Notification.ResizeSplitDirectionKey: resizeDirection,
|
||||||
|
Notification.ResizeSplitAmountKey: amount,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
static func toggleSplitZoom(_ userdata: UnsafeMutableRawPointer?) {
|
static func toggleSplitZoom(_ userdata: UnsafeMutableRawPointer?) {
|
||||||
guard let surface = self.surfaceUserdata(from: userdata) else { return }
|
guard let surface = self.surfaceUserdata(from: userdata) else { return }
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import Combine
|
||||||
import GhosttyKit
|
import GhosttyKit
|
||||||
|
|
||||||
extension Ghostty {
|
extension Ghostty {
|
||||||
@ -123,6 +124,8 @@ extension Ghostty {
|
|||||||
@Published var topLeft: SplitNode
|
@Published var topLeft: SplitNode
|
||||||
@Published var bottomRight: SplitNode
|
@Published var bottomRight: SplitNode
|
||||||
|
|
||||||
|
var resizeEvent: PassthroughSubject<Double, Never> = .init()
|
||||||
|
|
||||||
weak var parent: SplitNode.Container?
|
weak var parent: SplitNode.Container?
|
||||||
|
|
||||||
/// A container is always initialized from some prior leaf because a split has to originate
|
/// A container is always initialized from some prior leaf because a split has to originate
|
||||||
@ -143,6 +146,30 @@ extension Ghostty {
|
|||||||
from.parent = self
|
from.parent = self
|
||||||
bottomRight.parent = self
|
bottomRight.parent = self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resize the split by moving the split divider in the given
|
||||||
|
/// direction by the given amount. If this container is not split
|
||||||
|
/// in the given direction, navigate up the tree until we find a
|
||||||
|
/// container that is
|
||||||
|
func resize(direction: SplitResizeDirection, amount: UInt16) {
|
||||||
|
// We send a resize event to our publisher which will be
|
||||||
|
// received by the SplitView.
|
||||||
|
switch (self.direction) {
|
||||||
|
case .horizontal:
|
||||||
|
switch (direction) {
|
||||||
|
case .left: resizeEvent.send(-Double(amount))
|
||||||
|
case .right: resizeEvent.send(Double(amount))
|
||||||
|
default: parent?.resize(direction: direction, amount: amount)
|
||||||
|
}
|
||||||
|
case .vertical:
|
||||||
|
switch (direction) {
|
||||||
|
case .up: resizeEvent.send(-Double(amount))
|
||||||
|
case .down: resizeEvent.send(Double(amount))
|
||||||
|
default: parent?.resize(direction: direction, amount: amount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Hashable
|
// MARK: - Hashable
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ extension Ghostty {
|
|||||||
.focusedValue(\.ghosttySurfaceZoomed, zoomedSurface != nil)
|
.focusedValue(\.ghosttySurfaceZoomed, zoomedSurface != nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The root of a split tree. This sets up the initial SplitNode state and renders. There is only ever
|
/// The root of a split tree. This sets up the initial SplitNode state and renders. There is only ever
|
||||||
/// one of these in a split tree.
|
/// one of these in a split tree.
|
||||||
private struct TerminalSplitRoot: View {
|
private struct TerminalSplitRoot: View {
|
||||||
@ -153,11 +153,13 @@ extension Ghostty {
|
|||||||
let pub = center.publisher(for: Notification.ghosttyNewSplit, object: leaf.surface)
|
let pub = center.publisher(for: Notification.ghosttyNewSplit, object: leaf.surface)
|
||||||
let pubClose = center.publisher(for: Notification.ghosttyCloseSurface, object: leaf.surface)
|
let pubClose = center.publisher(for: Notification.ghosttyCloseSurface, object: leaf.surface)
|
||||||
let pubFocus = center.publisher(for: Notification.ghosttyFocusSplit, object: leaf.surface)
|
let pubFocus = center.publisher(for: Notification.ghosttyFocusSplit, object: leaf.surface)
|
||||||
|
let pubResize = center.publisher(for: Notification.didResizeSplit, object: leaf.surface)
|
||||||
|
|
||||||
InspectableSurface(surfaceView: leaf.surface, isSplit: !neighbors.isEmpty())
|
InspectableSurface(surfaceView: leaf.surface, isSplit: !neighbors.isEmpty())
|
||||||
.onReceive(pub) { onNewSplit(notification: $0) }
|
.onReceive(pub) { onNewSplit(notification: $0) }
|
||||||
.onReceive(pubClose) { onClose(notification: $0) }
|
.onReceive(pubClose) { onClose(notification: $0) }
|
||||||
.onReceive(pubFocus) { onMoveFocus(notification: $0) }
|
.onReceive(pubFocus) { onMoveFocus(notification: $0) }
|
||||||
|
.onReceive(pubResize) { onResize(notification: $0) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private func onClose(notification: SwiftUI.Notification) {
|
private func onClose(notification: SwiftUI.Notification) {
|
||||||
@ -243,6 +245,20 @@ extension Ghostty {
|
|||||||
to: next.preferredFocus(direction)
|
to: next.preferredFocus(direction)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handle a resize event.
|
||||||
|
private func onResize(notification: SwiftUI.Notification) {
|
||||||
|
// If this leaf is not part of a split then there is nothing to do
|
||||||
|
guard let parent = leaf.parent else { return }
|
||||||
|
|
||||||
|
guard let directionAny = notification.userInfo?[Ghostty.Notification.ResizeSplitDirectionKey] else { return }
|
||||||
|
guard let direction = directionAny as? Ghostty.SplitResizeDirection else { return }
|
||||||
|
|
||||||
|
guard let amountAny = notification.userInfo?[Ghostty.Notification.ResizeSplitAmountKey] else { return }
|
||||||
|
guard let amount = amountAny as? UInt16 else { return }
|
||||||
|
|
||||||
|
parent.resize(direction: direction, amount: amount)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This represents a split view that is in the horizontal or vertical split state.
|
/// This represents a split view that is in the horizontal or vertical split state.
|
||||||
@ -251,6 +267,15 @@ extension Ghostty {
|
|||||||
@Binding var node: SplitNode?
|
@Binding var node: SplitNode?
|
||||||
@StateObject var container: SplitNode.Container
|
@StateObject var container: SplitNode.Container
|
||||||
|
|
||||||
|
@EnvironmentObject private var ghostty: Ghostty.AppState
|
||||||
|
|
||||||
|
/// The cell size of the currently focused view. We use this to
|
||||||
|
/// (optionally) set the resizeIncrements binding for the SplitView
|
||||||
|
@FocusedValue(\.ghosttySurfaceCellSize) private var focusedCellSize
|
||||||
|
|
||||||
|
/// Resize increments used by the split view
|
||||||
|
@State private var resizeIncrements: NSSize = .init(width: 1.0, height: 1.0)
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
SplitView(container.direction, left: {
|
SplitView(container.direction, left: {
|
||||||
let neighborKey: WritableKeyPath<SplitNode.Neighbors, SplitNode?> = container.direction == .horizontal ? \.right : \.bottom
|
let neighborKey: WritableKeyPath<SplitNode.Neighbors, SplitNode?> = container.direction == .horizontal ? \.right : \.bottom
|
||||||
@ -273,6 +298,13 @@ extension Ghostty {
|
|||||||
])
|
])
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
.onChange(of: focusedCellSize) { newValue in
|
||||||
|
guard let increments = newValue else { return }
|
||||||
|
guard ghostty.windowStepResize else { return }
|
||||||
|
self.resizeIncrements = increments
|
||||||
|
}
|
||||||
|
.resizeIncrements(resizeIncrements)
|
||||||
|
.resizePublisher(container.resizeEvent)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func closeableTopLeft() -> Binding<SplitNode?> {
|
private func closeableTopLeft() -> Binding<SplitNode?> {
|
||||||
|
@ -61,6 +61,39 @@ extension Ghostty {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Enum used for resizing splits. This is the direction the split divider will move.
|
||||||
|
enum SplitResizeDirection {
|
||||||
|
case up, down, left, right
|
||||||
|
|
||||||
|
static func from(direction: ghostty_split_resize_direction_e) -> Self? {
|
||||||
|
switch (direction) {
|
||||||
|
case GHOSTTY_SPLIT_RESIZE_UP:
|
||||||
|
return .up;
|
||||||
|
case GHOSTTY_SPLIT_RESIZE_DOWN:
|
||||||
|
return .down;
|
||||||
|
case GHOSTTY_SPLIT_RESIZE_LEFT:
|
||||||
|
return .left;
|
||||||
|
case GHOSTTY_SPLIT_RESIZE_RIGHT:
|
||||||
|
return .right;
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toNative() -> ghostty_split_resize_direction_e {
|
||||||
|
switch (self) {
|
||||||
|
case .up:
|
||||||
|
return GHOSTTY_SPLIT_RESIZE_UP;
|
||||||
|
case .down:
|
||||||
|
return GHOSTTY_SPLIT_RESIZE_DOWN;
|
||||||
|
case .left:
|
||||||
|
return GHOSTTY_SPLIT_RESIZE_LEFT;
|
||||||
|
case .right:
|
||||||
|
return GHOSTTY_SPLIT_RESIZE_RIGHT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Ghostty.Notification {
|
extension Ghostty.Notification {
|
||||||
@ -112,6 +145,11 @@ extension Ghostty.Notification {
|
|||||||
static let confirmUnsafePaste = Notification.Name("com.mitchellh.ghostty.confirmUnsafePaste")
|
static let confirmUnsafePaste = Notification.Name("com.mitchellh.ghostty.confirmUnsafePaste")
|
||||||
static let UnsafePasteStrKey = confirmUnsafePaste.rawValue + ".str"
|
static let UnsafePasteStrKey = confirmUnsafePaste.rawValue + ".str"
|
||||||
static let UnsafePasteStateKey = confirmUnsafePaste.rawValue + ".state"
|
static let UnsafePasteStateKey = confirmUnsafePaste.rawValue + ".state"
|
||||||
|
|
||||||
|
/// Notification sent to the active split view to resize the split.
|
||||||
|
static let didResizeSplit = Notification.Name("com.mitchellh.ghostty.didResizeSplit")
|
||||||
|
static let ResizeSplitDirectionKey = didResizeSplit.rawValue + ".direction"
|
||||||
|
static let ResizeSplitAmountKey = didResizeSplit.rawValue + ".amount"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make the input enum hashable.
|
// Make the input enum hashable.
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import Combine
|
||||||
|
|
||||||
/// A split view shows a left and right (or top and bottom) view with a divider in the middle to do resizing.
|
/// A split view shows a left and right (or top and bottom) view with a divider in the middle to do resizing.
|
||||||
/// The terminlogy "left" and "right" is always used but for vertical splits "left" is "top" and "right" is "bottom".
|
/// The terminlogy "left" and "right" is always used but for vertical splits "left" is "top" and "right" is "bottom".
|
||||||
@ -15,7 +16,14 @@ struct SplitView<L: View, R: View>: View {
|
|||||||
|
|
||||||
/// The current fractional width of the split view. 0.5 means L/R are equally sized, for example.
|
/// The current fractional width of the split view. 0.5 means L/R are equally sized, for example.
|
||||||
@State var split: CGFloat = 0.5
|
@State var split: CGFloat = 0.5
|
||||||
|
|
||||||
|
/// Minimum increment (in points) that this split can be resized by, in
|
||||||
|
/// each direction. Both `height` and `width` should be whole numbers
|
||||||
|
/// greater than or equal to 1.0
|
||||||
|
@Environment(\.resizeIncrements) var resizeIncrements
|
||||||
|
|
||||||
|
@Environment(\.resizePublisher) var resizePublisher
|
||||||
|
|
||||||
/// The visible size of the splitter, in points. The invisible size is a transparent hitbox that can still
|
/// The visible size of the splitter, in points. The invisible size is a transparent hitbox that can still
|
||||||
/// be used for getting a resize handle. The total width/height of the splitter is the sum of both.
|
/// be used for getting a resize handle. The total width/height of the splitter is the sum of both.
|
||||||
private let splitterVisibleSize: CGFloat = 1
|
private let splitterVisibleSize: CGFloat = 1
|
||||||
@ -38,6 +46,9 @@ struct SplitView<L: View, R: View>: View {
|
|||||||
.position(splitterPoint)
|
.position(splitterPoint)
|
||||||
.gesture(dragGesture(geo.size, splitterPoint: splitterPoint))
|
.gesture(dragGesture(geo.size, splitterPoint: splitterPoint))
|
||||||
}
|
}
|
||||||
|
.onReceive(resizePublisher) { value in
|
||||||
|
resize(for: geo.size, amount: value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,6 +58,18 @@ struct SplitView<L: View, R: View>: View {
|
|||||||
self.right = right()
|
self.right = right()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func resize(for size: CGSize, amount: Double) {
|
||||||
|
switch (direction) {
|
||||||
|
case .horizontal:
|
||||||
|
split += amount / size.width
|
||||||
|
case .vertical:
|
||||||
|
split += amount / size.height
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure split is clamped between 0 and 1
|
||||||
|
split = max(0.0, min(split, 1.0))
|
||||||
|
}
|
||||||
|
|
||||||
private func dragGesture(_ size: CGSize, splitterPoint: CGPoint) -> some Gesture {
|
private func dragGesture(_ size: CGSize, splitterPoint: CGPoint) -> some Gesture {
|
||||||
return DragGesture()
|
return DragGesture()
|
||||||
.onChanged { gesture in
|
.onChanged { gesture in
|
||||||
@ -72,10 +95,12 @@ struct SplitView<L: View, R: View>: View {
|
|||||||
case .horizontal:
|
case .horizontal:
|
||||||
result.size.width = result.size.width * split
|
result.size.width = result.size.width * split
|
||||||
result.size.width -= splitterVisibleSize / 2
|
result.size.width -= splitterVisibleSize / 2
|
||||||
|
result.size.width -= result.size.width.truncatingRemainder(dividingBy: self.resizeIncrements.width)
|
||||||
|
|
||||||
case .vertical:
|
case .vertical:
|
||||||
result.size.height = result.size.height * split
|
result.size.height = result.size.height * split
|
||||||
result.size.height -= splitterVisibleSize / 2
|
result.size.height -= splitterVisibleSize / 2
|
||||||
|
result.size.height -= result.size.height.truncatingRemainder(dividingBy: self.resizeIncrements.height)
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
@ -117,3 +142,34 @@ struct SplitView<L: View, R: View>: View {
|
|||||||
enum SplitViewDirection {
|
enum SplitViewDirection {
|
||||||
case horizontal, vertical
|
case horizontal, vertical
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private struct ResizeIncrementsKey: EnvironmentKey {
|
||||||
|
static let defaultValue: CGSize = .init(width: 1.0, height: 1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct ResizePublisherKey: EnvironmentKey {
|
||||||
|
static let defaultValue: PassthroughSubject<Double, Never> = .init()
|
||||||
|
}
|
||||||
|
|
||||||
|
extension EnvironmentValues {
|
||||||
|
/// An environment value that specifies the resize increments of a resizable view
|
||||||
|
var resizeIncrements: CGSize {
|
||||||
|
get { self[ResizeIncrementsKey.self] }
|
||||||
|
set { self[ResizeIncrementsKey.self] = newValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
var resizePublisher: PassthroughSubject<Double, Never> {
|
||||||
|
get { self[ResizePublisherKey.self] }
|
||||||
|
set { self[ResizePublisherKey.self] = newValue }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
func resizeIncrements(_ increments: CGSize) -> some View {
|
||||||
|
environment(\.resizeIncrements, increments)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resizePublisher(_ publisher: PassthroughSubject<Double, Never>) -> some View {
|
||||||
|
environment(\.resizePublisher, publisher)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -19,6 +19,10 @@
|
|||||||
<outlet property="menuCopy" destination="Jqf-pv-Zcu" id="bKd-1C-oy9"/>
|
<outlet property="menuCopy" destination="Jqf-pv-Zcu" id="bKd-1C-oy9"/>
|
||||||
<outlet property="menuDecreaseFontSize" destination="kzb-SZ-dOA" id="Y1B-Vh-6Z2"/>
|
<outlet property="menuDecreaseFontSize" destination="kzb-SZ-dOA" id="Y1B-Vh-6Z2"/>
|
||||||
<outlet property="menuIncreaseFontSize" destination="CIH-ey-Z6x" id="hkc-9C-80E"/>
|
<outlet property="menuIncreaseFontSize" destination="CIH-ey-Z6x" id="hkc-9C-80E"/>
|
||||||
|
<outlet property="menuMoveSplitDividerDown" destination="Zj7-2W-fdF" id="997-LL-nlN"/>
|
||||||
|
<outlet property="menuMoveSplitDividerLeft" destination="wSR-ny-j1a" id="HCZ-CI-2ob"/>
|
||||||
|
<outlet property="menuMoveSplitDividerRight" destination="CcX-ql-QU4" id="rIn-PK-fVM"/>
|
||||||
|
<outlet property="menuMoveSplitDividerUp" destination="h9Y-40-3oo" id="dDi-Vq-I3r"/>
|
||||||
<outlet property="menuNewTab" destination="uTG-Vz-hJU" id="eMg-R3-SeS"/>
|
<outlet property="menuNewTab" destination="uTG-Vz-hJU" id="eMg-R3-SeS"/>
|
||||||
<outlet property="menuNewWindow" destination="Was-JA-tGl" id="lK7-3I-CPG"/>
|
<outlet property="menuNewWindow" destination="Was-JA-tGl" id="lK7-3I-CPG"/>
|
||||||
<outlet property="menuNextSplit" destination="bD7-ei-wKU" id="LeT-xw-eh4"/>
|
<outlet property="menuNextSplit" destination="bD7-ei-wKU" id="LeT-xw-eh4"/>
|
||||||
@ -262,6 +266,37 @@
|
|||||||
</items>
|
</items>
|
||||||
</menu>
|
</menu>
|
||||||
</menuItem>
|
</menuItem>
|
||||||
|
<menuItem title="Resize Split" id="BJO-3W-fkO" userLabel="Resize Split">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Resize Split" id="t7T-Ti-0im">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Move Divider Up" id="h9Y-40-3oo">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="moveSplitDividerUp:" target="-1" id="NhD-6U-Eq2"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Move Divider Down" id="Zj7-2W-fdF">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="moveSplitDividerDown:" target="-1" id="jeD-bm-wJX"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Move Divider Left" id="wSR-ny-j1a">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="moveSplitDividerLeft:" target="-1" id="mlg-SJ-ZZO"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Move Divider Right" id="CcX-ql-QU4">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="moveSplitDividerRight:" target="-1" id="h3W-wY-PI7"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
</items>
|
</items>
|
||||||
</menu>
|
</menu>
|
||||||
</menuItem>
|
</menuItem>
|
||||||
|
@ -92,6 +92,9 @@ pub const App = struct {
|
|||||||
/// Focus the previous/next split (if any).
|
/// Focus the previous/next split (if any).
|
||||||
focus_split: ?*const fn (SurfaceUD, input.SplitFocusDirection) callconv(.C) void = null,
|
focus_split: ?*const fn (SurfaceUD, input.SplitFocusDirection) callconv(.C) void = null,
|
||||||
|
|
||||||
|
/// Resize the current split.
|
||||||
|
resize_split: ?*const fn (SurfaceUD, input.SplitResizeDirection, u16) callconv(.C) void = null,
|
||||||
|
|
||||||
/// Zoom the current split.
|
/// Zoom the current split.
|
||||||
toggle_split_zoom: ?*const fn (SurfaceUD) callconv(.C) void = null,
|
toggle_split_zoom: ?*const fn (SurfaceUD) callconv(.C) void = null,
|
||||||
|
|
||||||
@ -384,6 +387,15 @@ pub const Surface = struct {
|
|||||||
func(self.opts.userdata, direction);
|
func(self.opts.userdata, direction);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn resizeSplit(self: *const Surface, direction: input.SplitResizeDirection, amount: u16) void {
|
||||||
|
const func = self.app.opts.resize_split orelse {
|
||||||
|
log.info("runtime embedder does not support resize split", .{});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
func(self.opts.userdata, direction, amount);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn toggleSplitZoom(self: *const Surface) void {
|
pub fn toggleSplitZoom(self: *const Surface) void {
|
||||||
const func = self.app.opts.toggle_split_zoom orelse {
|
const func = self.app.opts.toggle_split_zoom orelse {
|
||||||
log.info("runtime embedder does not support split zoom", .{});
|
log.info("runtime embedder does not support split zoom", .{});
|
||||||
@ -1374,6 +1386,14 @@ pub const CAPI = struct {
|
|||||||
ptr.gotoSplit(direction);
|
ptr.gotoSplit(direction);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resize the current split by moving the split divider in the given
|
||||||
|
/// direction. `direction` specifies which direction the split divider will
|
||||||
|
/// move relative to the focused split. `amount` is a fractional value
|
||||||
|
/// between 0 and 1 that specifies by how much the divider will move.
|
||||||
|
export fn ghostty_surface_split_resize(ptr: *Surface, direction: input.SplitResizeDirection, amount: u16) void {
|
||||||
|
ptr.resizeSplit(direction, amount);
|
||||||
|
}
|
||||||
|
|
||||||
/// Invoke an action on the surface.
|
/// Invoke an action on the surface.
|
||||||
export fn ghostty_surface_binding_action(
|
export fn ghostty_surface_binding_action(
|
||||||
ptr: *Surface,
|
ptr: *Surface,
|
||||||
|
Reference in New Issue
Block a user