Swiftgram/submodules/DrawingUI/Sources/DrawingEntitiesView.swift
2024-02-27 10:42:45 +04:00

1255 lines
49 KiB
Swift

import Foundation
import UIKit
import Display
import LegacyComponents
import SwiftSignalKit
import AccountContext
import MediaEditor
import ComponentFlow
import LottieAnimationComponent
import ReactionSelectionNode
private func makeEntityView(context: AccountContext, entity: DrawingEntity) -> DrawingEntityView? {
if let entity = entity as? DrawingBubbleEntity {
return DrawingBubbleEntityView(context: context, entity: entity)
} else if let entity = entity as? DrawingSimpleShapeEntity {
return DrawingSimpleShapeEntityView(context: context, entity: entity)
} else if let entity = entity as? DrawingStickerEntity {
if case let .file(_, type) = entity.content, case .reaction = type {
return DrawingReactionEntityView(context: context, entity: entity)
} else {
return DrawingStickerEntityView(context: context, entity: entity)
}
} else if let entity = entity as? DrawingTextEntity {
return DrawingTextEntityView(context: context, entity: entity)
} else if let entity = entity as? DrawingVectorEntity {
return DrawingVectorEntityView(context: context, entity: entity)
} else if let entity = entity as? DrawingMediaEntity {
return DrawingMediaEntityView(context: context, entity: entity)
} else if let entity = entity as? DrawingLocationEntity {
return DrawingLocationEntityView(context: context, entity: entity)
} else {
return nil
}
}
private func prepareForRendering(entityView: DrawingEntityView) {
if let entityView = entityView as? DrawingStickerEntityView {
entityView.entity.renderImage = entityView.getRenderImage()
entityView.entity.renderSubEntities = entityView.getRenderSubEntities()
}
if let entityView = entityView as? DrawingBubbleEntityView {
entityView.entity.renderImage = entityView.getRenderImage()
}
if let entityView = entityView as? DrawingSimpleShapeEntityView {
entityView.entity.renderImage = entityView.getRenderImage()
}
if let entityView = entityView as? DrawingTextEntityView {
entityView.entity.renderImage = entityView.getRenderImage()
entityView.entity.renderSubEntities = entityView.getRenderSubEntities()
}
if let entityView = entityView as? DrawingVectorEntityView {
entityView.entity.renderImage = entityView.getRenderImage()
}
if let entityView = entityView as? DrawingLocationEntityView {
entityView.entity.renderImage = entityView.getRenderImage()
}
}
public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
private let context: AccountContext
private let size: CGSize
private let hasBin: Bool
private let isStickerEditor: Bool
weak var drawingView: DrawingView?
public weak var selectionContainerView: DrawingSelectionContainerView?
private var tapGestureRecognizer: UITapGestureRecognizer!
public private(set) var selectedEntityView: DrawingEntityView?
public var getEntityEdgePositions: () -> UIEdgeInsets? = { return nil }
public var getEntityCenterPosition: () -> CGPoint = { return .zero }
public var getEntityInitialRotation: () -> CGFloat = { return 0.0 }
public var getEntityAdditionalScale: () -> CGFloat = { return 1.0 }
public var getAvailableReactions: () -> [ReactionItem] = { return [] }
public var present: (ViewController) -> Void = { _ in }
public var push: (ViewController) -> Void = { _ in }
public var canInteract: () -> Bool = { return true }
public var hasSelectionChanged: (Bool) -> Void = { _ in }
var selectionChanged: (DrawingEntity?) -> Void = { _ in }
var requestedMenuForEntityView: (DrawingEntityView, Bool) -> Void = { _, _ in }
var entityAdded: (DrawingEntity) -> Void = { _ in }
var entityRemoved: (DrawingEntity) -> Void = { _ in }
public var externalEntityRemoved: (DrawingEntity) -> Void = { _ in }
var autoSelectEntities = false
private let topEdgeView = UIView()
private let leftEdgeView = UIView()
private let rightEdgeView = UIView()
private let bottomEdgeView = UIView()
private let xAxisView = UIView()
private let yAxisView = UIView()
private let angleLayer = SimpleShapeLayer()
private let bin = ComponentView<Empty>()
private let stickerOverlayLayer = SimpleShapeLayer()
private let stickerFrameLayer = SimpleShapeLayer()
public var onInteractionUpdated: (Bool) -> Void = { _ in }
public var edgePreviewUpdated: (Bool) -> Void = { _ in }
private let hapticFeedback = HapticFeedback()
public init(context: AccountContext, size: CGSize, hasBin: Bool = false, isStickerEditor: Bool = false) {
self.context = context
self.size = size
self.hasBin = hasBin
self.isStickerEditor = isStickerEditor
super.init(frame: CGRect(origin: .zero, size: size))
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))
self.addGestureRecognizer(tapGestureRecognizer)
self.tapGestureRecognizer = tapGestureRecognizer
self.topEdgeView.alpha = 0.0
self.topEdgeView.backgroundColor = UIColor(rgb: 0x5fc1f0)
self.topEdgeView.isUserInteractionEnabled = false
self.leftEdgeView.alpha = 0.0
self.leftEdgeView.backgroundColor = UIColor(rgb: 0x5fc1f0)
self.leftEdgeView.isUserInteractionEnabled = false
self.rightEdgeView.alpha = 0.0
self.rightEdgeView.backgroundColor = UIColor(rgb: 0x5fc1f0)
self.rightEdgeView.isUserInteractionEnabled = false
self.bottomEdgeView.alpha = 0.0
self.bottomEdgeView.backgroundColor = UIColor(rgb: 0x5fc1f0)
self.bottomEdgeView.isUserInteractionEnabled = false
self.xAxisView.alpha = 0.0
self.xAxisView.backgroundColor = UIColor(rgb: 0x5fc1f0)
self.xAxisView.isUserInteractionEnabled = false
self.yAxisView.alpha = 0.0
self.yAxisView.backgroundColor = UIColor(rgb: 0x5fc1f0)
self.yAxisView.isUserInteractionEnabled = false
self.angleLayer.strokeColor = UIColor(rgb: 0xffd70a).cgColor
self.angleLayer.opacity = 0.0
self.angleLayer.lineDashPattern = [12, 12] as [NSNumber]
self.stickerOverlayLayer.fillColor = UIColor(rgb: 0x000000, alpha: 0.6).cgColor
self.stickerFrameLayer.fillColor = UIColor.clear.cgColor
self.stickerFrameLayer.strokeColor = UIColor(rgb: 0xffffff, alpha: 0.55).cgColor
self.stickerFrameLayer.lineDashPattern = [24, 24] as [NSNumber]
self.stickerFrameLayer.lineCap = .round
self.addSubview(self.topEdgeView)
self.addSubview(self.leftEdgeView)
self.addSubview(self.rightEdgeView)
self.addSubview(self.bottomEdgeView)
self.addSubview(self.xAxisView)
self.addSubview(self.yAxisView)
self.layer.addSublayer(self.angleLayer)
if isStickerEditor {
self.layer.addSublayer(self.stickerOverlayLayer)
self.layer.addSublayer(self.stickerFrameLayer)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func addSubview(_ view: UIView) {
super.addSubview(view)
if self.stickerOverlayLayer.superlayer != nil, view is DrawingEntityView {
self.layer.addSublayer(self.stickerOverlayLayer)
self.layer.addSublayer(self.stickerFrameLayer)
}
}
public override func layoutSubviews() {
super.layoutSubviews()
let referenceSize = self.convert(CGRect(origin: .zero, size: CGSize(width: 1.0 + UIScreenPixel, height: 1.0)), from: nil)
let width = ceil(referenceSize.width)
if let edges = self.getEntityEdgePositions() {
self.topEdgeView.bounds = CGRect(origin: .zero, size: CGSize(width: 3000.0, height: width))
self.topEdgeView.center = CGPoint(x: self.bounds.width / 2.0, y: edges.top)
self.bottomEdgeView.bounds = CGRect(origin: .zero, size: CGSize(width: 3000.0, height: width))
self.bottomEdgeView.center = CGPoint(x: self.bounds.width / 2.0, y: edges.bottom)
self.leftEdgeView.bounds = CGRect(origin: .zero, size: CGSize(width: width, height: 3000.0))
self.leftEdgeView.center = CGPoint(x: edges.left, y: self.bounds.height / 2.0)
self.rightEdgeView.bounds = CGRect(origin: .zero, size: CGSize(width: width, height: 3000.0))
self.rightEdgeView.center = CGPoint(x: edges.right, y: self.bounds.height / 2.0)
}
let point = self.getEntityCenterPosition()
self.xAxisView.bounds = CGRect(origin: .zero, size: CGSize(width: width, height: 3000.0))
self.xAxisView.center = point
self.xAxisView.transform = CGAffineTransform(rotationAngle: self.getEntityInitialRotation())
self.yAxisView.bounds = CGRect(origin: .zero, size: CGSize(width: 3000.0, height: width))
self.yAxisView.center = point
self.yAxisView.transform = CGAffineTransform(rotationAngle: self.getEntityInitialRotation())
let anglePath = CGMutablePath()
anglePath.move(to: CGPoint(x: 0.0, y: width / 2.0))
anglePath.addLine(to: CGPoint(x: 3000.0, y: width / 2.0))
self.angleLayer.path = anglePath
self.angleLayer.lineWidth = width
self.angleLayer.bounds = CGRect(origin: .zero, size: CGSize(width: 3000.0, height: width))
let frameWidth = floor(self.bounds.width * 0.97)
let frameRect = CGRect(origin: CGPoint(x: floor((self.bounds.width - frameWidth) / 2.0), y: floor((self.bounds.height - frameWidth) / 2.0)), size: CGSize(width: frameWidth, height: frameWidth))
self.stickerOverlayLayer.frame = self.bounds
let overlayOuterRect = UIBezierPath(rect: self.bounds)
let overlayInnerRect = UIBezierPath(cgPath: CGPath(roundedRect: frameRect, cornerWidth: frameWidth / 8.0, cornerHeight: frameWidth / 8.0, transform: nil))
let overlayLineWidth: CGFloat = 2.0 * 2.2
overlayOuterRect.append(overlayInnerRect)
overlayOuterRect.usesEvenOddFillRule = true
self.stickerOverlayLayer.path = overlayOuterRect.cgPath
self.stickerOverlayLayer.fillRule = .evenOdd
self.stickerFrameLayer.frame = self.bounds
self.stickerFrameLayer.lineWidth = overlayLineWidth
self.stickerFrameLayer.path = CGPath(roundedRect: frameRect.insetBy(dx: -overlayLineWidth / 2.0, dy: -overlayLineWidth / 2.0), cornerWidth: frameWidth / 8.0 * 1.02, cornerHeight: frameWidth / 8.0 * 1.02, transform: nil)
}
public var entities: [DrawingEntity] {
var entities: [DrawingEntity] = []
for case let view as DrawingEntityView in self.subviews {
entities.append(view.entity)
}
return entities
}
public var hasEntities: Bool {
let entities = self.entities.filter { !($0 is DrawingMediaEntity) }
return !entities.isEmpty
}
private var initialEntitiesData: Data?
public func setup(withEntitiesData entitiesData: Data?) {
self.clear()
self.initialEntitiesData = entitiesData
if let entitiesData = entitiesData, let codableEntities = try? JSONDecoder().decode([CodableDrawingEntity].self, from: entitiesData) {
let entities = codableEntities.map { $0.entity }
for entity in entities {
self.add(entity, announce: false)
}
}
}
public func setup(with entities: [DrawingEntity]) {
self.clear()
for entity in entities {
if entity is DrawingMediaEntity {
continue
}
self.add(entity, announce: false)
}
}
public static func encodeEntities(_ entities: [DrawingEntity], entitiesView: DrawingEntitiesView? = nil) -> [CodableDrawingEntity] {
let entities = entities
guard !entities.isEmpty else {
return []
}
if let entitiesView {
for entity in entities {
if let entityView = entitiesView.getView(for: entity.uuid) {
prepareForRendering(entityView: entityView)
}
}
}
return entities.compactMap({ CodableDrawingEntity(entity: $0) })
}
public static func encodeEntitiesData(_ entities: [DrawingEntity], entitiesView: DrawingEntitiesView? = nil) -> Data? {
let codableEntities = encodeEntities(entities, entitiesView: entitiesView)
if let data = try? JSONEncoder().encode(codableEntities) {
return data
} else {
return nil
}
}
var entitiesData: Data? {
return DrawingEntitiesView.encodeEntitiesData(self.entities, entitiesView: self)
}
var hasChanges: Bool {
if let initialEntitiesData = self.initialEntitiesData {
let entitiesData = self.entitiesData
return entitiesData != initialEntitiesData
} else {
let filteredEntities = self.entities.filter { entity in
if entity.isMedia {
return false
} else if let stickerEntity = entity as? DrawingStickerEntity, case .dualVideoReference = stickerEntity.content {
return false
}
return true
}
return !filteredEntities.isEmpty
}
}
private func startPosition(relativeTo entity: DrawingEntity?, onlyVertical: Bool = false) -> CGPoint {
let offsetLength = round(self.size.width * 0.1)
let offset = CGPoint(x: onlyVertical ? 0.0 : offsetLength, y: offsetLength)
if let entity = entity {
return entity.center.offsetBy(dx: offset.x, dy: offset.y)
} else {
let minimalDistance: CGFloat = round(offsetLength * 0.5)
var position = self.getEntityCenterPosition()
while true {
var occupied = false
for case let view as DrawingEntityView in self.subviews {
if view.entity.isMedia {
continue
}
let location = view.entity.center
let distance = sqrt(pow(location.x - position.x, 2) + pow(location.y - position.y, 2))
if distance < minimalDistance {
occupied = true
}
}
if !occupied {
break
} else {
position = position.offsetBy(dx: offset.x, dy: offset.y)
}
}
return position
}
}
private func newEntitySize() -> CGSize {
let zoomScale = 1.0 / (self.drawingView?.zoomScale ?? 1.0)
let width = round(self.size.width * 0.5) * zoomScale
return CGSize(width: width, height: width)
}
public func prepareNewEntity(_ entity: DrawingEntity, setup: Bool = true, relativeTo: DrawingEntity? = nil, scale: CGFloat? = nil, position: CGPoint? = nil) {
var center = self.startPosition(relativeTo: relativeTo, onlyVertical: entity is DrawingTextEntity)
if let position {
center = position
}
let rotation = self.getEntityInitialRotation()
var zoomScale = 1.0 / (self.drawingView?.zoomScale ?? 1.0)
if let scale {
zoomScale = scale
}
if let shape = entity as? DrawingSimpleShapeEntity {
shape.position = center
if setup {
shape.rotation = rotation
let size = self.newEntitySize()
shape.referenceDrawingSize = self.size
if shape.shapeType == .star {
shape.size = size
} else {
shape.size = CGSize(width: size.width, height: round(size.height * 0.75))
}
}
} else if let vector = entity as? DrawingVectorEntity {
if setup {
vector.drawingSize = self.size
vector.referenceDrawingSize = self.size
vector.start = CGPoint(x: center.x * 0.5, y: center.y)
vector.mid = (0.5, 0.0)
vector.end = CGPoint(x: center.x * 1.5, y: center.y)
vector.type = .oneSidedArrow
}
} else if let sticker = entity as? DrawingStickerEntity {
sticker.position = center
if setup {
sticker.rotation = rotation
sticker.referenceDrawingSize = self.size
sticker.scale = zoomScale
}
} else if let bubble = entity as? DrawingBubbleEntity {
bubble.position = center
if setup {
bubble.rotation = rotation
let size = self.newEntitySize()
bubble.referenceDrawingSize = self.size
bubble.size = CGSize(width: size.width, height: round(size.height * 0.7))
bubble.tailPosition = CGPoint(x: 0.16, y: size.height * 0.18)
}
} else if let text = entity as? DrawingTextEntity {
text.position = center
if setup {
text.rotation = rotation
text.referenceDrawingSize = self.size
text.width = floor(self.size.width * 0.9)
text.fontSize = 0.08
text.scale = zoomScale
}
} else if let location = entity as? DrawingLocationEntity {
location.position = center
if setup {
location.rotation = rotation
location.referenceDrawingSize = self.size
location.width = floor(self.size.width * 0.85)
location.scale = zoomScale
}
}
}
@discardableResult
public func add(_ entity: DrawingEntity, announce: Bool = true) -> DrawingEntityView {
guard let view = makeEntityView(context: self.context, entity: entity) else {
fatalError()
}
view.containerView = self
let processSnap: (Bool, UIView) -> Void = { [weak self] snapped, snapView in
guard let self else {
return
}
if snapped {
let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
self.insertSubview(snapView, belowSubview: view)
if snapView.alpha < 1.0 {
self.hapticFeedback.impact(.light)
}
transition.updateAlpha(layer: snapView.layer, alpha: 1.0)
} else {
let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .easeInOut)
transition.updateAlpha(layer: snapView.layer, alpha: 0.0)
}
}
let isMediaEntity = entity is DrawingMediaEntity
view.onSnapUpdated = { [weak self, weak view] type, snapped in
guard let self else {
return
}
switch type {
case .centerX:
processSnap(snapped, self.xAxisView)
case .centerY:
processSnap(snapped, self.yAxisView)
case .top:
processSnap(snapped, self.topEdgeView)
self.edgePreviewUpdated(snapped)
case .left:
processSnap(snapped, self.leftEdgeView)
self.edgePreviewUpdated(snapped)
case .right:
processSnap(snapped, self.rightEdgeView)
self.edgePreviewUpdated(snapped)
case .bottom:
processSnap(snapped, self.bottomEdgeView)
self.edgePreviewUpdated(snapped)
case let .rotation(angle):
if let angle, let view {
let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
self.layer.insertSublayer(self.angleLayer, below: view.layer)
self.angleLayer.transform = CATransform3DMakeRotation(angle, 0.0, 0.0, 1.0)
if self.angleLayer.opacity < 1.0 {
self.hapticFeedback.impact(.light)
}
transition.updateAlpha(layer: self.angleLayer, alpha: 1.0)
self.angleLayer.isHidden = isMediaEntity
} else {
let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .easeInOut)
transition.updateAlpha(layer: self.angleLayer, alpha: 0.0)
}
}
}
view.onPositionUpdated = { [weak self] position in
if let self {
self.angleLayer.position = position
}
}
view.onInteractionUpdated = { [weak self] interacting in
if let self {
self.onInteractionUpdated(interacting)
}
}
view.update()
self.addSubview(view)
if announce {
self.entityAdded(entity)
}
return view
}
public func invalidate() {
for case let view as DrawingEntityView in self.subviews {
view.invalidate()
}
}
func duplicate(_ entity: DrawingEntity) -> DrawingEntity {
let newEntity = entity.duplicate(copy: false)
self.prepareNewEntity(newEntity, setup: false, relativeTo: entity)
guard let view = makeEntityView(context: self.context, entity: newEntity) else {
fatalError()
}
if let initialView = self.getView(for: entity.uuid) {
view.onSnapUpdated = initialView.onSnapUpdated
view.onPositionUpdated = initialView.onPositionUpdated
view.onInteractionUpdated = initialView.onInteractionUpdated
}
view.containerView = self
view.update()
self.addSubview(view)
return newEntity
}
public func remove(uuid: UUID, animated: Bool = false, announce: Bool = true) {
if let view = self.getView(for: uuid) {
if self.selectedEntityView === view {
if let stickerEntityView = self.selectedEntityView as? DrawingStickerEntityView {
stickerEntityView.onDeselection()
}
self.selectedEntityView = nil
self.selectionChanged(nil)
self.hasSelectionChanged(false)
view.selectionView?.removeFromSuperview()
}
if animated {
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak view] _ in
view?.removeFromSuperview()
})
if !(view.entity is DrawingVectorEntity) {
view.layer.animateScale(from: view.entity.scale, to: 0.1, duration: 0.2, removeOnCompletion: false)
}
if let selectionView = view.selectionView {
selectionView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak selectionView] _ in
selectionView?.removeFromSuperview()
})
if !(view.entity is DrawingVectorEntity) {
selectionView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false)
}
}
} else {
view.removeFromSuperview()
}
if announce {
self.entityRemoved(view.entity)
self.externalEntityRemoved(view.entity)
}
}
}
func removeAll() {
self.clear(animated: true)
self.selectionChanged(nil)
self.hasSelectionChanged(false)
}
private func clear(animated: Bool = false) {
if animated {
for case let view as DrawingEntityView in self.subviews {
if view.entity.isMedia {
continue
}
if let selectionView = view.selectionView {
selectionView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, removeOnCompletion: false, completion: { [weak selectionView] _ in
selectionView?.removeFromSuperview()
})
}
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak view] _ in
view?.removeFromSuperview()
})
if !(view.entity is DrawingVectorEntity) {
view.layer.animateScale(from: 0.0, to: -0.99, duration: 0.2, removeOnCompletion: false, additive: true)
}
}
} else {
for case let view as DrawingEntityView in self.subviews {
if view.entity.isMedia {
continue
}
view.selectionView?.removeFromSuperview()
view.removeFromSuperview()
}
}
}
func bringToFront(uuid: UUID) {
if let view = self.getView(for: uuid) {
self.bringSubviewToFront(view)
}
}
public func getView(for uuid: UUID) -> DrawingEntityView? {
for case let view as DrawingEntityView in self.subviews {
if view.entity.uuid == uuid {
return view
}
}
return nil
}
public func getView(where f: (DrawingEntityView) -> Bool) -> DrawingEntityView? {
for case let view as DrawingEntityView in self.subviews {
if f(view) {
return view
}
}
return nil
}
public func getView(at point: CGPoint) -> DrawingEntityView? {
for case let view as DrawingEntityView in self.subviews {
if view is DrawingMediaEntityView {
continue
}
if view.frame.contains(point) {
return view
}
}
return nil
}
public func eachView(_ f: (DrawingEntityView) -> Void) {
for case let view as DrawingEntityView in self.subviews {
f(view)
}
}
public func play() {
for case let view as DrawingEntityView in self.subviews {
view.play()
}
}
public func pause() {
for case let view as DrawingEntityView in self.subviews {
view.pause()
}
}
public func seek(to timestamp: Double) {
for case let view as DrawingEntityView in self.subviews {
view.seek(to: timestamp)
}
}
public func resetToStart() {
for case let view as DrawingEntityView in self.subviews {
view.resetToStart()
}
}
public func updateVisibility(_ visibility: Bool) {
for case let view as DrawingEntityView in self.subviews {
view.updateVisibility(visibility)
}
}
@objc private func handleTap(_ gestureRecognzier: UITapGestureRecognizer) {
guard self.canInteract() else {
return
}
let location = gestureRecognzier.location(in: self)
if let entityView = self.entity(at: location) {
self.selectEntity(entityView.entity)
entityView.onSelection()
}
}
private func entity(at location: CGPoint) -> DrawingEntityView? {
var intersectedViews: [DrawingEntityView] = []
for case let view as DrawingEntityView in self.subviews {
if view is DrawingMediaEntityView {
continue
}
if view.precisePoint(inside: self.convert(location, to: view)) {
intersectedViews.append(view)
}
}
return intersectedViews.last
}
public func selectEntity(_ entity: DrawingEntity?, animate: Bool = true) {
if entity?.isMedia == true {
return
}
var selectionChanged = false
if entity !== self.selectedEntityView?.entity {
if let selectedEntityView = self.selectedEntityView {
if let textEntityView = selectedEntityView as? DrawingTextEntityView, textEntityView.isEditing {
if entity == nil {
textEntityView.endEditing()
} else {
return
}
} else if let stickerEntityView = selectedEntityView as? DrawingStickerEntityView {
stickerEntityView.onDeselection()
}
self.selectedEntityView = nil
if let selectionView = selectedEntityView.selectionView {
selectedEntityView.selectionView = nil
selectionView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak selectionView] _ in
selectionView?.removeFromSuperview()
})
}
}
selectionChanged = true
}
if let entity = entity, let entityView = self.getView(for: entity.uuid) {
self.selectedEntityView = entityView
if let selectionView = entityView.makeSelectionView() {
selectionView.tapped = { [weak self, weak entityView] in
if let self, let entityView {
let entityViews = self.subviews.filter { $0 is DrawingEntityView }
if !entityView.selectedTapAction() {
self.requestedMenuForEntityView(entityView, entityViews.last === entityView)
}
}
}
selectionView.longPressed = { [weak self, weak entityView] in
if let self, let entityView {
let entityViews = self.subviews.filter { $0 is DrawingEntityView }
self.requestedMenuForEntityView(entityView, entityViews.last === entityView)
}
}
entityView.selectionView = selectionView
self.selectionContainerView?.addSubview(selectionView)
}
entityView.update()
if selectionChanged && animate {
entityView.animateSelection()
}
}
self.selectionChanged(self.selectedEntityView?.entity)
self.hasSelectionChanged(self.selectedEntityView != nil)
}
var isTrackingAnyEntity: Bool {
for case let view as DrawingEntityView in self.subviews {
if view.isTracking {
return true
}
}
return false
}
public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return super.point(inside: point, with: event)
}
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event)
if result === self {
return nil
}
if let result = result as? DrawingEntityView, !result.precisePoint(inside: self.convert(point, to: result)) {
return nil
}
return result
}
public func clearSelection() {
self.selectEntity(nil)
}
public func onZoom() {
self.selectedEntityView?.updateSelectionView()
}
public var hasSelection: Bool {
return self.selectedEntityView != nil
}
public var isEditingText: Bool {
if let entityView = self.selectedEntityView as? DrawingTextEntityView, entityView.isEditing {
return true
} else {
return false
}
}
public func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
guard self.canInteract() else {
return
}
let location = gestureRecognizer.location(in: self)
if let selectedEntityView = self.selectedEntityView, let selectionView = selectedEntityView.selectionView {
if !self.hasBin {
selectionView.handlePan(gestureRecognizer)
} else if let stickerEntity = selectedEntityView.entity as? DrawingStickerEntity, case .dualVideoReference = stickerEntity.content {
selectionView.handlePan(gestureRecognizer)
} else if let stickerEntity = selectedEntityView.entity as? DrawingStickerEntity, case .message = stickerEntity.content {
selectionView.handlePan(gestureRecognizer)
} else {
var isTrappedInBin = false
let scale = 100.0 / selectedEntityView.bounds.size.width
switch gestureRecognizer.state {
case .changed:
if self.updateBin(location: location) {
isTrappedInBin = true
}
case .ended, .cancelled:
let _ = self.updateBin(location: nil)
if selectedEntityView.isTrappedInBin {
selectedEntityView.layer.animateScale(from: scale, to: 0.01, duration: 0.2, removeOnCompletion: false)
selectedEntityView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
self.remove(uuid: selectedEntityView.entity.uuid)
})
selectedEntityView.selectionView?.removeFromSuperview()
self.selectEntity(nil)
Queue.mainQueue().after(0.3, {
self.onInteractionUpdated(false)
})
return
}
default:
break
}
let transition = Transition.easeInOut(duration: 0.2)
if isTrappedInBin, let binView = self.bin.view {
if !selectedEntityView.isTrappedInBin {
let refs = [
self.xAxisView,
self.yAxisView,
self.topEdgeView,
self.leftEdgeView,
self.rightEdgeView,
self.bottomEdgeView
]
for ref in refs {
transition.setAlpha(view: ref, alpha: 0.0)
}
self.edgePreviewUpdated(false)
selectedEntityView.isTrappedInBin = true
transition.setAlpha(view: selectionView, alpha: 0.0)
transition.animatePosition(view: selectionView, from: selectionView.center, to: self.convert(binView.center, to: selectionView.superview))
transition.animateScale(view: selectionView, from: 0.0, to: -0.5, additive: true)
transition.setPosition(view: selectedEntityView, position: binView.center)
let rotation = selectedEntityView.layer.transform.decompose().rotation
var transform = CATransform3DMakeScale(scale, scale, 1.0)
transform = CATransform3DRotate(transform, CGFloat(rotation.z), 0.0, 0.0, 1.0)
transition.setTransform(view: selectedEntityView, transform: transform)
}
} else {
if selectedEntityView.isTrappedInBin {
selectedEntityView.isTrappedInBin = false
transition.setAlpha(view: selectionView, alpha: 1.0)
selectedEntityView.layer.animateScale(from: scale, to: selectedEntityView.entity.scale, duration: 0.13)
}
selectionView.handlePan(gestureRecognizer)
}
}
} else if self.autoSelectEntities, gestureRecognizer.numberOfTouches == 1, let viewToSelect = self.entity(at: location) {
self.selectEntity(viewToSelect.entity, animate: false)
self.onInteractionUpdated(true)
} else if gestureRecognizer.numberOfTouches == 2 || self.isStickerEditor, let mediaEntityView = self.subviews.first(where: { $0 is DrawingEntityMediaView }) as? DrawingEntityMediaView {
mediaEntityView.handlePan(gestureRecognizer)
}
}
public func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) {
guard self.canInteract() else {
return
}
if !self.hasSelection, let mediaEntityView = self.subviews.first(where: { $0 is DrawingEntityMediaView }) as? DrawingEntityMediaView {
mediaEntityView.handlePinch(gestureRecognizer)
} else if let selectedEntityView = self.selectedEntityView, let selectionView = selectedEntityView.selectionView {
selectionView.handlePinch(gestureRecognizer)
}
}
public func handleRotate(_ gestureRecognizer: UIRotationGestureRecognizer) {
guard self.canInteract() else {
return
}
if !self.hasSelection, let mediaEntityView = self.subviews.first(where: { $0 is DrawingEntityMediaView }) as? DrawingEntityMediaView {
mediaEntityView.handleRotate(gestureRecognizer)
} else if let selectedEntityView = self.selectedEntityView, let selectionView = selectedEntityView.selectionView {
selectionView.handleRotate(gestureRecognizer)
}
}
private var binWasOpened = false
private func updateBin(location: CGPoint?) -> Bool {
let binSize = CGSize(width: 180.0, height: 180.0)
let binFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((self.bounds.width - binSize.width) / 2.0), y: self.bounds.height - binSize.height - 20.0), size: binSize)
let wasOpened = self.binWasOpened
var isOpened = false
if let location {
isOpened = binFrame.insetBy(dx: 20.0, dy: 20.0).contains(location)
}
self.binWasOpened = isOpened
if wasOpened != isOpened {
self.hapticFeedback.impact(.medium)
}
let _ = self.bin.update(
transition: .immediate,
component: AnyComponent(EntityBinComponent(isOpened: isOpened)),
environment: {},
containerSize: binSize
)
if let binView = self.bin.view {
if binView.superview == nil {
self.addSubview(binView)
} else if self.subviews.last !== binView {
self.bringSubviewToFront(binView)
}
binView.frame = binFrame
Transition.easeInOut(duration: 0.2).setAlpha(view: binView, alpha: location != nil ? 1.0 : 0.0, delay: location == nil && wasOpened ? 0.4 : 0.0)
}
return isOpened
}
}
protocol DrawingEntityMediaView: DrawingEntityView {
func handlePan(_ gestureRecognizer: UIPanGestureRecognizer)
func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer)
func handleRotate(_ gestureRecognizer: UIRotationGestureRecognizer)
}
public class DrawingEntityView: UIView {
let context: AccountContext
public let entity: DrawingEntity
var isTracking = false
var isTrappedInBin = false
public weak var selectionView: DrawingEntitySelectionView?
weak var containerView: DrawingEntitiesView?
var onSnapUpdated: (DrawingEntitySnapTool.SnapType, Bool) -> Void = { _, _ in }
var onPositionUpdated: (CGPoint) -> Void = { _ in }
var onInteractionUpdated: (Bool) -> Void = { _ in }
init(context: AccountContext, entity: DrawingEntity) {
self.context = context
self.entity = entity
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
if let selectionView = self.selectionView {
selectionView.removeFromSuperview()
}
}
var selectionBounds: CGRect {
return self.bounds
}
func animateInsertion() {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
let values = [0.0, self.entity.scale * 1.1, self.entity.scale]
let keyTimes = [0.0, 0.67, 1.0]
self.layer.animateKeyframes(values: values as [NSNumber], keyTimes: keyTimes as [NSNumber], duration: 0.35, keyPath: "transform.scale")
if let selectionView = self.selectionView {
selectionView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.3)
}
}
func animateSelection() {
guard let selectionView = self.selectionView else {
return
}
selectionView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.1)
selectionView.layer.animateScale(from: 0.88, to: 1.0, duration: 0.23, delay: 0.1)
let values = [self.entity.scale, self.entity.scale * 0.88, self.entity.scale]
let keyTimes = [0.0, 0.33, 1.0]
self.layer.animateKeyframes(values: values as [NSNumber], keyTimes: keyTimes as [NSNumber], duration: 0.3, keyPath: "transform.scale")
}
func onSelection() {
}
func selectedTapAction() -> Bool {
return false
}
public func play() {
}
public func pause() {
}
public func seek(to timestamp: Double) {
}
func resetToStart() {
}
func updateVisibility(_ visibility: Bool) {
}
func invalidate() {
self.selectionView = nil
self.containerView = nil
self.onSnapUpdated = { _, _ in }
self.onPositionUpdated = { _ in }
self.onInteractionUpdated = { _ in }
}
public func update(animated: Bool = false) {
self.updateSelectionView()
}
func updateSelectionView() {
guard let selectionView = self.selectionView 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 * scale + selectionView.selectionInset * 2.0, height: bounds.height * scale + selectionView.selectionInset * 2.0))
self.popIdentityTransformForMeasurement()
}
private var realTransform: CGAffineTransform?
func pushIdentityTransformForMeasurement() {
guard self.realTransform == nil else {
return
}
self.realTransform = self.transform
self.transform = .identity
}
func popIdentityTransformForMeasurement() {
guard let realTransform = self.realTransform else {
return
}
self.transform = realTransform
self.realTransform = nil
}
public func precisePoint(inside point: CGPoint) -> Bool {
return self.point(inside: point, with: nil)
}
func makeSelectionView() -> DrawingEntitySelectionView? {
if let selectionView = self.selectionView {
return selectionView
}
return DrawingEntitySelectionView()
}
}
let entitySelectionViewHandleSize = CGSize(width: 44.0, height: 44.0)
public class DrawingEntitySelectionView: UIView {
public weak var entityView: DrawingEntityView?
public var tapGestureRecognizer: UITapGestureRecognizer?
var tapped: () -> Void = { }
var longPressed: () -> Void = { }
override init(frame: CGRect) {
super.init(frame: frame)
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))
self.tapGestureRecognizer = tapGestureRecognizer
self.addGestureRecognizer(tapGestureRecognizer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc func handleTap(_ gestureRecognizer: UITapGestureRecognizer) {
self.tapped()
}
@objc func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) {
}
@objc func handleRotate(_ gestureRecognizer: UIRotationGestureRecognizer) {
}
@objc public func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
}
var selectionInset: CGFloat {
return 0.0
}
}
public class DrawingSelectionContainerView: UIView {
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event)
if result === self {
return nil
}
return result
}
public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
let result = super.point(inside: point, with: event)
if !result {
for subview in self.subviews {
let subpoint = self.convert(point, to: subview)
if subview.point(inside: subpoint, with: event) {
return true
}
}
}
return result
}
}
private final class EntityBinComponent: Component {
typealias EnvironmentType = Empty
let isOpened: Bool
init(
isOpened: Bool
) {
self.isOpened = isOpened
}
static func ==(lhs: EntityBinComponent, rhs: EntityBinComponent) -> Bool {
if lhs.isOpened != rhs.isOpened {
return false
}
return true
}
public final class View: UIView {
private let circle = SimpleShapeLayer()
private let animation = ComponentView<Empty>()
private var component: EntityBinComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = .clear
self.circle.strokeColor = UIColor.white.cgColor
self.circle.fillColor = UIColor.clear.cgColor
self.circle.lineWidth = 5.0
self.layer.addSublayer(self.circle)
self.circle.path = CGPath(ellipseIn: CGRect(origin: .zero, size: CGSize(width: 160.0, height: 160.0)).insetBy(dx: 3.0, dy: 3.0), transform: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private var wasOpened = false
func update(component: EntityBinComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
self.component = component
self.state = state
if !self.wasOpened {
self.wasOpened = component.isOpened
}
let animationSize = self.animation.update(
transition: transition,
component: AnyComponent(LottieAnimationComponent(
animation: LottieAnimationComponent.AnimationItem(
name: "anim_entitybin",
mode: component.isOpened ? .animating(loop: false) : (self.wasOpened ? .animating(loop: false) : .still(position: .end)),
range: component.isOpened ? (0.0, 0.5) : (0.5, 1.0)
),
colors: [:],
size: CGSize(width: 140.0, height: 140.0)
)),
environment: {},
containerSize: CGSize(width: 140.0, height: 140.0)
)
let animationFrame = CGRect(
origin: CGPoint(x: 20.0, y: 20.0),
size: animationSize
)
if let animationView = self.animation.view {
if animationView.superview == nil {
self.addSubview(animationView)
}
transition.setPosition(view: animationView, position: animationFrame.center)
transition.setBounds(view: animationView, bounds: CGRect(origin: .zero, size: animationFrame.size))
}
let circleSize = CGSize(width: 160.0, height: 160.0)
self.circle.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - circleSize.width) / 2.0), y: floorToScreenPixels((availableSize.height - circleSize.height) / 2.0)), size: CGSize(width: 100.0, height: 100.0))
return availableSize
}
}
func makeView() -> View {
return View()
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}