|
@@ -46,8 +46,6 @@ private final class TextEditorScrollView: NSScrollView {
|
|
|
|
|
|
|
|
private final class ThinCaretTextView: NSTextView {
|
|
private final class ThinCaretTextView: NSTextView {
|
|
|
private let caretWidth: CGFloat = 1
|
|
private let caretWidth: CGFloat = 1
|
|
|
- private let caretHeightBoost: CGFloat = 2
|
|
|
|
|
- private let caretVerticalOffset: CGFloat = 1
|
|
|
|
|
|
|
|
|
|
var placeholder: String = "" {
|
|
var placeholder: String = "" {
|
|
|
didSet {
|
|
didSet {
|
|
@@ -58,23 +56,39 @@ private final class ThinCaretTextView: NSTextView {
|
|
|
|
|
|
|
|
var placeholderColor: NSColor = .placeholderTextColor
|
|
var placeholderColor: NSColor = .placeholderTextColor
|
|
|
|
|
|
|
|
|
|
+ override func viewDidMoveToWindow() {
|
|
|
|
|
+ super.viewDidMoveToWindow()
|
|
|
|
|
+ // Opt into TextKit 1 so custom caret drawing is honored on modern macOS.
|
|
|
|
|
+ _ = layoutManager
|
|
|
|
|
+ hideSystemInsertionIndicator()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ override func becomeFirstResponder() -> Bool {
|
|
|
|
|
+ let becameFirstResponder = super.becomeFirstResponder()
|
|
|
|
|
+ hideSystemInsertionIndicator()
|
|
|
|
|
+ return becameFirstResponder
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ override func layout() {
|
|
|
|
|
+ super.layout()
|
|
|
|
|
+ hideSystemInsertionIndicator()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
override func drawInsertionPoint(in rect: NSRect, color: NSColor, turnedOn flag: Bool) {
|
|
override func drawInsertionPoint(in rect: NSRect, color: NSColor, turnedOn flag: Bool) {
|
|
|
var caretRect = rect
|
|
var caretRect = rect
|
|
|
caretRect.size.width = caretWidth
|
|
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)
|
|
super.drawInsertionPoint(in: caretRect, color: color, turnedOn: flag)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ override func setNeedsDisplay(_ rect: NSRect, avoidAdditionalLayout flag: Bool) {
|
|
|
|
|
+ var invalidRect = rect
|
|
|
|
|
+ invalidRect.size.width += caretWidth - 1
|
|
|
|
|
+ super.setNeedsDisplay(invalidRect, avoidAdditionalLayout: flag)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
override func draw(_ dirtyRect: NSRect) {
|
|
override func draw(_ dirtyRect: NSRect) {
|
|
|
- super.draw(dirtyRect)
|
|
|
|
|
drawPlaceholderIfNeeded()
|
|
drawPlaceholderIfNeeded()
|
|
|
|
|
+ super.draw(dirtyRect)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
override func didChangeText() {
|
|
override func didChangeText() {
|
|
@@ -82,6 +96,20 @@ private final class ThinCaretTextView: NSTextView {
|
|
|
needsDisplay = true
|
|
needsDisplay = true
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private func hideSystemInsertionIndicator() {
|
|
|
|
|
+ hideInsertionIndicators(in: self)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func hideInsertionIndicators(in view: NSView) {
|
|
|
|
|
+ for subview in view.subviews {
|
|
|
|
|
+ if #available(macOS 14.0, *),
|
|
|
|
|
+ let indicator = subview as? NSTextInsertionIndicator {
|
|
|
|
|
+ indicator.displayMode = .hidden
|
|
|
|
|
+ }
|
|
|
|
|
+ hideInsertionIndicators(in: subview)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private func drawPlaceholderIfNeeded() {
|
|
private func drawPlaceholderIfNeeded() {
|
|
|
guard string.isEmpty, !placeholder.isEmpty else { return }
|
|
guard string.isEmpty, !placeholder.isEmpty else { return }
|
|
|
|
|
|
|
@@ -131,6 +159,7 @@ struct ThinCaretTextEditor: NSViewRepresentable {
|
|
|
scrollView.autohidesScrollers = true
|
|
scrollView.autohidesScrollers = true
|
|
|
scrollView.borderType = .noBorder
|
|
scrollView.borderType = .noBorder
|
|
|
scrollView.drawsBackground = false
|
|
scrollView.drawsBackground = false
|
|
|
|
|
+ scrollView.focusRingType = .none
|
|
|
|
|
|
|
|
let textView = ThinCaretTextView()
|
|
let textView = ThinCaretTextView()
|
|
|
textView.isEditable = isEditable
|
|
textView.isEditable = isEditable
|
|
@@ -139,6 +168,7 @@ struct ThinCaretTextEditor: NSViewRepresentable {
|
|
|
textView.allowsUndo = true
|
|
textView.allowsUndo = true
|
|
|
textView.drawsBackground = false
|
|
textView.drawsBackground = false
|
|
|
textView.backgroundColor = .clear
|
|
textView.backgroundColor = .clear
|
|
|
|
|
+ textView.focusRingType = .none
|
|
|
textView.isVerticallyResizable = true
|
|
textView.isVerticallyResizable = true
|
|
|
textView.isHorizontallyResizable = false
|
|
textView.isHorizontallyResizable = false
|
|
|
textView.autoresizingMask = [.width]
|
|
textView.autoresizingMask = [.width]
|
|
@@ -158,6 +188,7 @@ struct ThinCaretTextEditor: NSViewRepresentable {
|
|
|
textView.isAutomaticTextReplacementEnabled = false
|
|
textView.isAutomaticTextReplacementEnabled = false
|
|
|
textView.font = NSFont.systemFont(ofSize: 14)
|
|
textView.font = NSFont.systemFont(ofSize: 14)
|
|
|
textView.textColor = NSColor(AppTheme.textPrimary)
|
|
textView.textColor = NSColor(AppTheme.textPrimary)
|
|
|
|
|
+ textView.insertionPointColor = NSColor(AppTheme.textPrimary)
|
|
|
textView.placeholder = placeholder
|
|
textView.placeholder = placeholder
|
|
|
textView.placeholderColor = NSColor(AppTheme.textMuted)
|
|
textView.placeholderColor = NSColor(AppTheme.textMuted)
|
|
|
textView.delegate = context.coordinator
|
|
textView.delegate = context.coordinator
|
|
@@ -185,6 +216,7 @@ struct ThinCaretTextEditor: NSViewRepresentable {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
textView.textColor = NSColor(AppTheme.textPrimary)
|
|
textView.textColor = NSColor(AppTheme.textPrimary)
|
|
|
|
|
+ textView.insertionPointColor = NSColor(AppTheme.textPrimary)
|
|
|
textView.font = NSFont.systemFont(ofSize: 14)
|
|
textView.font = NSFont.systemFont(ofSize: 14)
|
|
|
|
|
|
|
|
if let scrollView = scrollView as? TextEditorScrollView {
|
|
if let scrollView = scrollView as? TextEditorScrollView {
|
|
@@ -214,6 +246,7 @@ struct ThinCaretTextEditor: NSViewRepresentable {
|
|
|
@objc private func applyThemeColors() {
|
|
@objc private func applyThemeColors() {
|
|
|
guard let textView = textView as? ThinCaretTextView else { return }
|
|
guard let textView = textView as? ThinCaretTextView else { return }
|
|
|
textView.textColor = NSColor(AppTheme.textPrimary)
|
|
textView.textColor = NSColor(AppTheme.textPrimary)
|
|
|
|
|
+ textView.insertionPointColor = NSColor(AppTheme.textPrimary)
|
|
|
textView.placeholderColor = NSColor(AppTheme.textMuted)
|
|
textView.placeholderColor = NSColor(AppTheme.textMuted)
|
|
|
textView.needsDisplay = true
|
|
textView.needsDisplay = true
|
|
|
}
|
|
}
|