import Foundation import UIKit import Display import SwiftSignalKit import AccountContext import TextFormat import EmojiTextAttachmentView public final class DrawingTextEntity: DrawingEntity, Codable { final class CustomEmojiAttribute: Codable { private enum CodingKeys: String, CodingKey { case attribute case rangeOrigin case rangeLength } let attribute: ChatTextInputTextCustomEmojiAttribute let range: NSRange init(attribute: ChatTextInputTextCustomEmojiAttribute, range: NSRange) { self.attribute = attribute self.range = range } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.attribute = try container.decode(ChatTextInputTextCustomEmojiAttribute.self, forKey: .attribute) let rangeOrigin = try container.decode(Int.self, forKey: .rangeOrigin) let rangeLength = try container.decode(Int.self, forKey: .rangeLength) self.range = NSMakeRange(rangeOrigin, rangeLength) } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(self.attribute, forKey: .attribute) try container.encode(self.range.location, forKey: .rangeOrigin) try container.encode(self.range.length, forKey: .rangeLength) } } private enum CodingKeys: String, CodingKey { case uuid case text case textAttributes case style case font case alignment case fontSize case color case referenceDrawingSize case position case width case scale case rotation case renderImage case renderSubEntities } enum Style: Codable { case regular case filled case semi case stroke init(style: DrawingTextEntity.Style) { switch style { case .regular: self = .regular case .filled: self = .filled case .semi: self = .semi case .stroke: self = .stroke } } } enum Font: Codable { case sanFrancisco case newYork case monospaced case round case custom(String, String) } enum Alignment: Codable { case left case center case right var alignment: NSTextAlignment { switch self { case .left: return .left case .center: return .center case .right: return .right } } } public var uuid: UUID public var isAnimated: Bool { var isAnimated = false self.text.enumerateAttributes(in: NSMakeRange(0, self.text.length), options: [], using: { attributes, range, _ in if let _ = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute { isAnimated = true } }) return isAnimated } var text: NSAttributedString var style: Style var font: Font var alignment: Alignment var fontSize: CGFloat public var color: DrawingColor public var lineWidth: CGFloat = 0.0 var referenceDrawingSize: CGSize public var position: CGPoint var width: CGFloat public var scale: CGFloat public var rotation: CGFloat public var center: CGPoint { return self.position } public var renderImage: UIImage? public var renderSubEntities: [DrawingStickerEntity]? init(text: NSAttributedString, style: Style, font: Font, alignment: Alignment, fontSize: CGFloat, color: DrawingColor) { self.uuid = UUID() self.text = text self.style = style self.font = font self.alignment = alignment self.fontSize = fontSize self.color = color self.referenceDrawingSize = .zero self.position = .zero self.width = 100.0 self.scale = 1.0 self.rotation = 0.0 } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.uuid = try container.decode(UUID.self, forKey: .uuid) let text = try container.decode(String.self, forKey: .text) let attributedString = NSMutableAttributedString(string: text) let textAttributes = try container.decode([CustomEmojiAttribute].self, forKey: .textAttributes) for attribute in textAttributes { attributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: attribute.attribute, range: attribute.range) } self.text = attributedString self.style = try container.decode(Style.self, forKey: .style) self.font = try container.decode(Font.self, forKey: .font) self.alignment = try container.decode(Alignment.self, forKey: .alignment) self.fontSize = try container.decode(CGFloat.self, forKey: .fontSize) self.color = try container.decode(DrawingColor.self, forKey: .color) self.referenceDrawingSize = try container.decode(CGSize.self, forKey: .referenceDrawingSize) self.position = try container.decode(CGPoint.self, forKey: .position) self.width = try container.decode(CGFloat.self, forKey: .width) self.scale = try container.decode(CGFloat.self, forKey: .scale) self.rotation = try container.decode(CGFloat.self, forKey: .rotation) if let renderImageData = try? container.decodeIfPresent(Data.self, forKey: .renderImage) { self.renderImage = UIImage(data: renderImageData) } if let renderSubEntities = try? container.decodeIfPresent([CodableDrawingEntity].self, forKey: .renderSubEntities) { self.renderSubEntities = renderSubEntities.compactMap { $0.entity as? DrawingStickerEntity } } } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(self.uuid, forKey: .uuid) try container.encode(self.text.string, forKey: .text) var textAttributes: [CustomEmojiAttribute] = [] self.text.enumerateAttributes(in: NSMakeRange(0, self.text.length), options: [], using: { attributes, range, _ in if let value = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute { textAttributes.append(CustomEmojiAttribute(attribute: value, range: range)) } }) try container.encode(textAttributes, forKey: .textAttributes) try container.encode(self.style, forKey: .style) try container.encode(self.font, forKey: .font) try container.encode(self.alignment, forKey: .alignment) try container.encode(self.fontSize, forKey: .fontSize) try container.encode(self.color, forKey: .color) try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize) try container.encode(self.position, forKey: .position) try container.encode(self.width, forKey: .width) try container.encode(self.scale, forKey: .scale) try container.encode(self.rotation, forKey: .rotation) if let renderImage, let data = renderImage.pngData() { try container.encode(data, forKey: .renderImage) } if let renderSubEntities = self.renderSubEntities { let codableEntities: [CodableDrawingEntity] = renderSubEntities.map { .sticker($0) } try container.encode(codableEntities, forKey: .renderSubEntities) } } public func duplicate() -> DrawingEntity { let newEntity = DrawingTextEntity(text: self.text, style: self.style, font: self.font, alignment: self.alignment, fontSize: self.fontSize, color: self.color) newEntity.referenceDrawingSize = self.referenceDrawingSize newEntity.position = self.position newEntity.width = self.width newEntity.scale = self.scale newEntity.rotation = self.rotation return newEntity } public weak var currentEntityView: DrawingEntityView? public func makeView(context: AccountContext) -> DrawingEntityView { let entityView = DrawingTextEntityView(context: context, entity: self) self.currentEntityView = entityView return entityView } public func prepareForRender() { self.renderImage = (self.currentEntityView as? DrawingTextEntityView)?.getRenderImage() self.renderSubEntities = (self.currentEntityView as? DrawingTextEntityView)?.getRenderSubEntities() } } final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate { private var textEntity: DrawingTextEntity { return self.entity as! DrawingTextEntity } let textView: DrawingTextView var customEmojiContainerView: CustomEmojiContainerView? var emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)? init(context: AccountContext, entity: DrawingTextEntity) { self.textView = DrawingTextView(frame: .zero) self.textView.clipsToBounds = false self.textView.backgroundColor = .clear self.textView.isEditable = false self.textView.isSelectable = false self.textView.contentInset = .zero self.textView.showsHorizontalScrollIndicator = false self.textView.showsVerticalScrollIndicator = false self.textView.scrollsToTop = false self.textView.isScrollEnabled = false self.textView.textContainerInset = .zero self.textView.minimumZoomScale = 1.0 self.textView.maximumZoomScale = 1.0 self.textView.keyboardAppearance = .dark self.textView.autocorrectionType = .no self.textView.spellCheckingType = .no super.init(context: context, entity: entity) self.textView.delegate = self self.addSubview(self.textView) self.emojiViewProvider = { [weak self] emoji in guard let strongSelf = self else { return UIView() } let pointSize: CGFloat = 128.0 return EmojiTextAttachmentView(context: context, userLocation: .other, emoji: emoji, file: emoji.file, cache: strongSelf.context.animationCache, renderer: strongSelf.context.animationRenderer, placeholderColor: UIColor.white.withAlphaComponent(0.12), pointSize: CGSize(width: pointSize, height: pointSize)) } self.update(animated: false) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private var isSuspended = false private var _isEditing = false var isEditing: Bool { return self._isEditing || self.isSuspended } private var previousEntity: DrawingTextEntity? private var fadeView: UIView? @objc private func fadePressed() { self.endEditing() } private var emojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)] = [] func updateEntities() { self.textView.drawingLayoutManager.ensureLayout(for: self.textView.textContainer) var customEmojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)] = [] var shouldRepeat = false if let attributedText = self.textView.attributedText { let beginning = self.textView.beginningOfDocument attributedText.enumerateAttributes(in: NSMakeRange(0, attributedText.length), options: [], using: { attributes, range, _ in if let value = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute { if let start = self.textView.position(from: beginning, offset: range.location), let end = self.textView.position(from: start, offset: range.length), let textRange = self.textView.textRange(from: start, to: end) { let rect = self.textView.firstRect(for: textRange) customEmojiRects.append((rect, value)) if rect.origin.x.isInfinite { shouldRepeat = true } } } }) } let color = self.textEntity.color.toUIColor() let textColor: UIColor switch self.textEntity.style { case .regular: textColor = color case .filled: textColor = color.lightness > 0.99 ? UIColor.black : UIColor.white case .semi: textColor = color case .stroke: textColor = color.lightness > 0.99 ? UIColor.black : UIColor.white } self.emojiRects = customEmojiRects if !customEmojiRects.isEmpty && !shouldRepeat { let customEmojiContainerView: CustomEmojiContainerView if let current = self.customEmojiContainerView { customEmojiContainerView = current } else { customEmojiContainerView = CustomEmojiContainerView(emojiViewProvider: { [weak self] emoji in guard let strongSelf = self, let emojiViewProvider = strongSelf.emojiViewProvider else { return nil } return emojiViewProvider(emoji) }) customEmojiContainerView.isUserInteractionEnabled = false customEmojiContainerView.center = customEmojiContainerView.center.offsetBy(dx: 0.0, dy: 10.0) self.addSubview(customEmojiContainerView) self.customEmojiContainerView = customEmojiContainerView } customEmojiContainerView.update(fontSize: self.displayFontSize * 0.78, textColor: textColor, emojiRects: customEmojiRects) } else if let customEmojiContainerView = self.customEmojiContainerView { customEmojiContainerView.removeFromSuperview() self.customEmojiContainerView = nil } if shouldRepeat { Queue.mainQueue().after(0.01) { self.updateEntities() } } } func beginEditing(accessoryView: UIView?) { self._isEditing = true if !self.textEntity.text.string.isEmpty { let previousEntity = self.textEntity.duplicate() as? DrawingTextEntity previousEntity?.uuid = self.textEntity.uuid self.previousEntity = previousEntity } self.update(animated: false) if let superview = self.superview { let fadeView = UIButton(frame: CGRect(origin: .zero, size: superview.frame.size)) fadeView.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.4) fadeView.addTarget(self, action: #selector(self.fadePressed), for: .touchUpInside) superview.insertSubview(fadeView, belowSubview: self) self.fadeView = fadeView fadeView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) } self.textView.inputAccessoryView = accessoryView self.textView.isEditable = true self.textView.isSelectable = true self.textView.window?.makeKey() self.textView.becomeFirstResponder() UIView.animate(withDuration: 0.4, delay: 0.0, usingSpringWithDamping: 0.65, initialSpringVelocity: 0.0) { self.transform = .identity if let superview = self.superview { self.center = CGPoint(x: superview.bounds.width / 2.0, y: superview.bounds.height / 2.0) } } if let selectionView = self.selectionView as? DrawingTextEntititySelectionView { selectionView.alpha = 0.0 if !self.textEntity.text.string.isEmpty { selectionView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) } } } func endEditing(reset: Bool = false) { self._isEditing = false self.textView.resignFirstResponder() self.textView.inputView = nil self.textView.inputAccessoryView = nil self.textView.isEditable = false self.textView.isSelectable = false if reset { if let previousEntity = self.previousEntity { self.textEntity.color = previousEntity.color self.textEntity.style = previousEntity.style self.textEntity.alignment = previousEntity.alignment self.textEntity.font = previousEntity.font self.textEntity.text = previousEntity.text self.previousEntity = nil } else { self.containerView?.remove(uuid: self.textEntity.uuid) } } else { // self.textEntity.text = self.textView.text.trimmingCharacters(in: .whitespacesAndNewlines) if self.textEntity.text.string.isEmpty { self.containerView?.remove(uuid: self.textEntity.uuid) } } if let fadeView = self.fadeView { self.fadeView = nil fadeView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak fadeView] _ in fadeView?.removeFromSuperview() }) } UIView.animate(withDuration: 0.4, delay: 0.0, usingSpringWithDamping: 0.65, initialSpringVelocity: 0.0) { self.transform = CGAffineTransformMakeRotation(self.textEntity.rotation) self.center = self.textEntity.position } self.update(animated: false) if let selectionView = self.selectionView as? DrawingTextEntititySelectionView { selectionView.alpha = 1.0 selectionView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } } func suspendEditing() { self.isSuspended = true self.textView.resignFirstResponder() if let fadeView = self.fadeView { fadeView.alpha = 0.0 fadeView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) } } func resumeEditing() { self.isSuspended = false self.textView.becomeFirstResponder() if let fadeView = self.fadeView { fadeView.alpha = 1.0 fadeView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) } } func textViewDidChange(_ textView: UITextView) { guard let updatedText = self.textView.attributedText.mutableCopy() as? NSMutableAttributedString else { return } let range = NSMakeRange(0, updatedText.length) updatedText.removeAttribute(.font, range: range) updatedText.removeAttribute(.paragraphStyle, range: range) updatedText.removeAttribute(.foregroundColor, range: range) self.textEntity.text = updatedText self.sizeToFit() self.update() } func insertText(_ text: NSAttributedString) { guard let updatedText = self.textView.attributedText.mutableCopy() as? NSMutableAttributedString else { return } let range = NSMakeRange(0, updatedText.length) updatedText.removeAttribute(.font, range: range) updatedText.removeAttribute(.paragraphStyle, range: range) updatedText.removeAttribute(.foregroundColor, range: range) updatedText.replaceCharacters(in: self.textView.selectedRange, with: text) self.textEntity.text = updatedText self.update() } override func sizeThatFits(_ size: CGSize) -> CGSize { var result = self.textView.sizeThatFits(CGSize(width: self.textEntity.width, height: .greatestFiniteMagnitude)) result.width = max(224.0, ceil(result.width) + 20.0) result.height = ceil(result.height) //+ 20.0 + (self.textView.font?.pointSize ?? 0.0) // * _font.sizeCorrection; return result; } override func sizeToFit() { let center = self.center let transform = self.transform self.transform = .identity super.sizeToFit() self.center = center self.transform = transform //entity changed } override func layoutSubviews() { super.layoutSubviews() var rect = self.bounds rect = rect.offsetBy(dx: 0.0, dy: 10.0) // CGRectOffset(rect, 0.0f, 10.0f); self.textView.frame = rect } private var displayFontSize: CGFloat { let minFontSize = max(10.0, max(self.textEntity.referenceDrawingSize.width, self.textEntity.referenceDrawingSize.height) * 0.025) let maxFontSize = max(10.0, max(self.textEntity.referenceDrawingSize.width, self.textEntity.referenceDrawingSize.height) * 0.25) let fontSize = minFontSize + (maxFontSize - minFontSize) * self.textEntity.fontSize return fontSize } private func updateText() { guard let text = self.textEntity.text.mutableCopy() as? NSMutableAttributedString else { return } let range = NSMakeRange(0, text.length) let fontSize = self.displayFontSize self.textView.drawingLayoutManager.textContainers.first?.lineFragmentPadding = floor(fontSize * 0.24) if let (font, name) = availableFonts[text.string.lowercased()] { self.textEntity.font = .custom(font, name) } var font: UIFont switch self.textEntity.font { case .sanFrancisco: font = Font.with(size: fontSize, design: .regular, weight: .semibold) case .newYork: font = Font.with(size: fontSize, design: .serif, weight: .semibold) case .monospaced: font = Font.with(size: fontSize, design: .monospace, weight: .semibold) case .round: font = Font.with(size: fontSize, design: .round, weight: .semibold) case let .custom(fontName, _): font = UIFont(name: fontName, size: fontSize) ?? Font.with(size: fontSize, design: .regular, weight: .semibold) } text.addAttribute(.font, value: font, range: range) self.textView.font = font let color = self.textEntity.color.toUIColor() let textColor: UIColor switch self.textEntity.style { case .regular: textColor = color case .filled: textColor = color.lightness > 0.99 ? UIColor.black : UIColor.white case .semi: textColor = color case .stroke: textColor = color.lightness > 0.99 ? UIColor.black : UIColor.white } text.addAttribute(.foregroundColor, value: textColor, range: range) text.enumerateAttributes(in: range) { attributes, subrange, _ in if let _ = attributes[ChatTextInputAttributes.customEmoji] { text.addAttribute(.foregroundColor, value: UIColor.clear, range: subrange) } } let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.alignment = self.textEntity.alignment.alignment text.addAttribute(.paragraphStyle, value: paragraphStyle, range: range) self.textView.attributedText = text } override func update(animated: Bool = false) { if !self.isEditing { self.center = self.textEntity.position self.transform = CGAffineTransformScale(CGAffineTransformMakeRotation(self.textEntity.rotation), self.textEntity.scale, self.textEntity.scale) } let color = self.textEntity.color.toUIColor() switch self.textEntity.style { case .regular: self.textView.textColor = color self.textView.strokeColor = nil self.textView.frameColor = nil case .filled: self.textView.textColor = color.lightness > 0.99 ? UIColor.black : UIColor.white self.textView.strokeColor = nil self.textView.frameColor = color case .semi: self.textView.textColor = color self.textView.strokeColor = nil self.textView.frameColor = UIColor(rgb: 0xffffff, alpha: 0.75) case .stroke: self.textView.textColor = color.lightness > 0.99 ? UIColor.black : UIColor.white self.textView.strokeColor = color self.textView.frameColor = nil } if case .regular = self.textEntity.style { self.textView.layer.shadowColor = UIColor.black.cgColor self.textView.layer.shadowOffset = CGSize(width: 0.0, height: 4.0) self.textView.layer.shadowOpacity = 0.4 self.textView.layer.shadowRadius = 4.0 } else { self.textView.layer.shadowColor = nil self.textView.layer.shadowOffset = .zero self.textView.layer.shadowOpacity = 0.0 self.textView.layer.shadowRadius = 0.0 } self.updateText() self.sizeToFit() Queue.mainQueue().after(0.001) { self.updateEntities() } super.update(animated: animated) } override func updateSelectionView() { guard let selectionView = self.selectionView as? DrawingTextEntititySelectionView else { return } self.pushIdentityTransformForMeasurement() selectionView.transform = .identity let bounds = self.selectionBounds let center = bounds.center let scale = self.superview?.superview?.layer.value(forKeyPath: "transform.scale.x") as? CGFloat ?? 1.0 selectionView.center = self.convert(center, to: selectionView.superview) selectionView.bounds = CGRect(origin: .zero, size: CGSize(width: (bounds.width * self.textEntity.scale) * scale + selectionView.selectionInset * 2.0, height: (bounds.height * self.textEntity.scale) * scale + selectionView.selectionInset * 2.0)) selectionView.transform = CGAffineTransformMakeRotation(self.textEntity.rotation) self.popIdentityTransformForMeasurement() } override func makeSelectionView() -> DrawingEntitySelectionView { if let selectionView = self.selectionView { return selectionView } let selectionView = DrawingTextEntititySelectionView() selectionView.entityView = self return selectionView } func getRenderImage() -> UIImage? { let rect = self.bounds UIGraphicsBeginImageContextWithOptions(rect.size, false, 1.0) self.textView.drawHierarchy(in: rect.offsetBy(dx: 0.0, dy: 10.0), afterScreenUpdates: true) let image = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return image } func getRenderSubEntities() -> [DrawingStickerEntity] { let textSize = self.textView.bounds.size let textPosition = self.textEntity.position let scale = self.textEntity.scale let rotation = self.textEntity.rotation let itemSize: CGFloat = floor(24.0 * self.displayFontSize * 0.78 / 17.0) var entities: [DrawingStickerEntity] = [] for (emojiRect, emojiAttribute) in self.emojiRects { guard let file = emojiAttribute.file else { continue } let emojiTextPosition = emojiRect.offsetBy(dx: 0.0, dy: 10.0).center.offsetBy(dx: -textSize.width / 2.0, dy: -textSize.height / 2.0) let entity = DrawingStickerEntity(file: file) entity.referenceDrawingSize = CGSize(width: itemSize * 2.5, height: itemSize * 2.5) entity.scale = scale entity.position = textPosition.offsetBy( dx: (emojiTextPosition.x * cos(rotation) + emojiTextPosition.y * sin(rotation)) * scale, dy: (emojiTextPosition.y * cos(rotation) + emojiTextPosition.x * sin(rotation)) * scale ) entity.rotation = rotation entities.append(entity) } return entities } } final class DrawingTextEntititySelectionView: DrawingEntitySelectionView, UIGestureRecognizerDelegate { private let border = SimpleShapeLayer() private let leftHandle = SimpleShapeLayer() private let rightHandle = SimpleShapeLayer() private var panGestureRecognizer: UIPanGestureRecognizer! override init(frame: CGRect) { let handleBounds = CGRect(origin: .zero, size: entitySelectionViewHandleSize) let handles = [ self.leftHandle, self.rightHandle ] super.init(frame: frame) self.backgroundColor = .clear self.isOpaque = false self.border.lineCap = .round self.border.fillColor = UIColor.clear.cgColor self.border.strokeColor = UIColor(rgb: 0xffffff, alpha: 0.5).cgColor self.layer.addSublayer(self.border) for handle in handles { handle.bounds = handleBounds handle.fillColor = UIColor(rgb: 0x0a60ff).cgColor handle.strokeColor = UIColor(rgb: 0xffffff).cgColor handle.rasterizationScale = UIScreen.main.scale handle.shouldRasterize = true self.layer.addSublayer(handle) } let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:))) panGestureRecognizer.delegate = self self.addGestureRecognizer(panGestureRecognizer) self.panGestureRecognizer = panGestureRecognizer self.snapTool.onSnapXUpdated = { [weak self] snapped in if let strongSelf = self, let entityView = strongSelf.entityView { entityView.onSnapToXAxis(snapped) } } self.snapTool.onSnapYUpdated = { [weak self] snapped in if let strongSelf = self, let entityView = strongSelf.entityView { entityView.onSnapToYAxis(snapped) } } } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } var scale: CGFloat = 1.0 { didSet { self.setNeedsLayout() } } override var selectionInset: CGFloat { return 15.0 } override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { if let entityView = self.entityView as? DrawingTextEntityView, entityView.isEditing { return false } return true } private let snapTool = DrawingEntitySnapTool() private var currentHandle: CALayer? @objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { guard let entityView = self.entityView, let entity = entityView.entity as? DrawingTextEntity else { return } let location = gestureRecognizer.location(in: self) switch gestureRecognizer.state { case .began: self.snapTool.maybeSkipFromStart(entityView: entityView, position: entity.position) if let sublayers = self.layer.sublayers { for layer in sublayers { if layer.frame.contains(location) { self.currentHandle = layer return } } } self.currentHandle = self.layer case .changed: let delta = gestureRecognizer.translation(in: entityView.superview) let parentLocation = gestureRecognizer.location(in: self.superview) let velocity = gestureRecognizer.velocity(in: entityView.superview) var updatedScale = entity.scale var updatedPosition = entity.position var updatedRotation = entity.rotation if self.currentHandle === self.leftHandle || self.currentHandle === self.rightHandle { var deltaX = gestureRecognizer.translation(in: self).x if self.currentHandle === self.leftHandle { deltaX *= -1.0 } let scaleDelta = (self.bounds.size.width + deltaX * 2.0) / self.bounds.size.width updatedScale = max(0.01, updatedScale * scaleDelta) let deltaAngle: CGFloat if self.currentHandle === self.leftHandle { deltaAngle = atan2(self.center.y - parentLocation.y, self.center.x - parentLocation.x) } else { deltaAngle = atan2(parentLocation.y - self.center.y, parentLocation.x - self.center.x) } updatedRotation = deltaAngle } else if self.currentHandle === self.layer { updatedPosition.x += delta.x updatedPosition.y += delta.y updatedPosition = self.snapTool.update(entityView: entityView, velocity: velocity, delta: delta, updatedPosition: updatedPosition) } entity.scale = updatedScale entity.position = updatedPosition entity.rotation = updatedRotation entityView.update() gestureRecognizer.setTranslation(.zero, in: entityView) case .ended: self.snapTool.reset() case .cancelled: self.snapTool.reset() default: break } } override func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) { guard let entityView = self.entityView as? DrawingTextEntityView, !entityView.isEditing, let entity = entityView.entity as? DrawingTextEntity else { return } switch gestureRecognizer.state { case .began, .changed: let scale = gestureRecognizer.scale entity.fontSize = max(0.1, entity.scale * scale) entityView.update() gestureRecognizer.scale = 1.0 default: break } } override func handleRotate(_ gestureRecognizer: UIRotationGestureRecognizer) { guard let entityView = self.entityView as? DrawingTextEntityView, !entityView.isEditing, let entity = entityView.entity as? DrawingTextEntity else { return } switch gestureRecognizer.state { case .began, .changed: let rotation = gestureRecognizer.rotation entity.rotation += rotation entityView.update() gestureRecognizer.rotation = 0.0 default: break } } override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { return self.bounds.insetBy(dx: -22.0, dy: -22.0).contains(point) } override func layoutSubviews() { let inset = self.selectionInset - 10.0 let bounds = CGRect(origin: .zero, size: CGSize(width: entitySelectionViewHandleSize.width / self.scale, height: entitySelectionViewHandleSize.height / self.scale)) let handleSize = CGSize(width: 9.0 / self.scale, height: 9.0 / self.scale) let handlePath = CGPath(ellipseIn: CGRect(origin: CGPoint(x: (bounds.width - handleSize.width) / 2.0, y: (bounds.height - handleSize.height) / 2.0), size: handleSize), transform: nil) let lineWidth = (1.0 + UIScreenPixel) / self.scale let handles = [ self.leftHandle, self.rightHandle ] for handle in handles { handle.path = handlePath handle.bounds = bounds handle.lineWidth = lineWidth } self.leftHandle.position = CGPoint(x: inset, y: self.bounds.midY) self.rightHandle.position = CGPoint(x: self.bounds.maxX - inset, y: self.bounds.midY) let width: CGFloat = self.bounds.width - inset * 2.0 let height: CGFloat = self.bounds.height - inset * 2.0 let cornerRadius: CGFloat = 12.0 - self.scale let perimeter: CGFloat = 2.0 * (width + height - cornerRadius * (4.0 - .pi)) let count = 12 let relativeDashLength: CGFloat = 0.25 let dashLength = perimeter / CGFloat(count) self.border.lineDashPattern = [dashLength * relativeDashLength, dashLength * relativeDashLength] as [NSNumber] self.border.lineWidth = 2.0 / self.scale self.border.path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: inset, y: inset), size: CGSize(width: width, height: height)), cornerRadius: cornerRadius).cgPath } } private class DrawingTextLayoutManager: NSLayoutManager { var radius: CGFloat var maxIndex: Int = 0 private(set) var path: UIBezierPath? var rectArray: [CGRect] = [] var strokeColor: UIColor? var strokeWidth: CGFloat = 0.0 var strokeOffset: CGPoint = .zero var frameColor: UIColor? var frameWidthInset: CGFloat = 0.0 override init() { self.radius = 8.0 super.init() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func prepare() { self.path = nil self.rectArray.removeAll() self.enumerateLineFragments(forGlyphRange: NSRange(location: 0, length: ((self.textStorage?.string ?? "") as NSString).length)) { rect, usedRect, textContainer, glyphRange, _ in var ignoreRange = false let charecterRange = self.characterRange(forGlyphRange: glyphRange, actualGlyphRange: nil) let substring = ((self.textStorage?.string ?? "") as NSString).substring(with: charecterRange) if substring.trimmingCharacters(in: .newlines).isEmpty { ignoreRange = true } if !ignoreRange { let newRect = CGRect(origin: CGPoint(x: usedRect.minX - self.frameWidthInset, y: usedRect.minY), size: CGSize(width: usedRect.width + self.frameWidthInset * 2.0, height: usedRect.height)) self.rectArray.append(newRect) } } self.preprocess() } private func preprocess() { self.maxIndex = 0 if self.rectArray.count < 2 { return } for i in 1 ..< self.rectArray.count { self.maxIndex = i self.processRectIndex(i) } } private func processRectIndex(_ index: Int) { guard self.rectArray.count >= 2 && index > 0 && index <= self.maxIndex else { return } let last = self.rectArray[index - 1] let cur = self.rectArray[index] self.radius = cur.height * 0.18 let t1 = ((cur.minX - last.minX < 2.0 * self.radius) && (cur.minX > last.minX)) || ((cur.maxX - last.maxX > -2.0 * self.radius) && (cur.maxX < last.maxX)) let t2 = ((last.minX - cur.minX < 2.0 * self.radius) && (last.minX > cur.minX)) || ((last.maxX - cur.maxX > -2.0 * self.radius) && (last.maxX < cur.maxX)) if t2 { let newRect = CGRect(origin: CGPoint(x: cur.minX, y: last.minY), size: CGSize(width: cur.width, height: last.height)) self.rectArray[index - 1] = newRect self.processRectIndex(index - 1) } if t1 { let newRect = CGRect(origin: CGPoint(x: last.minX, y: cur.minY), size: CGSize(width: last.width, height: cur.height)) self.rectArray[index] = newRect self.processRectIndex(index + 1) } } override func showCGGlyphs(_ glyphs: UnsafePointer, positions: UnsafePointer, count glyphCount: Int, font: UIFont, matrix textMatrix: CGAffineTransform, attributes: [NSAttributedString.Key : Any] = [:], in graphicsContext: CGContext) { if let strokeColor = self.strokeColor { graphicsContext.setStrokeColor(strokeColor.cgColor) graphicsContext.setLineJoin(.round) let lineWidth = self.strokeWidth > 0.0 ? self.strokeWidth : font.pointSize / 9.0 graphicsContext.setLineWidth(lineWidth) graphicsContext.setTextDrawingMode(.stroke) graphicsContext.saveGState() graphicsContext.translateBy(x: self.strokeOffset.x, y: self.strokeOffset.y) super.showCGGlyphs(glyphs, positions: positions, count: glyphCount, font: font, matrix: textMatrix, attributes: attributes, in: graphicsContext) graphicsContext.restoreGState() let textColor: UIColor = attributes[NSAttributedString.Key.foregroundColor] as? UIColor ?? UIColor.white graphicsContext.setFillColor(textColor.cgColor) graphicsContext.setTextDrawingMode(.fill) } super.showCGGlyphs(glyphs, positions: positions, count: glyphCount, font: font, matrix: textMatrix, attributes: attributes, in: graphicsContext) } override func drawBackground(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint) { if let frameColor = self.frameColor, let context = UIGraphicsGetCurrentContext() { context.saveGState() context.translateBy(x: origin.x, y: origin.y) context.setBlendMode(.copy) context.setFillColor(frameColor.cgColor) context.setStrokeColor(frameColor.cgColor) self.prepare() self.preprocess() let path = UIBezierPath() var last: CGRect = .null for i in 0 ..< self.rectArray.count { let cur = self.rectArray[i] self.radius = cur.height * 0.18 path.append(UIBezierPath(roundedRect: cur, cornerRadius: self.radius)) if i == 0 { last = cur } else if i > 0 && abs(last.maxY - cur.minY) < 10.0 { let a = cur.origin let b = CGPoint(x: cur.maxX, y: cur.minY) let c = CGPoint(x: last.minX, y: last.maxY) let d = CGPoint(x: last.maxX, y: last.maxY) if a.x - c.x >= 2.0 * self.radius { let addPath = UIBezierPath(arcCenter: CGPoint(x: a.x - self.radius, y: a.y + self.radius), radius: self.radius, startAngle: .pi * 0.5 * 3.0, endAngle: 0.0, clockwise: true) addPath.append( UIBezierPath(arcCenter: CGPoint(x: a.x + self.radius, y: a.y + self.radius), radius: self.radius, startAngle: .pi, endAngle: 3.0 * .pi * 0.5, clockwise: true) ) addPath.addLine(to: CGPoint(x: a.x - self.radius, y: a.y)) path.append(addPath) } if a.x == c.x { path.move(to: CGPoint(x: a.x, y: a.y - self.radius)) path.addLine(to: CGPoint(x: a.x, y: a.y + self.radius)) path.addArc(withCenter: CGPoint(x: a.x + self.radius, y: a.y + self.radius), radius: self.radius, startAngle: .pi, endAngle: .pi * 0.5 * 3.0, clockwise: true) path.addArc(withCenter: CGPoint(x: a.x + self.radius, y: a.y - self.radius), radius: self.radius, startAngle: .pi * 0.5, endAngle: .pi, clockwise: true) } if d.x - b.x >= 2.0 * self.radius { let addPath = UIBezierPath(arcCenter: CGPoint(x: b.x + self.radius, y: b.y + self.radius), radius: self.radius, startAngle: .pi * 0.5 * 3.0, endAngle: .pi, clockwise: false) addPath.append( UIBezierPath(arcCenter: CGPoint(x: b.x - self.radius, y: b.y + self.radius), radius: self.radius, startAngle: 0.0, endAngle: 3.0 * .pi * 0.5, clockwise: false) ) addPath.addLine(to: CGPoint(x: b.x + self.radius, y: b.y)) path.append(addPath) } if d.x == b.x { path.move(to: CGPoint(x: b.x, y: b.y - self.radius)) path.addLine(to: CGPoint(x: b.x, y: b.y + self.radius)) path.addArc(withCenter: CGPoint(x: b.x - self.radius, y: b.y + self.radius), radius: self.radius, startAngle: 0.0, endAngle: 3.0 * .pi * 0.5, clockwise: false) path.addArc(withCenter: CGPoint(x: b.x - self.radius, y: b.y - self.radius), radius: self.radius, startAngle: .pi * 0.5, endAngle: 0.0, clockwise: false) } if c.x - a.x >= 2.0 * self.radius { let addPath = UIBezierPath(arcCenter: CGPoint(x: c.x - self.radius, y: c.y - self.radius), radius: self.radius, startAngle: .pi * 0.5, endAngle: 0.0, clockwise: false) addPath.append( UIBezierPath(arcCenter: CGPoint(x: c.x + self.radius, y: c.y - self.radius), radius: self.radius, startAngle: .pi, endAngle: .pi * 0.5, clockwise: false) ) addPath.addLine(to: CGPoint(x: c.x - self.radius, y: c.y)) path.append(addPath) } if b.x - d.x >= 2.0 * self.radius { let addPath = UIBezierPath(arcCenter: CGPoint(x: d.x + self.radius, y: d.y - self.radius), radius: self.radius, startAngle: .pi * 0.5, endAngle: .pi, clockwise: true) addPath.append( UIBezierPath(arcCenter: CGPoint(x: d.x - self.radius, y: d.y - self.radius), radius: self.radius, startAngle: 0.0, endAngle: .pi * 0.5, clockwise: true) ) addPath.addLine(to: CGPoint(x: d.x + self.radius, y: d.y)) path.append(addPath) } last = cur } } self.path = path self.path?.fill() self.path?.stroke() context.restoreGState() } } } private class DrawingTextStorage: NSTextStorage { let impl: NSTextStorage override init() { self.impl = NSTextStorage() super.init() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override var string: String { return self.impl.string } override func attributes(at location: Int, effectiveRange range: NSRangePointer?) -> [NSAttributedString.Key : Any] { return self.impl.attributes(at: location, effectiveRange: range) } override func replaceCharacters(in range: NSRange, with str: String) { self.beginEditing() self.impl.replaceCharacters(in: range, with: str) self.edited(.editedCharacters, range: range, changeInLength: (str as NSString).length - range.length) self.endEditing() } override func setAttributes(_ attrs: [NSAttributedString.Key : Any]?, range: NSRange) { self.beginEditing() self.impl.setAttributes(attrs, range: range) self.edited(.editedAttributes, range: range, changeInLength: 0) self.endEditing() } } class DrawingTextView: UITextView { fileprivate var drawingLayoutManager: DrawingTextLayoutManager { return self.layoutManager as! DrawingTextLayoutManager } var strokeColor: UIColor? { didSet { self.drawingLayoutManager.strokeColor = self.strokeColor self.setNeedsDisplay() } } var strokeWidth: CGFloat = 0.0 { didSet { self.drawingLayoutManager.strokeWidth = self.strokeWidth self.setNeedsDisplay() } } var strokeOffset: CGPoint = .zero { didSet { self.drawingLayoutManager.strokeOffset = self.strokeOffset self.setNeedsDisplay() } } var frameColor: UIColor? { didSet { self.drawingLayoutManager.frameColor = self.frameColor self.setNeedsDisplay() } } var frameWidthInset: CGFloat = 0.0 { didSet { self.drawingLayoutManager.frameWidthInset = self.frameWidthInset self.setNeedsDisplay() } } override var textColor: UIColor? { get { return super.textColor } set { super.textColor = newValue self.fixTypingAttributes() } } init(frame: CGRect) { let textStorage = DrawingTextStorage() let layoutManager = DrawingTextLayoutManager() let textContainer = NSTextContainer(size: CGSize(width: 0.0, height: .greatestFiniteMagnitude)) textContainer.widthTracksTextView = true layoutManager.addTextContainer(textContainer) textStorage.addLayoutManager(layoutManager) super.init(frame: frame, textContainer: textContainer) self.tintColor = UIColor.white } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func caretRect(for position: UITextPosition) -> CGRect { var rect = super.caretRect(for: position) rect.size.width = floorToScreenPixels(rect.size.height / 25.0) return rect } override func insertText(_ text: String) { self.fixTypingAttributes() super.insertText(text) self.fixTypingAttributes() } override func paste(_ sender: Any?) { self.fixTypingAttributes() super.paste(sender) self.fixTypingAttributes() } fileprivate func fixTypingAttributes() { var attributes: [NSAttributedString.Key: Any] = [:] if let font = self.font { attributes[NSAttributedString.Key.font] = font } if let textColor = self.textColor { attributes[NSAttributedString.Key.foregroundColor] = textColor } let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.alignment = self.textAlignment attributes[NSAttributedString.Key.paragraphStyle] = paragraphStyle self.typingAttributes = attributes } } private var availableFonts: [String: (String, String)] = { let familyNames = UIFont.familyNames var result: [String: (String, String)] = [:] for family in familyNames { let names = UIFont.fontNames(forFamilyName: family) var preferredFont: String? for name in names { let originalName = name let name = name.lowercased() if (!name.contains("-") || name.contains("regular")) && preferredFont == nil { preferredFont = originalName } if name.contains("bold") && !name.contains("italic") { preferredFont = originalName } } if let preferredFont { let shortname = family.lowercased().replacingOccurrences(of: " ", with: "", options: []) result[shortname] = (preferredFont, family) } } print(result) return result }()