diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 1debd502c..f97a91ae7 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -17,7 +17,7 @@ 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 */; }; + A5CEAFDE29B8058B00646FDA /* SplitView.Divider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFDD29B8058B00646FDA /* SplitView.Divider.swift */; }; A5D495A2299BEC7E00DD1313 /* GhosttyKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */; }; /* End PBXBuildFile section */ @@ -34,7 +34,7 @@ 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 = ""; }; + A5CEAFDD29B8058B00646FDA /* SplitView.Divider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitView.Divider.swift; sourceTree = ""; }; A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = GhosttyKit.xcframework; sourceTree = ""; }; /* End PBXFileReference section */ @@ -98,7 +98,7 @@ isa = PBXGroup; children = ( A5CEAFDB29B8009000646FDA /* SplitView.swift */, - A5CEAFDD29B8058B00646FDA /* SplitView.Splitter.swift */, + A5CEAFDD29B8058B00646FDA /* SplitView.Divider.swift */, ); path = SplitView; sourceTree = ""; @@ -196,7 +196,7 @@ A55685E029A03A9F004303CE /* AppError.swift in Sources */, A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */, A5B30535299BEAAA0047F10C /* GhosttyApp.swift in Sources */, - A5CEAFDE29B8058B00646FDA /* SplitView.Splitter.swift in Sources */, + A5CEAFDE29B8058B00646FDA /* SplitView.Divider.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/macos/Sources/Ghostty/Ghostty.SplitView.swift b/macos/Sources/Ghostty/Ghostty.SplitView.swift index 402ad6564..e0fd35ec9 100644 --- a/macos/Sources/Ghostty/Ghostty.SplitView.swift +++ b/macos/Sources/Ghostty/Ghostty.SplitView.swift @@ -5,17 +5,17 @@ extension Ghostty { /// 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 TerminalSplitView: View { + struct TerminalSplit: View { @Environment(\.ghosttyApp) private var app var body: some View { if let app = app { - SplitViewChild(app) + TerminalSplitChild(app) } } } - private struct SplitViewChild: View { + private struct TerminalSplitChild: View { enum Direction { case none case vertical @@ -80,6 +80,7 @@ extension Ghostty { assert(state.bottomRight != nil) state.topLeft = state.bottomRight! state.direction = .none + focusedSide = .TopLeft } func closeBottomRight() { @@ -87,6 +88,7 @@ extension Ghostty { assert(state.bottomRight != nil) state.bottomRight = nil state.direction = .none + focusedSide = .TopLeft } var body: some View { @@ -109,10 +111,10 @@ extension Ghostty { } SplitView(.horizontal, left: { - SplitViewChild(app, topLeft: state.topLeft) + TerminalSplitChild(app, topLeft: state.topLeft) .focused($focusedSide, equals: .TopLeft) }, right: { - SplitViewChild(app, topLeft: state.bottomRight!) + TerminalSplitChild(app, topLeft: state.bottomRight!) .focused($focusedSide, equals: .BottomRight) }) } @@ -124,10 +126,10 @@ extension Ghostty { } SplitView(.vertical, left: { - SplitViewChild(app, topLeft: state.topLeft) + TerminalSplitChild(app, topLeft: state.topLeft) .focused($focusedSide, equals: .TopLeft) }, right: { - SplitViewChild(app, topLeft: state.bottomRight!) + TerminalSplitChild(app, topLeft: state.bottomRight!) .focused($focusedSide, equals: .BottomRight) }) } diff --git a/macos/Sources/GhosttyApp.swift b/macos/Sources/GhosttyApp.swift index be2dff50b..2eb459623 100644 --- a/macos/Sources/GhosttyApp.swift +++ b/macos/Sources/GhosttyApp.swift @@ -21,7 +21,7 @@ struct GhosttyApp: App { case .error: ErrorView() case .ready: - Ghostty.TerminalSplitView() + Ghostty.TerminalSplit() .ghosttyApp(ghostty.app!) } }.commands { diff --git a/macos/Sources/SplitView/SplitView.Splitter.swift b/macos/Sources/SplitView/SplitView.Divider.swift similarity index 94% rename from macos/Sources/SplitView/SplitView.Splitter.swift rename to macos/Sources/SplitView/SplitView.Divider.swift index de2ac6628..a5e3eb946 100644 --- a/macos/Sources/SplitView/SplitView.Splitter.swift +++ b/macos/Sources/SplitView/SplitView.Divider.swift @@ -1,7 +1,8 @@ import SwiftUI extension SplitView { - struct Splitter: View { + /// The split divider that is rendered and can be used to resize a split view. + struct Divider: View { let direction: Direction let visibleSize: CGFloat let invisibleSize: CGFloat diff --git a/macos/Sources/SplitView/SplitView.swift b/macos/Sources/SplitView/SplitView.swift index 1afc6887a..68973bdbb 100644 --- a/macos/Sources/SplitView/SplitView.swift +++ b/macos/Sources/SplitView/SplitView.swift @@ -1,15 +1,26 @@ import SwiftUI +/// 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". +/// +/// This view is purpose built for our use case and I imagine we'll continue to make it more configurable +/// as time goes on. For example, the splitter divider size and styling is all hardcoded. struct SplitView: View { + /// Direction of the split let direction: Direction + + /// The left and right views to render. let left: L let right: R + /// The current fractional width of the split view. 0.5 means L/R are equally sized, for example. + @State var split: CGFloat = 0.5 + + /// 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. private let splitterVisibleSize: CGFloat = 1 private let splitterInvisibleSize: CGFloat = 6 - @State var split: CGFloat = 0.5 - var body: some View { GeometryReader { geo in let leftRect = self.leftRect(for: geo.size) @@ -23,14 +34,20 @@ struct SplitView: View { 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) + Divider(direction: direction, visibleSize: splitterVisibleSize, invisibleSize: splitterInvisibleSize) .position(splitterPoint) .gesture(dragGesture(geo.size, splitterPoint: splitterPoint)) } } } - func dragGesture(_ size: CGSize, splitterPoint: CGPoint) -> some Gesture { + init(_ direction: Direction, @ViewBuilder left: (() -> L), @ViewBuilder right: (() -> R)) { + self.direction = direction + self.left = left() + self.right = right() + } + + private func dragGesture(_ size: CGSize, splitterPoint: CGPoint) -> some Gesture { return DragGesture() .onChanged { gesture in let minSize: CGFloat = 10 @@ -47,7 +64,8 @@ struct SplitView: View { } } - func leftRect(for size: CGSize) -> CGRect { + /// Calculates the bounding rect for the left view. + private 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) { @@ -63,7 +81,8 @@ struct SplitView: View { return result } - func rightRect(for size: CGSize, leftRect: CGRect) -> CGRect { + /// Calculates the bounding rect for the right view. + private 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) { @@ -83,7 +102,8 @@ struct SplitView: View { return result } - func splitterPoint(for size: CGSize, leftRect: CGRect) -> CGPoint { + /// Calculates the point at which the splitter should be rendered. + private func splitterPoint(for size: CGSize, leftRect: CGRect) -> CGPoint { switch (direction) { case .horizontal: return CGPoint(x: leftRect.size.width, y: size.height / 2) @@ -92,12 +112,6 @@ struct SplitView: View { 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 {