diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index f05c8e74c..450a994a7 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -65,6 +65,7 @@ A5CBD05C2CA0C5C70017A1AE /* SlideTerminal.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5CBD05B2CA0C5C70017A1AE /* SlideTerminal.xib */; }; A5CBD05E2CA0C5EC0017A1AE /* SlideTerminalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD05D2CA0C5E70017A1AE /* SlideTerminalController.swift */; }; A5CBD0602CA0C90A0017A1AE /* SlideTerminalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD05F2CA0C9080017A1AE /* SlideTerminalWindow.swift */; }; + A5CBD0642CA122E70017A1AE /* SlideTerminalPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD0632CA122E70017A1AE /* SlideTerminalPosition.swift */; }; A5CC36132C9CD72D004D6760 /* SecureInputOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CC36122C9CD729004D6760 /* SecureInputOverlay.swift */; }; A5CC36152C9CDA06004D6760 /* View+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CC36142C9CDA03004D6760 /* View+Extension.swift */; }; A5CDF1912AAF9A5800513312 /* ConfigurationErrors.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5CDF1902AAF9A5800513312 /* ConfigurationErrors.xib */; }; @@ -139,6 +140,7 @@ A5CBD05B2CA0C5C70017A1AE /* SlideTerminal.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SlideTerminal.xib; sourceTree = ""; }; A5CBD05D2CA0C5E70017A1AE /* SlideTerminalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlideTerminalController.swift; sourceTree = ""; }; A5CBD05F2CA0C9080017A1AE /* SlideTerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlideTerminalWindow.swift; sourceTree = ""; }; + A5CBD0632CA122E70017A1AE /* SlideTerminalPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlideTerminalPosition.swift; sourceTree = ""; }; A5CC36122C9CD729004D6760 /* SecureInputOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureInputOverlay.swift; sourceTree = ""; }; A5CC36142C9CDA03004D6760 /* View+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Extension.swift"; sourceTree = ""; }; A5CDF1902AAF9A5800513312 /* ConfigurationErrors.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ConfigurationErrors.xib; sourceTree = ""; }; @@ -393,6 +395,7 @@ children = ( A5CBD05B2CA0C5C70017A1AE /* SlideTerminal.xib */, A5CBD05D2CA0C5E70017A1AE /* SlideTerminalController.swift */, + A5CBD0632CA122E70017A1AE /* SlideTerminalPosition.swift */, A5CBD05F2CA0C9080017A1AE /* SlideTerminalWindow.swift */, ); path = SlideTerminal; @@ -548,6 +551,7 @@ A5D0AF3D2B37804400D21823 /* CodableBridge.swift in Sources */, A5D0AF3B2B36A1DE00D21823 /* TerminalRestorable.swift in Sources */, C1F26EA72B738B9900404083 /* NSView+Extension.swift in Sources */, + A5CBD0642CA122E70017A1AE /* SlideTerminalPosition.swift in Sources */, A596309C2AEE1C9E00D64628 /* TerminalController.swift in Sources */, A5CC36152C9CDA06004D6760 /* View+Extension.swift in Sources */, A56D58892ACDE6CA00508D2C /* ServiceProvider.swift in Sources */, diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 5e520df00..1611541d1 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -170,7 +170,7 @@ class AppDelegate: NSObject, //terminalManager.newWindow() } - foo = SlideTerminalController(window: nil) + foo = SlideTerminalController(ghostty, baseConfig: nil) foo?.showWindow(self) } diff --git a/macos/Sources/Features/SlideTerminal/SlideTerminalController.swift b/macos/Sources/Features/SlideTerminal/SlideTerminalController.swift index 4bd183508..3db695eab 100644 --- a/macos/Sources/Features/SlideTerminal/SlideTerminalController.swift +++ b/macos/Sources/Features/SlideTerminal/SlideTerminalController.swift @@ -4,39 +4,77 @@ import SwiftUI import GhosttyKit /// Controller for the slide-style terminal. -class SlideTerminalController: NSWindowController { +class SlideTerminalController: NSWindowController, TerminalViewDelegate, TerminalViewModel { override var windowNibName: NSNib.Name? { "SlideTerminal" } + /// The app instance that this terminal view will represent. + let ghostty: Ghostty.App + + /// The position fo the slide terminal. + let position: SlideTerminalPosition + + /// The surface tree for this window. + @Published var surfaceTree: Ghostty.SplitNode? = nil + + init(_ ghostty: Ghostty.App, + position: SlideTerminalPosition = .top, + baseConfig base: Ghostty.SurfaceConfiguration? = nil, + surfaceTree tree: Ghostty.SplitNode? = nil + ) { + self.ghostty = ghostty + self.position = position + + super.init(window: nil) + + // Initialize our initial surface. + guard let ghostty_app = ghostty.app else { preconditionFailure("app must be loaded") } + self.surfaceTree = tree ?? .leaf(.init(ghostty_app, baseConfig: base)) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) is not supported for this view") + } + + // MARK: NSWindowController + override func windowDidLoad() { guard let window = self.window else { return } - // Make the window full width - let screenFrame = NSScreen.main?.frame ?? .zero - window.setFrame(NSRect( - x: 0, - y: 0, - width: screenFrame.size.width, - height: window.frame.size.height - ), display: false) + // The slide window is not restorable (yet!). "Yet" because in theory we can + // make this restorable, but it isn't currently implemented. + window.isRestorable = false - slideWindowIn(window: window) + // Setup our content + window.contentView = NSHostingView(rootView: TerminalView( + ghostty: self.ghostty, + viewModel: self, + delegate: self + )) + + // Animate the window in + slideWindowIn(window: window, from: position) } - private func slideWindowIn(window: NSWindow) { + //MARK: TerminalViewDelegate + + func cellSizeDidChange(to: NSSize) { + guard ghostty.config.windowStepResize else { return } + self.window?.contentResizeIncrements = to + } + + func surfaceTreeDidChange() { + if (surfaceTree == nil) { + self.window?.close() + } + } + + // MARK: Slide Logic + + private func slideWindowIn(window: NSWindow, from position: SlideTerminalPosition) { guard let screen = NSScreen.main else { return } - // Determine our final position. Our final position is exactly - // pinned against the top menu bar. - let windowFrame = window.frame - let finalY = screen.visibleFrame.maxY - windowFrame.height - // Move our window off screen to the top - window.setFrameOrigin(.init( - x: windowFrame.origin.x, - y: screen.frame.maxY)) - - // Set the window invisible - window.alphaValue = 0 + position.setInitial(in: window, on: screen) // Move it to the visible position since animation requires this window.makeKeyAndOrderFront(nil) @@ -46,17 +84,7 @@ class SlideTerminalController: NSWindowController { NSAnimationContext.runAnimationGroup { context in context.duration = 0.3 context.timingFunction = .init(name: .easeIn) - - let animator = window.animator() - animator.setFrame(.init( - origin: .init(x: windowFrame.origin.x, y: finalY), - size: windowFrame.size - ), display: true) - animator.alphaValue = 1 + position.setFinal(in: window.animator(), on: screen) } } } - -enum SlideTerminalLocation { - case top -} diff --git a/macos/Sources/Features/SlideTerminal/SlideTerminalPosition.swift b/macos/Sources/Features/SlideTerminal/SlideTerminalPosition.swift new file mode 100644 index 000000000..89c521a47 --- /dev/null +++ b/macos/Sources/Features/SlideTerminal/SlideTerminalPosition.swift @@ -0,0 +1,41 @@ +import Cocoa + +enum SlideTerminalPosition { + case top + + /// Set the initial state for a window for animating out of this position. + func setInitial(in window: NSWindow, on screen: NSScreen) { + // We always start invisible + window.alphaValue = 0 + + // Position depends + switch (self) { + case .top: + window.setFrame(.init( + origin: .init( + x: 0, + y: screen.frame.maxY), + size: .init( + width: screen.frame.width, + height: window.frame.height) + ), display: false) + } + } + + /// Set the final state for a window in this position. + func setFinal(in window: NSWindow, on screen: NSScreen) { + // We always end visible + window.alphaValue = 1 + + // Position depends + switch (self) { + case .top: + window.setFrame(.init( + origin: .init( + x: window.frame.origin.x, + y: screen.visibleFrame.maxY - window.frame.height), + size: window.frame.size + ), display: true) + } + } +} diff --git a/macos/Sources/Features/SlideTerminal/SlideTerminalWindow.swift b/macos/Sources/Features/SlideTerminal/SlideTerminalWindow.swift index f2eac6b4d..c170b3a8b 100644 --- a/macos/Sources/Features/SlideTerminal/SlideTerminalWindow.swift +++ b/macos/Sources/Features/SlideTerminal/SlideTerminalWindow.swift @@ -23,9 +23,6 @@ class SlideTerminalWindow: NSWindow { // and lets us render off screen. self.level = .popUpMenu - self.isMovableByWindowBackground = true - self.isMovable = true - self.collectionBehavior = [ // We want this to be part of every space because it is a singleton. .canJoinAllSpaces, diff --git a/macos/Sources/Features/Terminal/TerminalView.swift b/macos/Sources/Features/Terminal/TerminalView.swift index 248c09056..64ce37885 100644 --- a/macos/Sources/Features/Terminal/TerminalView.swift +++ b/macos/Sources/Features/Terminal/TerminalView.swift @@ -18,6 +18,7 @@ protocol TerminalViewDelegate: AnyObject { /// not called initially. func surfaceTreeDidChange() + /// This is called when a split is zoomed. func zoomStateDidChange(to: Bool) }