|
|
@@ -3,17 +3,75 @@ import SwiftUI
|
|
|
|
|
|
private final class ThinCaretTextView: NSTextView {
|
|
|
private let caretWidth: CGFloat = 1
|
|
|
+ private let caretHeightBoost: CGFloat = 2
|
|
|
+ private let caretVerticalOffset: CGFloat = 1
|
|
|
+
|
|
|
+ var placeholder: String = "" {
|
|
|
+ didSet {
|
|
|
+ guard placeholder != oldValue else { return }
|
|
|
+ needsDisplay = true
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ var placeholderColor: NSColor = .placeholderTextColor
|
|
|
|
|
|
override func drawInsertionPoint(in rect: NSRect, color: NSColor, turnedOn flag: Bool) {
|
|
|
var caretRect = rect
|
|
|
caretRect.size.width = caretWidth
|
|
|
caretRect.origin.x += (rect.width - caretWidth) / 2
|
|
|
+
|
|
|
+ if let font {
|
|
|
+ let caretHeight = font.ascender + abs(font.descender) + caretHeightBoost
|
|
|
+ caretRect.size.height = caretHeight
|
|
|
+ caretRect.origin.y = rect.origin.y + (rect.height - caretHeight) / 2 + caretVerticalOffset
|
|
|
+ }
|
|
|
+
|
|
|
super.drawInsertionPoint(in: caretRect, color: color, turnedOn: flag)
|
|
|
}
|
|
|
+
|
|
|
+ override func draw(_ dirtyRect: NSRect) {
|
|
|
+ super.draw(dirtyRect)
|
|
|
+ drawPlaceholderIfNeeded()
|
|
|
+ }
|
|
|
+
|
|
|
+ override func didChangeText() {
|
|
|
+ super.didChangeText()
|
|
|
+ needsDisplay = true
|
|
|
+ }
|
|
|
+
|
|
|
+ private func drawPlaceholderIfNeeded() {
|
|
|
+ guard string.isEmpty, !placeholder.isEmpty else { return }
|
|
|
+
|
|
|
+ let font = font ?? NSFont.systemFont(ofSize: 14)
|
|
|
+ let paragraphStyle = NSMutableParagraphStyle()
|
|
|
+ paragraphStyle.lineBreakMode = .byWordWrapping
|
|
|
+
|
|
|
+ let attributes: [NSAttributedString.Key: Any] = [
|
|
|
+ .font: font,
|
|
|
+ .foregroundColor: placeholderColor,
|
|
|
+ .paragraphStyle: paragraphStyle
|
|
|
+ ]
|
|
|
+
|
|
|
+ let horizontalInset = textContainerInset.width + (textContainer?.lineFragmentPadding ?? 0)
|
|
|
+ let verticalInset = textContainerInset.height
|
|
|
+ let drawRect = NSRect(
|
|
|
+ x: horizontalInset,
|
|
|
+ y: verticalInset,
|
|
|
+ width: bounds.width - horizontalInset - textContainerInset.width,
|
|
|
+ height: bounds.height - verticalInset - textContainerInset.height
|
|
|
+ )
|
|
|
+
|
|
|
+ placeholder.draw(
|
|
|
+ with: drawRect,
|
|
|
+ options: [.usesLineFragmentOrigin, .usesFontLeading],
|
|
|
+ attributes: attributes
|
|
|
+ )
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
struct ThinCaretTextEditor: NSViewRepresentable {
|
|
|
@Binding var text: String
|
|
|
+ var placeholder: String = ""
|
|
|
var isEditable: Bool = true
|
|
|
var onSubmit: (() -> Void)?
|
|
|
|
|
|
@@ -36,7 +94,15 @@ struct ThinCaretTextEditor: NSViewRepresentable {
|
|
|
textView.allowsUndo = true
|
|
|
textView.drawsBackground = false
|
|
|
textView.backgroundColor = .clear
|
|
|
+ textView.isVerticallyResizable = true
|
|
|
+ textView.isHorizontallyResizable = false
|
|
|
+ textView.autoresizingMask = [.width]
|
|
|
textView.textContainer?.lineFragmentPadding = 0
|
|
|
+ textView.textContainer?.widthTracksTextView = true
|
|
|
+ textView.textContainer?.containerSize = NSSize(
|
|
|
+ width: 0,
|
|
|
+ height: CGFloat.greatestFiniteMagnitude
|
|
|
+ )
|
|
|
textView.textContainerInset = NSSize(
|
|
|
width: AppTheme.textEditorHorizontalInset,
|
|
|
height: AppTheme.textEditorVerticalInset
|
|
|
@@ -46,6 +112,8 @@ struct ThinCaretTextEditor: NSViewRepresentable {
|
|
|
textView.isAutomaticTextReplacementEnabled = false
|
|
|
textView.font = NSFont.systemFont(ofSize: 14)
|
|
|
textView.textColor = NSColor(AppTheme.textPrimary)
|
|
|
+ textView.placeholder = placeholder
|
|
|
+ textView.placeholderColor = NSColor(AppTheme.textMuted)
|
|
|
textView.delegate = context.coordinator
|
|
|
textView.string = text
|
|
|
|
|
|
@@ -56,7 +124,15 @@ struct ThinCaretTextEditor: NSViewRepresentable {
|
|
|
|
|
|
func updateNSView(_ scrollView: NSScrollView, context: Context) {
|
|
|
guard let textView = scrollView.documentView as? ThinCaretTextView else { return }
|
|
|
+
|
|
|
textView.isEditable = isEditable
|
|
|
+ textView.placeholder = placeholder
|
|
|
+ textView.placeholderColor = NSColor(AppTheme.textMuted)
|
|
|
+ textView.textContainerInset = NSSize(
|
|
|
+ width: AppTheme.textEditorHorizontalInset,
|
|
|
+ height: AppTheme.textEditorVerticalInset
|
|
|
+ )
|
|
|
+
|
|
|
if textView.string != text {
|
|
|
textView.string = text
|
|
|
}
|
|
|
@@ -73,6 +149,7 @@ struct ThinCaretTextEditor: NSViewRepresentable {
|
|
|
func textDidChange(_ notification: Notification) {
|
|
|
guard let textView = notification.object as? NSTextView else { return }
|
|
|
parent.text = textView.string
|
|
|
+ textView.needsDisplay = true
|
|
|
}
|
|
|
|
|
|
func textView(_ textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
|