import AppKit import ByreAPI import SwiftUI @MainActor final class PaneFocus { private let api: API private weak var window: NSWindow? private var paneFrame: CGRect = .zero private var drafts: Set = [] private var inside = false private var focused = false private(set) var drafting = false private var sending: Task? private var observers: [NotificationCenter.ObservationToken] = [] private var responder: NSKeyValueObservation? private var clicks: Any? init(api: API) { self.api = api } func attach(_ window: NSWindow) { if self.window === window { return } log.notice("pane focus: attach window=\(ObjectIdentifier(window).debugDescription, privacy: .public) title=\(window.title, privacy: .public)") detach() self.window = window let center = NotificationCenter.default observers = [ center.addObserver(of: window, for: .didBecomeKey) { [weak self] _ in self?.changed(NSWindow.didBecomeKeyNotification) }, center.addObserver(of: window, for: .didResignKey) { [weak self] _ in self?.changed(NSWindow.didResignKeyNotification) }, center.addObserver(of: NSApplication.self, for: .didBecomeActive) { [weak self] _ in self?.changed(NSApplication.didBecomeActiveNotification) }, center.addObserver(of: NSApplication.self, for: .didResignActive) { [weak self] _ in self?.changed(NSApplication.didResignActiveNotification) }, ] responder = window.observe(\.firstResponder, options: [.new]) { [weak self] _, _ in Task { @MainActor in self?.responderChanged() } } clicks = NSEvent.addLocalMonitorForEvents(matching: .leftMouseDown) { [weak self] event in let win = event.window.map(ObjectIdentifier.init) let point = event.locationInWindow MainActor.assumeIsolated { if let self, let window = self.window, win == ObjectIdentifier(window) { self.clicked(point) } } return event } responderChanged() } func detach() { log.notice("pane focus: detach window=\(self.window.map { ObjectIdentifier($0).debugDescription } ?? "nil", privacy: .public) observers=\(self.observers.count)") for o in observers { NotificationCenter.default.removeObserver(o) } observers = [] responder = nil if let clicks { NSEvent.removeMonitor(clicks) } clicks = nil window = nil } func setFrame(_ frame: CGRect) { paneFrame = frame } func setDraft(_ id: String, _ nonEmpty: Bool) { if nonEmpty { drafts.insert(id) } else { drafts.remove(id) } update() } func reconnected() { if focused { send() } } func leave() async { detach() if !focused { return } focused = false await sending?.value do { _ = try await api.client.setPaneFocus(body: .json(.init(focused: false, drafting: drafting))).noContent } catch { log.error("set pane focus: \(errorText(error), privacy: .public)") } } private func contentPoint(_ rect: CGRect, in view: NSView) -> CGPoint? { guard let content = window?.contentView else { return nil } let r = content.convert(rect, from: view) return CGPoint(x: r.midX, y: content.isFlipped ? r.midY : content.bounds.height - r.midY) } private func changed(_ name: Notification.Name) { logFocus(name.rawValue) update() } private func responderChanged() { guard let view = window?.firstResponder as? NSView, view !== window?.contentView else { update() return } if let p = contentPoint(view.bounds, in: view) { inside = paneFrame.contains(p) log.notice("pane focus: responder \(String(describing: type(of: view)), privacy: .public) point=\(p.debugDescription, privacy: .public) frame=\(self.paneFrame.debugDescription, privacy: .public) inside=\(self.inside)") } update() } private func clicked(_ location: CGPoint) { guard let content = window?.contentView else { return } let p = content.convert(location, from: nil) inside = paneFrame.contains(CGPoint(x: p.x, y: content.isFlipped ? p.y : content.bounds.height - p.y)) log.notice("pane focus: click point=\(p.debugDescription, privacy: .public) flipped=\(content.isFlipped) frame=\(self.paneFrame.debugDescription, privacy: .public) inside=\(self.inside)") DispatchQueue.main.async { [weak self] in self?.responderChanged() } } private func update() { let now = (window?.isKeyWindow ?? false) && NSApp.isActive && inside let draft = !drafts.isEmpty let attached = window.map { ObjectIdentifier($0).debugDescription } ?? "nil" let keyWindow = NSApp.keyWindow.map { ObjectIdentifier($0).debugDescription } ?? "nil" log.notice("pane focus: update window=\(attached, privacy: .public) keyWindow=\(keyWindow, privacy: .public) key=\(self.window?.isKeyWindow ?? false) active=\(NSApp.isActive) inside=\(self.inside) now=\(now) draft=\(draft) focused=\(self.focused) drafting=\(self.drafting)") if now == focused && draft == drafting { return } focused = now drafting = draft send() } private func send() { let body = Components.Schemas.PaneFocus(focused: focused, drafting: drafting) let previous = sending sending = Task { [api] in await previous?.value do { _ = try await api.client.setPaneFocus(body: .json(body)).noContent log.notice("pane focus: sent focused=\(body.focused) drafting=\(body.drafting)") } catch { log.error("set pane focus: \(errorText(error), privacy: .public)") } } } } struct WindowReader: NSViewRepresentable { let found: (NSWindow) -> Void func makeNSView(context: Context) -> NSView { let view = NSView() DispatchQueue.main.async { if let w = unsafe view.window { found(w) } } return view } func updateNSView(_ nsView: NSView, context: Context) { DispatchQueue.main.async { if let w = unsafe nsView.window { found(w) } } } }