From 93b2fe60f828539adb3877a904bc20672d5ca597 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 22 Sep 2024 14:44:57 -0700 Subject: [PATCH] macos: start work on SlideTerminal, slides in window from top --- macos/Ghostty.xcodeproj/project.pbxproj | 20 ++++++ macos/Sources/App/macOS/AppDelegate.swift | 7 ++- .../Features/SlideTerminal/SlideTerminal.xib | 31 ++++++++++ .../SlideTerminalController.swift | 62 +++++++++++++++++++ .../SlideTerminal/SlideTerminalWindow.swift | 39 ++++++++++++ 5 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 macos/Sources/Features/SlideTerminal/SlideTerminal.xib create mode 100644 macos/Sources/Features/SlideTerminal/SlideTerminalController.swift create mode 100644 macos/Sources/Features/SlideTerminal/SlideTerminalWindow.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 9bff35757..f05c8e74c 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -62,6 +62,9 @@ A5CBD0582C9F30960017A1AE /* Cursor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD0572C9F30860017A1AE /* Cursor.swift */; }; A5CBD0592C9F37B10017A1AE /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFFE29C2410700646FDA /* Backport.swift */; }; A5CBD06B2CA322430017A1AE /* GlobalEventTap.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD06A2CA322320017A1AE /* GlobalEventTap.swift */; }; + 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 */; }; 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 */; }; @@ -133,6 +136,9 @@ A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraggableWindowView.swift; sourceTree = ""; }; A5CBD0572C9F30860017A1AE /* Cursor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cursor.swift; sourceTree = ""; }; A5CBD06A2CA322320017A1AE /* GlobalEventTap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalEventTap.swift; sourceTree = ""; }; + 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 = ""; }; 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 = ""; }; @@ -204,6 +210,7 @@ A5CBD0672CA2704E0017A1AE /* Global Keybinds */, A56D58872ACDE6BE00508D2C /* Services */, A59630982AEE1C4400D64628 /* Terminal */, + A5CBD05A2CA0C5910017A1AE /* SlideTerminal */, A5E112912AF73E4D00C6E0C2 /* ClipboardConfirmation */, A57D79252C9C8782001D522E /* Secure Input */, A534263E2A7DCC5800EBB7A2 /* Settings */, @@ -381,6 +388,16 @@ path = "Global Keybinds"; sourceTree = ""; }; + A5CBD05A2CA0C5910017A1AE /* SlideTerminal */ = { + isa = PBXGroup; + children = ( + A5CBD05B2CA0C5C70017A1AE /* SlideTerminal.xib */, + A5CBD05D2CA0C5E70017A1AE /* SlideTerminalController.swift */, + A5CBD05F2CA0C9080017A1AE /* SlideTerminalWindow.swift */, + ); + path = SlideTerminal; + sourceTree = ""; + }; A5CEAFDA29B8005900646FDA /* SplitView */ = { isa = PBXGroup; children = ( @@ -506,6 +523,7 @@ A5985CE62C33060F00C57AD3 /* man in Resources */, A5A1F8852A489D6800D1E8BC /* terminfo in Resources */, 552964E62B34A9B400030505 /* vim in Resources */, + A5CBD05C2CA0C5C70017A1AE /* SlideTerminal.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -533,6 +551,8 @@ A596309C2AEE1C9E00D64628 /* TerminalController.swift in Sources */, A5CC36152C9CDA06004D6760 /* View+Extension.swift in Sources */, A56D58892ACDE6CA00508D2C /* ServiceProvider.swift in Sources */, + A5CBD0602CA0C90A0017A1AE /* SlideTerminalWindow.swift in Sources */, + A5CBD05E2CA0C5EC0017A1AE /* SlideTerminalController.swift in Sources */, A51BFC222B2FB6B400E92F16 /* AboutView.swift in Sources */, A5278A9B2AA05B2600CD3039 /* Ghostty.Input.swift in Sources */, A5CBD0562C9E65B80017A1AE /* DraggableWindowView.swift in Sources */, diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index fc24345a8..5e520df00 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -156,6 +156,8 @@ class AppDelegate: NSObject, center.delegate = self } + var foo: SlideTerminalController? = nil + func applicationDidBecomeActive(_ notification: Notification) { guard !applicationHasBecomeActive else { return } applicationHasBecomeActive = true @@ -165,8 +167,11 @@ class AppDelegate: NSObject, // - if we're opening a URL since `application(_:openFile:)` is called before this. // - if we're restoring from persisted state if terminalManager.windows.count == 0 { - terminalManager.newWindow() + //terminalManager.newWindow() } + + foo = SlideTerminalController(window: nil) + foo?.showWindow(self) } func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { diff --git a/macos/Sources/Features/SlideTerminal/SlideTerminal.xib b/macos/Sources/Features/SlideTerminal/SlideTerminal.xib new file mode 100644 index 000000000..4bb068a7e --- /dev/null +++ b/macos/Sources/Features/SlideTerminal/SlideTerminal.xib @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Sources/Features/SlideTerminal/SlideTerminalController.swift b/macos/Sources/Features/SlideTerminal/SlideTerminalController.swift new file mode 100644 index 000000000..4bd183508 --- /dev/null +++ b/macos/Sources/Features/SlideTerminal/SlideTerminalController.swift @@ -0,0 +1,62 @@ +import Foundation +import Cocoa +import SwiftUI +import GhosttyKit + +/// Controller for the slide-style terminal. +class SlideTerminalController: NSWindowController { + override var windowNibName: NSNib.Name? { "SlideTerminal" } + + 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) + + slideWindowIn(window: window) + } + + private func slideWindowIn(window: NSWindow) { + 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 + + // Move it to the visible position since animation requires this + window.makeKeyAndOrderFront(nil) + + // Run the animation that moves our window into the proper place and makes + // it visible. + 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 + } + } +} + +enum SlideTerminalLocation { + case top +} diff --git a/macos/Sources/Features/SlideTerminal/SlideTerminalWindow.swift b/macos/Sources/Features/SlideTerminal/SlideTerminalWindow.swift new file mode 100644 index 000000000..f2eac6b4d --- /dev/null +++ b/macos/Sources/Features/SlideTerminal/SlideTerminalWindow.swift @@ -0,0 +1,39 @@ +import Cocoa + +class SlideTerminalWindow: NSWindow { + // Both of these must be true for windows without decorations to be able to + // still become key/main and receive events. + override var canBecomeKey: Bool { return true } + override var canBecomeMain: Bool { return true } + + override func awakeFromNib() { + super.awakeFromNib() + + // Note: almost all of this stuff can be done in the nib/xib directly + // but I prefer to do it programmatically because the properties we + // care about are less hidden. + + // Remove the title completely. This will make the window square. One + // downside is it also hides the cursor indications of resize but the + // window remains resizable. + self.styleMask.remove(.titled) + + // We need to set our window level to a high value. In testing, only + // popUpMenu and above do what we want. This gets it above the menu bar + // 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, + + // We don't want to be part of command-tilde + .ignoresCycle, + + // We never support fullscreen + .fullScreenNone] + } +}