From d38fb7fac21de07cfe6ed038ea4fca03593f65fc Mon Sep 17 00:00:00 2001 From: Nucleic Date: Sat, 1 Aug 2026 04:21:00 -0700 Subject: [PATCH] Merge nucleic/dapper-velvet-badger-dtr3 into dev --- Sources/NucleicApp/ChatInputField.swift | 28 +++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/Sources/NucleicApp/ChatInputField.swift b/Sources/NucleicApp/ChatInputField.swift index cd96f553..026148aa 100644 --- a/Sources/NucleicApp/ChatInputField.swift +++ b/Sources/NucleicApp/ChatInputField.swift @@ -266,6 +266,15 @@ struct ChatInputField: NSViewRepresentable { /// so the fitted height can be recomputed for the new text wrapping. final class HeightTrackingScrollView: NSScrollView { var onLayout: (() -> Void)? + /// `NSTextView` installs the I-beam directly when it handles a click. In a normal AppKit + /// hierarchy the next cursor rect under the pointer replaces it as soon as the pointer leaves, + /// but the SwiftUI views surrounding this representable do not consistently install an arrow + /// rect. That is especially visible in Home's tall, otherwise-empty hero field: the I-beam can + /// remain current across the whole window until AppKit's next cursor-rect rebuild. + /// + /// Track the scroll view rather than its document view so this covers the field's exact visible + /// bounds even when an empty `NSTextView` is shorter than the Home composer's minimum height. + private var cursorExitTrackingArea: NSTrackingArea? /// Force overlay-style scrollers, rejecting the system's legacy style ("Show scroll /// bars: Always", common with a mouse attached). A legacy scroller consumes layout @@ -283,6 +292,25 @@ final class HeightTrackingScrollView: NSScrollView { set { _ = newValue; super.scrollerStyle = .overlay } } + override func updateTrackingAreas() { + super.updateTrackingAreas() + if let cursorExitTrackingArea { removeTrackingArea(cursorExitTrackingArea) } + let area = NSTrackingArea( + rect: .zero, + options: [.mouseEnteredAndExited, .activeInKeyWindow, .inVisibleRect], + owner: self, + userInfo: nil) + addTrackingArea(area) + cursorExitTrackingArea = area + } + + override func mouseExited(with event: NSEvent) { + // `set()` is intentional: the text view also sets (rather than pushes) its cursor, so + // there is no balanced cursor-stack entry to pop here. + NSCursor.arrow.set() + super.mouseExited(with: event) + } + override func layout() { super.layout() onLayout?()