fix invisible text by replacing NSRulerView with integrated gutter drawing
This commit is contained in:
parent
5a715fa480
commit
695571e90e
|
|
@ -537,7 +537,6 @@ struct EditorView: View {
|
|||
EditorTextView(text: $state.documentText, evalResults: state.evalResults, onEvaluate: {
|
||||
state.evaluate()
|
||||
})
|
||||
.background(Color(ns: Theme.current.base))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -551,20 +550,20 @@ struct EditorTextView: NSViewRepresentable {
|
|||
}
|
||||
|
||||
func makeNSView(context: Context) -> NSScrollView {
|
||||
let scrollView = NSScrollView()
|
||||
scrollView.hasVerticalScroller = true
|
||||
scrollView.hasHorizontalScroller = false
|
||||
scrollView.autohidesScrollers = true
|
||||
scrollView.borderType = .noBorder
|
||||
let scrollView = NSTextView.scrollableTextView()
|
||||
let defaultTV = scrollView.documentView as! NSTextView
|
||||
|
||||
let textStorage = NSTextStorage()
|
||||
let layoutManager = MarkdownLayoutManager()
|
||||
let textContainer = NSTextContainer(containerSize: NSSize(width: 0, height: CGFloat.greatestFiniteMagnitude))
|
||||
textContainer.widthTracksTextView = true
|
||||
layoutManager.addTextContainer(textContainer)
|
||||
textStorage.addLayoutManager(layoutManager)
|
||||
// Build LineNumberTextView reusing the default text container
|
||||
let tc = defaultTV.textContainer!
|
||||
tc.replaceLayoutManager(MarkdownLayoutManager())
|
||||
|
||||
let textView = LineNumberTextView(frame: defaultTV.frame, textContainer: tc)
|
||||
textView.minSize = defaultTV.minSize
|
||||
textView.maxSize = defaultTV.maxSize
|
||||
textView.isVerticallyResizable = defaultTV.isVerticallyResizable
|
||||
textView.isHorizontallyResizable = defaultTV.isHorizontallyResizable
|
||||
textView.autoresizingMask = defaultTV.autoresizingMask
|
||||
|
||||
let textView = LineNumberTextView(frame: .zero, textContainer: textContainer)
|
||||
textView.isEditable = true
|
||||
textView.isSelectable = true
|
||||
textView.allowsUndo = true
|
||||
|
|
@ -585,33 +584,16 @@ struct EditorTextView: NSViewRepresentable {
|
|||
textView.smartInsertDeleteEnabled = false
|
||||
textView.isAutomaticLinkDetectionEnabled = false
|
||||
|
||||
textView.autoresizingMask = [.width]
|
||||
textView.isVerticallyResizable = true
|
||||
textView.isHorizontallyResizable = false
|
||||
textView.textContainer?.containerSize = NSSize(
|
||||
width: scrollView.contentSize.width,
|
||||
height: CGFloat.greatestFiniteMagnitude
|
||||
)
|
||||
textView.textContainerInset = NSSize(width: 4, height: 8)
|
||||
textView.textContainer?.widthTracksTextView = true
|
||||
textView.maxSize = NSSize(
|
||||
width: CGFloat.greatestFiniteMagnitude,
|
||||
height: CGFloat.greatestFiniteMagnitude
|
||||
)
|
||||
|
||||
textView.registerForDraggedTypes([.fileURL])
|
||||
|
||||
scrollView.documentView = textView
|
||||
|
||||
let ruler = LineNumberRulerView(textView: textView)
|
||||
ruler.evalResults = evalResults
|
||||
scrollView.verticalRulerView = ruler
|
||||
scrollView.hasVerticalRuler = true
|
||||
scrollView.rulersVisible = true
|
||||
|
||||
textView.string = text
|
||||
textView.evalResults = evalResults
|
||||
textView.delegate = context.coordinator
|
||||
context.coordinator.textView = textView
|
||||
context.coordinator.rulerView = ruler
|
||||
|
||||
if let ts = textView.textStorage {
|
||||
ts.beginEditing()
|
||||
|
|
@ -652,16 +634,13 @@ struct EditorTextView: NSViewRepresentable {
|
|||
.foregroundColor: Theme.current.text
|
||||
]
|
||||
|
||||
if let ruler = scrollView.verticalRulerView as? LineNumberRulerView {
|
||||
ruler.evalResults = evalResults
|
||||
ruler.needsDisplay = true
|
||||
}
|
||||
textView.evalResults = evalResults
|
||||
textView.needsDisplay = true
|
||||
}
|
||||
|
||||
class Coordinator: NSObject, NSTextViewDelegate {
|
||||
var parent: EditorTextView
|
||||
weak var textView: NSTextView?
|
||||
weak var rulerView: LineNumberRulerView?
|
||||
private var isUpdatingImages = false
|
||||
private var isUpdatingTables = false
|
||||
private var embeddedTableViews: [MarkdownTableView] = []
|
||||
|
|
@ -684,7 +663,7 @@ struct EditorTextView: NSViewRepresentable {
|
|||
]
|
||||
tv.selectedRanges = sel
|
||||
updateBlockRanges(for: tv)
|
||||
rulerView?.needsDisplay = true
|
||||
tv.needsDisplay = true
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.updateInlineImages()
|
||||
|
|
@ -1042,6 +1021,7 @@ private func highlightMarkdownLine(_ trimmed: String, line: String, lineRange: N
|
|||
if contentRange.length > 0 {
|
||||
let h2Font = NSFont.systemFont(ofSize: 18, weight: .bold)
|
||||
textStorage.addAttribute(.font, value: h2Font, range: contentRange)
|
||||
textStorage.addAttribute(.foregroundColor, value: palette.text, range: contentRange)
|
||||
}
|
||||
}
|
||||
return true
|
||||
|
|
@ -1056,6 +1036,7 @@ private func highlightMarkdownLine(_ trimmed: String, line: String, lineRange: N
|
|||
if contentRange.length > 0 {
|
||||
let h1Font = NSFont.systemFont(ofSize: 22, weight: .bold)
|
||||
textStorage.addAttribute(.font, value: h1Font, range: contentRange)
|
||||
textStorage.addAttribute(.foregroundColor, value: palette.text, range: contentRange)
|
||||
}
|
||||
}
|
||||
return true
|
||||
|
|
@ -1167,6 +1148,7 @@ private func highlightInlineMarkdown(textStorage: NSTextStorage, lineRange: NSRa
|
|||
textStorage.addAttribute(.foregroundColor, value: palette.overlay0, range: closeMarker)
|
||||
if contentRange.length > 0 {
|
||||
textStorage.addAttribute(.font, value: font, range: contentRange)
|
||||
textStorage.addAttribute(.foregroundColor, value: palette.text, range: contentRange)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1311,6 +1293,7 @@ private func highlightFencedCodeBlocks(textStorage: NSTextStorage, palette: Catp
|
|||
openFence = nil
|
||||
} else {
|
||||
textStorage.addAttribute(.font, value: monoFont, range: lineRange)
|
||||
textStorage.addAttribute(.foregroundColor, value: palette.text, range: lineRange)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1344,6 +1327,7 @@ private func highlightInlineCode(textStorage: NSTextStorage, lineRange: NSRange,
|
|||
textStorage.addAttribute(.foregroundColor, value: palette.overlay0, range: closeTick)
|
||||
if contentRange.length > 0 {
|
||||
textStorage.addAttribute(.font, value: monoFont, range: contentRange)
|
||||
textStorage.addAttribute(.foregroundColor, value: palette.text, range: contentRange)
|
||||
textStorage.addAttribute(.backgroundColor, value: palette.surface0, range: contentRange)
|
||||
}
|
||||
}
|
||||
|
|
@ -1398,6 +1382,7 @@ private func highlightTableLine(_ trimmed: String, lineRange: NSRange, textStora
|
|||
let boldMono = NSFontManager.shared.convert(monoFont, toHaveTrait: .boldFontMask)
|
||||
|
||||
textStorage.addAttribute(.font, value: monoFont, range: lineRange)
|
||||
textStorage.addAttribute(.foregroundColor, value: palette.text, range: lineRange)
|
||||
|
||||
if isSeparator {
|
||||
textStorage.addAttribute(.foregroundColor, value: palette.overlay0, range: lineRange)
|
||||
|
|
@ -1708,12 +1693,80 @@ private func resolveLocalImagePath(_ rawPath: String) -> String? {
|
|||
// MARK: - LineNumberTextView
|
||||
|
||||
class LineNumberTextView: NSTextView {
|
||||
static let gutterWidth: CGFloat = 50
|
||||
var evalResults: [Int: String] = [:]
|
||||
|
||||
override var textContainerOrigin: NSPoint {
|
||||
return NSPoint(x: LineNumberTextView.gutterWidth, y: textContainerInset.height)
|
||||
}
|
||||
|
||||
override func drawInsertionPoint(in rect: NSRect, color: NSColor, turnedOn flag: Bool) {
|
||||
var widened = rect
|
||||
widened.size.width = 2
|
||||
super.drawInsertionPoint(in: widened, color: color, turnedOn: flag)
|
||||
}
|
||||
|
||||
override func drawBackground(in rect: NSRect) {
|
||||
super.drawBackground(in: rect)
|
||||
|
||||
let origin = textContainerOrigin
|
||||
let gutterRect = NSRect(x: 0, y: rect.origin.y, width: LineNumberTextView.gutterWidth, height: rect.height)
|
||||
Theme.current.mantle.setFill()
|
||||
gutterRect.fill()
|
||||
|
||||
drawLineNumbers(origin: origin)
|
||||
}
|
||||
|
||||
private func drawLineNumbers(origin: NSPoint) {
|
||||
guard let lm = layoutManager, let tc = textContainer else { return }
|
||||
let palette = Theme.current
|
||||
let text = string as NSString
|
||||
guard text.length > 0 else { return }
|
||||
|
||||
var containerVisible = visibleRect
|
||||
containerVisible.origin.x -= origin.x
|
||||
containerVisible.origin.y -= origin.y
|
||||
let visibleGlyphs = lm.glyphRange(forBoundingRect: containerVisible, in: tc)
|
||||
let visibleChars = lm.characterRange(forGlyphRange: visibleGlyphs, actualGlyphRange: nil)
|
||||
|
||||
var lineNumber = 1
|
||||
var idx = 0
|
||||
while idx < visibleChars.location {
|
||||
if text.character(at: idx) == 0x0A { lineNumber += 1 }
|
||||
idx += 1
|
||||
}
|
||||
|
||||
let lineAttrs: [NSAttributedString.Key: Any] = [
|
||||
.font: Theme.gutterFont,
|
||||
.foregroundColor: palette.overlay0
|
||||
]
|
||||
let resultAttrs: [NSAttributedString.Key: Any] = [
|
||||
.font: Theme.gutterFont,
|
||||
.foregroundColor: palette.teal
|
||||
]
|
||||
|
||||
var charIndex = visibleChars.location
|
||||
while charIndex < NSMaxRange(visibleChars) {
|
||||
let lineRange = text.lineRange(for: NSRange(location: charIndex, length: 0))
|
||||
let lineGlyphRange = lm.glyphRange(forCharacterRange: lineRange, actualCharacterRange: nil)
|
||||
let lineRect = lm.boundingRect(forGlyphRange: lineGlyphRange, in: tc)
|
||||
let y = lineRect.origin.y + origin.y
|
||||
|
||||
if let result = evalResults[lineNumber - 1] {
|
||||
let resultStr = NSAttributedString(string: "\u{2192} \(result)", attributes: resultAttrs)
|
||||
let size = resultStr.size()
|
||||
resultStr.draw(at: NSPoint(x: LineNumberTextView.gutterWidth - size.width - 4, y: y))
|
||||
} else {
|
||||
let numStr = NSAttributedString(string: "\(lineNumber)", attributes: lineAttrs)
|
||||
let size = numStr.size()
|
||||
numStr.draw(at: NSPoint(x: LineNumberTextView.gutterWidth - size.width - 8, y: y))
|
||||
}
|
||||
|
||||
lineNumber += 1
|
||||
charIndex = NSMaxRange(lineRange)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Paste (image from clipboard)
|
||||
|
||||
override func paste(_ sender: Any?) {
|
||||
|
|
@ -1790,95 +1843,3 @@ class LineNumberTextView: NSTextView {
|
|||
}
|
||||
}
|
||||
|
||||
class LineNumberRulerView: NSRulerView {
|
||||
var evalResults: [Int: String] = [:]
|
||||
|
||||
private weak var editorTextView: NSTextView?
|
||||
|
||||
init(textView: NSTextView) {
|
||||
self.editorTextView = textView
|
||||
super.init(scrollView: textView.enclosingScrollView!, orientation: .verticalRuler)
|
||||
self.clientView = textView
|
||||
self.ruleThickness = 50
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(textDidChange(_:)),
|
||||
name: NSText.didChangeNotification,
|
||||
object: textView
|
||||
)
|
||||
}
|
||||
|
||||
required init(coder: NSCoder) {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
@objc private func textDidChange(_ notification: Notification) {
|
||||
needsDisplay = true
|
||||
}
|
||||
|
||||
override func drawHashMarksAndLabels(in rect: NSRect) {
|
||||
guard let tv = editorTextView,
|
||||
let layoutManager = tv.layoutManager,
|
||||
let textContainer = tv.textContainer else { return }
|
||||
|
||||
let palette = Theme.current
|
||||
|
||||
palette.mantle.setFill()
|
||||
rect.fill()
|
||||
|
||||
let visibleRect = scrollView!.contentView.bounds
|
||||
let visibleGlyphRange = layoutManager.glyphRange(
|
||||
forBoundingRect: visibleRect, in: textContainer
|
||||
)
|
||||
let visibleCharRange = layoutManager.characterRange(
|
||||
forGlyphRange: visibleGlyphRange, actualGlyphRange: nil
|
||||
)
|
||||
|
||||
let text = tv.string as NSString
|
||||
var lineNumber = 1
|
||||
var index = 0
|
||||
while index < visibleCharRange.location {
|
||||
if text.character(at: index) == 0x0A { lineNumber += 1 }
|
||||
index += 1
|
||||
}
|
||||
|
||||
let attrs: [NSAttributedString.Key: Any] = [
|
||||
.font: Theme.gutterFont,
|
||||
.foregroundColor: palette.overlay0
|
||||
]
|
||||
let resultAttrs: [NSAttributedString.Key: Any] = [
|
||||
.font: Theme.gutterFont,
|
||||
.foregroundColor: palette.teal
|
||||
]
|
||||
|
||||
var charIndex = visibleCharRange.location
|
||||
while charIndex < NSMaxRange(visibleCharRange) {
|
||||
let lineRange = text.lineRange(for: NSRange(location: charIndex, length: 0))
|
||||
let glyphRange = layoutManager.glyphRange(forCharacterRange: lineRange, actualCharacterRange: nil)
|
||||
var lineRect = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
|
||||
lineRect.origin.y += tv.textContainerInset.height - visibleRect.origin.y
|
||||
|
||||
if let result = evalResults[lineNumber - 1] {
|
||||
let resultStr = NSAttributedString(string: "→ \(result)", attributes: resultAttrs)
|
||||
let resultSize = resultStr.size()
|
||||
let resultPoint = NSPoint(
|
||||
x: ruleThickness - resultSize.width - 4,
|
||||
y: lineRect.origin.y
|
||||
)
|
||||
resultStr.draw(at: resultPoint)
|
||||
} else {
|
||||
let numStr = NSAttributedString(string: "\(lineNumber)", attributes: attrs)
|
||||
let size = numStr.size()
|
||||
let point = NSPoint(
|
||||
x: ruleThickness - size.width - 8,
|
||||
y: lineRect.origin.y
|
||||
)
|
||||
numStr.draw(at: point)
|
||||
}
|
||||
|
||||
lineNumber += 1
|
||||
charIndex = NSMaxRange(lineRange)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue