mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-17 04:52:47 +00:00
83 lines
3.5 KiB
Swift
83 lines
3.5 KiB
Swift
import AppKit
|
||
|
||
extension NSWindow {
|
||
/// Get the CGWindowID type for the window (used for low level CoreGraphics APIs).
|
||
var cgWindowId: CGWindowID? {
|
||
// "If the window doesn’t have a window device, the value of this
|
||
// property is equal to or less than 0." - Docs. In practice I've
|
||
// found this is true if a window is not visible.
|
||
guard windowNumber > 0 else { return nil }
|
||
return CGWindowID(windowNumber)
|
||
}
|
||
|
||
/// Adjusts the window frame if necessary to ensure the window remains visible on screen.
|
||
/// This constrains both the size (to not exceed the screen) and the origin (to keep the window on screen).
|
||
func constrainToScreen() {
|
||
guard let screen = screen ?? NSScreen.main else { return }
|
||
let visibleFrame = screen.visibleFrame
|
||
var windowFrame = frame
|
||
|
||
windowFrame.size.width = min(windowFrame.size.width, visibleFrame.size.width)
|
||
windowFrame.size.height = min(windowFrame.size.height, visibleFrame.size.height)
|
||
|
||
windowFrame.origin.x = max(visibleFrame.minX,
|
||
min(windowFrame.origin.x, visibleFrame.maxX - windowFrame.width))
|
||
windowFrame.origin.y = max(visibleFrame.minY,
|
||
min(windowFrame.origin.y, visibleFrame.maxY - windowFrame.height))
|
||
|
||
if windowFrame != frame {
|
||
setFrame(windowFrame, display: true)
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: Native Tabbing
|
||
|
||
extension NSWindow {
|
||
/// True if this is the first window in the tab group.
|
||
var isFirstWindowInTabGroup: Bool {
|
||
guard let firstWindow = tabGroup?.windows.first else { return true }
|
||
return firstWindow === self
|
||
}
|
||
}
|
||
|
||
/// Native tabbing private API usage. :(
|
||
extension NSWindow {
|
||
var titlebarView: NSView? {
|
||
// In normal window, `NSTabBar` typically appears as a subview of `NSTitlebarView` within `NSThemeFrame`.
|
||
// In fullscreen, the system creates a dedicated fullscreen window and the view hierarchy changes;
|
||
// in that case, the `titlebarView` is only accessible via a reference on `NSThemeFrame`.
|
||
// ref: https://github.com/mozilla-firefox/firefox/blob/054e2b072785984455b3b59acad9444ba1eeffb4/widget/cocoa/nsCocoaWindow.mm#L7205
|
||
guard let themeFrameView = contentView?.rootView else { return nil }
|
||
guard themeFrameView.responds(to: Selector(("titlebarView"))) else { return nil }
|
||
return themeFrameView.value(forKey: "titlebarView") as? NSView
|
||
}
|
||
|
||
/// Returns the [private] NSTabBar view, if it exists.
|
||
var tabBarView: NSView? {
|
||
titlebarView?.firstDescendant(withClassName: "NSTabBar")
|
||
}
|
||
|
||
/// Returns the index of the tab button at the given screen point, if any.
|
||
func tabIndex(atScreenPoint screenPoint: NSPoint) -> Int? {
|
||
guard let tabBarView else { return nil }
|
||
let locationInWindow = convertPoint(fromScreen: screenPoint)
|
||
let locationInTabBar = tabBarView.convert(locationInWindow, from: nil)
|
||
guard tabBarView.bounds.contains(locationInTabBar) else { return nil }
|
||
|
||
// Find all tab buttons and sort by x position to get visual order.
|
||
// The view hierarchy order doesn't match the visual tab order.
|
||
let tabItemViews = tabBarView.descendants(withClassName: "NSTabButton")
|
||
.sorted { $0.frame.origin.x < $1.frame.origin.x }
|
||
|
||
for (index, tabItemView) in tabItemViews.enumerated() {
|
||
let locationInTab = tabItemView.convert(locationInWindow, from: nil)
|
||
if tabItemView.bounds.contains(locationInTab) {
|
||
return index
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
}
|