native macOS codings agent orchestrator prowl.onev.cat
Something went wrong. Try again.
Swift
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165import AppKitimport SwiftUI
struct PlainTextEditor: NSViewRepresentable { @Binding var text: String var isMonospaced: Bool = false var shouldFocus: Bool = false var placeholder: String? var hidesPlaceholderWhenFocused: Bool = false
func makeCoordinator() -> Coordinator { Coordinator(text: $text) }
func makeNSView(context: Context) -> NSScrollView { let textView = PlaceholderTextView(frame: .zero) textView.delegate = context.coordinator textView.drawsBackground = false textView.isRichText = false textView.importsGraphics = false textView.isAutomaticDashSubstitutionEnabled = false textView.isAutomaticQuoteSubstitutionEnabled = false textView.isAutomaticTextReplacementEnabled = false textView.font = editorFont textView.textContainerInset = NSSize(width: 4, height: 6) textView.minSize = NSSize(width: 0, height: 0) textView.maxSize = NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude) textView.isVerticallyResizable = true textView.isHorizontallyResizable = false textView.autoresizingMask = [.width] textView.textContainer?.containerSize = NSSize(width: 0, height: CGFloat.greatestFiniteMagnitude) textView.textContainer?.widthTracksTextView = true textView.string = text textView.placeholder = placeholder textView.hidesPlaceholderWhenFocused = hidesPlaceholderWhenFocused
let scrollView = NSScrollView(frame: .zero) scrollView.drawsBackground = false scrollView.borderType = .noBorder scrollView.hasVerticalScroller = true scrollView.hasHorizontalScroller = false scrollView.autohidesScrollers = true scrollView.documentView = textView return scrollView }
func updateNSView(_ nsView: NSScrollView, context: Context) { guard let textView = nsView.documentView as? PlaceholderTextView else { return } // Sync from state, but skip stale echoes of an in-flight user edit: resetting to an // older value would clobber newer text and jump the caret (#608). if textView.string == text { context.coordinator.lastPushedText = nil } else if textView.string != context.coordinator.lastPushedText { let selectedRange = textView.selectedRange() textView.string = text let selectedLocation = min(selectedRange.location, text.utf16.count) let selectedLength = min(selectedRange.length, text.utf16.count - selectedLocation) textView.setSelectedRange(NSRange(location: selectedLocation, length: selectedLength)) textView.needsDisplay = true } let updatedFont = editorFont if textView.font != updatedFont { textView.font = updatedFont textView.needsDisplay = true } textView.placeholder = placeholder textView.hidesPlaceholderWhenFocused = hidesPlaceholderWhenFocused if shouldFocus, textView.window?.firstResponder !== textView { textView.window?.makeFirstResponder(textView) } }
private var editorFont: NSFont { if isMonospaced { return NSFont.monospacedSystemFont(ofSize: NSFont.systemFontSize, weight: .regular) } return NSFont.preferredFont(forTextStyle: .body) }
final class Coordinator: NSObject, NSTextViewDelegate { @Binding var text: String
/// Last text pushed to the binding from a user edit; cleared by `updateNSView` /// once the value round-trips back from state. var lastPushedText: String?
init(text: Binding<String>) { _text = text }
func textDidChange(_ notification: Notification) { guard let textView = notification.object as? PlaceholderTextView else { return } lastPushedText = textView.string text = textView.string textView.needsDisplay = true } }
final class PlaceholderTextView: NSTextView { var placeholder: String? { didSet { needsDisplay = true } } var hidesPlaceholderWhenFocused: Bool = true { didSet { needsDisplay = true } }
override func becomeFirstResponder() -> Bool { let didBecomeFirstResponder = super.becomeFirstResponder() if didBecomeFirstResponder { needsDisplay = true } return didBecomeFirstResponder }
override func resignFirstResponder() -> Bool { let didResignFirstResponder = super.resignFirstResponder() if didResignFirstResponder { needsDisplay = true } return didResignFirstResponder }
override func draw(_ dirtyRect: NSRect) { super.draw(dirtyRect) drawPlaceholderIfNeeded() }
private func drawPlaceholderIfNeeded() { guard let placeholder, !placeholder.isEmpty, string.isEmpty else { return }
if hidesPlaceholderWhenFocused, window?.firstResponder === self { return }
let lineFragmentPadding = textContainer?.lineFragmentPadding ?? 0 let horizontalInset = textContainerInset.width + lineFragmentPadding let verticalInset = textContainerInset.height - 2 let placeholderRect = NSRect( x: bounds.minX + horizontalInset, y: bounds.minY + verticalInset, width: max(0, bounds.width - horizontalInset - textContainerInset.width), height: max(0, bounds.height - verticalInset) ) let placeholderAttributes: [NSAttributedString.Key: Any] = [ .foregroundColor: NSColor.placeholderTextColor, .font: font ?? NSFont.preferredFont(forTextStyle: .body), ] (placeholder as NSString).draw(in: placeholderRect, withAttributes: placeholderAttributes) } }}