Files
Macaw/Source/render/TextRenderer.swift

176 lines
5.9 KiB
Swift

import Foundation
#if os(iOS)
import UIKit
#elseif os(OSX)
import AppKit
#endif
class TextRenderer: NodeRenderer {
var text: Text
override var node: Node {
return text
}
init(text: Text, view: DrawingView?, parentRenderer: GroupRenderer? = nil) {
self.text = text
super.init(node: text, view: view, parentRenderer: parentRenderer)
}
deinit {
dispose()
}
override func doAddObservers() {
super.doAddObservers()
observe(text.textVar)
observe(text.fontVar)
observe(text.fillVar)
observe(text.strokeVar)
observe(text.alignVar)
observe(text.baselineVar)
observe(text.kerningVar)
}
override func doRender(in context: CGContext, force: Bool, opacity: Double, coloringMode: ColoringMode = .rgb) {
let message = text.text
let font = getMFont()
// positive NSBaselineOffsetAttributeName values don't work, couldn't find why
// for now move the rect itself
var attributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: font]
var hasFill = false
if var color = text.fill as? Color {
color = RenderUtils.applyOpacity(color, opacity: opacity)
attributes[NSAttributedString.Key.foregroundColor] = getTextColor(color)
hasFill = true
}
if let stroke = text.stroke {
if let c = stroke.fill as? Color {
attributes[NSAttributedString.Key.strokeColor] = getTextColor(c)
}
var width = stroke.width
if hasFill {
// To use fill and stroke at the same time width should be negative:
// https://developer.apple.com/library/archive/qa/qa1531/_index.html
width *= -1
}
attributes[NSAttributedString.Key.strokeWidth] = width as NSObject?
}
if text.kerning != 0.0 {
attributes[NSAttributedString.Key.kern] = NSNumber(value: text.kerning)
}
if attributes.count > 1 {
MGraphicsPushContext(context)
message.draw(in: getBounds(font), withAttributes: attributes)
MGraphicsPopContext()
}
}
override func doFindNodeAt(path: NodePath, ctx: CGContext) -> NodePath? {
guard let contains = node.bounds?.toCG().contains(path.location) else {
return .none
}
if contains {
return path
}
return .none
}
fileprivate func getMFont() -> MFont {
guard #available(iOS 9.0, macOS 10.11, *) else {
// This case should never happen, since the deployment target is set to iOS 9.0/macOS 10.11.
// However it is needed for the Swift Package Manager to work accordingly.
return MFont()
}
guard let textFont = text.font else {
return MFont.systemFont(ofSize: MFont.mSystemFontSize)
}
if let customFont = RenderUtils.loadFont(name: textFont.name, size: textFont.size, weight: textFont.weight) {
return customFont
} else {
if let weight = getWeight(textFont.weight) {
return MFont.systemFont(ofSize: CGFloat(textFont.size), weight: weight)
}
return MFont.systemFont(ofSize: CGFloat(textFont.size))
}
}
fileprivate func getWeight(_ weight: String) -> MFont.Weight? {
guard #available(iOS 9.0, macOS 10.11, *) else {
// This case should never happen, since the deployment target is set to iOS 9.0/macOS 10.11.
// However it is needed for the Swift Package Manager to work accordingly.
return .none
}
switch weight {
case "normal":
return MFont.Weight.regular
case "bold":
return MFont.Weight.bold
case "bolder":
return MFont.Weight.semibold
case "lighter":
return MFont.Weight.light
default:
return .none
}
}
fileprivate func getBounds(_ font: MFont) -> CGRect {
var textAttributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: font]
if text.kerning != 0.0 {
textAttributes[NSAttributedString.Key.kern] = NSNumber(value: text.kerning)
}
if let stroke = text.stroke {
textAttributes[NSAttributedString.Key.strokeWidth] = NSNumber(value: stroke.width)
}
let textSize = NSString(string: text.text).size(withAttributes: textAttributes)
return CGRect(x: calculateAlignmentOffset(text, font: font),
y: calculateBaselineOffset(text, font: font),
width: CGFloat(textSize.width), height: CGFloat(textSize.height))
}
fileprivate func calculateBaselineOffset(_ text: Text, font: MFont) -> CGFloat {
var baselineOffset = CGFloat(0)
switch text.baseline {
case Baseline.alphabetic:
baselineOffset = font.ascender
case Baseline.bottom:
baselineOffset = font.ascender - font.descender
case Baseline.mid:
baselineOffset = (font.ascender - font.descender) / 2
default:
break
}
return -baselineOffset
}
fileprivate func calculateAlignmentOffset(_ text: Text, font: MFont) -> CGFloat {
let textAttributes = [
NSAttributedString.Key.font: font
]
let textSize = NSString(string: text.text).size(withAttributes: textAttributes)
return -CGFloat(text.align.align(size: textSize.width.doubleValue))
}
fileprivate func getTextColor(_ fill: Fill) -> MColor {
if let color = fill as? Color {
#if os(iOS)
return MColor(cgColor: color.toCG())
#elseif os(OSX)
return MColor(cgColor: color.toCG()) ?? .black
#endif
}
return MColor.black
}
}