mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-17 13:02:42 +00:00
This will fix surface focus state is not consistent with first responder state when the search bar is open
223 lines
6.1 KiB
Swift
223 lines
6.1 KiB
Swift
import AppKit
|
|
import SwiftUI
|
|
|
|
extension NSView {
|
|
/// Returns true if this view is currently in the responder chain
|
|
var isInResponderChain: Bool {
|
|
var responder = window?.firstResponder
|
|
while let currentResponder = responder {
|
|
if currentResponder === self {
|
|
return true
|
|
}
|
|
responder = currentResponder.nextResponder
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
/// Returns true if this view is currently the first responder
|
|
var isFirstResponder: Bool {
|
|
window?.firstResponder === self
|
|
}
|
|
}
|
|
|
|
// MARK: Screenshot
|
|
|
|
extension NSView {
|
|
/// Take a screenshot of just this view.
|
|
func screenshot() -> NSImage? {
|
|
guard let bitmapRep = bitmapImageRepForCachingDisplay(in: bounds) else { return nil }
|
|
cacheDisplay(in: bounds, to: bitmapRep)
|
|
let image = NSImage(size: bounds.size)
|
|
image.addRepresentation(bitmapRep)
|
|
return image
|
|
}
|
|
|
|
func screenshot() -> Image? {
|
|
guard let nsImage: NSImage = self.screenshot() else { return nil }
|
|
return Image(nsImage: nsImage)
|
|
}
|
|
}
|
|
|
|
// MARK: View Traversal and Search
|
|
|
|
extension NSView {
|
|
/// Returns the absolute root view by walking up the superview chain.
|
|
var rootView: NSView {
|
|
var root: NSView = self
|
|
while let superview = root.superview {
|
|
root = superview
|
|
}
|
|
return root
|
|
}
|
|
|
|
/// Checks if a view contains another view in its hierarchy.
|
|
func contains(_ view: NSView) -> Bool {
|
|
if self == view {
|
|
return true
|
|
}
|
|
|
|
for subview in subviews where subview.contains(view) {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
/// Checks if the view contains the given class in its hierarchy.
|
|
func contains(className name: String) -> Bool {
|
|
if String(describing: type(of: self)) == name {
|
|
return true
|
|
}
|
|
|
|
for subview in subviews where subview.contains(className: name) {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
/// Finds the superview with the given class name.
|
|
func firstSuperview(withClassName name: String) -> NSView? {
|
|
guard let superview else { return nil }
|
|
if String(describing: type(of: superview)) == name {
|
|
return superview
|
|
}
|
|
|
|
return superview.firstSuperview(withClassName: name)
|
|
}
|
|
|
|
/// Recursively finds and returns the first descendant view that has the given class name.
|
|
func firstDescendant(withClassName name: String) -> NSView? {
|
|
for subview in subviews {
|
|
if String(describing: type(of: subview)) == name {
|
|
return subview
|
|
} else if let found = subview.firstDescendant(withClassName: name) {
|
|
return found
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
/// Recursively finds and returns descendant views that have the given class name.
|
|
func descendants(withClassName name: String) -> [NSView] {
|
|
var result = [NSView]()
|
|
|
|
for subview in subviews {
|
|
if String(describing: type(of: subview)) == name {
|
|
result.append(subview)
|
|
}
|
|
|
|
result += subview.descendants(withClassName: name)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
/// Recursively finds and returns the first descendant view that has the given identifier.
|
|
func firstDescendant(withID id: String) -> NSView? {
|
|
for subview in subviews {
|
|
if subview.identifier == NSUserInterfaceItemIdentifier(id) {
|
|
return subview
|
|
} else if let found = subview.firstDescendant(withID: id) {
|
|
return found
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
/// Finds and returns the first view with the given class name starting from the absolute root of the view hierarchy.
|
|
/// This includes private views like title bar views.
|
|
func firstViewFromRoot(withClassName name: String) -> NSView? {
|
|
let root = rootView
|
|
|
|
// Check if the root view itself matches
|
|
if String(describing: type(of: root)) == name {
|
|
return root
|
|
}
|
|
|
|
// Otherwise search descendants
|
|
return root.firstDescendant(withClassName: name)
|
|
}
|
|
}
|
|
|
|
// MARK: Debug
|
|
|
|
extension NSView {
|
|
/// Prints the view hierarchy from the root in a tree-like ASCII format.
|
|
///
|
|
/// I need this because the "Capture View Hierarchy" was broken under some scenarios in
|
|
/// Xcode 26 (FB17912569). But, I kept it around because it might be useful to print out
|
|
/// the view hierarchy without halting the program.
|
|
func printViewHierarchy() {
|
|
let root = rootView
|
|
print("View Hierarchy from Root:")
|
|
print(root.viewHierarchyDescription())
|
|
}
|
|
|
|
/// Returns a string representation of the view hierarchy in a tree-like format.
|
|
func viewHierarchyDescription(indent: String = "", isLast: Bool = true) -> String {
|
|
var result = ""
|
|
|
|
// Add the tree branch characters
|
|
result += indent
|
|
if !indent.isEmpty {
|
|
result += isLast ? "└── " : "├── "
|
|
}
|
|
|
|
// Add the class name and optional identifier
|
|
let className = String(describing: type(of: self))
|
|
result += className
|
|
|
|
// Add identifier if present
|
|
if let identifier = self.identifier {
|
|
result += " (id: \(identifier.rawValue))"
|
|
}
|
|
|
|
// Add frame info
|
|
result += " [frame: \(frame)]"
|
|
|
|
// Add visual properties
|
|
var properties: [String] = []
|
|
|
|
// Hidden status
|
|
if isHidden {
|
|
properties.append("hidden")
|
|
}
|
|
|
|
// Opaque status
|
|
properties.append(isOpaque ? "opaque" : "transparent")
|
|
|
|
// Layer backing
|
|
if wantsLayer {
|
|
properties.append("layer-backed")
|
|
if let bgColor = layer?.backgroundColor {
|
|
let color = NSColor(cgColor: bgColor)
|
|
if let rgb = color?.usingColorSpace(.deviceRGB) {
|
|
properties.append(String(format: "bg:rgba(%.0f,%.0f,%.0f,%.2f)",
|
|
rgb.redComponent * 255,
|
|
rgb.greenComponent * 255,
|
|
rgb.blueComponent * 255,
|
|
rgb.alphaComponent))
|
|
} else {
|
|
properties.append("bg:\(bgColor)")
|
|
}
|
|
}
|
|
}
|
|
|
|
result += " [\(properties.joined(separator: ", "))]"
|
|
result += "\n"
|
|
|
|
// Process subviews
|
|
for (index, subview) in subviews.enumerated() {
|
|
let isLastSubview = index == subviews.count - 1
|
|
let newIndent = indent + (isLast ? " " : "│ ")
|
|
result += subview.viewHierarchyDescription(indent: newIndent, isLast: isLastSubview)
|
|
}
|
|
|
|
return result
|
|
}
|
|
}
|