import AppKit import ObjectiveC #if DEBUG import Bonsplit #endif private var cmuxWindowTerminalPortalKey: UInt8 = 0 private var cmuxWindowTerminalPortalCloseObserverKey: UInt8 = 0 #if DEBUG private func portalDebugToken(_ view: NSView?) -> String { guard let view else { return "nil" } let ptr = Unmanaged.passUnretained(view).toOpaque() return String(describing: ptr) } private func portalDebugFrame(_ rect: NSRect) -> String { String(format: "%.1f,%.1f %.1fx%.1f", rect.origin.x, rect.origin.y, rect.size.width, rect.size.height) } #endif final class WindowTerminalHostView: NSView { override var isOpaque: Bool { false } #if DEBUG private var lastDragRouteSignature: String? #endif override func hitTest(_ point: NSPoint) -> NSView? { if shouldPassThroughToSplitDivider(at: point) { return nil } let dragPasteboardTypes = NSPasteboard(name: .drag).types let eventType = NSApp.currentEvent?.type let shouldPassThrough = DragOverlayRoutingPolicy.shouldPassThroughPortalHitTesting( pasteboardTypes: dragPasteboardTypes, eventType: eventType ) if shouldPassThrough { #if DEBUG logDragRouteDecision( passThrough: true, eventType: eventType, pasteboardTypes: dragPasteboardTypes, hitView: nil ) #endif return nil } let hitView = super.hitTest(point) #if DEBUG logDragRouteDecision( passThrough: false, eventType: eventType, pasteboardTypes: dragPasteboardTypes, hitView: hitView ) #endif return hitView === self ? nil : hitView } private func shouldPassThroughToSplitDivider(at point: NSPoint) -> Bool { guard let window else { return false } let windowPoint = convert(point, to: nil) guard let rootView = window.contentView else { return false } return Self.containsSplitDivider(at: windowPoint, in: rootView) } private static func containsSplitDivider(at windowPoint: NSPoint, in view: NSView) -> Bool { guard !view.isHidden else { return false } if let splitView = view as? NSSplitView { let pointInSplit = splitView.convert(windowPoint, from: nil) if splitView.bounds.contains(pointInSplit) { // Keep divider interactions reliable even when portal-hosted terminal frames // temporarily overlap divider edges during rapid layout churn. let expansion: CGFloat = 5 let dividerCount = max(0, splitView.arrangedSubviews.count - 1) for dividerIndex in 0.. 1, second.width > 1 else { continue } let x = max(0, first.maxX) dividerRect = NSRect( x: x, y: 0, width: thickness, height: splitView.bounds.height ) } else { guard first.height > 1, second.height > 1 else { continue } let y = max(0, first.maxY) dividerRect = NSRect( x: 0, y: y, width: splitView.bounds.width, height: thickness ) } let expandedDividerRect = dividerRect.insetBy(dx: -expansion, dy: -expansion) if expandedDividerRect.contains(pointInSplit) { return true } } } } for subview in view.subviews.reversed() { if containsSplitDivider(at: windowPoint, in: subview) { return true } } return false } #if DEBUG private func logDragRouteDecision( passThrough: Bool, eventType: NSEvent.EventType?, pasteboardTypes: [NSPasteboard.PasteboardType]?, hitView: NSView? ) { let hasRelevantTypes = DragOverlayRoutingPolicy.hasBonsplitTabTransfer(pasteboardTypes) || DragOverlayRoutingPolicy.hasSidebarTabReorder(pasteboardTypes) guard passThrough || hasRelevantTypes else { return } let targetClass = hitView.map { NSStringFromClass(type(of: $0)) } ?? "nil" let signature = [ passThrough ? "1" : "0", debugEventName(eventType), debugPasteboardTypes(pasteboardTypes), targetClass, ].joined(separator: "|") guard lastDragRouteSignature != signature else { return } lastDragRouteSignature = signature dlog( "portal.dragRoute passThrough=\(passThrough ? 1 : 0) " + "event=\(debugEventName(eventType)) target=\(targetClass) " + "types=\(debugPasteboardTypes(pasteboardTypes))" ) } private func debugPasteboardTypes(_ types: [NSPasteboard.PasteboardType]?) -> String { guard let types, !types.isEmpty else { return "-" } return types.map(\.rawValue).joined(separator: ",") } private func debugEventName(_ eventType: NSEvent.EventType?) -> String { guard let eventType else { return "none" } switch eventType { case .cursorUpdate: return "cursorUpdate" case .appKitDefined: return "appKitDefined" case .systemDefined: return "systemDefined" case .applicationDefined: return "applicationDefined" case .periodic: return "periodic" case .mouseMoved: return "mouseMoved" case .mouseEntered: return "mouseEntered" case .mouseExited: return "mouseExited" case .flagsChanged: return "flagsChanged" case .leftMouseDragged: return "leftMouseDragged" case .rightMouseDragged: return "rightMouseDragged" case .otherMouseDragged: return "otherMouseDragged" case .leftMouseDown: return "leftMouseDown" case .leftMouseUp: return "leftMouseUp" case .rightMouseDown: return "rightMouseDown" case .rightMouseUp: return "rightMouseUp" case .otherMouseDown: return "otherMouseDown" case .otherMouseUp: return "otherMouseUp" default: return "other(\(eventType.rawValue))" } } #endif } @MainActor final class WindowTerminalPortal: NSObject { private weak var window: NSWindow? private let hostView = WindowTerminalHostView(frame: .zero) private weak var installedContainerView: NSView? private weak var installedReferenceView: NSView? private var installConstraints: [NSLayoutConstraint] = [] private var hasDeferredFullSyncScheduled = false private struct Entry { weak var hostedView: GhosttySurfaceScrollView? weak var anchorView: NSView? var visibleInUI: Bool var zPriority: Int } private var entriesByHostedId: [ObjectIdentifier: Entry] = [:] private var hostedByAnchorId: [ObjectIdentifier: ObjectIdentifier] = [:] init(window: NSWindow) { self.window = window super.init() hostView.wantsLayer = false hostView.translatesAutoresizingMaskIntoConstraints = false _ = ensureInstalled() } @discardableResult private func ensureInstalled() -> Bool { guard let window else { return false } guard let (container, reference) = installationTarget(for: window) else { return false } if hostView.superview !== container || installedContainerView !== container || installedReferenceView !== reference { NSLayoutConstraint.deactivate(installConstraints) installConstraints.removeAll() hostView.removeFromSuperview() container.addSubview(hostView, positioned: .above, relativeTo: reference) installConstraints = [ hostView.leadingAnchor.constraint(equalTo: reference.leadingAnchor), hostView.trailingAnchor.constraint(equalTo: reference.trailingAnchor), hostView.topAnchor.constraint(equalTo: reference.topAnchor), hostView.bottomAnchor.constraint(equalTo: reference.bottomAnchor), ] NSLayoutConstraint.activate(installConstraints) installedContainerView = container installedReferenceView = reference } else if !Self.isView(hostView, above: reference, in: container) { container.addSubview(hostView, positioned: .above, relativeTo: reference) } // Keep the drag/mouse forwarding overlay above portal-hosted terminal views. if let overlay = objc_getAssociatedObject(window, &fileDropOverlayKey) as? NSView, overlay.superview === container, !Self.isView(overlay, above: hostView, in: container) { container.addSubview(overlay, positioned: .above, relativeTo: hostView) } return true } private func installationTarget(for window: NSWindow) -> (container: NSView, reference: NSView)? { guard let contentView = window.contentView else { return nil } // If NSGlassEffectView wraps the original content view, install inside the glass view // so terminals are above the glass background but below SwiftUI content. if contentView.className == "NSGlassEffectView", let foreground = contentView.subviews.first(where: { $0 !== hostView }) { return (contentView, foreground) } guard let themeFrame = contentView.superview else { return nil } return (themeFrame, contentView) } private static func isHiddenOrAncestorHidden(_ view: NSView) -> Bool { if view.isHidden { return true } var current = view.superview while let v = current { if v.isHidden { return true } current = v.superview } return false } private static func rectApproximatelyEqual(_ lhs: NSRect, _ rhs: NSRect, epsilon: CGFloat = 0.5) -> Bool { abs(lhs.origin.x - rhs.origin.x) <= epsilon && abs(lhs.origin.y - rhs.origin.y) <= epsilon && abs(lhs.size.width - rhs.size.width) <= epsilon && abs(lhs.size.height - rhs.size.height) <= epsilon } private static func isView(_ view: NSView, above reference: NSView, in container: NSView) -> Bool { guard let viewIndex = container.subviews.firstIndex(of: view), let referenceIndex = container.subviews.firstIndex(of: reference) else { return false } return viewIndex > referenceIndex } func detachHostedView(withId hostedId: ObjectIdentifier) { guard let entry = entriesByHostedId.removeValue(forKey: hostedId) else { return } if let anchor = entry.anchorView { hostedByAnchorId.removeValue(forKey: ObjectIdentifier(anchor)) } #if DEBUG let hadSuperview = (entry.hostedView?.superview === hostView) ? 1 : 0 dlog( "portal.detach hosted=\(portalDebugToken(entry.hostedView)) " + "anchor=\(portalDebugToken(entry.anchorView)) hadSuperview=\(hadSuperview)" ) #endif if let hostedView = entry.hostedView, hostedView.superview === hostView { hostedView.removeFromSuperview() } } /// Hide a portal entry without detaching it. Updates visibleInUI to false and /// sets isHidden = true so subsequent synchronizeHostedView calls keep it hidden. /// Used when a workspace is permanently unmounted (vs. transient bonsplit dismantles). func hideEntry(forHostedId hostedId: ObjectIdentifier) { guard var entry = entriesByHostedId[hostedId] else { return } guard entry.visibleInUI else { return } entry.visibleInUI = false entriesByHostedId[hostedId] = entry entry.hostedView?.isHidden = true #if DEBUG dlog("portal.hideEntry hosted=\(portalDebugToken(entry.hostedView)) reason=workspaceUnmount") #endif } /// Update the visibleInUI flag on an existing entry without rebinding. /// Used when a deferred bind is pending — this ensures synchronizeHostedView /// won't hide a view that updateNSView has already marked as visible. func updateEntryVisibility(forHostedId hostedId: ObjectIdentifier, visibleInUI: Bool) { guard var entry = entriesByHostedId[hostedId] else { return } entry.visibleInUI = visibleInUI entriesByHostedId[hostedId] = entry } func bind(hostedView: GhosttySurfaceScrollView, to anchorView: NSView, visibleInUI: Bool, zPriority: Int = 0) { guard ensureInstalled() else { return } let hostedId = ObjectIdentifier(hostedView) let anchorId = ObjectIdentifier(anchorView) let previousEntry = entriesByHostedId[hostedId] if let previousHostedId = hostedByAnchorId[anchorId], previousHostedId != hostedId { #if DEBUG let previousToken = entriesByHostedId[previousHostedId] .map { portalDebugToken($0.hostedView) } ?? String(describing: previousHostedId) dlog( "portal.bind.replace anchor=\(portalDebugToken(anchorView)) " + "oldHosted=\(previousToken) newHosted=\(portalDebugToken(hostedView))" ) #endif detachHostedView(withId: previousHostedId) } if let oldEntry = entriesByHostedId[hostedId], let oldAnchor = oldEntry.anchorView, oldAnchor !== anchorView { hostedByAnchorId.removeValue(forKey: ObjectIdentifier(oldAnchor)) } hostedByAnchorId[anchorId] = hostedId entriesByHostedId[hostedId] = Entry( hostedView: hostedView, anchorView: anchorView, visibleInUI: visibleInUI, zPriority: zPriority ) let didChangeAnchor: Bool = { guard let previousAnchor = previousEntry?.anchorView else { return true } return previousAnchor !== anchorView }() let becameVisible = (previousEntry?.visibleInUI ?? false) == false && visibleInUI let priorityIncreased = zPriority > (previousEntry?.zPriority ?? Int.min) #if DEBUG if previousEntry == nil || didChangeAnchor || becameVisible || priorityIncreased || hostedView.superview !== hostView { dlog( "portal.bind hosted=\(portalDebugToken(hostedView)) " + "anchor=\(portalDebugToken(anchorView)) prevAnchor=\(portalDebugToken(previousEntry?.anchorView)) " + "visible=\(visibleInUI ? 1 : 0) prevVisible=\((previousEntry?.visibleInUI ?? false) ? 1 : 0) " + "z=\(zPriority) prevZ=\(previousEntry?.zPriority ?? Int.min)" ) } #endif if hostedView.superview !== hostView { #if DEBUG dlog( "portal.reparent hosted=\(portalDebugToken(hostedView)) " + "reason=attach super=\(portalDebugToken(hostedView.superview))" ) #endif hostView.addSubview(hostedView, positioned: .above, relativeTo: nil) } else if (becameVisible || priorityIncreased), hostView.subviews.last !== hostedView { // Refresh z-order only when a view becomes visible or gets a higher priority. // Anchor-only churn is common during split tree updates; forcing remove/add there // causes transient inWindow=0 -> 1 bounces that can flash black. #if DEBUG dlog( "portal.reparent hosted=\(portalDebugToken(hostedView)) reason=raise " + "didChangeAnchor=\(didChangeAnchor ? 1 : 0) becameVisible=\(becameVisible ? 1 : 0) " + "priorityIncreased=\(priorityIncreased ? 1 : 0)" ) #endif hostView.addSubview(hostedView, positioned: .above, relativeTo: nil) } synchronizeHostedView(withId: hostedId) pruneDeadEntries() } func synchronizeHostedViewForAnchor(_ anchorView: NSView) { pruneDeadEntries() let anchorId = ObjectIdentifier(anchorView) let primaryHostedId = hostedByAnchorId[anchorId] if let primaryHostedId { synchronizeHostedView(withId: primaryHostedId) } // Failsafe: during aggressive divider drags/structural churn, one anchor can miss a // geometry callback while another fires. Reconcile all mapped hosted views so no stale // frame remains "stuck" onscreen until the next interaction. synchronizeAllHostedViews(excluding: primaryHostedId) scheduleDeferredFullSynchronizeAll() } private func scheduleDeferredFullSynchronizeAll() { guard !hasDeferredFullSyncScheduled else { return } hasDeferredFullSyncScheduled = true DispatchQueue.main.async { [weak self] in guard let self else { return } self.hasDeferredFullSyncScheduled = false self.synchronizeAllHostedViews(excluding: nil) } } private func synchronizeAllHostedViews(excluding hostedIdToSkip: ObjectIdentifier?) { guard ensureInstalled() else { return } pruneDeadEntries() let hostedIds = Array(entriesByHostedId.keys) for hostedId in hostedIds { if hostedId == hostedIdToSkip { continue } synchronizeHostedView(withId: hostedId) } } private func synchronizeHostedView(withId hostedId: ObjectIdentifier) { guard ensureInstalled() else { return } guard let entry = entriesByHostedId[hostedId] else { return } guard let hostedView = entry.hostedView else { entriesByHostedId.removeValue(forKey: hostedId) return } guard let anchorView = entry.anchorView, let window else { // Only hide if the entry is not marked visibleInUI. When a workspace is // remounting, updateNSView sets visibleInUI=true before the deferred bind // provides an anchor — hiding here would race with that and cause a flash. if !entry.visibleInUI { #if DEBUG if !hostedView.isHidden { dlog("portal.hidden hosted=\(portalDebugToken(hostedView)) value=1 reason=missingAnchorOrWindow") } #endif hostedView.isHidden = true } return } guard anchorView.window === window else { #if DEBUG if !hostedView.isHidden { dlog( "portal.hidden hosted=\(portalDebugToken(hostedView)) value=1 " + "reason=anchorWindowMismatch anchorWindow=\(portalDebugToken(anchorView.window?.contentView))" ) } #endif hostedView.isHidden = true return } let frameInWindow = anchorView.convert(anchorView.bounds, to: nil) let frameInHost = hostView.convert(frameInWindow, from: nil) let hasFiniteFrame = frameInHost.origin.x.isFinite && frameInHost.origin.y.isFinite && frameInHost.size.width.isFinite && frameInHost.size.height.isFinite let anchorHidden = Self.isHiddenOrAncestorHidden(anchorView) let tinyFrame = frameInHost.width <= 1 || frameInHost.height <= 1 let outsideHostBounds = !frameInHost.intersects(hostView.bounds) let shouldHide = !entry.visibleInUI || anchorHidden || tinyFrame || !hasFiniteFrame || outsideHostBounds let oldFrame = hostedView.frame #if DEBUG let collapsedToTiny = oldFrame.width > 1 && oldFrame.height > 1 && tinyFrame let restoredFromTiny = (oldFrame.width <= 1 || oldFrame.height <= 1) && !tinyFrame if collapsedToTiny { dlog( "portal.frame.collapse hosted=\(portalDebugToken(hostedView)) anchor=\(portalDebugToken(anchorView)) " + "old=\(portalDebugFrame(oldFrame)) new=\(portalDebugFrame(frameInHost))" ) } else if restoredFromTiny { dlog( "portal.frame.restore hosted=\(portalDebugToken(hostedView)) anchor=\(portalDebugToken(anchorView)) " + "old=\(portalDebugFrame(oldFrame)) new=\(portalDebugFrame(frameInHost))" ) } #endif if !Self.rectApproximatelyEqual(oldFrame, frameInHost) { CATransaction.begin() CATransaction.setDisableActions(true) hostedView.frame = frameInHost CATransaction.commit() if abs(oldFrame.size.width - frameInHost.size.width) > 0.5 || abs(oldFrame.size.height - frameInHost.size.height) > 0.5 { hostedView.reconcileGeometryNow() } } if hostedView.isHidden != shouldHide { #if DEBUG dlog( "portal.hidden hosted=\(portalDebugToken(hostedView)) value=\(shouldHide ? 1 : 0) " + "visibleInUI=\(entry.visibleInUI ? 1 : 0) anchorHidden=\(anchorHidden ? 1 : 0) " + "tiny=\(tinyFrame ? 1 : 0) finite=\(hasFiniteFrame ? 1 : 0) " + "outside=\(outsideHostBounds ? 1 : 0) frame=\(portalDebugFrame(frameInHost))" ) #endif hostedView.isHidden = shouldHide } } private func pruneDeadEntries() { let currentWindow = window let deadHostedIds = entriesByHostedId.compactMap { hostedId, entry -> ObjectIdentifier? in guard entry.hostedView != nil else { return hostedId } guard let anchor = entry.anchorView else { return hostedId } if anchor.window !== currentWindow || anchor.superview == nil { return hostedId } if let reference = installedReferenceView, !anchor.isDescendant(of: reference) { return hostedId } return nil } for hostedId in deadHostedIds { detachHostedView(withId: hostedId) } let validAnchorIds = Set(entriesByHostedId.compactMap { _, entry in entry.anchorView.map { ObjectIdentifier($0) } }) hostedByAnchorId = hostedByAnchorId.filter { validAnchorIds.contains($0.key) } } func hostedIds() -> Set { Set(entriesByHostedId.keys) } func tearDown() { for hostedId in Array(entriesByHostedId.keys) { detachHostedView(withId: hostedId) } NSLayoutConstraint.deactivate(installConstraints) installConstraints.removeAll() hostView.removeFromSuperview() installedContainerView = nil installedReferenceView = nil } #if DEBUG func debugEntryCount() -> Int { entriesByHostedId.count } func debugHostedSubviewCount() -> Int { hostView.subviews.count } #endif func viewAtWindowPoint(_ windowPoint: NSPoint) -> NSView? { guard ensureInstalled() else { return nil } let point = hostView.convert(windowPoint, from: nil) // Restrict hit-testing to currently mapped entries so stale detached views // can't steal file-drop/mouse routing. for subview in hostView.subviews.reversed() { guard let hostedView = subview as? GhosttySurfaceScrollView else { continue } let hostedId = ObjectIdentifier(hostedView) guard entriesByHostedId[hostedId] != nil else { continue } guard !hostedView.isHidden else { continue } guard hostedView.frame.contains(point) else { continue } let localPoint = hostedView.convert(point, from: hostView) return hostedView.hitTest(localPoint) ?? hostedView } return nil } func terminalViewAtWindowPoint(_ windowPoint: NSPoint) -> GhosttyNSView? { guard ensureInstalled() else { return nil } let point = hostView.convert(windowPoint, from: nil) for subview in hostView.subviews.reversed() { guard let hostedView = subview as? GhosttySurfaceScrollView else { continue } let hostedId = ObjectIdentifier(hostedView) guard entriesByHostedId[hostedId] != nil else { continue } guard !hostedView.isHidden else { continue } guard hostedView.frame.contains(point) else { continue } let localPoint = hostedView.convert(point, from: hostView) if let terminal = hostedView.terminalViewForDrop(at: localPoint) { return terminal } } return nil } } @MainActor enum TerminalWindowPortalRegistry { private static var portalsByWindowId: [ObjectIdentifier: WindowTerminalPortal] = [:] private static var hostedToWindowId: [ObjectIdentifier: ObjectIdentifier] = [:] private static func installWindowCloseObserverIfNeeded(for window: NSWindow) { guard objc_getAssociatedObject(window, &cmuxWindowTerminalPortalCloseObserverKey) == nil else { return } let windowId = ObjectIdentifier(window) let observer = NotificationCenter.default.addObserver( forName: NSWindow.willCloseNotification, object: window, queue: .main ) { [weak window] _ in MainActor.assumeIsolated { if let window { removePortal(for: window) } else { removePortal(windowId: windowId, window: nil) } } } objc_setAssociatedObject( window, &cmuxWindowTerminalPortalCloseObserverKey, observer, .OBJC_ASSOCIATION_RETAIN_NONATOMIC ) } private static func removePortal(for window: NSWindow) { removePortal(windowId: ObjectIdentifier(window), window: window) } private static func removePortal(windowId: ObjectIdentifier, window: NSWindow?) { if let portal = portalsByWindowId.removeValue(forKey: windowId) { portal.tearDown() } hostedToWindowId = hostedToWindowId.filter { $0.value != windowId } guard let window else { return } if let observer = objc_getAssociatedObject(window, &cmuxWindowTerminalPortalCloseObserverKey) { NotificationCenter.default.removeObserver(observer) } objc_setAssociatedObject(window, &cmuxWindowTerminalPortalCloseObserverKey, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) objc_setAssociatedObject(window, &cmuxWindowTerminalPortalKey, nil, .OBJC_ASSOCIATION_RETAIN) } private static func pruneHostedMappings(for windowId: ObjectIdentifier, validHostedIds: Set) { hostedToWindowId = hostedToWindowId.filter { hostedId, mappedWindowId in mappedWindowId != windowId || validHostedIds.contains(hostedId) } } private static func portal(for window: NSWindow) -> WindowTerminalPortal { if let existing = objc_getAssociatedObject(window, &cmuxWindowTerminalPortalKey) as? WindowTerminalPortal { portalsByWindowId[ObjectIdentifier(window)] = existing installWindowCloseObserverIfNeeded(for: window) return existing } let portal = WindowTerminalPortal(window: window) objc_setAssociatedObject(window, &cmuxWindowTerminalPortalKey, portal, .OBJC_ASSOCIATION_RETAIN) portalsByWindowId[ObjectIdentifier(window)] = portal installWindowCloseObserverIfNeeded(for: window) return portal } static func bind(hostedView: GhosttySurfaceScrollView, to anchorView: NSView, visibleInUI: Bool, zPriority: Int = 0) { guard let window = anchorView.window else { return } let windowId = ObjectIdentifier(window) let hostedId = ObjectIdentifier(hostedView) let nextPortal = portal(for: window) if let oldWindowId = hostedToWindowId[hostedId], oldWindowId != windowId { portalsByWindowId[oldWindowId]?.detachHostedView(withId: hostedId) } nextPortal.bind(hostedView: hostedView, to: anchorView, visibleInUI: visibleInUI, zPriority: zPriority) hostedToWindowId[hostedId] = windowId pruneHostedMappings(for: windowId, validHostedIds: nextPortal.hostedIds()) } static func synchronizeForAnchor(_ anchorView: NSView) { guard let window = anchorView.window else { return } let portal = portal(for: window) portal.synchronizeHostedViewForAnchor(anchorView) } static func hideHostedView(_ hostedView: GhosttySurfaceScrollView) { let hostedId = ObjectIdentifier(hostedView) guard let windowId = hostedToWindowId[hostedId], let portal = portalsByWindowId[windowId] else { return } portal.hideEntry(forHostedId: hostedId) } /// Update the visibleInUI flag on an existing portal entry without rebinding. /// Called when a bind is deferred (host not yet in window) to prevent stale /// portal syncs from hiding a view that is about to become visible. static func updateEntryVisibility(for hostedView: GhosttySurfaceScrollView, visibleInUI: Bool) { let hostedId = ObjectIdentifier(hostedView) guard let windowId = hostedToWindowId[hostedId], let portal = portalsByWindowId[windowId] else { return } portal.updateEntryVisibility(forHostedId: hostedId, visibleInUI: visibleInUI) } static func viewAtWindowPoint(_ windowPoint: NSPoint, in window: NSWindow) -> NSView? { let portal = portal(for: window) return portal.viewAtWindowPoint(windowPoint) } static func terminalViewAtWindowPoint(_ windowPoint: NSPoint, in window: NSWindow) -> GhosttyNSView? { let portal = portal(for: window) return portal.terminalViewAtWindowPoint(windowPoint) } #if DEBUG static func debugPortalCount() -> Int { portalsByWindowId.count } #endif }