import Foundation import UIKit import AsyncDisplayKit import Display private final class MultiScaleTextStateNode: ASDisplayNode { let textNode: ImmediateTextNode var currentLayout: MultiScaleTextLayout? override init() { self.textNode = ImmediateTextNode() self.textNode.displaysAsynchronously = false super.init() self.addSubnode(self.textNode) } } public final class MultiScaleTextState { public struct Attributes { public var font: UIFont public var color: UIColor public init(font: UIFont, color: UIColor) { self.font = font self.color = color } } public let attributes: Attributes public let constrainedSize: CGSize public init(attributes: Attributes, constrainedSize: CGSize) { self.attributes = attributes self.constrainedSize = constrainedSize } } public struct MultiScaleTextLayout { public var size: CGSize public init(size: CGSize) { self.size = size } } public final class MultiScaleTextNode: ASDisplayNode { private let stateNodes: [AnyHashable: MultiScaleTextStateNode] public init(stateKeys: [AnyHashable]) { self.stateNodes = Dictionary(stateKeys.map { ($0, MultiScaleTextStateNode()) }, uniquingKeysWith: { lhs, _ in lhs }) super.init() for (_, node) in self.stateNodes { self.addSubnode(node) } } public func stateNode(forKey key: AnyHashable) -> ASDisplayNode? { return self.stateNodes[key]?.textNode } public func updateTintColor(color: UIColor, transition: ContainedViewLayoutTransition) { for (_, node) in self.stateNodes { transition.updateTintColor(layer: node.textNode.layer, color: color) } } public func updateLayout(text: String, states: [AnyHashable: MultiScaleTextState], mainState: AnyHashable) -> [AnyHashable: MultiScaleTextLayout] { assert(Set(states.keys) == Set(self.stateNodes.keys)) assert(states[mainState] != nil) var result: [AnyHashable: MultiScaleTextLayout] = [:] var mainLayout: MultiScaleTextLayout? for (key, state) in states { if let node = self.stateNodes[key] { node.textNode.attributedText = NSAttributedString(string: text, font: state.attributes.font, textColor: state.attributes.color) node.textNode.isAccessibilityElement = true node.textNode.accessibilityLabel = text let nodeSize = node.textNode.updateLayout(state.constrainedSize) let nodeLayout = MultiScaleTextLayout(size: nodeSize) if key == mainState { mainLayout = nodeLayout } node.currentLayout = nodeLayout result[key] = nodeLayout } } if let mainLayout = mainLayout { let mainBounds = CGRect(origin: CGPoint(x: -mainLayout.size.width / 2.0, y: -mainLayout.size.height / 2.0), size: mainLayout.size) for (key, _) in states { if let node = self.stateNodes[key], let nodeLayout = result[key] { node.textNode.frame = CGRect(origin: CGPoint(x: mainBounds.minX, y: mainBounds.minY + floor((mainBounds.height - nodeLayout.size.height) / 2.0)), size: nodeLayout.size) } } } return result } public func update(stateFractions: [AnyHashable: CGFloat], alpha: CGFloat = 1.0, transition: ContainedViewLayoutTransition) { var fractionSum: CGFloat = 0.0 for (_, fraction) in stateFractions { fractionSum += fraction } for (key, fraction) in stateFractions { if let node = self.stateNodes[key], let _ = node.currentLayout { if !transition.isAnimated { node.layer.removeAllAnimations() } transition.updateAlpha(node: node, alpha: fraction / fractionSum * alpha) } } } }