NSLayoutManager: Как заполнить фоновые цвета, где есть только визуализируемые глифы - PullRequest
0 голосов
/ 12 ноября 2018

Диспетчер компоновки по умолчанию заполняет цвет фона (указанный с помощью атрибута NSAttributedString .backgroundColor), где нет текста (кроме последней строки).

enter image description here

Мне удалось добиться желаемого эффекта путем подкласса NSLayoutManager и переопределения func drawBackground(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint) следующим образом:

override func drawBackground(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint) {
    guard let textContainer = textContainers.first, let textStorage = textStorage else { fatalError() }

    // This just takes the color of the first character assuming the entire container has the same background color.
    // To support ranges of different colours, you'll need to draw each glyph separately, querying the attributed string for the
    // background color attribute for the range of each character.
    guard textStorage.length > 0, let backgroundColor = textStorage.attribute(.backgroundColor, at: 0, effectiveRange: nil) as? UIColor else { return }

    var lineRects = [CGRect]()

    // create an array of line rects to be drawn.
    enumerateLineFragments(forGlyphRange: glyphsToShow) { (_, usedRect, _, range, _) in
        var usedRect = usedRect
        let locationOfLastGlyphInLine = NSMaxRange(range)-1
        // Remove the space at the end of each line (except last).
        if self.isThereAWhitespace(at: locationOfLastGlyphInLine) {
            let lastGlyphInLineWidth = self.boundingRect(forGlyphRange: NSRange(location: locationOfLastGlyphInLine, length: 1), in: textContainer).width
            usedRect.size.width -= lastGlyphInLineWidth
        }
        lineRects.append(usedRect)
    }

    lineRects = adjustRectsToContainerHeight(rects: lineRects, containerHeight: textContainer.size.height)

    for (lineNumber, lineRect) in lineRects.enumerated() {
        guard let context = UIGraphicsGetCurrentContext() else { return }
        context.saveGState()
        context.setFillColor(backgroundColor.cgColor)
        context.fill(lineRect)
        context.restoreGState()
    }
}

private func isThereAWhitespace(at location: Int) -> Bool {
    return propertyForGlyph(at: location) == NSLayoutManager.GlyphProperty.elastic
}

enter image description here

Однако,это не обрабатывает возможность наличия нескольких цветов, указанных диапазоном в атрибутной строке.Как я могу достичь этого?Я посмотрел на fillBackgroundRectArray с небольшим успехом.

1 Ответ

0 голосов
/ 04 января 2019

В качестве альтернативы вы можете вообще обойтись без использования атрибутов, например:

Итак, сначала я определил эту структуру:

struct HighlightBackground {
    let range: NSRange
    let color: NSColor
}

Затем в моем подклассе NSTextView:

var highlightBackgrounds = [HighlightBackground]()

override func setSelectedRanges(_ ranges: [NSValue], affinity: NSSelectionAffinity, stillSelecting stillSelectingFlag: Bool) {
    if stillSelectingFlag == false {
        return
    }

 // remove old ranges first
    highlightBackgrounds = highlightBackgrounds.filter { $0.color != .green }

    for value in ranges {
        let range = value.rangeValue

        highlightBackgrounds.append(HighlightBackground(range: range, color: .green))
    }

    super.setSelectedRanges(ranges, affinity: affinity, stillSelecting: stillSelectingFlag)
}

И затем вызовите это из вашего draw(_ rect: NSRect) метода:

func showBackgrounds() {
    guard
        let context = NSGraphicsContext.current?.cgContext,
        let lm = self.layoutManager
    else { return }

    context.saveGState()
    //        context.translateBy(x: origin.x, y: origin.y)

    for bg in highlightBackgrounds {
        bg.color.setFill()

        let glRange = lm.glyphRange(forCharacterRange: bg.range, actualCharacterRange: nil)    
        for rect in lm.rectsForGlyphRange(glRange) {    
            let path = NSBezierPath(roundedRect: rect, xRadius: selectedTextCornerRadius, yRadius: selectedTextCornerRadius)
            path.fill()
        }
    }

    context.restoreGState()
}

Наконец, вам понадобится это в вашем подклассе NSLayoutManager, хотя вы, вероятно, также можете поместить его в подкласс NSTextView:

func rectsForGlyphRange(_ glyphsToShow: NSRange) -> [NSRect] {

    var rects = [NSRect]()
    guard
        let tc = textContainer(forGlyphAt: glyphsToShow.location, effectiveRange: nil)
    else { return rects }

    enumerateLineFragments(forGlyphRange: glyphsToShow) { _, _, _, effectiveRange, _ in
        let rect = self.boundingRect(forGlyphRange: NSIntersectionRange(glyphsToShow, effectiveRange), in: tc)
        rects.append(rect)
    }

    return rects
}

Надеюсь, это работает и в вашем случае.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...