diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 3ec37dec1..e0446a021 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -7,7 +7,7 @@ objects = { /* Begin PBXBuildFile section */ - A507573E299FF33C009D7DC7 /* TerminalSurfaceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A507573D299FF33C009D7DC7 /* TerminalSurfaceView.swift */; }; + A507573E299FF33C009D7DC7 /* TerminalSurface.swift in Sources */ = {isa = PBXBuildFile; fileRef = A507573D299FF33C009D7DC7 /* TerminalSurface.swift */; }; A518502429A197C700E4CC4F /* TerminalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A518502329A197C700E4CC4F /* TerminalView.swift */; }; A518502629A1A45100E4CC4F /* WindowTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = A518502529A1A45100E4CC4F /* WindowTracker.swift */; }; A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A535B9D9299C569B0017E2E4 /* ErrorView.swift */; }; @@ -19,7 +19,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ - A507573D299FF33C009D7DC7 /* TerminalSurfaceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalSurfaceView.swift; sourceTree = ""; }; + A507573D299FF33C009D7DC7 /* TerminalSurface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalSurface.swift; sourceTree = ""; }; A518502329A197C700E4CC4F /* TerminalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalView.swift; sourceTree = ""; }; A518502529A1A45100E4CC4F /* WindowTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowTracker.swift; sourceTree = ""; }; A535B9D9299C569B0017E2E4 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = ""; }; @@ -53,7 +53,7 @@ A55685DF29A03A9F004303CE /* AppError.swift */, A59444F629A2ED5200725BBA /* SettingsView.swift */, A518502329A197C700E4CC4F /* TerminalView.swift */, - A507573D299FF33C009D7DC7 /* TerminalSurfaceView.swift */, + A507573D299FF33C009D7DC7 /* TerminalSurface.swift */, A518502529A1A45100E4CC4F /* WindowTracker.swift */, ); path = Sources; @@ -166,7 +166,7 @@ A518502629A1A45100E4CC4F /* WindowTracker.swift in Sources */, A518502429A197C700E4CC4F /* TerminalView.swift in Sources */, A55685E029A03A9F004303CE /* AppError.swift in Sources */, - A507573E299FF33C009D7DC7 /* TerminalSurfaceView.swift in Sources */, + A507573E299FF33C009D7DC7 /* TerminalSurface.swift in Sources */, A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */, A5B30535299BEAAA0047F10C /* GhosttyApp.swift in Sources */, ); diff --git a/macos/Sources/GhosttyApp.swift b/macos/Sources/GhosttyApp.swift index ecb6c9906..1651c98f8 100644 --- a/macos/Sources/GhosttyApp.swift +++ b/macos/Sources/GhosttyApp.swift @@ -21,7 +21,7 @@ struct GhosttyApp: App { case .error: ErrorView() case .ready: - TerminalView(app: ghostty.app!) + TerminalSplittableView(app: ghostty.app!) .modifier(WindowObservationModifier()) } }.commands { @@ -160,7 +160,7 @@ class GhosttyState: ObservableObject { } static func setTitle(_ userdata: UnsafeMutableRawPointer?, title: UnsafePointer?) { - let surfaceView = Unmanaged.fromOpaque(userdata!).takeUnretainedValue() + let surfaceView = Unmanaged.fromOpaque(userdata!).takeUnretainedValue() guard let titleStr = String(cString: title!, encoding: .utf8) else { return } DispatchQueue.main.async { surfaceView.title = titleStr @@ -169,7 +169,7 @@ class GhosttyState: ObservableObject { /// Returns the GhosttyState from the given userdata value. static func appState(fromSurface userdata: UnsafeMutableRawPointer?) -> GhosttyState? { - let surfaceView = Unmanaged.fromOpaque(userdata!).takeUnretainedValue() + let surfaceView = Unmanaged.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 } diff --git a/macos/Sources/TerminalSurfaceView.swift b/macos/Sources/TerminalSurface.swift similarity index 97% rename from macos/Sources/TerminalSurfaceView.swift rename to macos/Sources/TerminalSurface.swift index c790646ed..ccde2e3ec 100644 --- a/macos/Sources/TerminalSurfaceView.swift +++ b/macos/Sources/TerminalSurface.swift @@ -9,7 +9,7 @@ import GhosttyKit /// We just wrap an AppKit NSView here at the moment so that we can behave as low level as possible /// since that is what the Metal renderer in Ghostty expects. In the future, it may make more sense to /// wrap an MTKView and use that, but for legacy reasons we didn't do that to begin with. -struct TerminalSurfaceView: NSViewRepresentable { +struct TerminalSurface: NSViewRepresentable { static let logger = Logger( subsystem: Bundle.main.bundleIdentifier!, category: String(describing: TerminalSurfaceView.self) @@ -34,16 +34,16 @@ struct TerminalSurfaceView: NSViewRepresentable { /// set the appropriate title of the window/tab/split/etc. if they care. @Binding var title: String - @StateObject private var state: TerminalSurfaceView_Real + @StateObject private var state: TerminalSurfaceView init(_ app: ghostty_app_t, hasFocus: Bool, size: CGSize, title: Binding) { - self._state = StateObject(wrappedValue: TerminalSurfaceView_Real(app)) + self._state = StateObject(wrappedValue: TerminalSurfaceView(app)) self._title = title self.hasFocus = hasFocus self.size = size } - func makeNSView(context: Context) -> TerminalSurfaceView_Real { + func makeNSView(context: Context) -> TerminalSurfaceView { // We need the view as part of the state to be created previously because // the view is sent to the Ghostty API so that it can manipulate it // directly since we draw on a render thread. @@ -51,7 +51,7 @@ struct TerminalSurfaceView: NSViewRepresentable { return state; } - func updateNSView(_ view: TerminalSurfaceView_Real, context: Context) { + func updateNSView(_ view: TerminalSurfaceView, context: Context) { state.delegate = context.coordinator state.focusDidChange(hasFocus) state.sizeDidChange(size) @@ -62,9 +62,9 @@ struct TerminalSurfaceView: NSViewRepresentable { } class Coordinator : TerminalSurfaceDelegate { - let view: TerminalSurfaceView + let view: TerminalSurface - init(_ view: TerminalSurfaceView) { + init(_ view: TerminalSurface) { self.view = view } @@ -80,7 +80,7 @@ protocol TerminalSurfaceDelegate: AnyObject { } /// The actual NSView implementation for the terminal surface. -class TerminalSurfaceView_Real: NSView, NSTextInputClient, ObservableObject { +class TerminalSurfaceView: NSView, NSTextInputClient, ObservableObject { weak var delegate: TerminalSurfaceDelegate? // The current title of the surface as defined by the pty. This can be diff --git a/macos/Sources/TerminalView.swift b/macos/Sources/TerminalView.swift index b03abbcb9..830b3de68 100644 --- a/macos/Sources/TerminalView.swift +++ b/macos/Sources/TerminalView.swift @@ -16,9 +16,68 @@ struct TerminalView: View { // is up to date. See TerminalSurfaceView for why we don't use the NSView // resize callback. GeometryReader { geo in - TerminalSurfaceView(app, hasFocus: hasFocus, size: geo.size, title: $title) + TerminalSurface(app, hasFocus: hasFocus, size: geo.size, title: $title) .focused($surfaceFocus) .navigationTitle(title) } } } + +/// A spittable terminal view is one where the terminal allows for "splits" (vertical and horizontal) within the +/// view. The terminal starts in the unsplit state (a plain ol' TerminalView) but responds to changes to the +/// split direction by splitting the terminal. +struct TerminalSplittableView: View { + enum Direction { + case none + case vertical + case horizontal + } + + enum Side: Hashable { + case TopLeft + case BottomRight + } + + @FocusState private var focusedSide: Side? + + let app: ghostty_app_t + + /// Direction of the current split. If this is "nil" then the terminal is not currently split at all. + @State var splitDirection: Direction = .none + + var body: some View { + switch (splitDirection) { + case .none: + VStack { + HStack { + Button("Split Horizontal") { splitDirection = .horizontal } + Button("Split Vertical") { splitDirection = .vertical } + } + + TerminalView(app: app) + .focused($focusedSide, equals: .TopLeft) + } + case .horizontal: + VStack { + HStack { + Button("Close Left") { splitDirection = .none } + Button("Close Right") { splitDirection = .none } + } + + HSplitView { + TerminalSplittableView(app: app) + .focused($focusedSide, equals: .TopLeft) + TerminalSplittableView(app: app) + .focused($focusedSide, equals: .BottomRight) + } + } + case .vertical: + VSplitView { + TerminalSplittableView(app: app) + .focused($focusedSide, equals: .TopLeft) + TerminalSplittableView(app: app) + .focused($focusedSide, equals: .BottomRight) + } + } + } +}