diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 8e0846e29..1debd502c 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -12,10 +12,12 @@ A55B7BB629B6F47F0055DE60 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BB529B6F47F0055DE60 /* AppState.swift */; }; A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BB729B6F53A0055DE60 /* Package.swift */; }; A55B7BBC29B6FC330055DE60 /* SurfaceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BBB29B6FC330055DE60 /* SurfaceView.swift */; }; - A55B7BBE29B701360055DE60 /* SplitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BBD29B701360055DE60 /* SplitView.swift */; }; + A55B7BBE29B701360055DE60 /* Ghostty.SplitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BBD29B701360055DE60 /* Ghostty.SplitView.swift */; }; A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59444F629A2ED5200725BBA /* SettingsView.swift */; }; A5B30535299BEAAA0047F10C /* GhosttyApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5B30534299BEAAA0047F10C /* GhosttyApp.swift */; }; A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; }; + A5CEAFDC29B8009000646FDA /* SplitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFDB29B8009000646FDA /* SplitView.swift */; }; + A5CEAFDE29B8058B00646FDA /* SplitView.Splitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFDD29B8058B00646FDA /* SplitView.Splitter.swift */; }; A5D495A2299BEC7E00DD1313 /* GhosttyKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */; }; /* End PBXBuildFile section */ @@ -25,12 +27,14 @@ A55B7BB529B6F47F0055DE60 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = ""; }; A55B7BB729B6F53A0055DE60 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; A55B7BBB29B6FC330055DE60 /* SurfaceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurfaceView.swift; sourceTree = ""; }; - A55B7BBD29B701360055DE60 /* SplitView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitView.swift; sourceTree = ""; }; + A55B7BBD29B701360055DE60 /* Ghostty.SplitView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.SplitView.swift; sourceTree = ""; }; A59444F629A2ED5200725BBA /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; A5B30531299BEAAA0047F10C /* Ghostty.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ghostty.app; sourceTree = BUILT_PRODUCTS_DIR; }; A5B30534299BEAAA0047F10C /* GhosttyApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyApp.swift; sourceTree = ""; }; A5B30538299BEAAB0047F10C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; A5B3053D299BEAAB0047F10C /* Ghostty.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Ghostty.entitlements; sourceTree = ""; }; + A5CEAFDB29B8009000646FDA /* SplitView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitView.swift; sourceTree = ""; }; + A5CEAFDD29B8058B00646FDA /* SplitView.Splitter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitView.Splitter.swift; sourceTree = ""; }; A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = GhosttyKit.xcframework; sourceTree = ""; }; /* End PBXFileReference section */ @@ -50,6 +54,7 @@ isa = PBXGroup; children = ( A5D495A0299BEC2200DD1313 /* Preview Content */, + A5CEAFDA29B8005900646FDA /* SplitView */, A55B7BB429B6F4410055DE60 /* Ghostty */, A5B30534299BEAAA0047F10C /* GhosttyApp.swift */, A535B9D9299C569B0017E2E4 /* ErrorView.swift */, @@ -65,7 +70,7 @@ A55B7BB729B6F53A0055DE60 /* Package.swift */, A55B7BB529B6F47F0055DE60 /* AppState.swift */, A55B7BBB29B6FC330055DE60 /* SurfaceView.swift */, - A55B7BBD29B701360055DE60 /* SplitView.swift */, + A55B7BBD29B701360055DE60 /* Ghostty.SplitView.swift */, ); path = Ghostty; sourceTree = ""; @@ -89,6 +94,15 @@ name = Products; sourceTree = ""; }; + A5CEAFDA29B8005900646FDA /* SplitView */ = { + isa = PBXGroup; + children = ( + A5CEAFDB29B8009000646FDA /* SplitView.swift */, + A5CEAFDD29B8058B00646FDA /* SplitView.Splitter.swift */, + ); + path = SplitView; + sourceTree = ""; + }; A5D495A0299BEC2200DD1313 /* Preview Content */ = { isa = PBXGroup; children = ( @@ -176,11 +190,13 @@ A55B7BBC29B6FC330055DE60 /* SurfaceView.swift in Sources */, A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */, A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */, - A55B7BBE29B701360055DE60 /* SplitView.swift in Sources */, + A55B7BBE29B701360055DE60 /* Ghostty.SplitView.swift in Sources */, A55B7BB629B6F47F0055DE60 /* AppState.swift in Sources */, + A5CEAFDC29B8009000646FDA /* SplitView.swift in Sources */, A55685E029A03A9F004303CE /* AppError.swift in Sources */, A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */, A5B30535299BEAAA0047F10C /* GhosttyApp.swift in Sources */, + A5CEAFDE29B8058B00646FDA /* SplitView.Splitter.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/macos/Sources/Ghostty/SplitView.swift b/macos/Sources/Ghostty/Ghostty.SplitView.swift similarity index 100% rename from macos/Sources/Ghostty/SplitView.swift rename to macos/Sources/Ghostty/Ghostty.SplitView.swift diff --git a/macos/Sources/GhosttyApp.swift b/macos/Sources/GhosttyApp.swift index be2dff50b..e170ede78 100644 --- a/macos/Sources/GhosttyApp.swift +++ b/macos/Sources/GhosttyApp.swift @@ -21,8 +21,15 @@ struct GhosttyApp: App { case .error: ErrorView() case .ready: - Ghostty.TerminalSplitView() - .ghosttyApp(ghostty.app!) + SplitView(.horizontal, left: { + Color.green + }, right: { + Color.red + }) +/* + Ghostty.Terminal() + .ghosttyApp(ghostty.app!) + */ } }.commands { CommandGroup(after: .newItem) { diff --git a/macos/Sources/SplitView/SplitView.Splitter.swift b/macos/Sources/SplitView/SplitView.Splitter.swift new file mode 100644 index 000000000..de2ac6628 --- /dev/null +++ b/macos/Sources/SplitView/SplitView.Splitter.swift @@ -0,0 +1,67 @@ +import SwiftUI + +extension SplitView { + struct Splitter: View { + let direction: Direction + let visibleSize: CGFloat + let invisibleSize: CGFloat + + private var visibleWidth: CGFloat? { + switch (direction) { + case .horizontal: + return visibleSize + case .vertical: + return nil + } + } + + private var visibleHeight: CGFloat? { + switch (direction) { + case .horizontal: + return nil + case .vertical: + return visibleSize + } + } + + private var invisibleWidth: CGFloat? { + switch (direction) { + case .horizontal: + return visibleSize + invisibleSize + case .vertical: + return nil + } + } + + private var invisibleHeight: CGFloat? { + switch (direction) { + case .horizontal: + return nil + case .vertical: + return visibleSize + invisibleSize + } + } + + var body: some View { + ZStack { + Color.clear + .frame(width: invisibleWidth, height: invisibleHeight) + Rectangle() + .fill(Color.gray) + .frame(width: visibleWidth, height: visibleHeight) + } + .onHover { isHovered in + if (isHovered) { + switch (direction) { + case .horizontal: + NSCursor.resizeLeftRight.push() + case .vertical: + NSCursor.resizeUpDown.push() + } + } else { + NSCursor.pop() + } + } + } + } +} diff --git a/macos/Sources/SplitView/SplitView.swift b/macos/Sources/SplitView/SplitView.swift new file mode 100644 index 000000000..cce9463ab --- /dev/null +++ b/macos/Sources/SplitView/SplitView.swift @@ -0,0 +1,87 @@ +import SwiftUI + +struct SplitView: View { + let direction: Direction + let left: L + let right: R + + private let splitterVisibleSize: CGFloat = 5 + private let splitterInvisibleSize: CGFloat = 5 + + @State var split: CGFloat = 0.5 + + var body: some View { + GeometryReader { geo in + let leftRect = self.leftRect(for: geo.size) + let rightRect = self.rightRect(for: geo.size, leftRect: leftRect) + let splitterPoint = self.splitterPoint(for: geo.size, leftRect: leftRect) + + ZStack(alignment: .topLeading) { + left + .frame(width: leftRect.size.width, height: leftRect.size.height) + .offset(x: leftRect.origin.x, y: leftRect.origin.y) + right + .frame(width: rightRect.size.width, height: rightRect.size.height) + .offset(x: rightRect.origin.x, y: rightRect.origin.y) + Splitter(direction: direction, visibleSize: splitterVisibleSize, invisibleSize: splitterInvisibleSize) + .position(splitterPoint) + } + } + } + + func leftRect(for size: CGSize) -> CGRect { + // Initially the rect is the full size + var result = CGRect(x: 0, y: 0, width: size.width, height: size.height) + switch (direction) { + case .horizontal: + result.size.width = result.size.width * split + result.size.width -= splitterVisibleSize / 2 + + case .vertical: + result.size.height = result.size.height * split + result.size.height -= splitterVisibleSize / 2 + } + + return result + } + + func rightRect(for size: CGSize, leftRect: CGRect) -> CGRect { + // Initially the rect is the full size + var result = CGRect(x: 0, y: 0, width: size.width, height: size.height) + switch (direction) { + case .horizontal: + // For horizontal layouts we offset the starting X by the left rect + // and make the width fit the remaining space. + result.origin.x += leftRect.size.width + result.origin.x += splitterVisibleSize / 2 + result.size.width -= result.origin.x + + case .vertical: + assert(false) + } + + return result + } + + func splitterPoint(for size: CGSize, leftRect: CGRect) -> CGPoint { + switch (direction) { + case .horizontal: + return CGPoint(x: leftRect.size.width, y: size.height / 2) + + case .vertical: + return CGPoint(x: size.width / 2, y: leftRect.size.height) + } + } + + init(_ direction: Direction, @ViewBuilder left: (() -> L), @ViewBuilder right: (() -> R)) { + self.direction = direction + self.left = left() + self.right = right() + } +} + +extension SplitView { + enum Direction { + case horizontal, vertical + } +}