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: {
|
EditorTextView(text: $state.documentText, evalResults: state.evalResults, onEvaluate: {
|
||||||
state.evaluate()
|
state.evaluate()
|
||||||
})
|
})
|
||||||
.background(Color(ns: Theme.current.base))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -551,20 +550,20 @@ struct EditorTextView: NSViewRepresentable {
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeNSView(context: Context) -> NSScrollView {
|
func makeNSView(context: Context) -> NSScrollView {
|
||||||
let scrollView = NSScrollView()
|
let scrollView = NSTextView.scrollableTextView()
|
||||||
scrollView.hasVerticalScroller = true
|
let defaultTV = scrollView.documentView as! NSTextView
|
||||||
scrollView.hasHorizontalScroller = false
|
|
||||||
scrollView.autohidesScrollers = true
|
|
||||||
scrollView.borderType = .noBorder
|
|
||||||
|
|
||||||
let textStorage = NSTextStorage()
|
// Build LineNumberTextView reusing the default text container
|
||||||
let layoutManager = MarkdownLayoutManager()
|
let tc = defaultTV.textContainer!
|
||||||
let textContainer = NSTextContainer(containerSize: NSSize(width: 0, height: CGFloat.greatestFiniteMagnitude))
|
tc.replaceLayoutManager(MarkdownLayoutManager())
|
||||||
textContainer.widthTracksTextView = true
|
|
||||||
layoutManager.addTextContainer(textContainer)
|
let textView = LineNumberTextView(frame: defaultTV.frame, textContainer: tc)
|
||||||
textStorage.addLayoutManager(layoutManager)
|
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.isEditable = true
|
||||||
textView.isSelectable = true
|
textView.isSelectable = true
|
||||||
textView.allowsUndo = true
|
textView.allowsUndo = true
|
||||||
|
|
@ -585,33 +584,16 @@ struct EditorTextView: NSViewRepresentable {
|
||||||
textView.smartInsertDeleteEnabled = false
|
textView.smartInsertDeleteEnabled = false
|
||||||
textView.isAutomaticLinkDetectionEnabled = false
|
textView.isAutomaticLinkDetectionEnabled = false
|
||||||
|
|
||||||
textView.autoresizingMask = [.width]
|
textView.textContainerInset = NSSize(width: 4, height: 8)
|
||||||
textView.isVerticallyResizable = true
|
|
||||||
textView.isHorizontallyResizable = false
|
|
||||||
textView.textContainer?.containerSize = NSSize(
|
|
||||||
width: scrollView.contentSize.width,
|
|
||||||
height: CGFloat.greatestFiniteMagnitude
|
|
||||||
)
|
|
||||||
textView.textContainer?.widthTracksTextView = true
|
textView.textContainer?.widthTracksTextView = true
|
||||||
textView.maxSize = NSSize(
|
|
||||||
width: CGFloat.greatestFiniteMagnitude,
|
|
||||||
height: CGFloat.greatestFiniteMagnitude
|
|
||||||
)
|
|
||||||
|
|
||||||
textView.registerForDraggedTypes([.fileURL])
|
textView.registerForDraggedTypes([.fileURL])
|
||||||
|
|
||||||
scrollView.documentView = textView
|
scrollView.documentView = textView
|
||||||
|
|
||||||
let ruler = LineNumberRulerView(textView: textView)
|
|
||||||
ruler.evalResults = evalResults
|
|
||||||
scrollView.verticalRulerView = ruler
|
|
||||||
scrollView.hasVerticalRuler = true
|
|
||||||
scrollView.rulersVisible = true
|
|
||||||
|
|
||||||
textView.string = text
|
textView.string = text
|
||||||
|
textView.evalResults = evalResults
|
||||||
textView.delegate = context.coordinator
|
textView.delegate = context.coordinator
|
||||||
context.coordinator.textView = textView
|
context.coordinator.textView = textView
|
||||||
context.coordinator.rulerView = ruler
|
|
||||||
|
|
||||||
if let ts = textView.textStorage {
|
if let ts = textView.textStorage {
|
||||||
ts.beginEditing()
|
ts.beginEditing()
|
||||||
|
|
@ -652,16 +634,13 @@ struct EditorTextView: NSViewRepresentable {
|
||||||
.foregroundColor: Theme.current.text
|
.foregroundColor: Theme.current.text
|
||||||
]
|
]
|
||||||
|
|
||||||
if let ruler = scrollView.verticalRulerView as? LineNumberRulerView {
|
textView.evalResults = evalResults
|
||||||
ruler.evalResults = evalResults
|
textView.needsDisplay = true
|
||||||
ruler.needsDisplay = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class Coordinator: NSObject, NSTextViewDelegate {
|
class Coordinator: NSObject, NSTextViewDelegate {
|
||||||
var parent: EditorTextView
|
var parent: EditorTextView
|
||||||
weak var textView: NSTextView?
|
weak var textView: NSTextView?
|
||||||
weak var rulerView: LineNumberRulerView?
|
|
||||||
private var isUpdatingImages = false
|
private var isUpdatingImages = false
|
||||||
private var isUpdatingTables = false
|
private var isUpdatingTables = false
|
||||||
private var embeddedTableViews: [MarkdownTableView] = []
|
private var embeddedTableViews: [MarkdownTableView] = []
|
||||||
|
|
@ -684,7 +663,7 @@ struct EditorTextView: NSViewRepresentable {
|
||||||
]
|
]
|
||||||
tv.selectedRanges = sel
|
tv.selectedRanges = sel
|
||||||
updateBlockRanges(for: tv)
|
updateBlockRanges(for: tv)
|
||||||
rulerView?.needsDisplay = true
|
tv.needsDisplay = true
|
||||||
|
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
self?.updateInlineImages()
|
self?.updateInlineImages()
|
||||||
|
|
@ -1042,6 +1021,7 @@ private func highlightMarkdownLine(_ trimmed: String, line: String, lineRange: N
|
||||||
if contentRange.length > 0 {
|
if contentRange.length > 0 {
|
||||||
let h2Font = NSFont.systemFont(ofSize: 18, weight: .bold)
|
let h2Font = NSFont.systemFont(ofSize: 18, weight: .bold)
|
||||||
textStorage.addAttribute(.font, value: h2Font, range: contentRange)
|
textStorage.addAttribute(.font, value: h2Font, range: contentRange)
|
||||||
|
textStorage.addAttribute(.foregroundColor, value: palette.text, range: contentRange)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
|
@ -1056,6 +1036,7 @@ private func highlightMarkdownLine(_ trimmed: String, line: String, lineRange: N
|
||||||
if contentRange.length > 0 {
|
if contentRange.length > 0 {
|
||||||
let h1Font = NSFont.systemFont(ofSize: 22, weight: .bold)
|
let h1Font = NSFont.systemFont(ofSize: 22, weight: .bold)
|
||||||
textStorage.addAttribute(.font, value: h1Font, range: contentRange)
|
textStorage.addAttribute(.font, value: h1Font, range: contentRange)
|
||||||
|
textStorage.addAttribute(.foregroundColor, value: palette.text, range: contentRange)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
|
@ -1167,6 +1148,7 @@ private func highlightInlineMarkdown(textStorage: NSTextStorage, lineRange: NSRa
|
||||||
textStorage.addAttribute(.foregroundColor, value: palette.overlay0, range: closeMarker)
|
textStorage.addAttribute(.foregroundColor, value: palette.overlay0, range: closeMarker)
|
||||||
if contentRange.length > 0 {
|
if contentRange.length > 0 {
|
||||||
textStorage.addAttribute(.font, value: font, range: contentRange)
|
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
|
openFence = nil
|
||||||
} else {
|
} else {
|
||||||
textStorage.addAttribute(.font, value: monoFont, range: lineRange)
|
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)
|
textStorage.addAttribute(.foregroundColor, value: palette.overlay0, range: closeTick)
|
||||||
if contentRange.length > 0 {
|
if contentRange.length > 0 {
|
||||||
textStorage.addAttribute(.font, value: monoFont, range: contentRange)
|
textStorage.addAttribute(.font, value: monoFont, range: contentRange)
|
||||||
|
textStorage.addAttribute(.foregroundColor, value: palette.text, range: contentRange)
|
||||||
textStorage.addAttribute(.backgroundColor, value: palette.surface0, 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)
|
let boldMono = NSFontManager.shared.convert(monoFont, toHaveTrait: .boldFontMask)
|
||||||
|
|
||||||
textStorage.addAttribute(.font, value: monoFont, range: lineRange)
|
textStorage.addAttribute(.font, value: monoFont, range: lineRange)
|
||||||
|
textStorage.addAttribute(.foregroundColor, value: palette.text, range: lineRange)
|
||||||
|
|
||||||
if isSeparator {
|
if isSeparator {
|
||||||
textStorage.addAttribute(.foregroundColor, value: palette.overlay0, range: lineRange)
|
textStorage.addAttribute(.foregroundColor, value: palette.overlay0, range: lineRange)
|
||||||
|
|
@ -1708,12 +1693,80 @@ private func resolveLocalImagePath(_ rawPath: String) -> String? {
|
||||||
// MARK: - LineNumberTextView
|
// MARK: - LineNumberTextView
|
||||||
|
|
||||||
class LineNumberTextView: NSTextView {
|
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) {
|
override func drawInsertionPoint(in rect: NSRect, color: NSColor, turnedOn flag: Bool) {
|
||||||
var widened = rect
|
var widened = rect
|
||||||
widened.size.width = 2
|
widened.size.width = 2
|
||||||
super.drawInsertionPoint(in: widened, color: color, turnedOn: flag)
|
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)
|
// MARK: - Paste (image from clipboard)
|
||||||
|
|
||||||
override func paste(_ sender: Any?) {
|
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