diff --git a/Telegram/BUILD b/Telegram/BUILD index e6fc4a82dc..2aa652fad6 100644 --- a/Telegram/BUILD +++ b/Telegram/BUILD @@ -345,6 +345,7 @@ swift_library( "//submodules/PasswordSetupUI:PasswordSetupUIResources", "//submodules/PasswordSetupUI:PasswordSetupUIAssets", "//submodules/PremiumUI:PremiumUIResources", + "//submodules/DrawingUI:DrawingUIResources", "//submodules/TelegramUI:TelegramUIResources", "//submodules/TelegramUI:TelegramUIAssets", ":GeneratedPresentationStrings/Resources/PresentationStrings.data", diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index e375f6f2d2..3257a7d587 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -809,7 +809,7 @@ public protocol SharedAccountContext: AnyObject { func makePremiumDemoController(context: AccountContext, subject: PremiumDemoSubject, action: @escaping () -> Void) -> ViewController func makeStickerPackScreen(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, mainStickerPack: StickerPackReference, stickerPacks: [StickerPackReference], loadedStickerPacks: [LoadedStickerPack], parentNavigationController: NavigationController?, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?) -> ViewController - + func makeProxySettingsController(sharedContext: SharedAccountContext, account: UnauthorizedAccount) -> ViewController func navigateToCurrentCall() diff --git a/submodules/ComponentFlow/Source/Base/ChildComponentTransitions.swift b/submodules/ComponentFlow/Source/Base/ChildComponentTransitions.swift index d5f40e0280..6871454b3b 100644 --- a/submodules/ComponentFlow/Source/Base/ChildComponentTransitions.swift +++ b/submodules/ComponentFlow/Source/Base/ChildComponentTransitions.swift @@ -35,10 +35,24 @@ public extension Transition.AppearWithGuide { } public extension Transition.Disappear { - static let `default` = Transition.Disappear { view, transition, completion in - transition.setAlpha(view: view, alpha: 0.0, completion: { _ in - completion() - }) + static func `default`(scale: Bool = false, alpha: Bool = true) -> Transition.Disappear { + return Transition.Disappear { view, transition, completion in + if scale { + transition.setScale(view: view, scale: 0.01, completion: { _ in + if !alpha { + completion() + } + }) + } + if alpha { + transition.setAlpha(view: view, alpha: 0.0, completion: { _ in + completion() + }) + } + if !alpha && !scale { + completion() + } + } } } diff --git a/submodules/ComponentFlow/Source/Base/Transition.swift b/submodules/ComponentFlow/Source/Base/Transition.swift index b7601ab3db..c253dda8ef 100644 --- a/submodules/ComponentFlow/Source/Base/Transition.swift +++ b/submodules/ComponentFlow/Source/Base/Transition.swift @@ -391,11 +391,11 @@ public struct Transition { } } - public func setAlpha(view: UIView, alpha: CGFloat, completion: ((Bool) -> Void)? = nil) { - self.setAlpha(layer: view.layer, alpha: alpha, completion: completion) + public func setAlpha(view: UIView, alpha: CGFloat, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) { + self.setAlpha(layer: view.layer, alpha: alpha, delay: delay, completion: completion) } - public func setAlpha(layer: CALayer, alpha: CGFloat, completion: ((Bool) -> Void)? = nil) { + public func setAlpha(layer: CALayer, alpha: CGFloat, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) { if layer.opacity == Float(alpha) { completion?(true) return @@ -408,15 +408,15 @@ public struct Transition { case .curve: let previousAlpha = layer.presentation()?.opacity ?? layer.opacity layer.opacity = Float(alpha) - self.animateAlpha(layer: layer, from: CGFloat(previousAlpha), to: alpha, completion: completion) + self.animateAlpha(layer: layer, from: CGFloat(previousAlpha), to: alpha, delay: delay, completion: completion) } } - public func setScale(view: UIView, scale: CGFloat, completion: ((Bool) -> Void)? = nil) { - self.setScale(layer: view.layer, scale: scale, completion: completion) + public func setScale(view: UIView, scale: CGFloat, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) { + self.setScale(layer: view.layer, scale: scale, delay: delay, completion: completion) } - public func setScale(layer: CALayer, scale: CGFloat, completion: ((Bool) -> Void)? = nil) { + public func setScale(layer: CALayer, scale: CGFloat, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) { let t = layer.presentation()?.transform ?? layer.transform let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) if currentScale == scale { @@ -435,7 +435,7 @@ public struct Transition { to: scale as NSNumber, keyPath: "transform.scale", duration: duration, - delay: 0.0, + delay: delay, curve: curve, removeOnCompletion: true, additive: false, @@ -476,19 +476,23 @@ public struct Transition { } public func setSublayerTransform(view: UIView, transform: CATransform3D, completion: ((Bool) -> Void)? = nil) { + self.setSublayerTransform(layer: view.layer, transform: transform, completion: completion) + } + + public func setSublayerTransform(layer: CALayer, transform: CATransform3D, completion: ((Bool) -> Void)? = nil) { switch self.animation { case .none: - view.layer.sublayerTransform = transform + layer.sublayerTransform = transform completion?(true) case let .curve(duration, curve): let previousValue: CATransform3D - if let presentation = view.layer.presentation() { + if let presentation = layer.presentation() { previousValue = presentation.sublayerTransform } else { - previousValue = view.layer.sublayerTransform + previousValue = layer.sublayerTransform } - view.layer.sublayerTransform = transform - view.layer.animate( + layer.sublayerTransform = transform + layer.animate( from: NSValue(caTransform3D: previousValue), to: NSValue(caTransform3D: transform), keyPath: "sublayerTransform", @@ -502,7 +506,7 @@ public struct Transition { } } - public func animateScale(view: UIView, from fromValue: CGFloat, to toValue: CGFloat, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { + public func animateScale(view: UIView, from fromValue: CGFloat, to toValue: CGFloat, delay: Double = 0.0, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { switch self.animation { case .none: completion?(true) @@ -512,7 +516,7 @@ public struct Transition { to: toValue as NSNumber, keyPath: "transform.scale", duration: duration, - delay: 0.0, + delay: delay, curve: curve, removeOnCompletion: true, additive: additive, @@ -540,11 +544,11 @@ public struct Transition { } } - public func animateAlpha(view: UIView, from fromValue: CGFloat, to toValue: CGFloat, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { - self.animateAlpha(layer: view.layer, from: fromValue, to: toValue, additive: additive, completion: completion) + public func animateAlpha(view: UIView, from fromValue: CGFloat, to toValue: CGFloat, delay: Double = 0.0, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { + self.animateAlpha(layer: view.layer, from: fromValue, to: toValue, delay: delay, additive: additive, completion: completion) } - public func animateAlpha(layer: CALayer, from fromValue: CGFloat, to toValue: CGFloat, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { + public func animateAlpha(layer: CALayer, from fromValue: CGFloat, to toValue: CGFloat, delay: Double = 0.0, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { switch self.animation { case .none: completion?(true) @@ -554,7 +558,7 @@ public struct Transition { to: toValue as NSNumber, keyPath: "opacity", duration: duration, - delay: 0.0, + delay: delay, curve: curve, removeOnCompletion: true, additive: additive, @@ -709,6 +713,28 @@ public struct Transition { } } + public func setShapeLayerLineWidth(layer: CAShapeLayer, lineWidth: CGFloat, completion: ((Bool) -> Void)? = nil) { + switch self.animation { + case .none: + layer.lineWidth = lineWidth + case let .curve(duration, curve): + let previousLineWidth = layer.lineWidth + layer.lineWidth = lineWidth + + layer.animate( + from: previousLineWidth as NSNumber, + to: lineWidth as NSNumber, + keyPath: "lineWidth", + duration: duration, + delay: 0.0, + curve: curve, + removeOnCompletion: true, + additive: false, + completion: completion + ) + } + } + public func setShapeLayerLineDashPattern(layer: CAShapeLayer, pattern: [NSNumber], completion: ((Bool) -> Void)? = nil) { switch self.animation { case .none: diff --git a/submodules/ComponentFlow/Source/Components/Button.swift b/submodules/ComponentFlow/Source/Components/Button.swift index 1332bd0af0..ed1e57024e 100644 --- a/submodules/ComponentFlow/Source/Components/Button.swift +++ b/submodules/ComponentFlow/Source/Components/Button.swift @@ -6,11 +6,13 @@ public final class Button: Component { public let minSize: CGSize? public let tag: AnyObject? public let automaticHighlight: Bool + public let isEnabled: Bool public let action: () -> Void public let holdAction: (() -> Void)? convenience public init( content: AnyComponent, + isEnabled: Bool = true, action: @escaping () -> Void ) { self.init( @@ -18,6 +20,7 @@ public final class Button: Component { minSize: nil, tag: nil, automaticHighlight: true, + isEnabled: isEnabled, action: action, holdAction: nil ) @@ -28,6 +31,7 @@ public final class Button: Component { minSize: CGSize? = nil, tag: AnyObject? = nil, automaticHighlight: Bool = true, + isEnabled: Bool = true, action: @escaping () -> Void, holdAction: (() -> Void)? ) { @@ -35,6 +39,7 @@ public final class Button: Component { self.minSize = minSize self.tag = tag self.automaticHighlight = automaticHighlight + self.isEnabled = isEnabled self.action = action self.holdAction = holdAction } @@ -45,6 +50,7 @@ public final class Button: Component { minSize: minSize, tag: self.tag, automaticHighlight: self.automaticHighlight, + isEnabled: self.isEnabled, action: self.action, holdAction: self.holdAction ) @@ -56,6 +62,7 @@ public final class Button: Component { minSize: self.minSize, tag: self.tag, automaticHighlight: self.automaticHighlight, + isEnabled: self.isEnabled, action: self.action, holdAction: holdAction ) @@ -67,6 +74,7 @@ public final class Button: Component { minSize: self.minSize, tag: tag, automaticHighlight: self.automaticHighlight, + isEnabled: self.isEnabled, action: self.action, holdAction: self.holdAction ) @@ -85,6 +93,9 @@ public final class Button: Component { if lhs.automaticHighlight != rhs.automaticHighlight { return false } + if lhs.isEnabled != rhs.isEnabled { + return false + } return true } @@ -98,11 +109,28 @@ public final class Button: Component { return } if self.currentIsHighlighted != oldValue { - self.contentView.alpha = self.currentIsHighlighted ? 0.6 : 1.0 + self.updateAlpha(transition: .immediate) } } } + private func updateAlpha(transition: Transition) { + guard let component = self.component else { + return + } + let alpha: CGFloat + if component.isEnabled { + if component.automaticHighlight { + alpha = self.currentIsHighlighted ? 0.6 : 1.0 + } else { + alpha = 1.0 + } + } else { + alpha = 0.4 + } + transition.setAlpha(view: self.contentView, alpha: alpha) + } + private var holdActionTriggerred: Bool = false private var holdActionTimer: Timer? @@ -218,6 +246,9 @@ public final class Button: Component { self.component = component + self.updateAlpha(transition: transition) + self.isEnabled = component.isEnabled + transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(x: floor((size.width - contentSize.width) / 2.0), y: floor((size.height - contentSize.height) / 2.0)), size: contentSize), completion: nil) return size diff --git a/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift b/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift index c584daf0e8..91fa37a01d 100644 --- a/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift +++ b/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift @@ -20,10 +20,27 @@ public final class LottieAnimationComponent: Component { public var name: String public var mode: Mode + public var range: (CGFloat, CGFloat)? - public init(name: String, mode: Mode) { + public init(name: String, mode: Mode, range: (CGFloat, CGFloat)? = nil) { self.name = name self.mode = mode + self.range = range + } + + public static func == (lhs: LottieAnimationComponent.AnimationItem, rhs: LottieAnimationComponent.AnimationItem) -> Bool { + if lhs.name != rhs.name { + return false + } + if lhs.mode != rhs.mode { + return false + } + if let lhsRange = lhs.range, let rhsRange = rhs.range, lhsRange != rhsRange { + return false + } else if (lhs.range == nil) != (rhs.range == nil) { + return false + } + return true } } @@ -248,8 +265,14 @@ public final class LottieAnimationComponent: Component { if updatePlayback { if case .animating = component.animation.mode { if !animationView.isAnimationPlaying { - animationView.play { [weak self] _ in - self?.currentCompletion?() + if let range = component.animation.range { + animationView.play(fromProgress: range.0, toProgress: range.1, completion: { [weak self] _ in + self?.currentCompletion?() + }) + } else { + animationView.play { [weak self] _ in + self?.currentCompletion?() + } } } } else { diff --git a/submodules/Components/SheetComponent/BUILD b/submodules/Components/SheetComponent/BUILD index 1b7f99b7f4..5d52191429 100644 --- a/submodules/Components/SheetComponent/BUILD +++ b/submodules/Components/SheetComponent/BUILD @@ -13,6 +13,7 @@ swift_library( "//submodules/Display:Display", "//submodules/ComponentFlow:ComponentFlow", "//submodules/Components/ViewControllerComponent:ViewControllerComponent", + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", ], visibility = [ "//visibility:public", diff --git a/submodules/Components/SheetComponent/Sources/SheetComponent.swift b/submodules/Components/SheetComponent/Sources/SheetComponent.swift index ad0576e9aa..e4bb4bc549 100644 --- a/submodules/Components/SheetComponent/Sources/SheetComponent.swift +++ b/submodules/Components/SheetComponent/Sources/SheetComponent.swift @@ -3,15 +3,18 @@ import UIKit import Display import ComponentFlow import ViewControllerComponent +import SwiftSignalKit public final class SheetComponentEnvironment: Equatable { public let isDisplaying: Bool public let isCentered: Bool + public let hasInputHeight: Bool public let dismiss: (Bool) -> Void - public init(isDisplaying: Bool, isCentered: Bool, dismiss: @escaping (Bool) -> Void) { + public init(isDisplaying: Bool, isCentered: Bool, hasInputHeight: Bool, dismiss: @escaping (Bool) -> Void) { self.isDisplaying = isDisplaying self.isCentered = isCentered + self.hasInputHeight = hasInputHeight self.dismiss = dismiss } @@ -22,6 +25,9 @@ public final class SheetComponentEnvironment: Equatable { if lhs.isCentered != rhs.isCentered { return false } + if lhs.hasInputHeight != rhs.hasInputHeight { + return false + } return true } } @@ -29,11 +35,21 @@ public final class SheetComponentEnvironment: Equatable { public final class SheetComponent: Component { public typealias EnvironmentType = (ChildEnvironmentType, SheetComponentEnvironment) + public enum BackgroundColor: Equatable { + public enum BlurStyle: Equatable { + case light + case dark + } + + case color(UIColor) + case blur(BlurStyle) + } + public let content: AnyComponent - public let backgroundColor: UIColor + public let backgroundColor: BackgroundColor public let animateOut: ActionSlot> - public init(content: AnyComponent, backgroundColor: UIColor, animateOut: ActionSlot>) { + public init(content: AnyComponent, backgroundColor: BackgroundColor, animateOut: ActionSlot>) { self.content = content self.backgroundColor = backgroundColor self.animateOut = animateOut @@ -53,20 +69,36 @@ public final class SheetComponent: Component { return true } + private class ScrollView: UIScrollView { + var ignoreScroll = false + override func setContentOffset(_ contentOffset: CGPoint, animated: Bool) { + guard !self.ignoreScroll else { + return + } + if animated && abs(contentOffset.y - self.contentOffset.y) > 200.0 { + return + } + super.setContentOffset(contentOffset, animated: animated) + } + } + public final class View: UIView, UIScrollViewDelegate { private let dimView: UIView - private let scrollView: UIScrollView + private let scrollView: ScrollView private let backgroundView: UIView + private var effectView: UIVisualEffectView? private let contentView: ComponentHostView private var previousIsDisplaying: Bool = false private var dismiss: ((Bool) -> Void)? + private var keyboardWillShowObserver: AnyObject? + override init(frame: CGRect) { self.dimView = UIView() self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.4) - self.scrollView = UIScrollView() + self.scrollView = ScrollView() self.scrollView.delaysContentTouches = false if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { self.scrollView.contentInsetAdjustmentBehavior = .never @@ -76,7 +108,7 @@ public final class SheetComponent: Component { self.scrollView.alwaysBounceVertical = true self.backgroundView = UIView() - self.backgroundView.layer.cornerRadius = 10.0 + self.backgroundView.layer.cornerRadius = 12.0 self.backgroundView.layer.masksToBounds = true self.contentView = ComponentHostView() @@ -91,12 +123,27 @@ public final class SheetComponent: Component { self.addSubview(self.scrollView) self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimViewTapGesture(_:)))) + + self.keyboardWillShowObserver = NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: nil, using: { [weak self] _ in + if let strongSelf = self { + strongSelf.scrollView.ignoreScroll = true + Queue.mainQueue().after(0.1, { + strongSelf.scrollView.ignoreScroll = false + }) + } + }) } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + deinit { + if let keyboardFrameChangeObserver = self.keyboardWillShowObserver { + NotificationCenter.default.removeObserver(keyboardFrameChangeObserver) + } + } + @objc private func dimViewTapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { self.dismiss?(true) @@ -183,8 +230,10 @@ public final class SheetComponent: Component { } } + private var currentHasInputHeight = false private var currentAvailableSize: CGSize? func update(component: SheetComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let previousHasInputHeight = self.currentHasInputHeight let sheetEnvironment = environment[SheetComponentEnvironment.self].value component.animateOut.connect { [weak self] completion in guard let strongSelf = self else { @@ -195,10 +244,25 @@ public final class SheetComponent: Component { } } - if self.backgroundView.backgroundColor != component.backgroundColor { - self.backgroundView.backgroundColor = component.backgroundColor - } + self.currentHasInputHeight = sheetEnvironment.hasInputHeight + switch component.backgroundColor { + case let .blur(style): + self.backgroundView.isHidden = true + if self.effectView == nil { + let effectView = UIVisualEffectView(effect: UIBlurEffect(style: style == .dark ? .dark : .light)) + effectView.layer.cornerRadius = self.backgroundView.layer.cornerRadius + effectView.layer.masksToBounds = true + self.backgroundView.superview?.insertSubview(effectView, aboveSubview: self.backgroundView) + self.effectView = effectView + } + case let .color(color): + self.backgroundView.backgroundColor = color + self.backgroundView.isHidden = false + self.effectView?.removeFromSuperview() + self.effectView = nil + } + transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize), completion: nil) let containerSize: CGSize @@ -226,9 +290,15 @@ public final class SheetComponent: Component { let y: CGFloat = floorToScreenPixels((availableSize.height - contentSize.height) / 2.0) transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - contentSize.width) / 2.0), y: -y), size: contentSize), completion: nil) transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - contentSize.width) / 2.0), y: -y), size: contentSize), completion: nil) + if let effectView = self.effectView { + transition.setFrame(view: effectView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - contentSize.width) / 2.0), y: -y), size: contentSize), completion: nil) + } } else { transition.setFrame(view: self.contentView, frame: CGRect(origin: .zero, size: contentSize), completion: nil) transition.setFrame(view: self.backgroundView, frame: CGRect(origin: .zero, size: CGSize(width: contentSize.width, height: contentSize.height + 1000.0)), completion: nil) + if let effectView = self.effectView { + transition.setFrame(view: effectView, frame: CGRect(origin: .zero, size: CGSize(width: contentSize.width, height: contentSize.height + 1000.0)), completion: nil) + } } transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: availableSize), completion: nil) @@ -239,6 +309,10 @@ public final class SheetComponent: Component { if let currentAvailableSize = self.currentAvailableSize, currentAvailableSize.height != availableSize.height { self.scrollView.contentOffset = CGPoint(x: 0.0, y: -(availableSize.height - contentSize.height)) } + if self.currentHasInputHeight != previousHasInputHeight { + transition.setBounds(view: self.scrollView, bounds: CGRect(origin: CGPoint(x: 0.0, y: -(availableSize.height - contentSize.height)), size: self.scrollView.bounds.size)) + } + self.currentAvailableSize = availableSize if environment[SheetComponentEnvironment.self].value.isDisplaying, !self.previousIsDisplaying, let _ = transition.userData(ViewControllerComponentContainer.AnimateInTransition.self) { diff --git a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift index 6115622c58..40b1117aac 100644 --- a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift @@ -769,6 +769,11 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo if let reactionContextNode = self.reactionContextNode { additionalVisibleOffsetY += reactionContextNode.visibleExtensionDistance } + if case .reference = self.source { + if actionsFrame.maxY > layout.size.height { + actionsFrame.origin.y = contentRect.minY - actionsSize.height - contentActionsSpacing + } + } if case .center = actionsHorizontalAlignment { actionsFrame.origin.x = floor(contentParentGlobalFrame.minX + contentRect.midX - actionsFrame.width / 2.0) if actionsFrame.maxX > layout.size.width - actionsEdgeInset { @@ -807,6 +812,11 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo actionsFrame.origin.x = actionsEdgeInset } } + + if case let .reference(reference) = self.source, let transitionInfo = reference.transitionInfo(), let customPosition = transitionInfo.customPosition { + actionsFrame = actionsFrame.offsetBy(dx: customPosition.x, dy: customPosition.y) + } + transition.updateFrame(node: self.actionsStackNode, frame: actionsFrame.offsetBy(dx: 0.0, dy: additionalVisibleOffsetY), beginWithCurrentState: true) if let contentNode = contentNode { @@ -1167,11 +1177,9 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo if case .center = actionsHorizontalAlignment { actionsPositionDeltaXDistance = currentContentScreenFrame.midX - self.actionsStackNode.frame.midX } - if case .reference = self.source { actionsPositionDeltaXDistance = currentContentScreenFrame.midX - self.actionsStackNode.frame.midX } - let actionsPositionDeltaYDistance = -animationInContentYDistance + actionsVerticalTransitionDirection * actionsSize.height / 2.0 - contentActionsSpacing self.actionsStackNode.layer.animate( from: NSValue(cgPoint: CGPoint()), diff --git a/submodules/DrawingUI/BUILD b/submodules/DrawingUI/BUILD new file mode 100644 index 0000000000..693fae0b44 --- /dev/null +++ b/submodules/DrawingUI/BUILD @@ -0,0 +1,93 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +load( + "@build_bazel_rules_apple//apple:resources.bzl", + "apple_resource_bundle", + "apple_resource_group", +) +load("//build-system/bazel-utils:plist_fragment.bzl", + "plist_fragment", +) + +filegroup( + name = "DrawingUIMetalResources", + srcs = glob([ + "MetalResources/**/*.*", + ]), + visibility = ["//visibility:public"], +) + +plist_fragment( + name = "DrawingUIBundleInfoPlist", + extension = "plist", + template = + """ + CFBundleIdentifier + org.telegram.DrawingUI + CFBundleDevelopmentRegion + en + CFBundleName + PremiumUI + """ +) + +apple_resource_bundle( + name = "DrawingUIBundle", + infoplists = [ + ":DrawingUIBundleInfoPlist", + ], + resources = [ + ":DrawingUIMetalResources", + ], +) + +filegroup( + name = "DrawingUIResources", + srcs = glob([ + "Resources/**/*", + ], exclude = ["Resources/**/.*"]), + visibility = ["//visibility:public"], +) + +swift_library( + name = "DrawingUI", + module_name = "DrawingUI", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + data = [ + ":DrawingUIBundle", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/AsyncDisplayKit:AsyncDisplayKit", + "//submodules/Display:Display", + "//submodules/Postbox:Postbox", + "//submodules/TelegramCore:TelegramCore", + "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/LegacyComponents:LegacyComponents", + "//submodules/AccountContext:AccountContext", + "//submodules/LegacyUI:LegacyUI", + "//submodules/AppBundle:AppBundle", + "//submodules/TelegramStringFormatting:TelegramStringFormatting", + "//submodules/SegmentedControlNode:SegmentedControlNode", + "//submodules/TelegramUIPreferences:TelegramUIPreferences", + "//submodules/PresentationDataUtils:PresentationDataUtils", + "//submodules/HexColor:HexColor", + "//submodules/ContextUI:ContextUI", + "//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters", + "//submodules/Components/LottieAnimationComponent:LottieAnimationComponent", + "//submodules/Components/ViewControllerComponent:ViewControllerComponent", + "//submodules/Components/SheetComponent:SheetComponent", + "//submodules/Components/MultilineTextComponent:MultilineTextComponent", + "//submodules/AnimatedStickerNode:AnimatedStickerNode", + "//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode", + "//submodules/StickerResources:StickerResources", + "//submodules/ImageBlur:ImageBlur", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/DrawingUI/MetalResources/Drawing.metal b/submodules/DrawingUI/MetalResources/Drawing.metal new file mode 100644 index 0000000000..bc4bd8eb9e --- /dev/null +++ b/submodules/DrawingUI/MetalResources/Drawing.metal @@ -0,0 +1,84 @@ +#include +using namespace metal; + +struct Vertex { + float4 position [[position]]; + float2 tex_coord; +}; + +struct Uniforms { + float4x4 scaleMatrix; +}; + +struct Point { + float4 position [[position]]; + float4 color; + float angle; + float size [[point_size]]; +}; + +vertex Vertex vertex_render_target(constant Vertex *vertexes [[ buffer(0) ]], + constant Uniforms &uniforms [[ buffer(1) ]], + uint vid [[vertex_id]]) +{ + Vertex out = vertexes[vid]; + out.position = uniforms.scaleMatrix * out.position; + return out; +}; + +fragment float4 fragment_render_target(Vertex vertex_data [[ stage_in ]], + texture2d tex2d [[ texture(0) ]]) +{ + constexpr sampler textureSampler(mag_filter::linear, min_filter::linear); + float4 color = float4(tex2d.sample(textureSampler, vertex_data.tex_coord)); + return color; +}; + +float2 transformPointCoord(float2 pointCoord, float a, float2 anchor) { + float2 point20 = pointCoord - anchor; + float x = point20.x * cos(a) - point20.y * sin(a); + float y = point20.x * sin(a) + point20.y * cos(a); + return float2(x, y) + anchor; +} + + +vertex Point vertex_point_func(constant Point *points [[ buffer(0) ]], + constant Uniforms &uniforms [[ buffer(1) ]], + uint vid [[ vertex_id ]]) +{ + Point out = points[vid]; + float2 pos = float2(out.position.x, out.position.y); + out.position = uniforms.scaleMatrix * float4(pos, 0, 1); + out.size = out.size; + return out; +}; + +fragment float4 fragment_point_func(Point point_data [[ stage_in ]], + texture2d tex2d [[ texture(0) ]], + float2 pointCoord [[ point_coord ]]) +{ + constexpr sampler textureSampler(mag_filter::linear, min_filter::linear); + float2 tex_coord = transformPointCoord(pointCoord, point_data.angle, float2(0.5)); + float4 color = float4(tex2d.sample(textureSampler, tex_coord)); + return float4(point_data.color.rgb, color.a * point_data.color.a); +}; + +// franment shader that applys original color of the texture +fragment half4 fragment_point_func_original(Point point_data [[ stage_in ]], + texture2d tex2d [[ texture(0) ]], + float2 pointCoord [[ point_coord ]]) +{ + constexpr sampler textureSampler(mag_filter::linear, min_filter::linear); + half4 color = half4(tex2d.sample(textureSampler, pointCoord)); + return half4(color.rgb, color.a * point_data.color.a); +}; + +fragment float4 fragment_point_func_without_texture(Point point_data [[ stage_in ]], + float2 pointCoord [[ point_coord ]]) +{ + float dist = length(pointCoord - float2(0.5)); + if (dist >= 0.5) { + return float4(0); + } + return point_data.color; +} diff --git a/submodules/DrawingUI/Resources/marker.png b/submodules/DrawingUI/Resources/marker.png new file mode 100644 index 0000000000..b6e2771a2f Binary files /dev/null and b/submodules/DrawingUI/Resources/marker.png differ diff --git a/submodules/DrawingUI/Resources/pencil.png b/submodules/DrawingUI/Resources/pencil.png new file mode 100644 index 0000000000..17c0a20afa Binary files /dev/null and b/submodules/DrawingUI/Resources/pencil.png differ diff --git a/submodules/DrawingUI/Resources/shape_arrow.json b/submodules/DrawingUI/Resources/shape_arrow.json new file mode 100644 index 0000000000..88278d60a8 --- /dev/null +++ b/submodules/DrawingUI/Resources/shape_arrow.json @@ -0,0 +1 @@ +{"name" : "Arrow", "points" : [[68,222],[70,220],[73,218],[75,217],[77,215],[80,213],[82,212],[84,210],[87,209],[89,208],[92,206],[95,204],[101,201],[106,198],[112,194],[118,191],[124,187],[127,186],[132,183],[138,181],[141,180],[146,178],[154,173],[159,171],[161,170],[166,167],[168,167],[171,166],[174,164],[177,162],[180,160],[182,158],[183,156],[181,154],[178,153],[171,153],[164,153],[160,153],[150,154],[147,155],[141,157],[137,158],[135,158],[137,158],[140,157],[143,156],[151,154],[160,152],[170,149],[179,147],[185,145],[192,144],[196,144],[198,144],[200,144],[201,147],[199,149],[194,157],[191,160],[186,167],[180,176],[177,179],[171,187],[169,189],[165,194],[164,196]]} \ No newline at end of file diff --git a/submodules/DrawingUI/Resources/shape_circle.json b/submodules/DrawingUI/Resources/shape_circle.json new file mode 100644 index 0000000000..cc447f832d --- /dev/null +++ b/submodules/DrawingUI/Resources/shape_circle.json @@ -0,0 +1 @@ +{"name" : "Circle", "points" : [[127,141],[124,140],[120,139],[118,139],[116,139],[111,140],[109,141],[104,144],[100,147],[96,152],[93,157],[90,163],[87,169],[85,175],[83,181],[82,190],[82,195],[83,200],[84,205],[88,213],[91,216],[96,219],[103,222],[108,224],[111,224],[120,224],[133,223],[142,222],[152,218],[160,214],[167,210],[173,204],[178,198],[179,196],[182,188],[182,177],[178,167],[170,150],[163,138],[152,130],[143,129],[140,131],[129,136],[126,139]]} \ No newline at end of file diff --git a/submodules/DrawingUI/Resources/shape_rectangle.json b/submodules/DrawingUI/Resources/shape_rectangle.json new file mode 100644 index 0000000000..188589a7c8 --- /dev/null +++ b/submodules/DrawingUI/Resources/shape_rectangle.json @@ -0,0 +1 @@ +{"name" : "Rectangle", "points" : [[78,149],[78,153],[78,157],[78,160],[79,162],[79,164],[79,167],[79,169],[79,173],[79,178],[79,183],[80,189],[80,193],[80,198],[80,202],[81,208],[81,210],[81,216],[82,222],[82,224],[82,227],[83,229],[83,231],[85,230],[88,232],[90,233],[92,232],[94,233],[99,232],[102,233],[106,233],[109,234],[117,235],[123,236],[126,236],[135,237],[142,238],[145,238],[152,238],[154,239],[165,238],[174,237],[179,236],[186,235],[191,235],[195,233],[197,233],[200,233],[201,235],[201,233],[199,231],[198,226],[198,220],[196,207],[195,195],[195,181],[195,173],[195,163],[194,155],[192,145],[192,143],[192,138],[191,135],[191,133],[191,130],[190,128],[188,129],[186,129],[181,132],[173,131],[162,131],[151,132],[149,132],[138,132],[136,132],[122,131],[120,131],[109,130],[107,130],[90,132],[81,133],[76,133]]} \ No newline at end of file diff --git a/submodules/DrawingUI/Resources/shape_star.json b/submodules/DrawingUI/Resources/shape_star.json new file mode 100644 index 0000000000..a113c1a82b --- /dev/null +++ b/submodules/DrawingUI/Resources/shape_star.json @@ -0,0 +1 @@ +{"name" : "Star", "points" : [[75,250],[75,247],[77,244],[78,242],[79,239],[80,237],[82,234],[82,232],[84,229],[85,225],[87,222],[88,219],[89,216],[91,212],[92,208],[94,204],[95,201],[96,196],[97,194],[98,191],[100,185],[102,178],[104,173],[104,171],[105,164],[106,158],[107,156],[107,152],[108,145],[109,141],[110,139],[112,133],[113,131],[116,127],[117,125],[119,122],[121,121],[123,120],[125,122],[125,125],[127,130],[128,133],[131,143],[136,153],[140,163],[144,172],[145,175],[151,189],[156,201],[161,213],[166,225],[169,233],[171,236],[174,243],[177,247],[178,249],[179,251],[180,253],[180,255],[179,257],[177,257],[174,255],[169,250],[164,247],[160,245],[149,238],[138,230],[127,221],[124,220],[112,212],[110,210],[96,201],[84,195],[74,190],[64,182],[55,175],[51,172],[49,170],[51,169],[56,169],[66,169],[78,168],[92,166],[107,164],[123,161],[140,162],[156,162],[171,160],[173,160],[186,160],[195,160],[198,161],[203,163],[208,163],[206,164],[200,167],[187,172],[174,179],[172,181],[153,192],[137,201],[123,211],[112,220],[99,229],[90,237],[80,244],[73,250],[69,254],[69,252]]} \ No newline at end of file diff --git a/submodules/DrawingUI/Sources/ColorPickerScreen.swift b/submodules/DrawingUI/Sources/ColorPickerScreen.swift new file mode 100644 index 0000000000..1e4ea084a7 --- /dev/null +++ b/submodules/DrawingUI/Sources/ColorPickerScreen.swift @@ -0,0 +1,2482 @@ +import Foundation +import UIKit +import Display +import SwiftSignalKit +import ComponentFlow +import LegacyComponents +import TelegramCore +import Postbox +import AccountContext +import TelegramPresentationData +import SheetComponent +import ViewControllerComponent +import SegmentedControlNode +import MultilineTextComponent +import HexColor + +private let palleteColors: [UInt32] = [ + 0xffffff, 0xebebeb, 0xd6d6d6, 0xc2c2c2, 0xadadad, 0x999999, 0x858585, 0x707070, 0x5c5c5c, 0x474747, 0x333333, 0x000000, + 0x00374a, 0x011d57, 0x11053b, 0x2e063d, 0x3c071b, 0x5c0701, 0x5a1c00, 0x583300, 0x563d00, 0x666100, 0x4f5504, 0x263e0f, + 0x004d65, 0x012f7b, 0x1a0a52, 0x450d59, 0x551029, 0x831100, 0x7b2900, 0x7a4a00, 0x785800, 0x8d8602, 0x6f760a, 0x38571a, + 0x016e8f, 0x0042a9, 0x2c0977, 0x61187c, 0x791a3d, 0xb51a00, 0xad3e00, 0xa96800, 0xa67b01, 0xc4bc00, 0x9ba50e, 0x4e7a27, + 0x008cb4, 0x0056d6, 0x371a94, 0x7a219e, 0x99244f, 0xe22400, 0xda5100, 0xd38301, 0xd19d01, 0xf5ec00, 0xc3d117, 0x669d34, + 0x00a1d8, 0x0061fe, 0x4d22b2, 0x982abc, 0xb92d5d, 0xff4015, 0xff6a00, 0xffab01, 0xfdc700, 0xfefb41, 0xd9ec37, 0x76bb40, + 0x01c7fc, 0x3a87fe, 0x5e30eb, 0xbe38f3, 0xe63b7a, 0xff6250, 0xff8648, 0xfeb43f, 0xfecb3e, 0xfff76b, 0xe4ef65, 0x96d35f, + 0x52d6fc, 0x74a7ff, 0x864ffe, 0xd357fe, 0xee719e, 0xff8c82, 0xffa57d, 0xffc777, 0xffd977, 0xfff994, 0xeaf28f, 0xb1dd8b, + 0x93e3fd, 0xa7c6ff, 0xb18cfe, 0xe292fe, 0xf4a4c0, 0xffb5af, 0xffc5ab, 0xffd9a8, 0xfee4a8, 0xfffbb9, 0xf2f7b7, 0xcde8b5, + 0xcbf0ff, 0xd3e2ff, 0xd9c9fe, 0xefcaff, 0xf9d3e0, 0xffdbd8, 0xffe2d6, 0xffecd4, 0xfff2d5, 0xfefcdd, 0xf7fadb, 0xdfeed4 +] + +private class GradientLayer: CAGradientLayer { + override func action(forKey event: String) -> CAAction? { + return nullAction + } +} + +private struct ColorSelectionImage: Equatable { + var _image: UIImage? + let size: CGSize + let topLeftRadius: CGFloat + let topRightRadius: CGFloat + let bottomLeftRadius: CGFloat + let bottomRightRadius: CGFloat + + init(size: CGSize, topLeftRadius: CGFloat, topRightRadius: CGFloat, bottomLeftRadius: CGFloat, bottomRightRadius: CGFloat) { + self.size = size + self.topLeftRadius = topLeftRadius + self.topRightRadius = topRightRadius + self.bottomLeftRadius = bottomLeftRadius + self.bottomRightRadius = bottomRightRadius + } + + public static func ==(lhs: ColorSelectionImage, rhs: ColorSelectionImage) -> Bool { + if lhs.size != rhs.size { + return false + } + if lhs.topLeftRadius != rhs.topLeftRadius { + return false + } + if lhs.topRightRadius != rhs.topRightRadius { + return false + } + if lhs.bottomLeftRadius != rhs.bottomLeftRadius { + return false + } + if lhs.bottomRightRadius != rhs.bottomRightRadius { + return false + } + return true + } + + mutating func getImage() -> UIImage { + if self._image == nil { + self._image = generateColorSelectionImage(size: self.size, topLeftRadius: self.topLeftRadius, topRightRadius: self.topRightRadius, bottomLeftRadius: self.bottomLeftRadius, bottomRightRadius: self.bottomRightRadius) + } + return self._image! + } +} + +private func generateColorSelectionImage(size: CGSize, topLeftRadius: CGFloat, topRightRadius: CGFloat, bottomLeftRadius: CGFloat, bottomRightRadius: CGFloat) -> UIImage? { + let margin: CGFloat = 10.0 + let realSize = size + + let image = generateImage(CGSize(width: size.width + margin * 2.0, height: size.height + margin * 2.0), opaque: false, rotatedContext: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + + let path = UIBezierPath(roundRect: CGRect(origin: CGPoint(x: margin, y: margin), size: realSize), topLeftRadius: topLeftRadius, topRightRadius: topRightRadius, bottomLeftRadius: bottomLeftRadius, bottomRightRadius: bottomRightRadius) + context.addPath(path.cgPath) + + context.setShadow(offset: CGSize(), blur: 9.0, color: UIColor(rgb: 0x000000, alpha: 0.15).cgColor) + context.setLineWidth(3.0 - UIScreenPixel) + context.setStrokeColor(UIColor(rgb: 0x1a1a1c).cgColor) + context.strokePath() + }) + return image +} + +private func generateColorGridImage(size: CGSize) -> UIImage? { + return generateImage(size, opaque: true, rotatedContext: { size, context in + let squareSize = floorToScreenPixels(size.width / 12.0) + var index = 0 + for row in 0 ..< 10 { + for col in 0 ..< 12 { + let color = palleteColors[index] + var correctedSize = squareSize + if col == 11 { + correctedSize = size.width - squareSize * 11.0 + } + let rect = CGRect(origin: CGPoint(x: CGFloat(col) * squareSize, y: CGFloat(row) * squareSize), size: CGSize(width: correctedSize, height: squareSize)) + + context.setFillColor(UIColor(rgb: color).cgColor) + context.fill(rect) + + index += 1 + } + } + }) +} + +private func generateCheckeredImage(size: CGSize, whiteColor: UIColor, blackColor: UIColor, length: CGFloat) -> UIImage? { + return generateImage(size, opaque: false, rotatedContext: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + let w = Int(ceil(size.width / length)) + let h = Int(ceil(size.height / length)) + for i in 0 ..< w { + for j in 0 ..< h { + if (i % 2) != (j % 2) { + context.setFillColor(whiteColor.cgColor) + } else { + context.setFillColor(blackColor.cgColor) + } + context.fill(CGRect(origin: CGPoint(x: CGFloat(i) * length, y: CGFloat(j) * length), size: CGSize(width: length, height: length))) + } + } + }) +} + +private func generateKnobImage() -> UIImage? { + let side: CGFloat = 32.0 + let margin: CGFloat = 10.0 + + let image = generateImage(CGSize(width: side + margin * 2.0, height: side + margin * 2.0), opaque: false, rotatedContext: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + + context.setShadow(offset: CGSize(width: 0.0, height: 0.0), blur: 9.0, color: UIColor(rgb: 0x000000, alpha: 0.3).cgColor) + context.setFillColor(UIColor(rgb: 0x1a1a1c).cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: margin, y: margin), size: CGSize(width: side, height: side))) + }) + return image +} + +private class ColorSliderComponent: Component { + let leftColor: DrawingColor + let rightColor: DrawingColor + let currentColor: DrawingColor + let value: CGFloat + let updated: (CGFloat) -> Void + + public init( + leftColor: DrawingColor, + rightColor: DrawingColor, + currentColor: DrawingColor, + value: CGFloat, + updated: @escaping (CGFloat) -> Void + ) { + self.leftColor = leftColor + self.rightColor = rightColor + self.currentColor = currentColor + self.value = value + self.updated = updated + } + + public static func ==(lhs: ColorSliderComponent, rhs: ColorSliderComponent) -> Bool { + if lhs.leftColor != rhs.leftColor { + return false + } + if lhs.rightColor != rhs.rightColor { + return false + } + if lhs.currentColor != rhs.currentColor { + return false + } + if lhs.value != rhs.value { + return false + } + return true + } + + final class View: UIView, UIGestureRecognizerDelegate { + private var validSize: CGSize? + + private let wrapper = UIView(frame: CGRect()) + private let transparencyLayer = SimpleLayer() + private let gradientLayer = GradientLayer() + private let knob = SimpleLayer() + private let circle = SimpleShapeLayer() + + fileprivate var updated: (CGFloat) -> Void = { _ in } + + @objc func handlePress(_ gestureRecognizer: UILongPressGestureRecognizer) { + let side: CGFloat = 36.0 + let location = gestureRecognizer.location(in: self).offsetBy(dx: -side * 0.5, dy: 0.0) + guard self.frame.width > 0.0, case .began = gestureRecognizer.state else { + return + } + let value = max(0.0, min(1.0, location.x / (self.frame.width - side))) + self.updated(value) + } + + @objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { + if gestureRecognizer.state == .changed { + let side: CGFloat = 36.0 + let location = gestureRecognizer.location(in: self).offsetBy(dx: -side * 0.5, dy: 0.0) + guard self.frame.width > 0.0 else { + return + } + let value = max(0.0, min(1.0, location.x / (self.frame.width - side))) + self.updated(value) + } + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + + func updateLayout(size: CGSize, leftColor: DrawingColor, rightColor: DrawingColor, currentColor: DrawingColor, value: CGFloat) -> CGSize { + let previousSize = self.validSize + + let sliderSize = CGSize(width: size.width, height: 36.0) + + self.validSize = sliderSize + + self.gradientLayer.type = .axial + self.gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5) + self.gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5) + self.gradientLayer.colors = [leftColor.toUIColor().cgColor, rightColor.toUIColor().cgColor] + + if leftColor.alpha < 1.0 || rightColor.alpha < 1.0 { + self.transparencyLayer.isHidden = false + } else { + self.transparencyLayer.isHidden = true + } + + if previousSize != sliderSize { + self.wrapper.frame = CGRect(origin: .zero, size: sliderSize) + if self.wrapper.superview == nil { + self.addSubview(self.wrapper) + } + + self.transparencyLayer.frame = CGRect(origin: .zero, size: sliderSize) + if self.transparencyLayer.superlayer == nil { + self.wrapper.layer.addSublayer(self.transparencyLayer) + } + + self.gradientLayer.frame = CGRect(origin: .zero, size: sliderSize) + if self.gradientLayer.superlayer == nil { + self.wrapper.layer.addSublayer(self.gradientLayer) + } + + if self.knob.superlayer == nil { + self.layer.addSublayer(self.knob) + } + + if self.circle.superlayer == nil { + self.circle.path = UIBezierPath(ovalIn: CGRect(origin: .zero, size: CGSize(width: 26.0, height: 26.0))).cgPath + self.layer.addSublayer(self.circle) + } + + if previousSize == nil { + self.isUserInteractionEnabled = true + self.wrapper.clipsToBounds = true + self.wrapper.layer.cornerRadius = 18.0 + + let pressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handlePress(_:))) + pressGestureRecognizer.minimumPressDuration = 0.01 + pressGestureRecognizer.delegate = self + self.addGestureRecognizer(pressGestureRecognizer) + self.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))) + + if !self.transparencyLayer.isHidden { + self.transparencyLayer.contents = generateCheckeredImage(size: sliderSize, whiteColor: UIColor(rgb: 0xffffff, alpha: 0.1), blackColor: .clear, length: 12.0)?.cgImage + } + + self.knob.contents = generateKnobImage()?.cgImage + } + } + + let margin: CGFloat = 10.0 + let knobSize = CGSize(width: 32.0, height: 32.0) + let knobFrame = CGRect(origin: CGPoint(x: 2.0 + floorToScreenPixels((sliderSize.width - 4.0 - knobSize.width) * value), y: 2.0), size: knobSize) + self.knob.frame = knobFrame.insetBy(dx: -margin, dy: -margin) + + self.circle.fillColor = currentColor.toUIColor().cgColor + self.circle.frame = knobFrame.insetBy(dx: 3.0, dy: 3.0) + + return sliderSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + view.updated = self.updated + return view.updateLayout(size: availableSize, leftColor: self.leftColor, rightColor: self.rightColor, currentColor: self.currentColor, value: self.value) + } +} + +private class ColorFieldComponent: Component { + enum FieldType { + case number + case text + } + let backgroundColor: UIColor + let textColor: UIColor + let type: FieldType + let value: String + let suffix: String? + let updated: (String) -> Void + let shouldUpdate: (String) -> Bool + + public init( + backgroundColor: UIColor, + textColor: UIColor, + type: FieldType, + value: String, + suffix: String? = nil, + updated: @escaping (String) -> Void, + shouldUpdate: @escaping (String) -> Bool + ) { + self.backgroundColor = backgroundColor + self.textColor = textColor + self.type = type + self.value = value + self.suffix = suffix + self.updated = updated + self.shouldUpdate = shouldUpdate + } + + public static func ==(lhs: ColorFieldComponent, rhs: ColorFieldComponent) -> Bool { + if lhs.backgroundColor != rhs.backgroundColor { + return false + } + if lhs.textColor != rhs.textColor { + return false + } + if lhs.type != rhs.type { + return false + } + if lhs.value != rhs.value { + return false + } + if lhs.suffix != rhs.suffix { + return false + } + return true + } + + final class View: UIView, UITextFieldDelegate { + private var validSize: CGSize? + + private let backgroundNode = NavigationBackgroundNode(color: .clear) + private let textField = UITextField(frame: CGRect()) + private let suffixLabel = UITextField(frame: CGRect()) + + fileprivate var updated: (String) -> Void = { _ in } + fileprivate var shouldUpdate: (String) -> Bool = { _ in return true } + + func updateLayout(size: CGSize, component: ColorFieldComponent) -> CGSize { + let previousSize = self.validSize + + self.updated = component.updated + self.shouldUpdate = component.shouldUpdate + + self.validSize = size + + self.backgroundNode.frame = CGRect(origin: .zero, size: size) + self.backgroundNode.update(size: size, cornerRadius: 9.0, transition: .immediate) + self.backgroundNode.updateColor(color: component.backgroundColor, transition: .immediate) + + if previousSize == nil { + self.insertSubview(self.backgroundNode.view, at: 0) + self.addSubview(self.textField) + + self.textField.textAlignment = component.suffix != nil ? .right : .center + self.textField.delegate = self + self.textField.font = Font.with(size: 17.0, design: .regular, weight: .semibold, traits: .monospacedNumbers) + self.textField.addTarget(self, action: #selector(self.textDidChange(_:)), for: .editingChanged) + self.textField.keyboardAppearance = .dark + self.textField.autocorrectionType = .no + self.textField.autocapitalizationType = .allCharacters + + switch component.type { + case .number: + self.textField.keyboardType = .numberPad + case .text: + self.textField.keyboardType = .asciiCapable + } + } + + self.textField.textColor = component.textColor + + var textFieldOffset: CGFloat = 0.0 + if let suffix = component.suffix { + if self.suffixLabel.superview == nil { + self.suffixLabel.isUserInteractionEnabled = false + self.suffixLabel.text = suffix + self.suffixLabel.font = self.textField.font + self.suffixLabel.textColor = self.textField.textColor + self.addSubview(self.suffixLabel) + + self.suffixLabel.sizeToFit() + self.suffixLabel.frame = CGRect(origin: CGPoint(x: size.width - self.suffixLabel.frame.width - 14.0, y: floorToScreenPixels((size.height - self.suffixLabel.frame.size.height) / 2.0)), size: self.suffixLabel.frame.size) + } + textFieldOffset = -33.0 + } else { + self.suffixLabel.removeFromSuperview() + } + + self.textField.frame = CGRect(origin: CGPoint(x: textFieldOffset, y: 0.0), size: size) + self.textField.text = component.value + + return size + } + + @objc private func textDidChange(_ textField: UITextField) { + self.updated(textField.text ?? "") + } + + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + var updated = textField.text ?? "" + updated.replaceSubrange(updated.index(updated.startIndex, offsetBy: range.lowerBound) ..< updated.index(updated.startIndex, offsetBy: range.upperBound), with: string) + if self.shouldUpdate(updated) { + return true + } else { + return false + } + } + + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + return false + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + view.updated = self.updated + return view.updateLayout(size: availableSize, component: self) + } +} + +private func generatePreviewBackgroundImage(size: CGSize) -> UIImage? { + return generateImage(size, opaque: true, rotatedContext: { size, context in + context.move(to: .zero) + context.addLine(to: CGPoint(x: size.width, y: 0.0)) + context.addLine(to: CGPoint(x: 0.0, y: size.height)) + context.closePath() + + context.setFillColor(UIColor.black.cgColor) + context.fillPath() + + context.move(to: CGPoint(x: size.width, y: 0.0)) + context.addLine(to: CGPoint(x: size.width, y: size.height)) + context.addLine(to: CGPoint(x: 0.0, y: size.height)) + context.closePath() + + context.setFillColor(UIColor.white.cgColor) + context.fillPath() + }) +} + +private class ColorPreviewComponent: Component { + let color: DrawingColor + + public init( + color: DrawingColor + ) { + self.color = color + } + + public static func ==(lhs: ColorPreviewComponent, rhs: ColorPreviewComponent) -> Bool { + if lhs.color != rhs.color { + return false + } + return true + } + + final class View: UIView { + private var validSize: CGSize? + + private let wrapper = UIView(frame: CGRect()) + private let background = SimpleLayer() + private let color = SimpleLayer() + + func updateLayout(size: CGSize, color: DrawingColor) -> CGSize { + let previousSize = self.validSize + + self.validSize = size + + if previousSize != size { + self.wrapper.frame = CGRect(origin: .zero, size: size) + if self.wrapper.superview == nil { + self.addSubview(self.wrapper) + } + + self.background.frame = CGRect(origin: .zero, size: size) + if self.background.superlayer == nil { + self.wrapper.layer.addSublayer(self.background) + } + + self.color.frame = CGRect(origin: .zero, size: size) + if self.color.superlayer == nil { + self.wrapper.layer.addSublayer(self.color) + } + + if previousSize == nil { + self.isUserInteractionEnabled = true + self.wrapper.clipsToBounds = true + self.wrapper.layer.cornerRadius = 12.0 + if #available(iOS 13.0, *) { + self.wrapper.layer.cornerCurve = .continuous + } + } + + self.background.contents = generatePreviewBackgroundImage(size: size)?.cgImage + } + + self.color.backgroundColor = color.toUIColor().cgColor + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.updateLayout(size: availableSize, color: self.color) + } +} + +final class ColorGridComponent: Component { + let color: DrawingColor? + let selected: (DrawingColor) -> Void + + init( + color: DrawingColor?, + selected: @escaping (DrawingColor) -> Void + ) { + self.color = color + self.selected = selected + } + + static func ==(lhs: ColorGridComponent, rhs: ColorGridComponent) -> Bool { + if lhs.color != rhs.color { + return false + } + return true + } + + final class View: UIView, UIGestureRecognizerDelegate { + private var validSize: CGSize? + private var selectedColor: DrawingColor? + private var selectedColorIndex: Int? + + private var wrapper = UIView(frame: CGRect()) + private var image = UIImageView(image: nil) + private var selectionKnob = UIImageView(image: nil) + private var selectionKnobImage: ColorSelectionImage? + + fileprivate var selected: (DrawingColor) -> Void = { _ in } + + func getColor(at point: CGPoint) -> DrawingColor? { + guard let size = self.validSize, + point.x >= 0 && point.x <= size.width, + point.y >= 0 && point.y <= size.height + else { + return nil + } + let row = Int(point.y / size.height * 10.0) + let col = Int(point.x / size.width * 12.0) + + let index = row * 12 + col + return DrawingColor(rgb: palleteColors[index]) + } + + @objc func handlePress(_ gestureRecognizer: UILongPressGestureRecognizer) { + guard case .began = gestureRecognizer.state else { + return + } + let location = gestureRecognizer.location(in: self) + if let color = self.getColor(at: location), color != self.selectedColor { + self.selected(color) + } + } + + @objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { + if gestureRecognizer.state == .changed { + let location = gestureRecognizer.location(in: self) + if let color = self.getColor(at: location), color != self.selectedColor { + self.selected(color) + } + } + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + + func updateLayout(size: CGSize, selectedColor: DrawingColor?) -> CGSize { + let previousSize = self.validSize + + let squareSize = floorToScreenPixels(size.width / 12.0) + let imageSize = CGSize(width: size.width, height: squareSize * 10.0) + + self.validSize = imageSize + + let previousColor = self.selectedColor + self.selectedColor = selectedColor + + if previousSize != imageSize { + if previousSize == nil { + self.isUserInteractionEnabled = true + self.wrapper.clipsToBounds = true + self.wrapper.layer.cornerRadius = 10.0 + + let pressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handlePress(_:))) + pressGestureRecognizer.delegate = self + pressGestureRecognizer.minimumPressDuration = 0.01 + self.addGestureRecognizer(pressGestureRecognizer) + self.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))) + } + + self.wrapper.frame = CGRect(origin: .zero, size: imageSize) + if self.wrapper.superview == nil { + self.addSubview(self.wrapper) + } + + self.image.image = generateColorGridImage(size: imageSize) + self.image.frame = CGRect(origin: .zero, size: imageSize) + if self.image.superview == nil { + self.wrapper.addSubview(self.image) + } + } + + if previousColor != selectedColor { + if let selectedColor = selectedColor { + let color = selectedColor.toUIColor().rgb + if let index = palleteColors.firstIndex(where: { $0 == color }) { + self.selectedColorIndex = index + } else { + self.selectedColorIndex = nil + } + } else { + self.selectedColorIndex = nil + } + } + + if let selectedColorIndex = self.selectedColorIndex { + if self.selectionKnob.superview == nil { + self.addSubview(self.selectionKnob) + } + + let smallCornerRadius: CGFloat = 2.0 + let largeCornerRadius: CGFloat = 10.0 + + var topLeftRadius = smallCornerRadius + var topRightRadius = smallCornerRadius + var bottomLeftRadius = smallCornerRadius + var bottomRightRadius = smallCornerRadius + + if selectedColorIndex == 0 { + topLeftRadius = largeCornerRadius + } else if selectedColorIndex == 11 { + topRightRadius = largeCornerRadius + } else if selectedColorIndex == palleteColors.count - 12 { + bottomLeftRadius = largeCornerRadius + } else if selectedColorIndex == palleteColors.count - 1 { + bottomRightRadius = largeCornerRadius + } + var selectionKnobImage = ColorSelectionImage(size: CGSize(width: squareSize, height: squareSize), topLeftRadius: topLeftRadius, topRightRadius: topRightRadius, bottomLeftRadius: bottomLeftRadius, bottomRightRadius: bottomRightRadius) + if selectionKnobImage != self.selectionKnobImage { + self.selectionKnob.image = selectionKnobImage.getImage() + self.selectionKnobImage = selectionKnobImage + } + + let row = Int(floor(CGFloat(selectedColorIndex) / 12.0)) + let col = selectedColorIndex % 12 + + let margin: CGFloat = 10.0 + var selectionFrame = CGRect(origin: CGPoint(x: CGFloat(col) * squareSize, y: CGFloat(row) * squareSize), size: CGSize(width: squareSize, height: squareSize)) + selectionFrame = selectionFrame.insetBy(dx: -margin, dy: -margin) + self.selectionKnob.frame = selectionFrame + } else { + self.selectionKnob.image = nil + } + + return CGSize(width: size.width, height: squareSize * 10.0) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + view.selected = self.selected + return view.updateLayout(size: availableSize, selectedColor: self.color) + } +} + +private func generateSpectrumImage(size: CGSize) -> UIImage? { + return generateImage(size, contextGenerator: { size, context in + if let image = UIImage(bundleImageName: "Media Editor/Spectrum") { + context.draw(image.cgImage!, in: CGRect(origin: .zero, size: size)) + } + + let colorSpace = CGColorSpaceCreateDeviceRGB() + var colors = [UIColor(rgb: 0xffffff).cgColor, UIColor(rgb: 0xffffff, alpha: 0.0).cgColor, UIColor(rgb: 0xffffff, alpha: 0.0).cgColor] + var locations: [CGFloat] = [0.0, 0.45, 1.0] + + var gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: size.width, y: 0.0), options: [.drawsAfterEndLocation]) + + colors = [UIColor(rgb: 0x000000, alpha: 0.0).cgColor, UIColor(rgb: 0x000000, alpha: 0.0).cgColor, UIColor(rgb: 0x000000).cgColor] + locations = [0.0, 0.5, 1.0] + + gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: size.width, y: 0.0), options: [.drawsAfterEndLocation]) + }) +} + +final class ColorSpectrumComponent: Component { + let color: DrawingColor? + let selected: (DrawingColor) -> Void + + init( + color: DrawingColor?, + selected: @escaping (DrawingColor) -> Void + ) { + self.color = color + self.selected = selected + } + + static func ==(lhs: ColorSpectrumComponent, rhs: ColorSpectrumComponent) -> Bool { + if lhs.color != rhs.color { + return false + } + return true + } + + final class View: UIView, UIGestureRecognizerDelegate { + private var validSize: CGSize? + private var selectedColor: DrawingColor? + + private var wrapper = UIView(frame: CGRect()) + private var image = UIImageView(image: nil) + + private let knob = SimpleLayer() + private let circle = SimpleShapeLayer() + + fileprivate var selected: (DrawingColor) -> Void = { _ in } + + private var bitmapData: UnsafeMutableRawPointer? + + func getColor(at point: CGPoint) -> DrawingColor? { + guard let size = self.validSize, + point.x >= 0 && point.x <= size.width, + point.y >= 0 && point.y <= size.height else { + return nil + } + let position = CGPoint(x: point.x / size.width, y: point.y / size.height) + let scale = self.image.image?.scale ?? 1.0 + let point = CGPoint(x: point.x * scale, y: point.y * scale) + guard let image = self.image.image?.cgImage else { + return nil + } + + var redComponent: CGFloat? + var greenComponent: CGFloat? + var blueComponent: CGFloat? + + let imageWidth = image.width + let imageHeight = image.height + + let bitmapBytesForRow = Int(imageWidth * 4) + let bitmapByteCount = bitmapBytesForRow * Int(imageHeight) + + if self.bitmapData == nil { + let imageRect = CGRect(origin: .zero, size: CGSize(width: imageWidth, height: imageHeight)) + + let colorSpace = CGColorSpaceCreateDeviceRGB() + + let bitmapData = malloc(bitmapByteCount) + let bitmapInformation = CGImageAlphaInfo.premultipliedFirst.rawValue + + let colorContext = CGContext( + data: bitmapData, + width: imageWidth, + height: imageHeight, + bitsPerComponent: 8, + bytesPerRow: bitmapBytesForRow, + space: colorSpace, + bitmapInfo: bitmapInformation + ) + + colorContext?.clear(imageRect) + colorContext?.draw(image, in: imageRect) + + self.bitmapData = bitmapData + } + + self.bitmapData?.withMemoryRebound(to: UInt8.self, capacity: bitmapByteCount) { pointer in + let offset = 4 * ((Int(imageWidth) * Int(point.y)) + Int(point.x)) + + redComponent = CGFloat(pointer[offset + 1]) / 255.0 + greenComponent = CGFloat(pointer[offset + 2]) / 255.0 + blueComponent = CGFloat(pointer[offset + 3]) / 255.0 + } + + if let redComponent = redComponent, let greenComponent = greenComponent, let blueComponent = blueComponent { + return DrawingColor(rgb: UIColor(red: redComponent, green: greenComponent, blue: blueComponent, alpha: 1.0).rgb).withUpdatedPosition(position) + } else { + return nil + } + } + + deinit { + if let bitmapData = self.bitmapData { + free(bitmapData) + } + } + + @objc func handlePress(_ gestureRecognizer: UILongPressGestureRecognizer) { + guard case .began = gestureRecognizer.state else { + return + } + let location = gestureRecognizer.location(in: self) + if let color = self.getColor(at: location), color != self.selectedColor { + self.selected(color) + } + } + + @objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { + if gestureRecognizer.state == .changed { + let location = gestureRecognizer.location(in: self) + if let color = self.getColor(at: location), color != self.selectedColor { + self.selected(color) + } + } + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + + func updateLayout(size: CGSize, selectedColor: DrawingColor?) -> CGSize { + let previousSize = self.validSize + + let imageSize = size + self.validSize = imageSize + + self.selectedColor = selectedColor + + if previousSize != imageSize { + if previousSize == nil { + self.layer.allowsGroupOpacity = true + self.isUserInteractionEnabled = true + self.wrapper.clipsToBounds = true + self.wrapper.layer.cornerRadius = 10.0 + + let pressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handlePress(_:))) + pressGestureRecognizer.delegate = self + pressGestureRecognizer.minimumPressDuration = 0.01 + self.addGestureRecognizer(pressGestureRecognizer) + self.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))) + } + + self.wrapper.frame = CGRect(origin: .zero, size: imageSize) + if self.wrapper.superview == nil { + self.addSubview(self.wrapper) + } + + if let bitmapData = self.bitmapData { + free(bitmapData) + } + self.image.image = generateSpectrumImage(size: imageSize) + self.image.frame = CGRect(origin: .zero, size: imageSize) + if self.image.superview == nil { + self.wrapper.addSubview(self.image) + } + } + + if let color = selectedColor, let position = color.position { + if self.knob.superlayer == nil { + self.knob.contents = generateKnobImage()?.cgImage + self.layer.addSublayer(self.knob) + } + if self.circle.superlayer == nil { + self.circle.path = UIBezierPath(ovalIn: CGRect(origin: .zero, size: CGSize(width: 26.0, height: 26.0))).cgPath + self.layer.addSublayer(self.circle) + } + + self.knob.isHidden = false + self.circle.isHidden = false + + let margin: CGFloat = 10.0 + let knobSize = CGSize(width: 32.0, height: 32.0) + let knobFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(size.width * position.x - knobSize.width / 2.0), y: floorToScreenPixels(size.height * position.y - knobSize.height / 2.0)), size: knobSize) + self.knob.frame = knobFrame.insetBy(dx: -margin, dy: -margin) + + self.circle.fillColor = color.toUIColor().cgColor + self.circle.frame = knobFrame.insetBy(dx: 3.0, dy: 3.0) + } else { + self.knob.isHidden = true + self.circle.isHidden = true + } + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + view.selected = self.selected + return view.updateLayout(size: availableSize, selectedColor: self.color) + } +} + +final class ColorSpectrumPickerView: UIView, UIGestureRecognizerDelegate { + private var validSize: CGSize? + private var selectedColor: DrawingColor? + + private var wrapper = UIView(frame: CGRect()) + private var image = UIImageView(image: nil) + + private let knob = SimpleLayer() + private let circle = SimpleShapeLayer() + + private var circleMaskView = UIView() + private let maskCircle = SimpleShapeLayer() + + var selected: (DrawingColor) -> Void = { _ in } + + private var bitmapData: UnsafeMutableRawPointer? + + func getColor(at point: CGPoint) -> DrawingColor? { + guard let size = self.validSize, + point.x >= 0 && point.x <= size.width, + point.y >= 0 && point.y <= size.height else { + return nil + } + let position = CGPoint(x: point.x / size.width, y: point.y / size.height) + let scale = self.image.image?.scale ?? 1.0 + let point = CGPoint(x: point.x * scale, y: point.y * scale) + guard let image = self.image.image?.cgImage else { + return nil + } + + var redComponent: CGFloat? + var greenComponent: CGFloat? + var blueComponent: CGFloat? + + let imageWidth = image.width + let imageHeight = image.height + + let bitmapBytesForRow = Int(imageWidth * 4) + let bitmapByteCount = bitmapBytesForRow * Int(imageHeight) + + if self.bitmapData == nil { + let imageRect = CGRect(origin: .zero, size: CGSize(width: imageWidth, height: imageHeight)) + + let colorSpace = CGColorSpaceCreateDeviceRGB() + + let bitmapData = malloc(bitmapByteCount) + let bitmapInformation = CGImageAlphaInfo.premultipliedFirst.rawValue + + let colorContext = CGContext( + data: bitmapData, + width: imageWidth, + height: imageHeight, + bitsPerComponent: 8, + bytesPerRow: bitmapBytesForRow, + space: colorSpace, + bitmapInfo: bitmapInformation + ) + + colorContext?.clear(imageRect) + colorContext?.draw(image, in: imageRect) + + self.bitmapData = bitmapData + } + + self.bitmapData?.withMemoryRebound(to: UInt8.self, capacity: bitmapByteCount) { pointer in + let offset = 4 * ((Int(imageWidth) * Int(point.y)) + Int(point.x)) + + redComponent = CGFloat(pointer[offset + 1]) / 255.0 + greenComponent = CGFloat(pointer[offset + 2]) / 255.0 + blueComponent = CGFloat(pointer[offset + 3]) / 255.0 + } + + if let redComponent = redComponent, let greenComponent = greenComponent, let blueComponent = blueComponent { + return DrawingColor(rgb: UIColor(red: redComponent, green: greenComponent, blue: blueComponent, alpha: 1.0).rgb).withUpdatedPosition(position) + } else { + return nil + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + + self.isUserInteractionEnabled = false + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + if let bitmapData = self.bitmapData { + free(bitmapData) + } + } + + @objc func handlePan(point: CGPoint) { + guard let size = self.validSize else { + return + } + var location = self.convert(point, from: nil) + location.x = max(0.0, min(size.width - 1.0, location.x)) + location.y = max(0.0, min(size.height - 1.0, location.y)) + if let color = self.getColor(at: location), color != self.selectedColor { + self.selected(color) + let _ = self.updateLayout(size: size, selectedColor: color) + } + } + + private var animatingIn = false + private var scheduledAnimateOut: (() -> Void)? + + func animateIn() { + self.animatingIn = true + + Queue.mainQueue().after(0.15) { + self.selected(DrawingColor(rgb: 0xffffff)) + } + + self.wrapper.mask = self.circleMaskView + self.circleMaskView.frame = self.bounds + + self.maskCircle.fillColor = UIColor.red.cgColor + self.circleMaskView.layer.addSublayer(self.maskCircle) + + self.maskCircle.path = UIBezierPath(ovalIn: CGRect(origin: .zero, size: CGSize(width: 300.0, height: 300.0))).cgPath + self.maskCircle.frame = CGRect(origin: .zero, size: CGSize(width: 300.0, height: 300.0)) + self.maskCircle.position = CGPoint(x: 15.0, y: self.bounds.height - 15.0) + + self.maskCircle.transform = CATransform3DMakeScale(3.0, 3.0, 1.0) + self.maskCircle.animateScale(from: 0.05, to: 3.0, duration: 0.35, completion: { _ in + self.animatingIn = false + self.wrapper.mask = nil + + if let scheduledAnimateOut = self.scheduledAnimateOut { + self.scheduledAnimateOut = nil + self.animateOut(completion: scheduledAnimateOut) + } + }) + } + + func animateOut(completion: @escaping () -> Void) { + guard !self.animatingIn else { + self.scheduledAnimateOut = completion + return + } + + if let selectedColor = self.selectedColor { + self.selected(selectedColor) + } + + self.knob.opacity = 0.0 + self.knob.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + + self.circle.opacity = 0.0 + self.circle.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + + let filler = UIView(frame: self.bounds) + filler.backgroundColor = self.selectedColor?.toUIColor() ?? .white + self.wrapper.addSubview(filler) + + filler.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + + self.wrapper.mask = self.circleMaskView + self.maskCircle.animatePosition(from: self.maskCircle.position, to: CGPoint(x: 16.0, y: self.bounds.height - 16.0), duration: 0.25, removeOnCompletion: false) + self.maskCircle.animateScale(from: 3.0, to: 0.06333, duration: 0.35, removeOnCompletion: false, completion: { _ in + + completion() + }) + } + + func updateLayout(size: CGSize, selectedColor: DrawingColor?) -> CGSize { + let previousSize = self.validSize + + let imageSize = size + self.validSize = imageSize + + self.selectedColor = selectedColor + + if previousSize != imageSize { + if previousSize == nil { + self.layer.allowsGroupOpacity = true + self.isUserInteractionEnabled = true + self.wrapper.clipsToBounds = true + self.wrapper.layer.cornerRadius = 17.0 + } + + self.wrapper.frame = CGRect(origin: .zero, size: imageSize) + if self.wrapper.superview == nil { + self.addSubview(self.wrapper) + } + + if let bitmapData = self.bitmapData { + free(bitmapData) + } + self.image.image = generateSpectrumImage(size: imageSize) + self.image.frame = CGRect(origin: .zero, size: imageSize) + if self.image.superview == nil { + self.wrapper.addSubview(self.image) + } + } + + if let color = selectedColor, let position = color.position { + if self.knob.superlayer == nil { + self.knob.contents = generateKnobImage()?.cgImage + self.layer.addSublayer(self.knob) + + self.knob.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + if self.circle.superlayer == nil { + self.circle.path = UIBezierPath(ovalIn: CGRect(origin: .zero, size: CGSize(width: 26.0, height: 26.0))).cgPath + self.layer.addSublayer(self.circle) + + self.circle.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + self.knob.isHidden = false + self.circle.isHidden = false + + let margin: CGFloat = 10.0 + let knobSize = CGSize(width: 32.0, height: 32.0) + let knobFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(size.width * position.x - knobSize.width / 2.0), y: floorToScreenPixels(size.height * position.y - knobSize.height / 2.0) - 33.0), size: knobSize) + self.knob.frame = knobFrame.insetBy(dx: -margin, dy: -margin) + + self.circle.fillColor = color.toUIColor().cgColor + self.circle.frame = knobFrame.insetBy(dx: 3.0, dy: 3.0) + } else { + self.knob.isHidden = true + self.circle.isHidden = true + } + + return size + } +} + +private final class ColorSlidersComponent: CombinedComponent { + typealias EnvironmentType = ComponentFlow.Empty + + let color: DrawingColor + let updated: (DrawingColor) -> Void + + init( + color: DrawingColor, + updated: @escaping (DrawingColor) -> Void + ) { + self.color = color + self.updated = updated + } + + static func ==(lhs: ColorSlidersComponent, rhs: ColorSlidersComponent) -> Bool { + if lhs.color != rhs.color { + return false + } + return true + } + + static var body: Body { + let redTitle = Child(MultilineTextComponent.self) + let redSlider = Child(ColorSliderComponent.self) + let redField = Child(ColorFieldComponent.self) + + let greenTitle = Child(MultilineTextComponent.self) + let greenSlider = Child(ColorSliderComponent.self) + let greenField = Child(ColorFieldComponent.self) + + let blueTitle = Child(MultilineTextComponent.self) + let blueSlider = Child(ColorSliderComponent.self) + let blueField = Child(ColorFieldComponent.self) + + let hexTitle = Child(MultilineTextComponent.self) + let hexField = Child(ColorFieldComponent.self) + + return { context in + let component = context.component + + var contentHeight: CGFloat = 0.0 + + let redTitle = redTitle.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: "RED", + font: Font.semibold(13.0), + textColor: UIColor(rgb: 0x9b9da5), + paragraphAlignment: .center + )), + horizontalAlignment: .center, + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + context.add(redTitle + .position(CGPoint(x: 5.0 + redTitle.size.width / 2.0, y: contentHeight + redTitle.size.height / 2.0)) + ) + contentHeight += redTitle.size.height + contentHeight += 8.0 + + let currentColor = component.color + let updateColor = component.updated + + let redSlider = redSlider.update( + component: ColorSliderComponent( + leftColor: component.color.withUpdatedRed(0.0).withUpdatedAlpha(1.0), + rightColor: component.color.withUpdatedRed(1.0).withUpdatedAlpha(1.0), + currentColor: component.color, + value: component.color.red, + updated: { value in + updateColor(currentColor.withUpdatedRed(value)) + } + ), + availableSize: CGSize(width: context.availableSize.width - 89.0, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + context.add(redSlider + .position(CGPoint(x: redSlider.size.width / 2.0, y: contentHeight + redSlider.size.height / 2.0)) + ) + + let redField = redField.update( + component: ColorFieldComponent( + backgroundColor: UIColor(rgb: 0x000000, alpha: 0.6), + textColor: .white, + type: .number, + value: "\(Int(component.color.red * 255.0))", + updated: { value in + if let intValue = Int(value) { + updateColor(currentColor.withUpdatedRed(CGFloat(intValue) / 255.0)) + } + }, + shouldUpdate: { value in + if let intValue = Int(value), intValue >= 0 && intValue <= 255 { + return true + } else { + return false + } + } + ), + availableSize: CGSize(width: 77.0, height: 36.0), + transition: .immediate + ) + context.add(redField + .position(CGPoint(x: context.availableSize.width - redField.size.width / 2.0, y: contentHeight + redField.size.height / 2.0)) + ) + + contentHeight += redSlider.size.height + contentHeight += 28.0 + + let greenTitle = greenTitle.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: "GREEN", + font: Font.semibold(13.0), + textColor: UIColor(rgb: 0x9b9da5), + paragraphAlignment: .center + )), + horizontalAlignment: .center, + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + context.add(greenTitle + .position(CGPoint(x: 5.0 + greenTitle.size.width / 2.0, y: contentHeight + greenTitle.size.height / 2.0)) + ) + contentHeight += greenTitle.size.height + contentHeight += 8.0 + + let greenSlider = greenSlider.update( + component: ColorSliderComponent( + leftColor: component.color.withUpdatedGreen(0.0).withUpdatedAlpha(1.0), + rightColor: component.color.withUpdatedGreen(1.0).withUpdatedAlpha(1.0), + currentColor: component.color, + value: component.color.green, + updated: { value in + updateColor(currentColor.withUpdatedGreen(value)) + } + ), + availableSize: CGSize(width: context.availableSize.width - 89.0, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + context.add(greenSlider + .position(CGPoint(x: greenSlider.size.width / 2.0, y: contentHeight + greenSlider.size.height / 2.0)) + ) + + let greenField = greenField.update( + component: ColorFieldComponent( + backgroundColor: UIColor(rgb: 0x000000, alpha: 0.6), + textColor: .white, + type: .number, + value: "\(Int(component.color.green * 255.0))", + updated: { value in + if let intValue = Int(value) { + updateColor(currentColor.withUpdatedGreen(CGFloat(intValue) / 255.0)) + } + }, + shouldUpdate: { value in + if let intValue = Int(value), intValue >= 0 && intValue <= 255 { + return true + } else { + return false + } + } + ), + availableSize: CGSize(width: 77.0, height: 36.0), + transition: .immediate + ) + context.add(greenField + .position(CGPoint(x: context.availableSize.width - greenField.size.width / 2.0, y: contentHeight + greenField.size.height / 2.0)) + ) + + contentHeight += greenSlider.size.height + contentHeight += 28.0 + + let blueTitle = blueTitle.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: "BLUE", + font: Font.semibold(13.0), + textColor: UIColor(rgb: 0x9b9da5), + paragraphAlignment: .center + )), + horizontalAlignment: .center, + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + context.add(blueTitle + .position(CGPoint(x: 5.0 + blueTitle.size.width / 2.0, y: contentHeight + blueTitle.size.height / 2.0)) + ) + contentHeight += blueTitle.size.height + contentHeight += 8.0 + + let blueSlider = blueSlider.update( + component: ColorSliderComponent( + leftColor: component.color.withUpdatedBlue(0.0).withUpdatedAlpha(1.0), + rightColor: component.color.withUpdatedBlue(1.0).withUpdatedAlpha(1.0), + currentColor: component.color, + value: component.color.blue, + updated: { value in + updateColor(currentColor.withUpdatedBlue(value)) + } + ), + availableSize: CGSize(width: context.availableSize.width - 89.0, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + context.add(blueSlider + .position(CGPoint(x: blueSlider.size.width / 2.0, y: contentHeight + blueSlider.size.height / 2.0)) + ) + + let blueField = blueField.update( + component: ColorFieldComponent( + backgroundColor: UIColor(rgb: 0x000000, alpha: 0.6), + textColor: .white, + type: .number, + value: "\(Int(component.color.blue * 255.0))", + updated: { value in + if let intValue = Int(value) { + updateColor(currentColor.withUpdatedBlue(CGFloat(intValue) / 255.0)) + } + }, + shouldUpdate: { value in + if let intValue = Int(value), intValue >= 0 && intValue <= 255 { + return true + } else { + return false + } + } + ), + availableSize: CGSize(width: 77.0, height: 36.0), + transition: .immediate + ) + context.add(blueField + .position(CGPoint(x: context.availableSize.width - blueField.size.width / 2.0, y: contentHeight + blueField.size.height / 2.0)) + ) + + contentHeight += blueSlider.size.height + contentHeight += 28.0 + + let hexField = hexField.update( + component: ColorFieldComponent( + backgroundColor: UIColor(rgb: 0x000000, alpha: 0.6), + textColor: .white, + type: .text, + value: component.color.toUIColor().hexString.uppercased(), + updated: { value in + if value.count == 6, let uiColor = UIColor(hexString: value) { + updateColor(DrawingColor(color: uiColor).withUpdatedAlpha(currentColor.alpha)) + } + }, + shouldUpdate: { value in + if value.count <= 6 && value.rangeOfCharacter(from: CharacterSet(charactersIn: "0123456789abcdefABCDEF").inverted) == nil { + return true + } else { + return false + } + } + ), + availableSize: CGSize(width: 77.0, height: 36.0), + transition: .immediate + ) + context.add(hexField + .position(CGPoint(x: context.availableSize.width - hexField.size.width / 2.0, y: contentHeight + hexField.size.height / 2.0)) + ) + + let hexTitle = hexTitle.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: "Hex Color #", + font: Font.regular(17.0), + textColor: UIColor(rgb: 0xffffff), + paragraphAlignment: .center + )), + horizontalAlignment: .center, + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + context.add(hexTitle + .position(CGPoint(x: context.availableSize.width - hexField.size.width - 12.0 - hexTitle.size.width / 2.0, y: contentHeight + hexField.size.height / 2.0)) + ) + + contentHeight += hexField.size.height + contentHeight += 8.0 + + return CGSize(width: context.availableSize.width, height: contentHeight) + } + } +} + +private func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? { + return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.setFillColor(backgroundColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + + context.setLineWidth(2.0) + context.setLineCap(.round) + context.setStrokeColor(foregroundColor.cgColor) + + context.move(to: CGPoint(x: 10.0, y: 10.0)) + context.addLine(to: CGPoint(x: 20.0, y: 20.0)) + context.strokePath() + + context.move(to: CGPoint(x: 20.0, y: 10.0)) + context.addLine(to: CGPoint(x: 10.0, y: 20.0)) + context.strokePath() + }) +} + +private class SegmentedControlComponent: Component { + let values: [String] + let selectedIndex: Int + let selectionChanged: (Int) -> Void + + init(values: [String], selectedIndex: Int, selectionChanged: @escaping (Int) -> Void) { + self.values = values + self.selectedIndex = selectedIndex + self.selectionChanged = selectionChanged + } + + static func ==(lhs: SegmentedControlComponent, rhs: SegmentedControlComponent) -> Bool { + if lhs.values != rhs.values { + return false + } + if lhs.selectedIndex != rhs.selectedIndex { + return false + } + return true + } + + final class View: UIView { + private let backgroundNode: NavigationBackgroundNode + private let node: SegmentedControlNode + + init() { + self.backgroundNode = NavigationBackgroundNode(color: UIColor(rgb: 0x888888, alpha: 0.1)) + self.node = SegmentedControlNode(theme: SegmentedControlTheme(backgroundColor: .clear, foregroundColor: UIColor(rgb: 0x6f7075, alpha: 0.6), shadowColor: .black, textColor: UIColor(rgb: 0xffffff), dividerColor: UIColor(rgb: 0x505155, alpha: 0.6)), items: [], selectedIndex: 0) + + super.init(frame: CGRect()) + + self.addSubview(self.backgroundNode.view) + self.addSubview(self.node.view) + } + + required init?(coder aDecoder: NSCoder) { + preconditionFailure() + } + + func update(component: SegmentedControlComponent, availableSize: CGSize, transition: Transition) -> CGSize { + self.node.items = component.values.map { SegmentedControlItem(title: $0) } + self.node.selectedIndex = component.selectedIndex + let selectionChanged = component.selectionChanged + self.node.selectedIndexChanged = { [weak self] index in + self?.window?.endEditing(true) + selectionChanged(index) + } + + let size = self.node.updateLayout(.stretchToFill(width: availableSize.width), transition: transition.containedViewLayoutTransition) + transition.setFrame(view: self.node.view, frame: CGRect(origin: CGPoint(), size: size)) + + transition.setFrame(view: self.backgroundNode.view, frame: CGRect(origin: CGPoint(), size: size)) + self.backgroundNode.update(size: size, cornerRadius: 10.0, transition: .immediate) + + return size + } + } + + func makeView() -> View { + return View() + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, transition: transition) + } +} + +final class ColorSwatchComponent: Component { + enum SwatchType: Equatable { + case main + case pallete(Bool) + } + + let type: SwatchType + let color: DrawingColor? + let tag: AnyObject? + let action: () -> Void + let holdAction: (() -> Void)? + let pan: ((CGPoint) -> Void)? + let release: (() -> Void)? + + init( + type: SwatchType, + color: DrawingColor?, + tag: AnyObject? = nil, + action: @escaping () -> Void, + holdAction: (() -> Void)? = nil, + pan: ((CGPoint) -> Void)? = nil, + release: (() -> Void)? = nil + ) { + self.type = type + self.color = color + self.tag = tag + self.action = action + self.holdAction = holdAction + self.pan = pan + self.release = release + } + + static func == (lhs: ColorSwatchComponent, rhs: ColorSwatchComponent) -> Bool { + return lhs.type == rhs.type && lhs.color == rhs.color + } + + final class View: UIButton, ComponentTaggedView { + private var component: ColorSwatchComponent? + + private var contentView: UIView + + private var ringLayer: CALayer? + private var ringMaskLayer: SimpleShapeLayer? + + private let circleLayer: SimpleShapeLayer + + private let fastCircleLayer: SimpleShapeLayer + + private var currentIsHighlighted: Bool = false { + didSet { + if self.currentIsHighlighted != oldValue { + self.contentView.alpha = self.currentIsHighlighted ? 0.6 : 1.0 + } + } + } + + private var holdActionTriggerred: Bool = false + private var holdActionTimer: Foundation.Timer? + + override init(frame: CGRect) { + self.contentView = UIView(frame: CGRect(origin: .zero, size: frame.size)) + self.contentView.isUserInteractionEnabled = false + self.circleLayer = SimpleShapeLayer() + self.fastCircleLayer = SimpleShapeLayer() + self.fastCircleLayer.fillColor = UIColor.white.cgColor + self.fastCircleLayer.isHidden = true + + super.init(frame: frame) + + self.addSubview(self.contentView) + self.contentView.layer.addSublayer(self.circleLayer) + + self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func matches(tag: Any) -> Bool { + if let component = self.component, let componentTag = component.tag { + let tag = tag as AnyObject + if componentTag === tag { + return true + } + } + return false + } + + @objc private func pressed() { + if self.holdActionTriggerred { + self.holdActionTriggerred = false + } else { + self.component?.action() + } + } + + override public func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { + self.currentIsHighlighted = true + + self.holdActionTriggerred = false + + if self.component?.holdAction != nil { + Queue.mainQueue().after(0.15, { + if self.currentIsHighlighted { + self.fastCircleLayer.isHidden = false + self.fastCircleLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + self.fastCircleLayer.animateScale(from: 0.57575, to: 1.0, duration: 0.25) + } + }) + + self.holdActionTimer?.invalidate() + if #available(iOS 10.0, *) { + let holdActionTimer = Timer(timeInterval: 0.4, repeats: false, block: { [weak self] _ in + guard let strongSelf = self else { + return + } + strongSelf.holdActionTriggerred = true + strongSelf.holdActionTimer?.invalidate() + strongSelf.component?.holdAction?() + Queue.mainQueue().after(0.1, { + strongSelf.fastCircleLayer.isHidden = true + }) + }) + self.holdActionTimer = holdActionTimer + RunLoop.main.add(holdActionTimer, forMode: .common) + } + } + + return super.beginTracking(touch, with: event) + } + + override public func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { + if self.holdActionTriggerred { + let location = touch.location(in: nil) + self.component?.pan?(location) + } + return true + } + + override public func endTracking(_ touch: UITouch?, with event: UIEvent?) { + if self.holdActionTriggerred { + self.component?.release?() + } + + self.currentIsHighlighted = false + Queue.mainQueue().after(0.1) { + self.holdActionTriggerred = false + } + if !self.fastCircleLayer.isHidden { + let currentAlpha: CGFloat = CGFloat(self.fastCircleLayer.presentation()?.opacity ?? 1.0) + self.fastCircleLayer.animateAlpha(from: currentAlpha, to: 0.0, duration: 0.1, completion: { _ in + self.fastCircleLayer.isHidden = true + }) + } + + self.holdActionTimer?.invalidate() + self.holdActionTimer = nil + + super.endTracking(touch, with: event) + } + + override public func cancelTracking(with event: UIEvent?) { + if self.holdActionTriggerred { + self.component?.release?() + } + + self.currentIsHighlighted = false + self.holdActionTriggerred = false + + self.holdActionTimer?.invalidate() + self.holdActionTimer = nil + + super.cancelTracking(with: event) + } + + func animateIn() { + self.contentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.contentView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3) + } + + func animateOut() { + self.contentView.alpha = 0.0 + self.contentView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + self.contentView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3) + } + + func update(component: ColorSwatchComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + let contentSize: CGSize + if case .pallete = component.type { + contentSize = availableSize + } else { + contentSize = CGSize(width: 33.0, height: 33.0) + } + self.contentView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - contentSize.width) / 2.0), y: floor((availableSize.height - contentSize.height) / 2.0)), size: contentSize) + + let bounds = CGRect(origin: .zero, size: contentSize) + switch component.type { + case .main: + self.circleLayer.frame = bounds + if self.circleLayer.path == nil { + self.circleLayer.path = UIBezierPath(ovalIn: bounds.insetBy(dx: 7.0, dy: 7.0)).cgPath + } + + if self.ringLayer == nil { + if #available(iOS 12.0, *) { + let ringLayer = SimpleGradientLayer() + ringLayer.rasterizationScale = UIScreen.main.scale + ringLayer.shouldRasterize = true + ringLayer.type = .conic + ringLayer.colors = [ + UIColor(rgb: 0xe1564c).cgColor, + UIColor(rgb: 0xad4dc9).cgColor, + UIColor(rgb: 0x579ff2).cgColor, + UIColor(rgb: 0x4fcbd0).cgColor, + UIColor(rgb: 0x80e655).cgColor, + UIColor(rgb: 0xdad138).cgColor, + UIColor(rgb: 0xe59645).cgColor, + UIColor(rgb: 0xe1564c).cgColor + ] + ringLayer.locations = [0.0, 0.15, 0.3, 0.45, 0.6, 0.75, 0.9, 1.0] + ringLayer.frame = bounds + ringLayer.startPoint = CGPoint(x: 0.5, y: 0.5) + ringLayer.endPoint = CGPoint(x: 1.0, y: 0.55) + self.contentView.layer.addSublayer(ringLayer) + + self.ringLayer = ringLayer + + } else { + let ringLayer = SimpleLayer() + ringLayer.contents = UIImage(bundleImageName: "Media Editor/RoundSpectrum")?.cgImage + ringLayer.frame = bounds + self.contentView.layer.addSublayer(ringLayer) + + self.ringLayer = ringLayer + } + + let ringMaskLayer = SimpleShapeLayer() + ringMaskLayer.frame = bounds + ringMaskLayer.strokeColor = UIColor.white.cgColor + ringMaskLayer.fillColor = UIColor.clear.cgColor + self.ringMaskLayer = ringMaskLayer + self.ringLayer?.mask = ringMaskLayer + } + + if let ringMaskLayer = self.ringMaskLayer { + if component.color == nil { + transition.setShapeLayerPath(layer: ringMaskLayer, path: UIBezierPath(ovalIn: CGRect(origin: .zero, size: contentSize).insetBy(dx: 8.25, dy: 8.25)).cgPath) + transition.setShapeLayerLineWidth(layer: ringMaskLayer, lineWidth: 16.5) + } else { + transition.setShapeLayerPath(layer: ringMaskLayer, path: UIBezierPath(ovalIn: CGRect(origin: .zero, size: contentSize).insetBy(dx: 1.5, dy: 1.5)).cgPath) + transition.setShapeLayerLineWidth(layer: ringMaskLayer, lineWidth: 3.0) + } + } + + if self.fastCircleLayer.path == nil { + self.fastCircleLayer.path = UIBezierPath(ovalIn: bounds).cgPath + self.fastCircleLayer.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - bounds.size.width) / 2.0), y: floorToScreenPixels((availableSize.height - bounds.size.height) / 2.0)), size: bounds.size) + self.layer.addSublayer(self.fastCircleLayer) + } + case let .pallete(selected): + self.layer.allowsGroupOpacity = true + self.contentView.layer.allowsGroupOpacity = true + + self.circleLayer.frame = bounds + if self.ringLayer == nil { + let ringLayer = SimpleLayer() + ringLayer.backgroundColor = UIColor.clear.cgColor + ringLayer.cornerRadius = contentSize.width / 2.0 + ringLayer.borderWidth = 3.0 + ringLayer.frame = CGRect(origin: .zero, size: contentSize) + self.contentView.layer.insertSublayer(ringLayer, at: 0) + self.ringLayer = ringLayer + } + + if selected { + transition.setShapeLayerPath(layer: self.circleLayer, path: CGPath(ellipseIn: bounds.insetBy(dx: 5.0, dy: 5.0), transform: nil)) + } else { + transition.setShapeLayerPath(layer: self.circleLayer, path: CGPath(ellipseIn: bounds, transform: nil)) + } + } + + if let color = component.color { + self.circleLayer.fillColor = color.toCGColor() + if case .pallete = component.type { + + if color.toUIColor().rgb == 0x000000 { + self.circleLayer.strokeColor = UIColor(rgb: 0x1f1f1f).cgColor + self.circleLayer.lineWidth = 1.0 + self.ringLayer?.borderColor = UIColor(rgb: 0x1f1f1f).cgColor + } else { + self.ringLayer?.borderColor = color.toCGColor() + } + } + } + + if let screenTransition = transition.userData(DrawingScreenTransition.self) { + switch screenTransition { + case .animateIn: + self.animateIn() + case .animateOut: + self.animateOut() + } + } + + return availableSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + + +class BlurredRectangle: Component { + let color: UIColor + let radius: CGFloat + + init(color: UIColor, radius: CGFloat = 0.0) { + self.color = color + self.radius = radius + } + + static func ==(lhs: BlurredRectangle, rhs: BlurredRectangle) -> Bool { + if !lhs.color.isEqual(rhs.color) { + return false + } + if lhs.radius != rhs.radius { + return false + } + return true + } + + final class View: UIView { + private let background: NavigationBackgroundNode + + init() { + self.background = NavigationBackgroundNode(color: .clear) + + super.init(frame: CGRect()) + + self.addSubview(self.background.view) + } + + required init?(coder aDecoder: NSCoder) { + preconditionFailure() + } + + func update(component: BlurredRectangle, availableSize: CGSize, transition: Transition) -> CGSize { + transition.setFrame(view: self.background.view, frame: CGRect(origin: CGPoint(), size: availableSize)) + self.background.updateColor(color: component.color, transition: .immediate) + self.background.update(size: availableSize, cornerRadius: component.radius, transition: .immediate) + + return availableSize + } + } + + func makeView() -> View { + return View() + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, transition: transition) + } +} + +private final class ColorPickerContent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let initialColor: DrawingColor + let colorChanged: (DrawingColor) -> Void + let eyedropper: () -> Void + let dismiss: () -> Void + + init( + context: AccountContext, + initialColor: DrawingColor, + colorChanged: @escaping (DrawingColor) -> Void, + eyedropper: @escaping () -> Void, + dismiss: @escaping () -> Void + ) { + self.context = context + self.initialColor = initialColor + self.colorChanged = colorChanged + self.eyedropper = eyedropper + self.dismiss = dismiss + } + + static func ==(lhs: ColorPickerContent, rhs: ColorPickerContent) -> Bool { + if lhs.context !== rhs.context { + return false + } + return true + } + + final class State: ComponentState { + var cachedEyedropperImage: UIImage? + var eyedropperImage: UIImage { + let eyedropperImage: UIImage + if let image = self.cachedEyedropperImage { + eyedropperImage = image + } else { + eyedropperImage = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Eyedropper"), color: .white)! + self.cachedEyedropperImage = eyedropperImage + } + return eyedropperImage + } + + var cachedCloseImage: UIImage? + var closeImage: UIImage { + let closeImage: UIImage + if let image = self.cachedCloseImage { + closeImage = image + } else { + closeImage = generateCloseButtonImage(backgroundColor: .clear, foregroundColor: UIColor(rgb: 0xa8aab1))! + self.cachedCloseImage = closeImage + } + return closeImage + } + + var selectedMode: Int = 0 + var selectedColor: DrawingColor + + var savedColors: [DrawingColor] = [] + + var colorChanged: (DrawingColor) -> Void = { _ in } + + init(initialColor: DrawingColor) { + self.selectedColor = initialColor + + self.savedColors = [DrawingColor(color: .red), DrawingColor(color: .green), DrawingColor(color: .blue)] + } + + func updateColor(_ color: DrawingColor, keepAlpha: Bool = false) { + self.selectedColor = keepAlpha ? color.withUpdatedAlpha(self.selectedColor.alpha) : color + self.colorChanged(self.selectedColor) + self.updated(transition: .immediate) + } + + func updateAlpha(_ alpha: CGFloat) { + self.selectedColor = self.selectedColor.withUpdatedAlpha(alpha) + self.colorChanged(self.selectedColor) + self.updated(transition: .immediate) + } + + func updateSelectedMode(_ mode: Int) { + self.selectedMode = mode + self.updated(transition: .easeInOut(duration: 0.2)) + } + + func saveCurrentColor() { + self.savedColors.append(self.selectedColor) + self.updated(transition: .easeInOut(duration: 0.2)) + } + } + + func makeState() -> State { + return State(initialColor: self.initialColor) + } + + static var body: Body { + let eyedropperButton = Child(Button.self) + let closeButton = Child(Button.self) + let title = Child(MultilineTextComponent.self) + let modeControl = Child(SegmentedControlComponent.self) + + let colorGrid = Child(ColorGridComponent.self) + let colorSpectrum = Child(ColorSpectrumComponent.self) + let colorSliders = Child(ColorSlidersComponent.self) + + let opacityTitle = Child(MultilineTextComponent.self) + let opacitySlider = Child(ColorSliderComponent.self) + let opacityField = Child(ColorFieldComponent.self) + + let divider = Child(Rectangle.self) + + let preview = Child(ColorPreviewComponent.self) + + let swatch1Button = Child(ColorSwatchComponent.self) + let swatch2Button = Child(ColorSwatchComponent.self) + let swatch3Button = Child(ColorSwatchComponent.self) + let swatch4Button = Child(ColorSwatchComponent.self) + let swatch5Button = Child(ColorSwatchComponent.self) + + return { context in + let environment = context.environment[ViewControllerComponentContainer.Environment.self].value + let component = context.component + let state = context.state + state.colorChanged = component.colorChanged + + let sideInset: CGFloat = 16.0 + + let eyedropperButton = eyedropperButton.update( + component: Button( + content: AnyComponent( + Image(image: state.eyedropperImage) + ), + action: { [weak component] in + component?.eyedropper() + } + ).minSize(CGSize(width: 30.0, height: 30.0)), + availableSize: CGSize(width: 19.0, height: 19.0), + transition: .immediate + ) + context.add(eyedropperButton + .position(CGPoint(x: environment.safeInsets.left + eyedropperButton.size.width + 1.0, y: 29.0)) + ) + + let closeButton = closeButton.update( + component: Button( + content: AnyComponent(ZStack([ + AnyComponentWithIdentity( + id: "background", + component: AnyComponent( + BlurredRectangle( + color: UIColor(rgb: 0x888888, alpha: 0.1), + radius: 15.0 + ) + ) + ), + AnyComponentWithIdentity( + id: "icon", + component: AnyComponent( + Image(image: state.closeImage) + ) + ), + ])), + action: { [weak component] in + component?.dismiss() + } + ), + availableSize: CGSize(width: 30.0, height: 30.0), + transition: .immediate + ) + context.add(closeButton + .position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - closeButton.size.width - 1.0, y: 29.0)) + ) + + let title = title.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: "Colors", + font: Font.semibold(17.0), + textColor: .white, + paragraphAlignment: .center + )), + horizontalAlignment: .center, + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width - 100.0, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + context.add(title + .position(CGPoint(x: context.availableSize.width / 2.0, y: 29.0)) + ) + + var contentHeight: CGFloat = 58.0 + + let modeControl = modeControl.update( + component: SegmentedControlComponent( + values: ["Grid", "Spectrum", "Sliders"], + selectedIndex: 0, + selectionChanged: { [weak state] index in + state?.updateSelectedMode(index) + } + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height), + transition: .immediate + ) + context.add(modeControl + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + modeControl.size.height / 2.0)) + ) + contentHeight += modeControl.size.height + contentHeight += 20.0 + + let squareSize = floorToScreenPixels((context.availableSize.width - sideInset * 2.0) / 12.0) + let fieldSize = CGSize(width: context.availableSize.width - sideInset * 2.0, height: squareSize * 10.0) + + if state.selectedMode == 0 { + let colorGrid = colorGrid.update( + component: ColorGridComponent( + color: state.selectedColor, + selected: { [weak state] color in + state?.updateColor(color, keepAlpha: true) + } + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height), + transition: .immediate + ) + context.add(colorGrid + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + colorGrid.size.height / 2.0)) + .appear(.default(alpha: true)) + .disappear(.default()) + ) + } else if state.selectedMode == 1 { + let colorSpectrum = colorSpectrum.update( + component: ColorSpectrumComponent( + color: state.selectedColor, + selected: { [weak state] color in + state?.updateColor(color, keepAlpha: true) + } + ), + availableSize: fieldSize, + transition: .immediate + ) + context.add(colorSpectrum + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + fieldSize.height / 2.0)) + .appear(.default(alpha: true)) + .disappear(.default()) + ) + } else if state.selectedMode == 2 { + let colorSliders = colorSliders.update( + component: ColorSlidersComponent( + color: state.selectedColor, + updated: { [weak state] color in + state?.updateColor(color, keepAlpha: true) + } + ), + availableSize: fieldSize, + transition: .immediate + ) + context.add(colorSliders + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + colorSliders.size.height / 2.0)) + .appear(.default(alpha: true)) + .disappear(.default()) + ) + } + + contentHeight += fieldSize.height + contentHeight += 21.0 + + let opacityTitle = opacityTitle.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: "OPACITY", + font: Font.semibold(13.0), + textColor: UIColor(rgb: 0x9b9da5), + paragraphAlignment: .center + )), + horizontalAlignment: .center, + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + context.add(opacityTitle + .position(CGPoint(x: sideInset + 5.0 + opacityTitle.size.width / 2.0, y: contentHeight + opacityTitle.size.height / 2.0)) + ) + contentHeight += opacityTitle.size.height + contentHeight += 8.0 + + let opacitySlider = opacitySlider.update( + component: ColorSliderComponent( + leftColor: state.selectedColor.withUpdatedAlpha(0.0), + rightColor: state.selectedColor.withUpdatedAlpha(1.0), + currentColor: state.selectedColor, + value: state.selectedColor.alpha, + updated: { value in + state.updateAlpha(value) + } + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 89.0, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + context.add(opacitySlider + .position(CGPoint(x: sideInset + opacitySlider.size.width / 2.0, y: contentHeight + opacitySlider.size.height / 2.0)) + ) + + let opacityField = opacityField.update( + component: ColorFieldComponent( + backgroundColor: UIColor(rgb: 0x000000, alpha: 0.6), + textColor: .white, + type: .number, + value: "\(Int(state.selectedColor.alpha * 100.0))", + suffix: "%", + updated: { _ in }, + shouldUpdate: { _ in return true } + ), + availableSize: CGSize(width: 77.0, height: 36.0), + transition: .immediate + ) + context.add(opacityField + .position(CGPoint(x: context.availableSize.width - sideInset - opacityField.size.width / 2.0, y: contentHeight + opacityField.size.height / 2.0)) + ) + + contentHeight += opacitySlider.size.height + contentHeight += 24.0 + + let divider = divider.update( + component: Rectangle(color: UIColor(rgb: 0x48484a)), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 1.0), + transition: .immediate + ) + context.add(divider + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight)) + ) + contentHeight += divider.size.height + contentHeight += 22.0 + + let preview = preview.update( + component: ColorPreviewComponent( + color: state.selectedColor + ), + availableSize: CGSize(width: 82.0, height: 82.0), + transition: .immediate + ) + context.add(preview + .position(CGPoint(x: sideInset + preview.size.width / 2.0, y: contentHeight + preview.size.height / 2.0)) + ) + + + var swatchOffset: CGFloat = sideInset + preview.size.width + 38.0 + let swatchSpacing: CGFloat = 20.0 + + let swatch1Button = swatch1Button.update( + component: ColorSwatchComponent( + type: .pallete(false), + color: DrawingColor(color: .black), + action: { + + } + ), + availableSize: CGSize(width: 30.0, height: 30.0), + transition: context.transition + ) + context.add(swatch1Button + .position(CGPoint(x: swatchOffset, y: contentHeight + swatch1Button.size.height / 2.0)) + ) + swatchOffset += swatch1Button.size.width + swatchSpacing + + let swatch2Button = swatch2Button.update( + component: ColorSwatchComponent( + type: .pallete(false), + color: DrawingColor(rgb: 0x0161fd), + action: { + + } + ), + availableSize: CGSize(width: 30.0, height: 30.0), + transition: context.transition + ) + context.add(swatch2Button + .position(CGPoint(x: swatchOffset, y: contentHeight + swatch2Button.size.height / 2.0)) + ) + swatchOffset += swatch2Button.size.width + swatchSpacing + + let swatch3Button = swatch3Button.update( + component: ColorSwatchComponent( + type: .pallete(false), + color: DrawingColor(rgb: 0x32c759), + action: { + } + ), + availableSize: CGSize(width: 30.0, height: 30.0), + transition: context.transition + ) + context.add(swatch3Button + .position(CGPoint(x: swatchOffset, y: contentHeight + swatch3Button.size.height / 2.0)) + ) + swatchOffset += swatch3Button.size.width + swatchSpacing + + let swatch4Button = swatch4Button.update( + component: ColorSwatchComponent( + type: .pallete(false), + color: DrawingColor(rgb: 0xffcc02), + action: { + } + ), + availableSize: CGSize(width: 30.0, height: 30.0), + transition: context.transition + ) + context.add(swatch4Button + .position(CGPoint(x: swatchOffset, y: contentHeight + swatch4Button.size.height / 2.0)) + ) + swatchOffset += swatch4Button.size.width + swatchSpacing + + let swatch5Button = swatch5Button.update( + component: ColorSwatchComponent( + type: .pallete(false), + color: DrawingColor(rgb: 0xff3a30), + action: { + } + ), + availableSize: CGSize(width: 30.0, height: 30.0), + transition: context.transition + ) + context.add(swatch5Button + .position(CGPoint(x: swatchOffset, y: contentHeight + swatch5Button.size.height / 2.0)) + ) + + contentHeight += preview.size.height + contentHeight += 10.0 + + let bottomPanelPadding: CGFloat = 12.0 + var bottomInset: CGFloat + if case .regular = environment.metrics.widthClass { + bottomInset = bottomPanelPadding + } else { + bottomInset = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding + } + + if environment.inputHeight > 0.0 { + bottomInset += environment.inputHeight - bottomInset - 120.0 + } + + return CGSize(width: context.availableSize.width, height: contentHeight + bottomInset) + } + } +} + +private final class ColorPickerSheetComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + private let context: AccountContext + private let initialColor: DrawingColor + private let updated: (DrawingColor) -> Void + private let openEyedropper: () -> Void + private let dismissed: () -> Void + + init(context: AccountContext, initialColor: DrawingColor, updated: @escaping (DrawingColor) -> Void, openEyedropper: @escaping () -> Void, dismissed: @escaping () -> Void) { + self.context = context + self.initialColor = initialColor + self.updated = updated + self.openEyedropper = openEyedropper + self.dismissed = dismissed + } + + static func ==(lhs: ColorPickerSheetComponent, rhs: ColorPickerSheetComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + return true + } + + static var body: Body { + let sheet = Child(SheetComponent<(EnvironmentType)>.self) + let animateOut = StoredActionSlot(Action.self) + + return { context in + let environment = context.environment[EnvironmentType.self] + + let controller = environment.controller + + let updated = context.component.updated + let openEyedropper = context.component.openEyedropper + let dismissed = context.component.dismissed + + let sheet = sheet.update( + component: SheetComponent( + content: AnyComponent(ColorPickerContent( + context: context.component.context, + initialColor: context.component.initialColor, + colorChanged: { color in + updated(color) + }, + eyedropper: { + openEyedropper() + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + }, + dismiss: { + dismissed() + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } + )), + backgroundColor: .blur(.dark), + animateOut: animateOut + ), + environment: { + environment + SheetComponentEnvironment( + isDisplaying: environment.value.isVisible, + isCentered: environment.metrics.widthClass == .regular, + hasInputHeight: !environment.inputHeight.isZero, + dismiss: { animated in + if animated { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } else { + if let controller = controller() { + controller.dismiss(completion: nil) + } + } + } + ) + }, + availableSize: context.availableSize, + transition: context.transition + ) + + context.add(sheet + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + ) + + return context.availableSize + } + } +} + +class ColorPickerScreen: ViewControllerComponentContainer { + init(context: AccountContext, initialColor: DrawingColor, updated: @escaping (DrawingColor) -> Void, openEyedropper: @escaping () -> Void, dismissed: @escaping () -> Void = {}) { + super.init(context: context, component: ColorPickerSheetComponent(context: context, initialColor: initialColor, updated: updated, openEyedropper: openEyedropper, dismissed: dismissed), navigationBarAppearance: .none) + + self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) + + self.navigationPresentation = .flatModal + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/submodules/DrawingUI/Sources/ConcaveHull.swift b/submodules/DrawingUI/Sources/ConcaveHull.swift new file mode 100644 index 0000000000..3fb50fd27f --- /dev/null +++ b/submodules/DrawingUI/Sources/ConcaveHull.swift @@ -0,0 +1,308 @@ +import Foundation + +private func intersect(seg1: [CGPoint], seg2: [CGPoint]) -> Bool { + func ccw(_ seg1: CGPoint, _ seg2: CGPoint, _ seg3: CGPoint) -> Bool { + let ccw = ((seg3.y - seg1.y) * (seg2.x - seg1.x)) - ((seg2.y - seg1.y) * (seg3.x - seg1.x)) + return ccw > 0 ? true : ccw < 0 ? false : true + } + let segment1 = seg1[0] + let segment2 = seg1[1] + let segment3 = seg2[0] + let segment4 = seg2[1] + return ccw(segment1, segment3, segment4) != ccw(segment2, segment3, segment4) + && ccw(segment1, segment2, segment3) != ccw(segment1, segment2, segment4) +} + +private func convex(points: [CGPoint]) -> [CGPoint] { + func cross(_ o: CGPoint, _ a: CGPoint, _ b: CGPoint) -> Double { + return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x) + } + + func upperTangent(_ points: [CGPoint]) -> [CGPoint] { + var lower: [CGPoint] = [] + for point in points { + while lower.count >= 2 && (cross(lower[lower.count - 2], lower[lower.count - 1], point) <= 0) { + _ = lower.popLast() + } + lower.append(point) + } + _ = lower.popLast() + return lower + } + + func lowerTangent(_ points: [CGPoint]) -> [CGPoint] { + let reversed = points.reversed() + var upper: [CGPoint] = [] + for point in reversed { + while upper.count >= 2 && (cross(upper[upper.count - 2], upper[upper.count - 1], point) <= 0) { + _ = upper.popLast() + } + upper.append(point) + } + _ = upper.popLast() + return upper + } + + var convex: [CGPoint] = [] + convex.append(contentsOf: upperTangent(points)) + convex.append(contentsOf: lowerTangent(points)) + return convex +} + +private class Grid { + var cells = [Int: [Int: [CGPoint]]]() + var cellSize: Double = 0 + + init(_ points: [CGPoint], _ cellSize: Double) { + self.cellSize = cellSize + for point in points { + let cellXY = point2CellXY(point) + let x = cellXY[0] + let y = cellXY[1] + if self.cells[x] == nil { + self.cells[x] = [Int: [CGPoint]]() + } + if self.cells[x]![y] == nil { + self.cells[x]![y] = [CGPoint]() + } + self.cells[x]![y]!.append(point) + } + } + + func point2CellXY(_ point: CGPoint) -> [Int] { + let x = Int(point.x / self.cellSize) + let y = Int(point.y / self.cellSize) + return [x, y] + } + + func extendBbox(_ bbox: [Double], _ scaleFactor: Double) -> [Double] { + return [ + bbox[0] - (scaleFactor * self.cellSize), + bbox[1] - (scaleFactor * self.cellSize), + bbox[2] + (scaleFactor * self.cellSize), + bbox[3] + (scaleFactor * self.cellSize) + ] + } + + func removePoint(_ point: CGPoint) { + let cellXY = point2CellXY(point) + let cell = self.cells[cellXY[0]]![cellXY[1]]! + var pointIdxInCell = 0 + for idx in 0 ..< cell.count { + if cell[idx].x == point.x && cell[idx].y == point.y { + pointIdxInCell = idx + break + } + } + self.cells[cellXY[0]]![cellXY[1]]!.remove(at: pointIdxInCell) + } + + func rangePoints(_ bbox: [Double]) -> [CGPoint] { + let tlCellXY = point2CellXY(CGPoint(x: bbox[0], y: bbox[1])) + let brCellXY = point2CellXY(CGPoint(x: bbox[2], y: bbox[3])) + var points: [CGPoint] = [] + for x in tlCellXY[0].. [CGPoint] { + if let x = self.cells[xAbs] { + if let y = x[yOrd] { + return y + } + } + return [] + } + +} + +private let maxConcaveAngleCos = cos(90.0 / (180.0 / Double.pi)) + +private func filterDuplicates(_ pointSet: [CGPoint]) -> [CGPoint] { + let sortedSet = sortByX(pointSet) + return sortedSet.filter { (point: CGPoint) -> Bool in + let index = pointSet.firstIndex(where: {(idx: CGPoint) -> Bool in + return idx.x == point.x && idx.y == point.y + }) + if index == 0 { + return true + } else { + let prevEl = pointSet[index! - 1] + if prevEl.x != point.x || prevEl.y != point.y { + return true + } + return false + } + } +} + +private func sortByX(_ pointSet: [CGPoint]) -> [CGPoint] { + return pointSet.sorted(by: { (lhs, rhs) -> Bool in + if lhs.x == rhs.x { + return lhs.y < rhs.y + } else { + return lhs.x < rhs.x + } + }) +} + +private func squaredLength(_ a: CGPoint, _ b: CGPoint) -> Double { + return pow(b.x - a.x, 2) + pow(b.y - a.y, 2) +} + +private func cosFunc(_ o: CGPoint, _ a: CGPoint, _ b: CGPoint) -> Double { + let aShifted = [a.x - o.x, a.y - o.y] + let bShifted = [b.x - o.x, b.y - o.y] + let sqALen = squaredLength(o, a) + let sqBLen = squaredLength(o, b) + let dot = aShifted[0] * bShifted[0] + aShifted[1] * bShifted[1] + return dot / sqrt(sqALen * sqBLen) +} + +private func intersectFunc(_ segment: [CGPoint], _ pointSet: [CGPoint]) -> Bool { + for idx in 0.. CGPoint { + var minX = Double.infinity + var minY = Double.infinity + var maxX = -Double.infinity + var maxY = -Double.infinity + for idx in 0 ..< points.reversed().count { + if points[idx].x < minX { + minX = points[idx].x + } + if points[idx].y < minY { + minY = points[idx].y + } + if points[idx].x > maxX { + maxX = points[idx].x + } + if points[idx].y > maxY { + maxY = points[idx].y + } + } + return CGPoint(x: maxX - minX, y: maxY - minY) +} + +private func bBoxAroundFunc(_ edge: [CGPoint]) -> [Double] { + return [min(edge[0].x, edge[1].x), + min(edge[0].y, edge[1].y), + max(edge[0].x, edge[1].x), + max(edge[0].y, edge[1].y)] +} + +private func midPointFunc(_ edge: [CGPoint], _ innerPoints: [CGPoint], _ convex: [CGPoint]) -> CGPoint? { + var point: CGPoint? + var angle1Cos = maxConcaveAngleCos + var angle2Cos = maxConcaveAngleCos + var a1Cos: Double = 0 + var a2Cos: Double = 0 + for innerPoint in innerPoints { + a1Cos = cosFunc(edge[0], edge[1], innerPoint) + a2Cos = cosFunc(edge[1], edge[0], innerPoint) + if a1Cos > angle1Cos && + a2Cos > angle2Cos && + !intersectFunc([edge[0], innerPoint], convex) && + !intersectFunc([edge[1], innerPoint], convex) { + angle1Cos = a1Cos + angle2Cos = a2Cos + point = innerPoint + } + } + return point +} + +private func concaveFunc(_ convex: inout [CGPoint], _ maxSqEdgeLen: Double, _ maxSearchArea: [Double], _ grid: Grid, _ edgeSkipList: inout [String: Bool]) -> [CGPoint] { + var edge: [CGPoint] + var keyInSkipList: String = "" + var scaleFactor: Double + var midPoint: CGPoint? + var bBoxAround: [Double] + var bBoxWidth: Double = 0 + var bBoxHeight: Double = 0 + var midPointInserted: Bool = false + + for idx in 0.. bBoxWidth || maxSearchArea[1] > bBoxHeight) + + if bBoxWidth >= maxSearchArea[0] && bBoxHeight >= maxSearchArea[1] { + edgeSkipList[keyInSkipList] = true + } + if let midPoint = midPoint { + convex.insert(midPoint, at: idx + 1) + grid.removePoint(midPoint) + midPointInserted = true + } + } + + if midPointInserted { + return concaveFunc(&convex, maxSqEdgeLen, maxSearchArea, grid, &edgeSkipList) + } + + return convex +} + +private extension CGPoint { + var key: String { + return "\(self.x),\(self.y)" + } +} + +func getHull(_ points: [CGPoint], concavity: Double) -> [CGPoint] { + let points = filterDuplicates(points) + let occupiedArea = occupiedAreaFunc(points) + let maxSearchArea: [Double] = [ + occupiedArea.x * 0.6, + occupiedArea.y * 0.6 + ] + + var convex = convex(points: points) + + var innerPoints = points.filter { (point: CGPoint) -> Bool in + let idx = convex.firstIndex(where: { (idx: CGPoint) -> Bool in + return idx.x == point.x && idx.y == point.y + }) + return idx == nil + } + + innerPoints.sort(by: { (lhs: CGPoint, rhs: CGPoint) -> Bool in + return lhs.x == rhs.x ? lhs.y > rhs.y : lhs.x > rhs.x + }) + + let cellSize = ceil(occupiedArea.x * occupiedArea.y / Double(points.count)) + let grid = Grid(innerPoints, cellSize) + + var skipList: [String: Bool] = [String: Bool]() + return concaveFunc(&convex, pow(concavity, 2), maxSearchArea, grid, &skipList) +} diff --git a/submodules/DrawingUI/Sources/DrawingBubbleEntity.swift b/submodules/DrawingUI/Sources/DrawingBubbleEntity.swift new file mode 100644 index 0000000000..c453ee95fa --- /dev/null +++ b/submodules/DrawingUI/Sources/DrawingBubbleEntity.swift @@ -0,0 +1,389 @@ +import Foundation +import UIKit +import Display +import AccountContext + +final class DrawingBubbleEntity: DrawingEntity { + public enum DrawType { + case fill + case stroke + } + + let uuid: UUID + let isAnimated: Bool + + var drawType: DrawType + var color: DrawingColor + var lineWidth: CGFloat + + var referenceDrawingSize: CGSize + var position: CGPoint + var size: CGSize + var rotation: CGFloat + var tailPosition: CGPoint + + init(drawType: DrawType, color: DrawingColor, lineWidth: CGFloat) { + self.uuid = UUID() + self.isAnimated = false + + self.drawType = drawType + self.color = color + self.lineWidth = lineWidth + + self.referenceDrawingSize = .zero + self.position = .zero + self.size = CGSize(width: 1.0, height: 1.0) + self.rotation = 0.0 + self.tailPosition = CGPoint(x: 0.16, y: 0.18) + } + + var center: CGPoint { + return self.position + } + + func duplicate() -> DrawingEntity { + let newEntity = DrawingBubbleEntity(drawType: self.drawType, color: self.color, lineWidth: self.lineWidth) + newEntity.referenceDrawingSize = self.referenceDrawingSize + newEntity.position = self.position + newEntity.size = self.size + newEntity.rotation = self.rotation + return newEntity + } + + weak var currentEntityView: DrawingEntityView? + func makeView(context: AccountContext) -> DrawingEntityView { + let entityView = DrawingBubbleEntityView(context: context, entity: self) + self.currentEntityView = entityView + return entityView + } +} + +final class DrawingBubbleEntityView: DrawingEntityView { + private var bubbleEntity: DrawingBubbleEntity { + return self.entity as! DrawingBubbleEntity + } + + private var currentSize: CGSize? + private var currentTailPosition: CGPoint? + + private let shapeLayer = SimpleShapeLayer() + + init(context: AccountContext, entity: DrawingBubbleEntity) { + super.init(context: context, entity: entity) + + self.layer.addSublayer(self.shapeLayer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func update(animated: Bool) { + let size = self.bubbleEntity.size + + self.center = self.bubbleEntity.position + self.bounds = CGRect(origin: .zero, size: size) + self.transform = CGAffineTransformMakeRotation(self.bubbleEntity.rotation) + + if size != self.currentSize || self.bubbleEntity.tailPosition != self.currentTailPosition { + self.currentSize = size + self.currentTailPosition = self.bubbleEntity.tailPosition + self.shapeLayer.frame = self.bounds + + let cornerRadius = max(10.0, min(self.bubbleEntity.referenceDrawingSize.width, self.bubbleEntity.referenceDrawingSize.height) * 0.066) + let smallCornerRadius = max(5.0, min(self.bubbleEntity.referenceDrawingSize.width, self.bubbleEntity.referenceDrawingSize.height) * 0.016) + let tailWidth = max(5.0, min(self.bubbleEntity.referenceDrawingSize.width, self.bubbleEntity.referenceDrawingSize.height) * 0.1) + + self.shapeLayer.path = CGPath.bubble(in: CGRect(origin: .zero, size: size), cornerRadius: cornerRadius, smallCornerRadius: smallCornerRadius, tailPosition: self.bubbleEntity.tailPosition, tailWidth: tailWidth) + } + + switch self.bubbleEntity.drawType { + case .fill: + self.shapeLayer.fillColor = self.bubbleEntity.color.toCGColor() + self.shapeLayer.strokeColor = UIColor.clear.cgColor + case .stroke: + let minLineWidth = max(10.0, min(self.bubbleEntity.referenceDrawingSize.width, self.bubbleEntity.referenceDrawingSize.height) * 0.02) + let maxLineWidth = max(10.0, min(self.bubbleEntity.referenceDrawingSize.width, self.bubbleEntity.referenceDrawingSize.height) * 0.1) + let lineWidth = minLineWidth + (maxLineWidth - minLineWidth) * self.bubbleEntity.lineWidth + + self.shapeLayer.fillColor = UIColor.clear.cgColor + self.shapeLayer.strokeColor = self.bubbleEntity.color.toCGColor() + self.shapeLayer.lineWidth = lineWidth + } + + super.update(animated: animated) + } + + fileprivate var visualLineWidth: CGFloat { + return self.shapeLayer.lineWidth + } + + override func precisePoint(inside point: CGPoint) -> Bool { + if case .stroke = self.bubbleEntity.drawType, var path = self.shapeLayer.path { + path = path.copy(strokingWithWidth: 20.0, lineCap: .square, lineJoin: .bevel, miterLimit: 0.0) + if path.contains(point) { + return true + } else { + return false + } + } else { + return super.precisePoint(inside: point) + } + } + + override func updateSelectionView() { + super.updateSelectionView() + + guard let selectionView = self.selectionView as? DrawingBubbleEntititySelectionView else { + return + } + +// let scale = self.superview?.superview?.layer.value(forKeyPath: "transform.scale.x") as? CGFloat ?? 1.0 +// selectionView.scale = scale + + selectionView.transform = CGAffineTransformMakeRotation(self.bubbleEntity.rotation) + selectionView.setNeedsLayout() + } + + override func makeSelectionView() -> DrawingEntitySelectionView { + if let selectionView = self.selectionView { + return selectionView + } + let selectionView = DrawingBubbleEntititySelectionView() + selectionView.entityView = self + return selectionView + } +} + +final class DrawingBubbleEntititySelectionView: DrawingEntitySelectionView, UIGestureRecognizerDelegate { + private let leftHandle = SimpleShapeLayer() + private let topLeftHandle = SimpleShapeLayer() + private let topHandle = SimpleShapeLayer() + private let topRightHandle = SimpleShapeLayer() + private let rightHandle = SimpleShapeLayer() + private let bottomLeftHandle = SimpleShapeLayer() + private let bottomHandle = SimpleShapeLayer() + private let bottomRightHandle = SimpleShapeLayer() + private let tailHandle = SimpleShapeLayer() + + private var panGestureRecognizer: UIPanGestureRecognizer! + + override init(frame: CGRect) { + let handleBounds = CGRect(origin: .zero, size: entitySelectionViewHandleSize) + let handles = [ + self.leftHandle, + self.topLeftHandle, + self.topHandle, + self.topRightHandle, + self.rightHandle, + self.bottomLeftHandle, + self.bottomHandle, + self.bottomRightHandle, + self.tailHandle + ] + + super.init(frame: frame) + + self.backgroundColor = .clear + self.isOpaque = false + + for handle in handles { + handle.bounds = handleBounds + if handle === self.tailHandle { + handle.fillColor = UIColor(rgb: 0x00ff00).cgColor + } else { + 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 + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + var scale: CGFloat = 1.0 { + didSet { + self.setNeedsLayout() + } + } + + override var selectionInset: CGFloat { + return 5.5 + } + + override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + + private var currentHandle: CALayer? + @objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { + guard let entityView = self.entityView, let entity = entityView.entity as? DrawingBubbleEntity else { + return + } + let location = gestureRecognizer.location(in: self) + + switch gestureRecognizer.state { + case .began: + 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) + + var updatedSize = entity.size + var updatedPosition = entity.position + var updatedTailPosition = entity.tailPosition + + if self.currentHandle === self.leftHandle { + updatedSize.width -= delta.x + updatedPosition.x -= delta.x * -0.5 + } else if self.currentHandle === self.rightHandle { + updatedSize.width += delta.x + updatedPosition.x += delta.x * 0.5 + } else if self.currentHandle === self.topHandle { + updatedSize.height -= delta.y + updatedPosition.y += delta.y * 0.5 + } else if self.currentHandle === self.bottomHandle { + updatedSize.height += delta.y + updatedPosition.y += delta.y * 0.5 + } else if self.currentHandle === self.topLeftHandle { + updatedSize.width -= delta.x + updatedPosition.x -= delta.x * -0.5 + updatedSize.height -= delta.y + updatedPosition.y += delta.y * 0.5 + } else if self.currentHandle === self.topRightHandle { + updatedSize.width += delta.x + updatedPosition.x += delta.x * 0.5 + updatedSize.height -= delta.y + updatedPosition.y += delta.y * 0.5 + } else if self.currentHandle === self.bottomLeftHandle { + updatedSize.width -= delta.x + updatedPosition.x -= delta.x * -0.5 + updatedSize.height += delta.y + updatedPosition.y += delta.y * 0.5 + } else if self.currentHandle === self.bottomRightHandle { + updatedSize.width += delta.x + updatedPosition.x += delta.x * 0.5 + updatedSize.height += delta.y + updatedPosition.y += delta.y * 0.5 + } else if self.currentHandle === self.tailHandle { + updatedTailPosition = CGPoint(x: max(0.0, min(1.0, updatedTailPosition.x + delta.x / updatedSize.width)), y: max(0.0, min(updatedSize.height, updatedTailPosition.y + delta.y))) + } else if self.currentHandle === self.layer { + updatedPosition.x += delta.x + updatedPosition.y += delta.y + } + + entity.size = updatedSize + entity.position = updatedPosition + entity.tailPosition = updatedTailPosition + entityView.update() + + gestureRecognizer.setTranslation(.zero, in: entityView) + case .ended: + break + default: + break + } + } + + override func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) { + guard let entityView = self.entityView, let entity = entityView.entity as? DrawingBubbleEntity else { + return + } + + switch gestureRecognizer.state { + case .began, .changed: + let scale = gestureRecognizer.scale + entity.size = CGSize(width: entity.size.width * scale, height: entity.size.height * scale) + entityView.update() + + gestureRecognizer.scale = 1.0 + default: + break + } + } + + override func handleRotate(_ gestureRecognizer: UIRotationGestureRecognizer) { + guard let entityView = self.entityView, let entity = entityView.entity as? DrawingBubbleEntity 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) || self.tailHandle.frame.contains(point) + } + + override func layoutSubviews() { + guard let entityView = self.entityView, let entity = entityView.entity as? DrawingBubbleEntity else { + return + } + + let inset = self.selectionInset + + 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.topLeftHandle, + self.topHandle, + self.topRightHandle, + self.rightHandle, + self.bottomLeftHandle, + self.bottomHandle, + self.bottomRightHandle, + self.tailHandle + ] + + for handle in handles { + handle.path = handlePath + handle.bounds = bounds + handle.lineWidth = lineWidth + } + + self.topLeftHandle.position = CGPoint(x: inset, y: inset) + self.topHandle.position = CGPoint(x: self.bounds.midX, y: inset) + self.topRightHandle.position = CGPoint(x: self.bounds.maxX - inset, y: inset) + self.leftHandle.position = CGPoint(x: inset, y: self.bounds.midY) + self.rightHandle.position = CGPoint(x: self.bounds.maxX - inset, y: self.bounds.midY) + self.bottomLeftHandle.position = CGPoint(x: inset, y: self.bounds.maxY - inset) + self.bottomHandle.position = CGPoint(x: self.bounds.midX, y: self.bounds.maxY - inset) + self.bottomRightHandle.position = CGPoint(x: self.bounds.maxX - inset, y: self.bounds.maxY - inset) + + let selectionScale = (self.bounds.width - inset * 2.0) / (max(0.001, entity.size.width)) + self.tailHandle.position = CGPoint(x: inset + (self.bounds.width - inset * 2.0) * entity.tailPosition.x, y: self.bounds.height - inset + entity.tailPosition.y * selectionScale) + } + + var isTracking: Bool { + return gestureIsTracking(self.panGestureRecognizer) + } +} diff --git a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift new file mode 100644 index 0000000000..bfa55a8952 --- /dev/null +++ b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift @@ -0,0 +1,477 @@ +import Foundation +import UIKit +import LegacyComponents +import AccountContext + +protocol DrawingEntity: AnyObject { + var uuid: UUID { get } + var isAnimated: Bool { get } + var center: CGPoint { get } + + var lineWidth: CGFloat { get set } + var color: DrawingColor { get set } + + func duplicate() -> DrawingEntity + + var currentEntityView: DrawingEntityView? { get } + func makeView(context: AccountContext) -> DrawingEntityView +} + +public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { + private let context: AccountContext + private let size: CGSize + + weak var selectionContainerView: DrawingSelectionContainerView? + + private var tapGestureRecognizer: UITapGestureRecognizer! + private(set) var selectedEntityView: DrawingEntityView? + + public var hasSelectionChanged: (Bool) -> Void = { _ in } + var selectionChanged: (DrawingEntity?) -> Void = { _ in } + var requestedMenuForEntityView: (DrawingEntityView, Bool) -> Void = { _, _ in } + + init(context: AccountContext, size: CGSize, entities: [DrawingEntity] = []) { + self.context = context + self.size = size + + super.init(frame: CGRect(origin: .zero, size: size)) + + for entity in entities { + self.add(entity) + } + + let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:))) + self.addGestureRecognizer(tapGestureRecognizer) + self.tapGestureRecognizer = tapGestureRecognizer + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + var entities: [DrawingEntity] { + var entities: [DrawingEntity] = [] + for case let view as DrawingEntityView in self.subviews { + entities.append(view.entity) + } + return entities + } + + private func startPosition(relativeTo entity: DrawingEntity?) -> CGPoint { + let offsetLength = round(self.size.width * 0.1) + let offset = CGPoint(x: 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 = CGPoint(x: self.size.width / 2.0, y: self.size.height / 2.0) // place good here + + while true { + var occupied = false + for case let view as DrawingEntityView in self.subviews { + 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 width = round(self.size.width * 0.5) + + return CGSize(width: width, height: width) + } + + func prepareNewEntity(_ entity: DrawingEntity, setup: Bool = true, relativeTo: DrawingEntity? = nil) { + let center = self.startPosition(relativeTo: relativeTo) + if let shape = entity as? DrawingSimpleShapeEntity { + shape.position = center + + if setup { + 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.referenceDrawingSize = self.size + sticker.scale = 1.0 + } + } else if let bubble = entity as? DrawingBubbleEntity { + bubble.position = center + if setup { + 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.referenceDrawingSize = self.size + text.width = floor(self.size.width * 0.9) + text.fontSize = 0.4 + } + } + } + + @discardableResult + func add(_ entity: DrawingEntity) -> DrawingEntityView { + let view = entity.makeView(context: self.context) + view.containerView = self + view.update() + self.addSubview(view) + return view + } + + func duplicate(_ entity: DrawingEntity) -> DrawingEntity { + let newEntity = entity.duplicate() + self.prepareNewEntity(newEntity, setup: false, relativeTo: entity) + + let view = newEntity.makeView(context: self.context) + view.containerView = self + view.update() + self.addSubview(view) + return newEntity + } + + func remove(uuid: UUID) { + if let view = self.getView(for: uuid) { + if self.selectedEntityView === view { + self.selectedEntityView?.removeFromSuperview() + self.selectedEntityView = nil + self.selectionChanged(nil) + self.hasSelectionChanged(false) + } + view.removeFromSuperview() + } + } + + func removeAll() { + for case let view as DrawingEntityView in self.subviews { + view.removeFromSuperview() + } + self.selectionChanged(nil) + self.hasSelectionChanged(false) + } + + func bringToFront(uuid: UUID) { + if let view = self.getView(for: uuid) { + self.bringSubviewToFront(view) + } + } + + 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 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) { + let location = gestureRecognzier.location(in: self) + + var intersectedViews: [DrawingEntityView] = [] + for case let view as DrawingEntityView in self.subviews { + if view.precisePoint(inside: self.convert(location, to: view)) { + intersectedViews.append(view) + } + } + + if let entityView = intersectedViews.last { + self.selectEntity(entityView.entity) + } + } + + func selectEntity(_ entity: DrawingEntity?) { + 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 + } + } + + self.selectedEntityView = nil + if let selectionView = selectedEntityView.selectionView { + selectedEntityView.selectionView = nil + selectionView.removeFromSuperview() + } + } + } + + if let entity = entity, let entityView = self.getView(for: entity.uuid) { + self.selectedEntityView = entityView + + let selectionView = entityView.makeSelectionView() + selectionView.tapped = { [weak self, weak entityView] in + if let strongSelf = self, let entityView = entityView { + strongSelf.requestedMenuForEntityView(entityView, strongSelf.subviews.last === entityView) + } + } + entityView.selectionView = selectionView + self.selectionContainerView?.addSubview(selectionView) + entityView.update() + } + + 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 func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer!) { + if let selectedEntityView = self.selectedEntityView, let selectionView = selectedEntityView.selectionView { + selectionView.handlePinch(gestureRecognizer) + } + } + + public func handleRotate(_ gestureRecognizer: UIRotationGestureRecognizer!) { + if let selectedEntityView = self.selectedEntityView, let selectionView = selectedEntityView.selectionView { + selectionView.handleRotate(gestureRecognizer) + } + } +} + +public class DrawingEntityView: UIView { + let context: AccountContext + let entity: DrawingEntity + var isTracking = false + + weak var selectionView: DrawingEntitySelectionView? + weak var containerView: DrawingEntitiesView? + + 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 play() { + + } + + func pause() { + + } + + func seek(to timestamp: Double) { + + } + + func resetToStart() { + + } + + func updateVisibility(_ visibility: Bool) { + + } + + 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 { + weak var entityView: DrawingEntityView? + + var tapped: () -> Void = { } + + override init(frame: CGRect) { + super.init(frame: frame) + self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))) + } + + 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) { + } + + 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 + } +} diff --git a/submodules/DrawingUI/Sources/DrawingGesture.swift b/submodules/DrawingUI/Sources/DrawingGesture.swift new file mode 100644 index 0000000000..6bdbfb7f0f --- /dev/null +++ b/submodules/DrawingUI/Sources/DrawingGesture.swift @@ -0,0 +1,1270 @@ +import Foundation +import UIKit + +public typealias TouchEventIdentifier = String +public typealias PointIdentifier = String +public typealias EstimationUpdateIndex = NSNumber + +class Touch: Equatable, Hashable { + public let touchIdentifier: UITouchIdentifier + lazy public var pointIdentifier: PointIdentifier = { + if let estimationUpdateIndex = estimationUpdateIndex { + return touchIdentifier + ":\(estimationUpdateIndex)" + } else { + return touchIdentifier + ":" + identifier + } + }() + public let identifier: String + public let timestamp: TimeInterval + public let type: UITouch.TouchType + public let phase: UITouch.Phase + public let force: CGFloat + public let maximumPossibleForce: CGFloat + public let altitudeAngle: CGFloat + public let azimuthUnitVector: CGVector + public let azimuth: CGFloat + public let location: CGPoint + public let estimationUpdateIndex: EstimationUpdateIndex? + public let estimatedProperties: UITouch.Properties + public let estimatedPropertiesExpectingUpdates: UITouch.Properties + public let isUpdate: Bool + public let isPrediction: Bool + + public let view: UIView? + + public var expectsLocationUpdate: Bool { + return estimatedPropertiesExpectingUpdates.contains(UITouch.Properties.location) + } + + public var expectsForceUpdate: Bool { + return estimatedPropertiesExpectingUpdates.contains(UITouch.Properties.force) + } + + public var expectsAzimuthUpdate: Bool { + return estimatedPropertiesExpectingUpdates.contains(UITouch.Properties.azimuth) + } + + public var expectsUpdate: Bool { + return expectsForceUpdate || expectsAzimuthUpdate || expectsLocationUpdate + } + + public convenience init( + coalescedTouch: UITouch, + touch: UITouch, + in view: UIView, + isUpdate: Bool, + isPrediction: Bool, + phase: UITouch.Phase? = nil, + transform: CGAffineTransform = .identity + ) { + let originalLocation = coalescedTouch.location(in: view) + let location = !transform.isIdentity ? originalLocation.applying(transform) : originalLocation + + self.init( + identifier: UUID.init().uuidString, + touchIdentifier: touch.identifer, + timestamp: coalescedTouch.timestamp, + type: coalescedTouch.type, + phase: phase ?? coalescedTouch.phase, + force: coalescedTouch.force, + maximumPossibleForce: coalescedTouch.maximumPossibleForce, + altitudeAngle: coalescedTouch.altitudeAngle, + azimuthUnitVector: coalescedTouch.azimuthUnitVector(in: view), + azimuth: coalescedTouch.azimuthAngle(in: view), + location: location, + estimationUpdateIndex: coalescedTouch.estimationUpdateIndex, + estimatedProperties: coalescedTouch.estimatedProperties, + estimatedPropertiesExpectingUpdates: coalescedTouch.estimatedPropertiesExpectingUpdates, + isUpdate: isUpdate, + isPrediction: isPrediction, + in: view + ) + } + + public init( + identifier: TouchEventIdentifier, + touchIdentifier: UITouchIdentifier, + timestamp: TimeInterval, + type: UITouch.TouchType, + phase: UITouch.Phase, + force: CGFloat, + maximumPossibleForce: CGFloat, + altitudeAngle: CGFloat, + azimuthUnitVector: CGVector, + azimuth: CGFloat, + location: CGPoint, + estimationUpdateIndex: EstimationUpdateIndex?, + estimatedProperties: UITouch.Properties, + estimatedPropertiesExpectingUpdates: UITouch.Properties, + isUpdate: Bool, + isPrediction: Bool, + in view: UIView? + ) { + self.identifier = identifier + self.touchIdentifier = touchIdentifier + self.timestamp = timestamp + self.type = type + self.phase = phase + self.force = force + self.maximumPossibleForce = maximumPossibleForce + self.altitudeAngle = altitudeAngle + self.azimuthUnitVector = azimuthUnitVector + self.azimuth = azimuth + self.location = location + self.estimationUpdateIndex = estimationUpdateIndex + self.estimatedProperties = estimatedProperties + self.estimatedPropertiesExpectingUpdates = estimatedPropertiesExpectingUpdates + self.isUpdate = isUpdate + self.isPrediction = isPrediction + self.view = view + } + + public static func == (lhs: Touch, rhs: Touch) -> Bool { + return lhs.identifier == rhs.identifier + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(identifier) + } +} + +class DrawingGestureRecognizer: UIGestureRecognizer, UIGestureRecognizerDelegate { + var shouldBegin: (CGPoint) -> Bool = { _ in return true } + var onTouches: ([Touch]) -> Void = { _ in } + + var transform: CGAffineTransform = .identity + + var usePredictedTouches = false + + private var currentTouches = Set() + + override init(target: Any?, action: Selector?) { + super.init(target: target, action: action) + + self.delegate = self + self.cancelsTouchesInView = false + self.delaysTouchesBegan = false + self.delaysTouchesEnded = false + self.allowedTouchTypes = [ + NSNumber(value: UITouch.TouchType.direct.rawValue), + NSNumber(value: UITouch.TouchType.stylus.rawValue) + ] + } + + func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + let location = gestureRecognizer.location(in: self.view) + if self.shouldBegin(location) { + return true + } else { + return false + } + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + if otherGestureRecognizer is UIPinchGestureRecognizer { + return true + } + return true + } + + override func touchesEstimatedPropertiesUpdated(_ touches: Set) { + self.process(touches: touches, with: nil, isUpdate: true) + } + + override func touchesBegan(_ touches: Set, with event: UIEvent) { + if let location = touches.first?.location(in: self.view), touches.count == 1 && self.shouldBegin(location) { + super.touchesBegan(touches, with: event) + + self.process(touches: touches, with: event) + self.state = .began + } else { + self.state = .cancelled + } + } + + override func touchesMoved(_ touches: Set, with event: UIEvent) { + if touches.count > 1 { + self.state = .cancelled + } else { + super.touchesMoved(touches, with: event) + self.process(touches: touches, with: event) + + self.state = .changed + } + } + + override func touchesEnded(_ touches: Set, with event: UIEvent) { + super.touchesEnded(touches, with: event) + + self.process(touches: touches, with: event) + + self.state = .ended + } + + override func touchesCancelled(_ touches: Set, with event: UIEvent) { + super.touchesCancelled(touches, with: event) + + self.state = .cancelled + } + + func process(touches: Set, with event: UIEvent?, isUpdate: Bool = false) { + guard let view = self.view else { + return + } + + var allTouches: [Touch] = [] + + for touch in touches { + var coalesced = event?.coalescedTouches(for: touch) ?? [touch] + if "".isEmpty || coalesced.isEmpty { + coalesced = [touch] + } + + for coalescedTouch in coalesced { + allTouches.append( + Touch( + coalescedTouch: coalescedTouch, + touch: touch, + in: view, + isUpdate: isUpdate, + isPrediction: false, + transform: self.transform + ) + ) + } + + if self.usePredictedTouches { + let predicted = event?.predictedTouches(for: touch) ?? [] + for predictedTouch in predicted { + allTouches.append( + Touch( + coalescedTouch: predictedTouch, + touch: touch, + in: view, + isUpdate: isUpdate, + isPrediction: true, + transform: self.transform + ) + ) + } + } + } + + self.onTouches(allTouches) + } +} + +class DrawingGesturePipeline { + enum Mode { + case location + case smoothCurve + case polyline + } + + enum DrawingGestureState { + case began + case changed + case ended + case cancelled + } + + enum DrawingResult { + case location(Polyline.Point) + case smoothCurve(BezierPath) + case polyline(Polyline) + } + + private var pendingTouches: [Touch] = [] + var onDrawing: (DrawingGestureState, DrawingResult) -> Void = { _, _ in } + + var gestureRecognizer: DrawingGestureRecognizer? + var transform: CGAffineTransform = .identity { + didSet { + self.gestureRecognizer?.transform = transform + } + } + + var mode: Mode = .polyline { + didSet { + if [.location, .polyline].contains(self.mode) { + self.gestureRecognizer?.usePredictedTouches = false + } else { + self.gestureRecognizer?.usePredictedTouches = true + } + } + } + + init(view: DrawingView) { + let gestureRecognizer = DrawingGestureRecognizer(target: self, action: #selector(self.handleGesture(_:))) + gestureRecognizer.onTouches = { [weak self] touches in + self?.pendingTouches.append(contentsOf: touches) + } + self.gestureRecognizer = gestureRecognizer + view.addGestureRecognizer(gestureRecognizer) + } + + @objc private func handleGesture(_ gestureRecognizer: DrawingGestureRecognizer) { + let state: DrawingGestureState + switch gestureRecognizer.state { + case .began: + state = .began + case .changed: + state = .changed + case .ended: + state = .ended + case .cancelled: + state = .cancelled + case .failed: + state = .cancelled + case .possible: + state = .cancelled + @unknown default: + state = .cancelled + } + + let touchDeltas = self.processTouchEvents(self.pendingTouches) + let polylineDeltas = self.processTouchPaths(inputDeltas: touchDeltas) + let simplifiedPolylineDeltas = self.simplifyPolylines(inputDeltas: polylineDeltas) + + switch self.mode { + case .location: + if let touchPath = self.touchPaths.last, let point = touchPath.points.last { + self.onDrawing(state, .location(Polyline.Point(touchPoint: point))) + } + case .smoothCurve: + if let path = self.processPolylines(inputDeltas: simplifiedPolylineDeltas) { + self.onDrawing(state, .smoothCurve(path)) + } + case .polyline: + if let polyline = self.simplifiedPolylines.last { + self.onDrawing(state, .polyline(polyline)) + } + } + + self.pendingTouches.removeAll() + } + + enum TouchPathDelta: Equatable { + case addedTouchPath(index: Int) + case updatedTouchPath(index: Int, updatedIndexes: MinMaxIndex) + case completedTouchPath(index: Int) + } + + private var touchPaths: [TouchPath] = [] + private var touchToIndex: [UITouchIdentifier: Int] = [:] + private func processTouchEvents(_ touches: [Touch]) -> [TouchPathDelta] { + var deltas: [TouchPathDelta] = [] + var processedTouchIdentifiers: [UITouchIdentifier] = [] + let updatedEventsPerTouch = touches.reduce(into: [String: [Touch]](), { (result, event) in + if result[event.touchIdentifier] != nil { + result[event.touchIdentifier]?.append(event) + } else { + result[event.touchIdentifier] = [event] + } + }) + + for touchToProcess in touches { + let touchIdentifier = touchToProcess.touchIdentifier + guard !processedTouchIdentifiers.contains(touchIdentifier), let events = updatedEventsPerTouch[touchIdentifier] else { + continue + } + + processedTouchIdentifiers.append(touchIdentifier) + if let index = self.touchToIndex[touchIdentifier] { + let path = self.touchPaths[index] + let updatedIndexes = path.add(touchEvents: events) + deltas.append(.updatedTouchPath(index: index, updatedIndexes: updatedIndexes)) + + if path.isComplete { + deltas.append(.completedTouchPath(index: index)) + } + } else if let touchIdentifier = events.first?.touchIdentifier, let path = TouchPath(touchEvents: events) { + let index = self.touchPaths.count + self.touchToIndex[touchIdentifier] = index + self.touchPaths.append(path) + deltas.append(.addedTouchPath(index: index)) + + if path.isComplete { + deltas.append(.completedTouchPath(index: index)) + } + } + } + return deltas + } + + enum PolylineDelta: Equatable { + case addedPolyline(index: Int) + case updatedPolyline(index: Int, updatedIndexes: MinMaxIndex) + case completedPolyline(index: Int) + } + + private var indexToIndex: [Int: Int] = [:] + private var polylines: [Polyline] = [] + func processTouchPaths(inputDeltas: [TouchPathDelta]) -> [PolylineDelta] { + var deltas: [PolylineDelta] = [] + for delta in inputDeltas { + switch delta { + case .addedTouchPath(let pathIndex): + let line = self.touchPaths[pathIndex] + let smoothStroke = Polyline(touchPath: line) + let index = polylines.count + indexToIndex[pathIndex] = index + polylines.append(smoothStroke) + deltas.append(.addedPolyline(index: index)) + case .updatedTouchPath(let pathIndex, let indexSet): + let line = self.touchPaths[pathIndex] + if let index = indexToIndex[pathIndex] { + let updates = polylines[index].update(with: line, indexSet: indexSet) + deltas.append(.updatedPolyline(index: index, updatedIndexes: updates)) + } + case .completedTouchPath(let pointCollectionIndex): + if let index = indexToIndex[pointCollectionIndex] { + deltas.append(.completedPolyline(index: index)) + } + } + } + + return deltas + } + + var simplifiedPolylines: [Polyline] = [] + func simplifyPolylines(inputDeltas: [PolylineDelta]) -> [PolylineDelta] { + var outDeltas: [PolylineDelta] = [] + + for delta in inputDeltas { + switch delta { + case .addedPolyline(let strokeIndex): + assert(strokeIndex == self.simplifiedPolylines.count) + let line = self.polylines[strokeIndex] + self.simplifiedPolylines.append(line) + let indexes = MinMaxIndex(0.. CGFloat { + guard abs(windowLoc) <= windowSize else { fatalError("Invalid coefficient") } + if let cached = cache { + return cached[abs(windowLoc)] + } + var coeffs: [CGFloat] = [] + for windowLoc in 0...windowSize { + coeffs.append(Self.calcWeight(index, windowLoc, windowSize, order, derivative)) + } + + cache = coeffs + + return coeffs[abs(windowLoc)] + } + + // MARK: - Coefficients + + /// calculates the generalised factorial (a)(a-1)...(a-b+1) + private static func genFact(_ a: Int, _ b: Int) -> CGFloat { + var gf: CGFloat = 1.0 + + for jj in (a - b + 1) ..< (a + 1) { + gf *= CGFloat(jj) + } + return gf + } + + /// Calculates the Gram Polynomial ( s = 0 ), or its s'th + /// derivative evaluated at i, order k, over 2m + 1 points + private static func gramPoly(_ index: Int, _ window: Int, _ order: Int, _ derivative: Int) -> CGFloat { + var gp_val: CGFloat + + if order > 0 { + let g1 = gramPoly(index, window, order - 1, derivative) + let g2 = gramPoly(index, window, order - 1, derivative - 1) + let g3 = gramPoly(index, window, order - 2, derivative) + let i: CGFloat = CGFloat(index) + let m: CGFloat = CGFloat(window) + let k: CGFloat = CGFloat(order) + let s: CGFloat = CGFloat(derivative) + gp_val = (4.0 * k - 2.0) / (k * (2.0 * m - k + 1.0)) * (i * g1 + s * g2) + - ((k - 1.0) * (2.0 * m + k)) / (k * (2.0 * m - k + 1.0)) * g3 + } else if order == 0 && derivative == 0 { + gp_val = 1.0 + } else { + gp_val = 0.0 + } + return gp_val + } + + /// calculates the weight of the i'th data point for the t'th Least-square + /// point of the s'th derivative, over 2m + 1 points, order n + private static func calcWeight(_ index: Int, _ windowLoc: Int, _ windowSize: Int, _ order: Int, _ derivative: Int) -> CGFloat { + var sum: CGFloat = 0.0 + + for k in 0 ..< order + 1 { + sum += CGFloat(2 * k + 1) * CGFloat(genFact(2 * windowSize, k) / genFact(2 * windowSize + k + 1, k + 1)) + * gramPoly(index, windowSize, k, 0) * gramPoly(windowLoc, windowSize, k, derivative) + } + + return sum + } + } + + + private var window: Int = 2 + private var strength: CGFloat = 1 + + var deriv: Int = 0 + var order: Int = 3 + var coeffs: [Coeffs] = [] + + private func smoothStroke(stroke: inout Polyline, at indexes: MinMaxIndex?, input: Polyline) -> MinMaxIndex { + if input.points.count > stroke.points.count { + stroke.points.append(contentsOf: input.points[stroke.points.count...]) + } else if input.points.count < stroke.points.count { + stroke.points.removeSubrange(input.points.count...) + } + let outIndexes = { () -> MinMaxIndex in + if let indexes = indexes, + let minIndex = indexes.first, + let maxIndex = indexes.last { + var outIndexes = MinMaxIndex() + let start = max(0, minIndex - window) + let end = min(stroke.points.count - 1, maxIndex + window) + outIndexes.insert(integersIn: start...end) + return outIndexes + } + return MinMaxIndex(stroke.points.indices) + }() + + for pIndex in outIndexes { + let minWin = min(min(window, pIndex), stroke.points.count - 1 - pIndex) + // copy over the point in question so that not only our location will be smoothed below, + // but also the azimuth/altitude/etc will be the same + stroke.points[pIndex] = input.points[pIndex] + while coeffs.count < minWin + 1 { + coeffs.append(Coeffs(index: 0, windowSize: coeffs.count)) + } + if minWin > 1 { + var outPoint = CGPoint.zero + for windowPos in -minWin ... minWin { + let wght = coeffs[minWin].weight(windowPos, order, deriv) + outPoint.x += wght * input.points[pIndex + windowPos].location.x + outPoint.y += wght * input.points[pIndex + windowPos].location.y + } + let origPoint = stroke.points[pIndex].location + + stroke.points[pIndex].location = origPoint * CGFloat(1 - strength) + outPoint * strength + } + } + + return outIndexes + } + + private var builders: [BezierBuilder] = [] + private var bezierIndexToIndex: [Int: Int] = [:] + func processPolylines(inputDeltas: [PolylineDelta]) -> BezierPath? { + for delta in inputDeltas { + switch delta { + case .addedPolyline(let lineIndex): + assert(bezierIndexToIndex[lineIndex] == nil, "Cannot add existing line") + let line = self.simplifiedPolylines[lineIndex] + let builder = BezierBuilder(smoother: Smoother()) + builder.update(with: line, at: MinMaxIndex(0 ..< line.points.count)) + let builderIndex = builders.count + bezierIndexToIndex[lineIndex] = builderIndex + builders.append(builder) + case .updatedPolyline(let lineIndex, let updatedIndexes): + let line = self.simplifiedPolylines[lineIndex] + guard let builderIndex = bezierIndexToIndex[lineIndex] else { + continue + } + let builder = builders[builderIndex] + let _ = builder.update(with: line, at: updatedIndexes) + case .completedPolyline: + break + } + } + + return builders.last?.path + } + +} + +private var TOUCH_IDENTIFIER: UInt8 = 0 +public typealias UITouchIdentifier = String + +extension UITouch { + var identifer: UITouchIdentifier { + if let identifier = objc_getAssociatedObject(self, &TOUCH_IDENTIFIER) as? String { + return identifier + } else { + let identifier = UUID().uuidString + objc_setAssociatedObject(self, &TOUCH_IDENTIFIER, identifier, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) + return identifier + } + } +} + +public struct MinMaxIndex: Sequence, Equatable { + private var start: Int + private var end: Int + + public static let null = MinMaxIndex() + + public init() { + start = .max + end = .max + } + + public init(_ indexes: ClosedRange) { + guard let first = indexes.first, let last = indexes.last else { self = .null; return } + start = first + end = last + } + + public init(_ indexes: Range) { + guard let first = indexes.first, let last = indexes.last else { self = .null; return } + start = first + end = last + } + + public init(_ integers: [Int]) { + guard !integers.isEmpty else { self = .null; return } + start = integers.min()! + end = integers.max()! + } + + public init(_ integer: Int) { + start = integer + end = integer + } + + public init(_ indexSet: IndexSet) { + guard !indexSet.isEmpty else { self = .null; return } + start = indexSet.min()! + end = indexSet.max()! + } + + public var count: Int { + guard self != .null else { return 0 } + return end - start + 1 + } + + public var first: Int? { + guard self != .null else { return nil } + return start + } + + public var last: Int? { + guard self != .null else { return nil } + return end + } + + @inlinable @inline(__always) + public func asIndexSet() -> IndexSet { + guard let first = first, let last = last else { return IndexSet() } + return IndexSet(integersIn: first...last) + } + + public mutating func insert(_ index: Int) { + if self == Self.null { + start = index + end = index + } else { + start = Swift.min(start, index) + end = Swift.max(end, index) + } + } + + @inlinable @inline(__always) + public mutating func insert(integersIn indexes: ClosedRange) { + guard let first = indexes.first, let last = indexes.last else { return } + insert(first) + insert(last) + } + + public mutating func remove(_ index: Int) { + if start == index { + start += 1 + } + if end == index { + end -= 1 + } + if start > end { + start = .max + end = .max + } + } + + @inlinable @inline(__always) + public func contains(_ index: Int) -> Bool { + guard let first = first, let last = last else { return false } + return index >= first && index <= last + } + + public func makeIterator() -> Iterator { + return Iterator(min: start, max: end) + } + + public struct Iterator: IteratorProtocol { + public typealias Element = Int + + var min: Int + let max: Int + + init(min: Int, max: Int) { + self.min = min + self.max = max + } + + public mutating func next() -> Int? { + if min == max, min == .max { + return nil + } else if min > max { + return nil + } else { + let ret = min + min += 1 + return ret + } + } + } +} + +extension Array { + @inline(__always) @inlinable + mutating func pop() -> Element? { + guard !isEmpty else { return nil } + return removeLast() + } + + @inline(__always) @inlinable + mutating func dequeue() -> Element? { + guard !isEmpty else { return nil } + return removeFirst() + } +} + + +class TouchPath: Hashable { + class Point: Hashable { + public private(set) var events: [Touch] + + public var event: Touch { + return events.last! + } + + public var expectsUpdate: Bool { + return self.event.isPrediction || self.event.expectsUpdate + } + + public var isPrediction: Bool { + return events.allSatisfy({ $0.isPrediction }) + } + + public init(event: Touch) { + events = [event] + events.reserveCapacity(10) + } + + func add(event: Touch) { + events.append(event) + } + + static func == (lhs: Point, rhs: Point) -> Bool { + return lhs.expectsUpdate == rhs.expectsUpdate && lhs.events == rhs.events + } + + func hash(into hasher: inout Hasher) { + hasher.combine(events) + } + } + + public private(set) var touchIdentifier: String + + private var _points: [Point]? + public var points: [Point] { + if let _points = _points { + return _points + } + let ret = confirmedPoints + predictedPoints + _points = ret + return ret + } + public var bounds: CGRect { + return points.reduce(.null) { partialResult, point -> CGRect in + return CGRect(x: min(partialResult.origin.x, point.event.location.x), + y: min(partialResult.origin.y, point.event.location.y), + width: max(partialResult.origin.x, point.event.location.x), + height: max(partialResult.origin.y, point.event.location.y)) + } + } + public var isComplete: Bool { + let phase = confirmedPoints.last?.event.phase + return (phase == .ended || phase == .cancelled) && predictedPoints.isEmpty + } + + private var confirmedPoints: [Point] { + didSet { + _points = nil + } + } + + private var predictedPoints: [Point] { + didSet { + _points = nil + } + } + + private var consumable: [Point] + private var expectingUpdate: Set + private var eventToPoint: [PointIdentifier: Point] + private var eventToIndex: [PointIdentifier: Int] + + init?(touchEvents: [Touch]) { + guard !touchEvents.isEmpty else { return nil } + self.confirmedPoints = [] + self.predictedPoints = [] + self.consumable = [] + self.eventToPoint = [:] + self.eventToIndex = [:] + self.expectingUpdate = Set() + self.touchIdentifier = touchEvents.first!.touchIdentifier + add(touchEvents: touchEvents) + } + + @discardableResult + func add(touchEvents: [Touch]) -> MinMaxIndex { + var indexSet = MinMaxIndex() + let startingCount = points.count + + for event in touchEvents { + assert(touchIdentifier == event.touchIdentifier) + if event.isPrediction { + if let prediction = consumable.dequeue() { + prediction.add(event: event) + predictedPoints.append(prediction) + let index = confirmedPoints.count + predictedPoints.count - 1 + indexSet.insert(index) + } else { + let prediction = Point(event: event) + predictedPoints.append(prediction) + let index = confirmedPoints.count + predictedPoints.count - 1 + indexSet.insert(index) + } + } else if eventToPoint[event.pointIdentifier] != nil, let index = eventToIndex[event.pointIdentifier] { + eventToPoint[event.pointIdentifier]?.add(event: event) + if !event.expectsUpdate { + self.expectingUpdate.remove(event.pointIdentifier) + } + indexSet.insert(index) + + if event.phase == .ended || event.phase == .cancelled { + consumable.append(contentsOf: predictedPoints) + predictedPoints.removeAll() + } + } else if isComplete { + } else { + consumable.append(contentsOf: predictedPoints) + predictedPoints.removeAll() + + if let point = consumable.dequeue() ?? predictedPoints.dequeue() { + if event.expectsUpdate { + self.expectingUpdate.insert(event.pointIdentifier) + } + point.add(event: event) + eventToPoint[event.pointIdentifier] = point + confirmedPoints.append(point) + let index = confirmedPoints.count - 1 + eventToIndex[event.pointIdentifier] = index + indexSet.insert(index) + } else { + if event.expectsUpdate { + self.expectingUpdate.insert(event.pointIdentifier) + } + let point = Point(event: event) + eventToPoint[event.pointIdentifier] = point + confirmedPoints.append(point) + let index = confirmedPoints.count - 1 + eventToIndex[event.pointIdentifier] = index + indexSet.insert(index) + } + } + } + + for index in consumable.indices { + let possiblyRemovedIndex = confirmedPoints.count + predictedPoints.count + index + if possiblyRemovedIndex < startingCount { + indexSet.insert(possiblyRemovedIndex) + } else { + indexSet.remove(possiblyRemovedIndex) + } + } + + return indexSet + } + + public static func == (lhs: TouchPath, rhs: TouchPath) -> Bool { + return lhs.touchIdentifier == rhs.touchIdentifier && lhs.points == rhs.points + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(touchIdentifier) + } +} + +struct Polyline { + struct Point: Equatable { + var location: CGPoint + var force: CGFloat + var altitudeAngle: CGFloat + var azimuth: CGFloat + var velocity: CGFloat = 0.0 + + let touchPoint: TouchPath.Point + var event: Touch { + return touchPoint.event + } + var expectsUpdate: Bool { + return touchPoint.expectsUpdate + } + + var x: CGFloat { + return self.location.x + } + + var y: CGFloat { + return self.location.y + } + + init( + location: CGPoint, + force: CGFloat, + altitudeAngle: CGFloat, + azimuth: CGFloat, + velocity: CGFloat, + touchPoint: TouchPath.Point + ) { + self.location = location + self.force = force + self.altitudeAngle = altitudeAngle + self.azimuth = azimuth + self.touchPoint = touchPoint + } + + init(touchPoint: TouchPath.Point) { + self.location = touchPoint.event.location + self.force = touchPoint.event.force + self.altitudeAngle = touchPoint.event.altitudeAngle + self.azimuth = touchPoint.event.azimuth + + self.touchPoint = touchPoint + } + + func offsetBy(_ point: CGPoint) -> Polyline.Point { + return Point( + location: self.location.offsetBy(dx: point.x, dy: point.y), + force: self.force, + altitudeAngle: self.altitudeAngle, + azimuth: self.azimuth, + velocity: self.velocity, + touchPoint: self.touchPoint + ) + } + } + + public internal(set) var isComplete: Bool + public let touchIdentifier: String + public var points: [Point] + public var bounds: CGRect { + return self.points.reduce(.null) { partialResult, point -> CGRect in + return CGRect(x: min(partialResult.origin.x, point.x), + y: min(partialResult.origin.y, point.y), + width: max(partialResult.size.width, point.x), + height: max(partialResult.size.height, point.y)) + } + } + init(touchPath: TouchPath) { + isComplete = touchPath.isComplete + touchIdentifier = touchPath.touchIdentifier + + var points: [Point] = [] + var previousTouchPoint: TouchPath.Point? + for touchPoint in touchPath.points { + var point = Point(touchPoint: touchPoint) + if let previousTouchPoint = previousTouchPoint { + let distance = touchPoint.event.location.distance(to: previousTouchPoint.event.location) + let elapsed = max(0.0, touchPoint.event.timestamp - previousTouchPoint.event.timestamp) + let velocity = elapsed > 0.0 ? distance / elapsed : 0.0 + point.velocity = velocity + } + points.append(point) + previousTouchPoint = touchPoint + } + self.points = points + } + + init(points: [Point]) { + assert(!points.isEmpty) + self.isComplete = true + self.touchIdentifier = points.first!.event.touchIdentifier + self.points = points + } + + mutating func update(with path: TouchPath, indexSet: MinMaxIndex) -> MinMaxIndex { + var indexesToRemove = MinMaxIndex() + for index in indexSet { + if index < path.points.count { + if index < points.count { + points[index].location = path.points[index].event.location + points[index].force = path.points[index].event.force + points[index].azimuth = path.points[index].event.azimuth + points[index].altitudeAngle = path.points[index].event.altitudeAngle + + if index > 0 { + let previousTouchPoint = points[index - 1] + let distance = path.points[index].event.location.distance(to: previousTouchPoint.event.location) + let elapsed = max(0.0, path.points[index].event.timestamp - previousTouchPoint.event.timestamp) + let velocity = elapsed > 0.0 ? distance / elapsed : 0.0 + points[index].velocity = velocity + } + } else if index == points.count { + points.append(Point(touchPoint: path.points[index])) + if index > 0 { + let previousTouchPoint = points[index - 1] + let distance = path.points[index].event.location.distance(to: previousTouchPoint.event.location) + let elapsed = max(0.0, path.points[index].event.timestamp - previousTouchPoint.event.timestamp) + let velocity = elapsed > 0.0 ? distance / elapsed : 0.0 + points[index].velocity = velocity + } + } else { + assertionFailure("Attempting to modify a point that doesn't yet exist. maybe an update is out of order?") + } + } else { + indexesToRemove.insert(index) + } + } + + // Remove points from the end of the list toward the beginning + for index in indexesToRemove.reversed() { + guard index < points.count else { + print("Error: unknown polyline index \(index)") + continue + } + points.remove(at: index) + } + + isComplete = path.isComplete + + return indexSet + } +} + +private class BezierBuilder { + private var elements: [BezierPath.Element] = [] + private let smoother: Smoother + private(set) var path = BezierPath() + + init(smoother: Smoother) { + self.smoother = smoother + } + + @discardableResult + func update(with line: Polyline, at lineIndexes: MinMaxIndex) -> MinMaxIndex { + let updatedPathIndexes = smoother.elementIndexes(for: line, at: lineIndexes, with: path) + guard + let min = updatedPathIndexes.first, + let max = updatedPathIndexes.last + else { + return updatedPathIndexes + } + let updatedPath: BezierPath + if min - 1 < path.elementCount, + min - 1 >= 0 { + updatedPath = path.trimming(to: min - 1) + } else { + updatedPath = BezierPath() + } + for elementIndex in min ... max { + assert(elementIndex <= elements.count, "Invalid element index") + if updatedPathIndexes.contains(elementIndex) { + if elementIndex > smoother.maxIndex(for: line) { + + } else { + let element = smoother.element(for: line, at: elementIndex) + if elementIndex == elements.count { + elements.append(element) + } else { + elements[elementIndex] = element + } + updatedPath.append(element) + } + } else { + // use the existing element + let element = elements[elementIndex] + updatedPath.append(element) + } + } + for elementIndex in max + 1 ..< elements.count { + let element = elements[elementIndex] + updatedPath.append(element) + } + path = updatedPath + return updatedPathIndexes + } +} + +private class Smoother { + let smoothFactor: CGFloat + + init(smoothFactor: CGFloat = 0.7) { + self.smoothFactor = smoothFactor + } + + func element(for line: Polyline, at elementIndex: Int) -> BezierPath.Element { + assert(elementIndex >= 0 && elementIndex <= maxIndex(for: line)) + + if elementIndex == 0 { + return BezierPath.Element(type: .moveTo, startPoint: line.points[0], endPoint: line.points[0], controlPoints: []) + } + + if elementIndex == 1 { + return Self.newCurve(smoothFactor: smoothFactor, + startPoint: line.points[0], + p1: line.points[0].location, + p2: line.points[1], + p3: line.points[2].location) + } + + if line.isComplete && elementIndex == maxIndex(for: line) { + return Self.newCurve(smoothFactor: smoothFactor, + startPoint: line.points[elementIndex - 1], + p0: line.points[elementIndex - 2].location, + p1: line.points[elementIndex - 1].location, + p2: line.points[elementIndex], + p3: line.points[elementIndex].location) + } + + return Self.newCurve(smoothFactor: smoothFactor, + startPoint: line.points[elementIndex - 1], + p0: line.points[elementIndex - 2].location, + p1: line.points[elementIndex - 1].location, + p2: line.points[elementIndex], + p3: line.points[elementIndex + 1].location) + } + + func maxIndex(for line: Polyline) -> Int { + let lastIndex = line.points.count - 1 + return Swift.max(0, lastIndex - 1) + (line.points.count > 2 && line.isComplete ? 1 : 0) + } + + func elementIndexes(for line: Polyline, at lineIndexes: MinMaxIndex, with bezier: BezierPath) -> MinMaxIndex { + var curveIndexes = MinMaxIndex() + + for index in lineIndexes { + elementIndexes(for: line, at: index, with: bezier, into: &curveIndexes) + } + + return curveIndexes + } + + func elementIndexes(for line: Polyline, at lineIndex: Int, with bezier: BezierPath) -> MinMaxIndex { + var ret = MinMaxIndex() + elementIndexes(for: line, at: lineIndex, with: bezier, into: &ret) + return ret + } + + // Below are the examples of input indexes, and which smoothed elements that point index affects + // 0 => 2, 1, 0 + // 1 => 3, 2, 1, 0 + // 2 => 4, 3, 2, 1 + // 3 => 5, 4, 3, 2 + // 4 => 6, 5, 4, 3 + // 5 => 7, 6, 5, 4 + // 6 => 8, 7, 6, 5 + // 7 => 9, 8, 7, 6 + private func elementIndexes(for line: Polyline, at lineIndex: Int, with bezier: BezierPath, into indexes: inout MinMaxIndex) { + guard lineIndex >= 0 else { + return + } + let max = maxIndex(for: line) + + if lineIndex > 1, + (lineIndex - 1 <= max) || (lineIndex - 1 < bezier.elementCount) { + indexes.insert(lineIndex - 1) + } + if (lineIndex <= max) || (lineIndex < bezier.elementCount) { + indexes.insert(lineIndex) + } + if (lineIndex + 1 <= max) || (lineIndex + 1 < bezier.elementCount) { + indexes.insert(lineIndex + 1) + } + if (lineIndex + 2 <= max) || (lineIndex + 2 < bezier.elementCount) { + indexes.insert(lineIndex + 2) + } + } + + // MARK: - Helper + + private static func newCurve( + smoothFactor: CGFloat, + startPoint: Polyline.Point, + p0: CGPoint? = nil, + p1: CGPoint, + p2: Polyline.Point, + p3: CGPoint + ) -> BezierPath.Element { + let p0 = p0 ?? p1 + + let c1 = CGPoint(x: (p0.x + p1.x) / 2.0, y: (p0.y + p1.y) / 2.0) + let c2 = CGPoint(x: (p1.x + p2.x) / 2.0, y: (p1.y + p2.y) / 2.0) + let c3 = CGPoint(x: (p2.x + p3.x) / 2.0, y: (p2.y + p3.y) / 2.0) + + let len1 = sqrt((p1.x - p0.x) * (p1.x - p0.x) + (p1.y - p0.y) * (p1.y - p0.y)) + let len2 = sqrt((p2.x - p1.x) * (p2.x - p1.x) + (p2.y - p1.y) * (p2.y - p1.y)) + let len3 = sqrt((p3.x - p2.x) * (p3.x - p2.x) + (p3.y - p2.y) * (p3.y - p2.y)) + + let k1 = len1 / (len1 + len2) + let k2 = len2 / (len2 + len3) + + let m1 = CGPoint(x: c1.x + (c2.x - c1.x) * k1, y: c1.y + (c2.y - c1.y) * k1) + let m2 = CGPoint(x: c2.x + (c3.x - c2.x) * k2, y: c2.y + (c3.y - c2.y) * k2) + + // Resulting control points. Here smooth_value is mentioned + // above coefficient K whose value should be in range [0...1]. + var ctrl1 = CGPoint(x: m1.x + (c2.x - m1.x) * smoothFactor + p1.x - m1.x, + y: m1.y + (c2.y - m1.y) * smoothFactor + p1.y - m1.y) + + var ctrl2 = CGPoint(x: m2.x + (c2.x - m2.x) * smoothFactor + p2.x - m2.x, + y: m2.y + (c2.y - m2.y) * smoothFactor + p2.y - m2.y) + + if ctrl1.x.isNaN || ctrl1.y.isNaN { + ctrl1 = p1 + } + + if ctrl2.x.isNaN || ctrl2.y.isNaN { + ctrl2 = p2.location + } + + return BezierPath.Element(type: .cubicCurve, startPoint: startPoint, endPoint: p2, controlPoints: [ctrl1, ctrl2]) + } +} + diff --git a/submodules/DrawingUI/Sources/DrawingMetalView.swift b/submodules/DrawingUI/Sources/DrawingMetalView.swift new file mode 100644 index 0000000000..1c548b6262 --- /dev/null +++ b/submodules/DrawingUI/Sources/DrawingMetalView.swift @@ -0,0 +1,554 @@ +import Foundation +import UIKit +import QuartzCore +import MetalKit +import AppBundle + +public final class DrawingMetalView: MTKView { + private let size: CGSize + + private let commandQueue: MTLCommandQueue + fileprivate let library: MTLLibrary + private var pipelineState: MTLRenderPipelineState! + + fileprivate var drawable: Drawable? + + private var render_target_vertex: MTLBuffer! + private var render_target_uniform: MTLBuffer! + + private var penBrush: Brush? + private var markerBrush: Brush? + private var pencilBrush: Brush? + + public init?(size: CGSize) { + var size = size + if Int(size.width) % 16 != 0 { + size.width = round(size.width / 16.0) * 16.0 + } + + let mainBundle = Bundle(for: DrawingView.self) + guard let path = mainBundle.path(forResource: "DrawingUIBundle", ofType: "bundle") else { + return nil + } + guard let bundle = Bundle(path: path) else { + return nil + } + guard let device = MTLCreateSystemDefaultDevice() else { + return nil + } + guard let defaultLibrary = try? device.makeDefaultLibrary(bundle: bundle) else { + return nil + } + self.library = defaultLibrary + + guard let commandQueue = device.makeCommandQueue() else { + return nil + } + self.commandQueue = commandQueue + + self.size = size + + super.init(frame: CGRect(origin: .zero, size: size), device: device) + + self.isOpaque = false + self.drawableSize = self.size + + self.setup() + } + + required init(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func makeTexture(with data: Data) -> MTLTexture? { + let textureLoader = MTKTextureLoader(device: device!) + return try? textureLoader.newTexture(data: data, options: [.SRGB : false]) + } + + func makeTexture(with image: UIImage) -> MTLTexture? { + if let data = image.pngData() { + return makeTexture(with: data) + } else { + return nil + } + } + + func drawInContext(_ cgContext: CGContext) { + guard let texture = self.drawable?.texture, let ciImage = CIImage(mtlTexture: texture, options: [.colorSpace: CGColorSpaceCreateDeviceRGB()])?.oriented(forExifOrientation: 1) else { + return + } + let context = CIContext(cgContext: cgContext) + let rect = CGRect(origin: .zero, size: ciImage.extent.size) + context.draw(ciImage, in: rect, from: rect) + } + + private func setup() { + self.drawable = Drawable(size: self.size, pixelFormat: self.colorPixelFormat, device: device) + + let size = self.size + let w = size.width, h = size.height + let vertices = [ + Vertex(position: CGPoint(x: 0 , y: 0), texCoord: CGPoint(x: 0, y: 0)), + Vertex(position: CGPoint(x: w , y: 0), texCoord: CGPoint(x: 1, y: 0)), + Vertex(position: CGPoint(x: 0 , y: h), texCoord: CGPoint(x: 0, y: 1)), + Vertex(position: CGPoint(x: w , y: h), texCoord: CGPoint(x: 1, y: 1)), + ] + self.render_target_vertex = self.device?.makeBuffer(bytes: vertices, length: MemoryLayout.stride * vertices.count, options: .cpuCacheModeWriteCombined) + + let matrix = Matrix.identity + matrix.scaling(x: 2.0 / Float(size.width), y: -2.0 / Float(size.height), z: 1) + matrix.translation(x: -1, y: 1, z: 0) + self.render_target_uniform = self.device?.makeBuffer(bytes: matrix.m, length: MemoryLayout.size * 16, options: []) + + let vertexFunction = self.library.makeFunction(name: "vertex_render_target") + let fragmentFunction = self.library.makeFunction(name: "fragment_render_target") + let pipelineDescription = MTLRenderPipelineDescriptor() + pipelineDescription.vertexFunction = vertexFunction + pipelineDescription.fragmentFunction = fragmentFunction + pipelineDescription.colorAttachments[0].pixelFormat = colorPixelFormat + + do { + self.pipelineState = try self.device?.makeRenderPipelineState(descriptor: pipelineDescription) + } catch { + fatalError(error.localizedDescription) + } + + self.penBrush = Brush(texture: nil, target: self, rotation: .ahead) + + if let url = getAppBundle().url(forResource: "marker", withExtension: "png"), let data = try? Data(contentsOf: url) { + self.markerBrush = Brush(texture: self.makeTexture(with: data), target: self, rotation: .fixed(0.0)) + } + + if let url = getAppBundle().url(forResource: "pencil", withExtension: "png"), let data = try? Data(contentsOf: url) { + self.pencilBrush = Brush(texture: self.makeTexture(with: data), target: self, rotation: .random) + } + } + + override public func draw(_ rect: CGRect) { + super.draw(rect) + + guard let drawable = self.drawable, let texture = drawable.texture else { + return + } + + let renderPassDescriptor = MTLRenderPassDescriptor() + let attachment = renderPassDescriptor.colorAttachments[0] + attachment?.clearColor = clearColor + attachment?.texture = self.currentDrawable?.texture + attachment?.loadAction = .clear + attachment?.storeAction = .store + + let commandBuffer = self.commandQueue.makeCommandBuffer() + let commandEncoder = commandBuffer?.makeRenderCommandEncoder(descriptor: renderPassDescriptor) + + commandEncoder?.setRenderPipelineState(self.pipelineState) + + commandEncoder?.setVertexBuffer(self.render_target_vertex, offset: 0, index: 0) + commandEncoder?.setVertexBuffer(self.render_target_uniform, offset: 0, index: 1) + commandEncoder?.setFragmentTexture(texture, index: 0) + commandEncoder?.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4) + + commandEncoder?.endEncoding() + if let drawable = self.currentDrawable { + commandBuffer?.present(drawable) + } + commandBuffer?.commit() + } + + func clear() { + guard let drawable = self.drawable else { + return + } + + drawable.updateBuffer(with: self.size) + drawable.clear() + + drawable.commit() + } + + enum BrushType { + case pen + case marker + case pencil + } + + func updated(_ point: Polyline.Point, state: DrawingGesturePipeline.DrawingGestureState, brush: BrushType, color: DrawingColor, size: CGFloat) { + switch brush { + case .pen: + self.penBrush?.updated(point, color: color, state: state, size: size) + case .marker: + self.markerBrush?.updated(point, color: color, state: state, size: size) + case .pencil: + self.pencilBrush?.updated(point, color: color, state: state, size: size) + } + } +} + +private class Drawable { + public private(set) var texture: MTLTexture? + + internal var pixelFormat: MTLPixelFormat = .bgra8Unorm + internal var size: CGSize + internal var uniform_buffer: MTLBuffer! + internal var renderPassDescriptor: MTLRenderPassDescriptor? + internal var commandBuffer: MTLCommandBuffer? + internal var commandQueue: MTLCommandQueue? + internal var device: MTLDevice? + + public init(size: CGSize, pixelFormat: MTLPixelFormat, device: MTLDevice?) { + self.size = size + self.pixelFormat = pixelFormat + self.device = device + self.texture = self.makeColorTexture(DrawingColor.clear) + self.commandQueue = device?.makeCommandQueue() + + self.renderPassDescriptor = MTLRenderPassDescriptor() + let attachment = self.renderPassDescriptor?.colorAttachments[0] + attachment?.texture = self.texture + attachment?.loadAction = .load + attachment?.storeAction = .store + + self.updateBuffer(with: size) + } + + func clear() { + self.texture = self.makeColorTexture(DrawingColor.clear) + self.renderPassDescriptor?.colorAttachments[0].texture = self.texture + self.commit() + } + + internal func updateBuffer(with size: CGSize) { + self.size = size + + let matrix = Matrix.identity + self.uniform_buffer = device?.makeBuffer(bytes: matrix.m, length: MemoryLayout.size * 16, options: []) + } + + internal func prepareForDraw() { + if self.commandBuffer == nil { + self.commandBuffer = commandQueue?.makeCommandBuffer() + } + } + + internal func makeCommandEncoder() -> MTLRenderCommandEncoder? { + guard let commandBuffer = self.commandBuffer, let rpd = renderPassDescriptor else { + return nil + } + return commandBuffer.makeRenderCommandEncoder(descriptor: rpd) + } + + internal func commit() { + self.commandBuffer?.commit() + self.commandBuffer = nil + } + + internal func makeColorTexture(_ color: DrawingColor) -> MTLTexture? { + guard self.size.width * self.size.height > 0 else { + return nil + } + let textureDescriptor = MTLTextureDescriptor.texture2DDescriptor( + pixelFormat: self.pixelFormat, + width: Int(self.size.width), + height: Int(self.size.height), + mipmapped: false + ) + textureDescriptor.usage = [.renderTarget, .shaderRead] + guard let texture = device?.makeTexture(descriptor: textureDescriptor) else { + return nil + } + let region = MTLRegion( + origin: MTLOrigin(x: 0, y: 0, z: 0), + size: MTLSize(width: texture.width, height: texture.height, depth: 1) + ) + let bytesPerRow = 4 * texture.width + let data = Data(capacity: Int(bytesPerRow * texture.height)) + if let bytes = data.withUnsafeBytes({ $0.baseAddress }) { + texture.replace(region: region, mipmapLevel: 0, withBytes: bytes, bytesPerRow: bytesPerRow) + } + return texture + } +} + +private class Brush { + private(set) var texture: MTLTexture? + private(set) var pipelineState: MTLRenderPipelineState! + + weak var target: DrawingMetalView? + + public enum Rotation { + case fixed(CGFloat) + case random + case ahead + } + + var rotation: Rotation + + required public init(texture: MTLTexture?, target: DrawingMetalView, rotation: Rotation) { + self.texture = texture + self.target = target + self.rotation = rotation + + self.setupPipeline() + } + + private func setupPipeline() { + guard let target = self.target, let device = target.device else { + return + } + + let renderPipelineDescriptor = MTLRenderPipelineDescriptor() + if let vertex_func = target.library.makeFunction(name: "vertex_point_func") { + renderPipelineDescriptor.vertexFunction = vertex_func + } + if let _ = self.texture { + if let fragment_func = target.library.makeFunction(name: "fragment_point_func") { + renderPipelineDescriptor.fragmentFunction = fragment_func + } + } else { + if let fragment_func = target.library.makeFunction(name: "fragment_point_func_without_texture") { + renderPipelineDescriptor.fragmentFunction = fragment_func + } + } + renderPipelineDescriptor.colorAttachments[0].pixelFormat = target.colorPixelFormat + + let attachment = renderPipelineDescriptor.colorAttachments[0] + attachment?.isBlendingEnabled = true + + attachment?.rgbBlendOperation = .add + attachment?.sourceRGBBlendFactor = .sourceAlpha + attachment?.destinationRGBBlendFactor = .oneMinusSourceAlpha + + attachment?.alphaBlendOperation = .add + attachment?.sourceAlphaBlendFactor = .one + attachment?.destinationAlphaBlendFactor = .oneMinusSourceAlpha + + self.pipelineState = try! device.makeRenderPipelineState(descriptor: renderPipelineDescriptor) + } + + func render(stroke: Stroke, in drawable: Drawable? = nil) { + let drawable = drawable ?? target?.drawable + + guard stroke.lines.count > 0, let target = drawable else { + return + } + + target.prepareForDraw() + + let commandEncoder = target.makeCommandEncoder() + + commandEncoder?.setRenderPipelineState(self.pipelineState) + + if let vertex_buffer = stroke.preparedBuffer(rotation: self.rotation) { + commandEncoder?.setVertexBuffer(vertex_buffer, offset: 0, index: 0) + commandEncoder?.setVertexBuffer(target.uniform_buffer, offset: 0, index: 1) + if let texture = texture { + commandEncoder?.setFragmentTexture(texture, index: 0) + } + commandEncoder?.drawPrimitives(type: .point, vertexStart: 0, vertexCount: stroke.vertexCount) + } + + commandEncoder?.endEncoding() + } + + private let bezier = BezierGenerator() + func updated(_ point: Polyline.Point, color: DrawingColor, state: DrawingGesturePipeline.DrawingGestureState, size: CGFloat) { + let point = point.location + switch state { + case .began: + self.bezier.begin(with: point) + self.pushPoint(point, color: color, size: size, isEnd: false) + case .changed: + if self.bezier.points.count > 0 && point != lastRenderedPoint { + self.pushPoint(point, color: color, size: size, isEnd: false) + } + case .ended, .cancelled: + if self.bezier.points.count >= 3 { + self.pushPoint(point, color: color, size: size, isEnd: true) + } + self.bezier.finish() + self.lastRenderedPoint = nil + } + } + + private var lastRenderedPoint: CGPoint? + func pushPoint(_ point: CGPoint, color: DrawingColor, size: CGFloat, isEnd: Bool) { + var pointStep: CGFloat + if case .random = self.rotation { + pointStep = size * 0.1 + } else { + pointStep = 2.0 + } + + var lines: [Line] = [] + let points = self.bezier.pushPoint(point) + guard points.count >= 2 else { + return + } + var previousPoint = self.lastRenderedPoint ?? points[0] + for i in 1 ..< points.count { + let p = points[i] + if (isEnd && i == points.count - 1) || pointStep <= 1 || (pointStep > 1 && previousPoint.distance(to: p) >= pointStep) { + let line = Line(start: previousPoint, end: p, pointSize: size, pointStep: pointStep) + lines.append(line) + previousPoint = p + } + } + + if let drawable = self.target?.drawable { + let stroke = Stroke(color: color, lines: lines, target: drawable) + + self.render(stroke: stroke, in: drawable) + + drawable.commit() + } + } +} + +private class Stroke { + private weak var target: Drawable? + + let color: DrawingColor + var lines: [Line] = [] + + private(set) var vertexCount: Int = 0 + private var vertex_buffer: MTLBuffer? + + init(color: DrawingColor, lines: [Line] = [], target: Drawable) { + self.color = color + self.lines = lines + self.target = target + + let _ = self.preparedBuffer(rotation: .fixed(0)) + } + + func append(_ lines: [Line]) { + self.lines.append(contentsOf: lines) + self.vertex_buffer = nil + } + + func preparedBuffer(rotation: Brush.Rotation) -> MTLBuffer? { + guard !self.lines.isEmpty else { + return nil + } + + var vertexes: [Point] = [] + + self.lines.forEach { (line) in + let count = max(line.length / line.pointStep, 1) + + let overlapping = max(1, line.pointSize / line.pointStep) + var renderingColor = self.color + renderingColor.alpha = renderingColor.alpha / overlapping * 2.5 + + for i in 0 ..< Int(count) { + let index = CGFloat(i) + let x = line.start.x + (line.end.x - line.start.x) * (index / count) + let y = line.start.y + (line.end.y - line.start.y) * (index / count) + + var angle: CGFloat = 0 + switch rotation { + case let .fixed(a): + angle = a + case .random: + angle = CGFloat.random(in: -CGFloat.pi ... CGFloat.pi) + case .ahead: + angle = line.angle + } + + vertexes.append(Point(x: x, y: y, color: renderingColor, size: line.pointSize, angle: angle)) + } + } + + self.vertexCount = vertexes.count + self.vertex_buffer = self.target?.device?.makeBuffer(bytes: vertexes, length: MemoryLayout.stride * vertexCount, options: .cpuCacheModeWriteCombined) + + return self.vertex_buffer + } +} + +class BezierGenerator { + init() { + } + + init(beginPoint: CGPoint) { + begin(with: beginPoint) + } + + func begin(with point: CGPoint) { + step = 0 + points.removeAll() + points.append(point) + } + + func pushPoint(_ point: CGPoint) -> [CGPoint] { + if point == points.last { + return [] + } + points.append(point) + if points.count < 3 { + return [] + } + step += 1 + let result = genericPathPoints() + return result + } + + func finish() { + step = 0 + points.removeAll() + } + + var points: [CGPoint] = [] + + private var step = 0 + private func genericPathPoints() -> [CGPoint] { + var begin: CGPoint + var control: CGPoint + let end = CGPoint.middle(p1: points[step], p2: points[step + 1]) + + var vertices: [CGPoint] = [] + if step == 1 { + begin = points[0] + let middle1 = CGPoint.middle(p1: points[0], p2: points[1]) + control = CGPoint.middle(p1: middle1, p2: points[1]) + } else { + begin = CGPoint.middle(p1: points[step - 1], p2: points[step]) + control = points[step] + } + + let distance = begin.distance(to: end) + let segements = max(Int(distance / 5), 2) + + for i in 0 ..< segements { + let t = CGFloat(i) / CGFloat(segements) + let x = pow(1 - t, 2) * begin.x + 2.0 * (1 - t) * t * control.x + t * t * end.x + let y = pow(1 - t, 2) * begin.y + 2.0 * (1 - t) * t * control.y + t * t * end.y + vertices.append(CGPoint(x: x, y: y)) + } + vertices.append(end) + return vertices + } +} + +private struct Line { + var start: CGPoint + var end: CGPoint + + var pointSize: CGFloat + var pointStep: CGFloat + + init(start: CGPoint, end: CGPoint, pointSize: CGFloat, pointStep: CGFloat) { + self.start = start + self.end = end + self.pointSize = pointSize + self.pointStep = pointStep + } + + var length: CGFloat { + return self.start.distance(to: self.end) + } + + var angle: CGFloat { + return self.end.angle(to: self.start) + } +} diff --git a/submodules/DrawingUI/Sources/DrawingScreen.swift b/submodules/DrawingUI/Sources/DrawingScreen.swift new file mode 100644 index 0000000000..133125fa98 --- /dev/null +++ b/submodules/DrawingUI/Sources/DrawingScreen.swift @@ -0,0 +1,2442 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import LegacyComponents +import TelegramCore +import Postbox +import SwiftSignalKit +import TelegramPresentationData +import AccountContext +import AppBundle +import PresentationDataUtils +import LegacyComponents +import ComponentDisplayAdapters +import LottieAnimationComponent +import ViewControllerComponent +import ContextUI + +enum DrawingToolState: Equatable { + enum Key: CaseIterable { + case pen + case marker + case neon + case pencil + case lasso + case eraser + } + + struct BrushState: Equatable { + enum Mode: Equatable { + case round + case arrow + } + + let color: DrawingColor + let size: CGFloat + let mode: Mode + + func withUpdatedColor(_ color: DrawingColor) -> BrushState { + return BrushState(color: color, size: self.size, mode: self.mode) + } + + func withUpdatedSize(_ size: CGFloat) -> BrushState { + return BrushState(color: self.color, size: size, mode: self.mode) + } + + func withUpdatedMode(_ mode: Mode) -> BrushState { + return BrushState(color: self.color, size: self.size, mode: mode) + } + } + + struct EraserState: Equatable { + enum Mode: Equatable { + case bitmap + case vector + case blur + } + + let size: CGFloat + let mode: Mode + + func withUpdatedSize(_ size: CGFloat) -> EraserState { + return EraserState(size: size, mode: self.mode) + } + + func withUpdatedMode(_ mode: Mode) -> EraserState { + return EraserState(size: self.size, mode: mode) + } + } + + case pen(BrushState) + case marker(BrushState) + case neon(BrushState) + case pencil(BrushState) + case lasso + case eraser(EraserState) + + func withUpdatedColor(_ color: DrawingColor) -> DrawingToolState { + switch self { + case let .pen(state): + return .pen(state.withUpdatedColor(color)) + case let .marker(state): + return .marker(state.withUpdatedColor(color)) + case let .neon(state): + return .neon(state.withUpdatedColor(color)) + case let .pencil(state): + return .pencil(state.withUpdatedColor(color)) + case .lasso, .eraser: + return self + } + } + + func withUpdatedSize(_ size: CGFloat) -> DrawingToolState { + switch self { + case let .pen(state): + return .pen(state.withUpdatedSize(size)) + case let .marker(state): + return .marker(state.withUpdatedSize(size)) + case let .neon(state): + return .neon(state.withUpdatedSize(size)) + case let .pencil(state): + return .pencil(state.withUpdatedSize(size)) + case .lasso: + return self + case let .eraser(state): + return .eraser(state.withUpdatedSize(size)) + } + } + + func withUpdatedBrushMode(_ mode: BrushState.Mode) -> DrawingToolState { + switch self { + case let .pen(state): + return .pen(state.withUpdatedMode(mode)) + case let .marker(state): + return .marker(state.withUpdatedMode(mode)) + case let .neon(state): + return .neon(state.withUpdatedMode(mode)) + case let .pencil(state): + return .pencil(state.withUpdatedMode(mode)) + case .lasso, .eraser: + return self + } + } + + func withUpdatedEraserMode(_ mode: EraserState.Mode) -> DrawingToolState { + switch self { + case .pen: + return self + case .marker: + return self + case .neon: + return self + case .pencil: + return self + case .lasso: + return self + case let .eraser(state): + return .eraser(state.withUpdatedMode(mode)) + } + } + + var color: DrawingColor? { + switch self { + case let .pen(state), let .marker(state), let .neon(state), let .pencil(state): + return state.color + default: + return nil + } + } + + var size: CGFloat? { + switch self { + case let .pen(state), let .marker(state), let .neon(state), let .pencil(state): + return state.size + case let .eraser(state): + return state.size + default: + return nil + } + } + + var brushMode: DrawingToolState.BrushState.Mode? { + switch self { + case let .pen(state), let .marker(state), let .neon(state), let .pencil(state): + return state.mode + default: + return nil + } + } + + var eraserMode: DrawingToolState.EraserState.Mode? { + switch self { + case let .eraser(state): + return state.mode + default: + return nil + } + } + + var key: DrawingToolState.Key { + switch self { + case .pen: + return .pen + case .marker: + return .marker + case .neon: + return .neon + case .pencil: + return .pencil + case .lasso: + return .lasso + case .eraser: + return .eraser + } + } +} + +struct DrawingState: Equatable { + let selectedTool: DrawingToolState.Key + let tools: [DrawingToolState] + + var currentToolState: DrawingToolState { + return self.toolState(for: self.selectedTool) + } + + func toolState(for key: DrawingToolState.Key) -> DrawingToolState { + for tool in self.tools { + if tool.key == key { + return tool + } + } + return .lasso + } + + func withUpdatedSelectedTool(_ selectedTool: DrawingToolState.Key) -> DrawingState { + return DrawingState( + selectedTool: selectedTool, + tools: self.tools + ) + } + + func withUpdatedColor(_ color: DrawingColor) -> DrawingState { + var tools = self.tools + if let index = tools.firstIndex(where: { $0.key == self.selectedTool }) { + let updated = tools[index].withUpdatedColor(color) + tools.remove(at: index) + tools.insert(updated, at: index) + } + + return DrawingState( + selectedTool: self.selectedTool, + tools: tools + ) + } + + func withUpdatedSize(_ size: CGFloat) -> DrawingState { + var tools = self.tools + if let index = tools.firstIndex(where: { $0.key == self.selectedTool }) { + let updated = tools[index].withUpdatedSize(size) + tools.remove(at: index) + tools.insert(updated, at: index) + } + + return DrawingState( + selectedTool: self.selectedTool, + tools: tools + ) + } + + func withUpdatedBrushMode(_ mode: DrawingToolState.BrushState.Mode) -> DrawingState { + var tools = self.tools + if let index = tools.firstIndex(where: { $0.key == self.selectedTool }) { + let updated = tools[index].withUpdatedBrushMode(mode) + tools.remove(at: index) + tools.insert(updated, at: index) + } + + return DrawingState( + selectedTool: self.selectedTool, + tools: tools + ) + } + + func withUpdatedEraserMode(_ mode: DrawingToolState.EraserState.Mode) -> DrawingState { + var tools = self.tools + if let index = tools.firstIndex(where: { $0.key == self.selectedTool }) { + let updated = tools[index].withUpdatedEraserMode(mode) + tools.remove(at: index) + tools.insert(updated, at: index) + } + + return DrawingState( + selectedTool: self.selectedTool, + tools: tools + ) + } + + static var initial: DrawingState { + return DrawingState( + selectedTool: .pen, + tools: [ + .pen(DrawingToolState.BrushState(color: DrawingColor(rgb: 0xffffff), size: 0.5, mode: .round)), + .marker(DrawingToolState.BrushState(color: DrawingColor(rgb: 0xfee21b), size: 0.5, mode: .round)), + .neon(DrawingToolState.BrushState(color: DrawingColor(rgb: 0x34ffab), size: 0.5, mode: .round)), + .pencil(DrawingToolState.BrushState(color: DrawingColor(rgb: 0x2570f0), size: 0.5, mode: .round)), + .lasso, + .eraser(DrawingToolState.EraserState(size: 0.5, mode: .bitmap)) + ] + ) + } +} + +private final class ReferenceContentSource: ContextReferenceContentSource { + private let sourceView: UIView + + init(sourceView: UIView) { + self.sourceView = sourceView + } + + func transitionInfo() -> ContextControllerReferenceViewInfo? { + return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds, customPosition: CGPoint(x: 7.0, y: 3.0)) + } +} + +enum DrawingScreenTransition { + case animateIn + case animateOut +} + +private let undoButtonTag = GenericComponentViewTag() +private let clearAllButtonTag = GenericComponentViewTag() +private let colorButtonTag = GenericComponentViewTag() +private let addButtonTag = GenericComponentViewTag() +private let toolsTag = GenericComponentViewTag() +private let modeTag = GenericComponentViewTag() +private let doneButtonTag = GenericComponentViewTag() + +private final class DrawingScreenComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let present: (ViewController) -> Void + let updateState: ActionSlot + let updateColor: ActionSlot + let performAction: ActionSlot + let updateToolState: ActionSlot + let updateSelectedEntity: ActionSlot + let insertEntity: ActionSlot + let deselectEntity: ActionSlot + let updatePlayback: ActionSlot + let previewBrushSize: ActionSlot + let apply: ActionSlot + let dismiss: ActionSlot + + let presentColorPicker: (DrawingColor) -> Void + let presentFastColorPicker: (UIView) -> Void + let updateFastColorPickerPan: (CGPoint) -> Void + let dismissFastColorPicker: () -> Void + + init( + context: AccountContext, + present: @escaping (ViewController) -> Void, + updateState: ActionSlot, + updateColor: ActionSlot, + performAction: ActionSlot, + updateToolState: ActionSlot, + updateSelectedEntity: ActionSlot, + insertEntity: ActionSlot, + deselectEntity: ActionSlot, + updatePlayback: ActionSlot, + previewBrushSize: ActionSlot, + apply: ActionSlot, + dismiss: ActionSlot, + presentColorPicker: @escaping (DrawingColor) -> Void, + presentFastColorPicker: @escaping (UIView) -> Void, + updateFastColorPickerPan: @escaping (CGPoint) -> Void, + dismissFastColorPicker: @escaping () -> Void + ) { + self.context = context + self.present = present + self.updateState = updateState + self.updateColor = updateColor + self.performAction = performAction + self.updateToolState = updateToolState + self.updateSelectedEntity = updateSelectedEntity + self.insertEntity = insertEntity + self.deselectEntity = deselectEntity + self.updatePlayback = updatePlayback + self.previewBrushSize = previewBrushSize + self.apply = apply + self.dismiss = dismiss + self.presentColorPicker = presentColorPicker + self.presentFastColorPicker = presentFastColorPicker + self.updateFastColorPickerPan = updateFastColorPickerPan + self.dismissFastColorPicker = dismissFastColorPicker + } + + static func ==(lhs: DrawingScreenComponent, rhs: DrawingScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + return true + } + + final class State: ComponentState { + enum ImageKey: Hashable { + case undo + case redo + case done + case add + case round + case arrow + case remove + case blur + case fill + case stroke + case flip + case zoomOut + } + private var cachedImages: [ImageKey: UIImage] = [:] + func image(_ key: ImageKey) -> UIImage { + if let image = self.cachedImages[key] { + return image + } else { + var image: UIImage + switch key { + case .undo: + image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Undo"), color: .white)! + case .redo: + image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Redo"), color: .white)! + case .done: + image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Done"), color: .white)! + case .add: + image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Add"), color: .white)! + case .round: + image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/BrushRound"), color: .white)! + case .arrow: + image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/BrushArrow"), color: .white)! + case .remove: + image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/BrushRemove"), color: .white)! + case .blur: + image = UIImage(bundleImageName: "Media Editor/BrushBlur")! + case .fill: + image = UIImage(bundleImageName: "Media Editor/Fill")! + case .stroke: + image = UIImage(bundleImageName: "Media Editor/Stroke")! + case .flip: + image = UIImage(bundleImageName: "Media Editor/Flip")! + case .zoomOut: + image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/ZoomOut"), color: .white)! + } + cachedImages[key] = image + return image + } + } + + enum Mode { + case drawing + case sticker + case text + } + + private let context: AccountContext + private let updateToolState: ActionSlot + private let insertEntity: ActionSlot + private let deselectEntity: ActionSlot + private let updatePlayback: ActionSlot + private let present: (ViewController) -> Void + + var currentMode: Mode + var drawingState: DrawingState + var drawingViewState: DrawingView.NavigationState + var toolIsFocused = false + var currentColor: DrawingColor + var selectedEntity: DrawingEntity? + + init(context: AccountContext, updateToolState: ActionSlot, insertEntity: ActionSlot, deselectEntity: ActionSlot, updatePlayback: ActionSlot, present: @escaping (ViewController) -> Void) { + self.context = context + self.updateToolState = updateToolState + self.insertEntity = insertEntity + self.deselectEntity = deselectEntity + self.updatePlayback = updatePlayback + self.present = present + + self.currentMode = .drawing + self.drawingState = .initial + self.drawingViewState = DrawingView.NavigationState(canUndo: false, canRedo: false, canClear: false, canZoomOut: false) + self.currentColor = self.drawingState.tools.first?.color ?? DrawingColor(rgb: 0xffffff) + } + + private var currentToolState: DrawingToolState { + return self.drawingState.toolState(for: self.drawingState.selectedTool) + } + + func updateColor(_ color: DrawingColor, animated: Bool = false) { + self.currentColor = color + if let selectedEntity = self.selectedEntity { + selectedEntity.color = color + selectedEntity.currentEntityView?.update() + } else { + self.drawingState = self.drawingState.withUpdatedColor(color) + self.updateToolState.invoke(self.drawingState.currentToolState) + } + self.updated(transition: animated ? .easeInOut(duration: 0.2) : .immediate) + } + + func updateSelectedTool(_ tool: DrawingToolState.Key) { + self.drawingState = self.drawingState.withUpdatedSelectedTool(tool) + self.currentColor = self.drawingState.currentToolState.color ?? self.currentColor + self.updateToolState.invoke(self.drawingState.currentToolState) + self.updated(transition: .easeInOut(duration: 0.2)) + } + + func updateBrushSize(_ size: CGFloat) { + if let selectedEntity = self.selectedEntity { + if let textEntity = selectedEntity as? DrawingTextEntity { + textEntity.fontSize = size + } else { + selectedEntity.lineWidth = size + } + selectedEntity.currentEntityView?.update() + } else { + self.drawingState = self.drawingState.withUpdatedSize(size) + self.updateToolState.invoke(self.drawingState.currentToolState) + } + self.updated(transition: .immediate) + } + + func updateBrushMode(_ mode: DrawingToolState.BrushState.Mode) { + self.drawingState = self.drawingState.withUpdatedBrushMode(mode) + self.updateToolState.invoke(self.drawingState.currentToolState) + self.updated(transition: .easeInOut(duration: 0.2)) + } + + func updateEraserMode(_ mode: DrawingToolState.EraserState.Mode) { + self.drawingState = self.drawingState.withUpdatedEraserMode(mode) + self.updateToolState.invoke(self.drawingState.currentToolState) + self.updated(transition: .easeInOut(duration: 0.2)) + } + + func updateToolIsFocused(_ isFocused: Bool) { + self.toolIsFocused = isFocused + self.updated(transition: .easeInOut(duration: 0.2)) + } + + func updateDrawingState(_ state: DrawingView.NavigationState) { + self.drawingViewState = state + self.updated(transition: .easeInOut(duration: 0.2)) + } + + func updateSelectedEntity(_ entity: DrawingEntity?) { + self.selectedEntity = entity + if let entity = entity { + if !entity.color.isClear { + self.currentColor = entity.color + } + if entity is DrawingStickerEntity { + self.currentMode = .sticker + } else if entity is DrawingTextEntity { + self.currentMode = .text + } else { + self.currentMode = .drawing + } + } else { + self.currentMode = .drawing + self.currentColor = self.drawingState.currentToolState.color ?? self.currentColor + } + self.updated(transition: .easeInOut(duration: 0.2)) + } + + func presentShapePicker(_ sourceView: UIView) { + let items: [ContextMenuItem] = [ + .action( + ContextMenuActionItem( + text: "Rectangle", + icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/ShapeRectangle"), color: theme.contextMenu.primaryColor)}, + action: { [weak self] f in + f.dismissWithResult(.default) + if let strongSelf = self { + strongSelf.insertEntity.invoke(DrawingSimpleShapeEntity(shapeType: .rectangle, drawType: .stroke, color: strongSelf.currentColor, lineWidth: 0.15)) + } + } + ) + ), + .action( + ContextMenuActionItem( + text: "Ellipse", + icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/ShapeEllipse"), color: theme.contextMenu.primaryColor)}, + action: { [weak self] f in + f.dismissWithResult(.default) + if let strongSelf = self { + strongSelf.insertEntity.invoke(DrawingSimpleShapeEntity(shapeType: .ellipse, drawType: .stroke, color: strongSelf.currentColor, lineWidth: 0.15)) + } + } + ) + ), + .action( + ContextMenuActionItem( + text: "Bubble", + icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/ShapeBubble"), color: theme.contextMenu.primaryColor)}, + action: { [weak self] f in + f.dismissWithResult(.default) + if let strongSelf = self { + strongSelf.insertEntity.invoke(DrawingBubbleEntity(drawType: .stroke, color: strongSelf.currentColor, lineWidth: 0.15)) + } + } + ) + ), + .action( + ContextMenuActionItem( + text: "Star", + icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/ShapeStar"), color: theme.contextMenu.primaryColor)}, + action: { [weak self] f in + f.dismissWithResult(.default) + if let strongSelf = self { + strongSelf.insertEntity.invoke(DrawingSimpleShapeEntity(shapeType: .star, drawType: .stroke, color: strongSelf.currentColor, lineWidth: 0.15)) + } + } + ) + ), + .action( + ContextMenuActionItem( + text: "Arrow", + icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/ShapeArrow"), color: theme.contextMenu.primaryColor)}, + action: { [weak self] f in + f.dismissWithResult(.default) + if let strongSelf = self { + strongSelf.insertEntity.invoke(DrawingVectorEntity(type: .oneSidedArrow, color: strongSelf.currentColor, lineWidth: 0.5)) + } + } + ) + ) + ] + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme) + let contextController = ContextController(account: self.context.account, presentationData: presentationData, source: .reference(ReferenceContentSource(sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items)))) + self.present(contextController) + } + + func presentBrushModePicker(_ sourceView: UIView) { + let items: [ContextMenuItem] = [ + .action( + ContextMenuActionItem( + text: "Round", + icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/BrushRound"), color: theme.contextMenu.primaryColor)}, + action: { [weak self] f in + f.dismissWithResult(.default) + if let strongSelf = self { + strongSelf.updateBrushMode(.round) + } + } + ) + ), + .action( + ContextMenuActionItem( + text: "Arrow", + icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/BrushArrow"), color: theme.contextMenu.primaryColor)}, + action: { [weak self] f in + f.dismissWithResult(.default) + if let strongSelf = self { + strongSelf.updateBrushMode(.arrow) + } + } + ) + ) + ] + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme) + let contextController = ContextController(account: self.context.account, presentationData: presentationData, source: .reference(ReferenceContentSource(sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items)))) + self.present(contextController) + } + + func presentEraserModePicker(_ sourceView: UIView) { + let items: [ContextMenuItem] = [ + .action( + ContextMenuActionItem( + text: "Eraser", + icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/BrushRound"), color: theme.contextMenu.primaryColor)}, + action: { [weak self] f in + f.dismissWithResult(.default) + self?.updateEraserMode(.bitmap) + } + ) + ), + .action( + ContextMenuActionItem( + text: "Object Eraser", + icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/BrushRemove"), color: theme.contextMenu.primaryColor)}, + action: { [weak self] f in + f.dismissWithResult(.default) + self?.updateEraserMode(.vector) + } + ) + ), + .action( + ContextMenuActionItem( + text: "Background Blur", + icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/BrushBlur"), color: theme.contextMenu.primaryColor)}, + action: { [weak self] f in + f.dismissWithResult(.default) + self?.updateEraserMode(.blur) + } + ) + ) + ] + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme) + let contextController = ContextController(account: self.context.account, presentationData: presentationData, source: .reference(ReferenceContentSource(sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items)))) + self.present(contextController) + } + + func updateCurrentMode(_ mode: Mode) { + self.currentMode = mode + if let selectedEntity = self.selectedEntity { + if selectedEntity is DrawingStickerEntity || selectedEntity is DrawingTextEntity { + self.deselectEntity.invoke(Void()) + } + } + self.updated(transition: .easeInOut(duration: 0.2)) + } + + func addTextEntity() { + let textEntity = DrawingTextEntity(text: "", style: .regular, font: .sanFrancisco, alignment: .center, fontSize: 1.0, color: self.currentColor) + self.insertEntity.invoke(textEntity) + } + + func presentStickerPicker() { + self.currentMode = .sticker + + self.updatePlayback.invoke(false) + let controller = StickerPickerScreen(context: self.context) + controller.completion = { [weak self] file in + self?.updatePlayback.invoke(true) + + if let file = file { + let stickerEntity = DrawingStickerEntity(file: file) + self?.insertEntity.invoke(stickerEntity) + } else { + self?.updateCurrentMode(.drawing) + } + } + self.present(controller) + self.updated(transition: .easeInOut(duration: 0.2)) + } + } + + func makeState() -> State { + return State(context: self.context, updateToolState: self.updateToolState, insertEntity: self.insertEntity, deselectEntity: self.deselectEntity, updatePlayback: self.updatePlayback, present: self.present) + } + + static var body: Body { + let undoButton = Child(Button.self) + + let redoButton = Child(Button.self) + let clearAllButton = Child(Button.self) + + let zoomOutButton = Child(Button.self) + + let tools = Child(ToolsComponent.self) + let modeAndSize = Child(ModeAndSizeComponent.self) + + let colorButton = Child(ColorSwatchComponent.self) + + let textSettings = Child(TextSettingsComponent.self) + + let swatch1Button = Child(ColorSwatchComponent.self) + let swatch2Button = Child(ColorSwatchComponent.self) + let swatch3Button = Child(ColorSwatchComponent.self) + let swatch4Button = Child(ColorSwatchComponent.self) + let swatch5Button = Child(ColorSwatchComponent.self) + let swatch6Button = Child(ColorSwatchComponent.self) + let swatch7Button = Child(ColorSwatchComponent.self) + + let addButton = Child(Button.self) + + let flipButton = Child(Button.self) + let fillButton = Child(Button.self) + let fillButtonTag = GenericComponentViewTag() + + let stickerFlipButton = Child(Button.self) + + let backButton = Child(Button.self) + let doneButton = Child(Button.self) + + let brushModeButton = Child(Button.self) + let brushModeButtonTag = GenericComponentViewTag() + + let textSize = Child(TextSizeSliderComponent.self) + let textCancelButton = Child(Button.self) + let textDoneButton = Child(Button.self) + + let presetColors: [DrawingColor] = [ + DrawingColor(rgb: 0xffffff), + DrawingColor(rgb: 0x000000), + DrawingColor(rgb: 0x106bff), + DrawingColor(rgb: 0x2ecb46), + DrawingColor(rgb: 0xfd8d0e), + DrawingColor(rgb: 0xfc1a4d), + DrawingColor(rgb: 0xaf39ee) + ] + + return { context in + let environment = context.environment[ViewControllerComponentContainer.Environment.self].value + let component = context.component + let state = context.state + let controller = environment.controller + + let previewBrushSize = component.previewBrushSize + let performAction = component.performAction + component.updateState.connect { [weak state] updatedState in + state?.updateDrawingState(updatedState) + } + component.updateColor.connect { [weak state] color in + state?.updateColor(color) + } + component.updateSelectedEntity.connect { [weak state] entity in + state?.updateSelectedEntity(entity) + } + + let apply = component.apply + let dismiss = component.dismiss + + let presentColorPicker = component.presentColorPicker + let presentFastColorPicker = component.presentFastColorPicker + let updateFastColorPickerPan = component.updateFastColorPickerPan + let dismissFastColorPicker = component.dismissFastColorPicker + + if let textEntity = state.selectedEntity as? DrawingTextEntity { + let textSettings = textSettings.update( + component: TextSettingsComponent( + color: nil, + style: DrawingTextStyle(style: textEntity.style), + alignment: DrawingTextAlignment(alignment: textEntity.alignment), + font: DrawingTextFont(font: textEntity.font), + toggleStyle: { [weak state, weak textEntity] in + guard let textEntity = textEntity else { + return + } + var nextStyle: DrawingTextEntity.Style + switch textEntity.style { + case .regular: + nextStyle = .filled + case .filled: + nextStyle = .semi + case .semi: + nextStyle = .stroke + case .stroke: + nextStyle = .regular + } + textEntity.style = nextStyle + if let entityView = textEntity.currentEntityView { + entityView.update() + } + state?.updated(transition: .easeInOut(duration: 0.2)) + }, + toggleAlignment: { [weak state, weak textEntity] in + guard let textEntity = textEntity else { + return + } + var nextAlignment: DrawingTextEntity.Alignment + switch textEntity.alignment { + case .left: + nextAlignment = .center + case .center: + nextAlignment = .right + case .right: + nextAlignment = .left + } + textEntity.alignment = nextAlignment + if let entityView = textEntity.currentEntityView { + entityView.update() + } + state?.updated(transition: .easeInOut(duration: 0.2)) + }, + updateFont: { [weak state, weak textEntity] font in + guard let textEntity = textEntity else { + return + } + textEntity.font = font.font + if let entityView = textEntity.currentEntityView { + entityView.update() + } + state?.updated(transition: .easeInOut(duration: 0.2)) + } + ), + availableSize: CGSize(width: context.availableSize.width - 84.0, height: 44.0), + transition: context.transition + ) + context.add(textSettings + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - environment.safeInsets.bottom - textSettings.size.height / 2.0 - 51.0)) + .appear(.default(scale: false, alpha: true)) + .disappear(.default(scale: false, alpha: true)) + ) + } else if state.currentMode == .sticker { + + } else if state.selectedEntity != nil { + let rightButtonPosition = context.availableSize.width - environment.safeInsets.right - 44.0 / 2.0 - 3.0 + var offsetX: CGFloat = environment.safeInsets.left + 44.0 / 2.0 + 3.0 + let delta: CGFloat = (rightButtonPosition - offsetX) / 7.0 + offsetX += delta + + var delay: Double = 0.0 + let swatch1Button = swatch1Button.update( + component: ColorSwatchComponent( + type: .pallete(state.currentColor == presetColors[0]), + color: presetColors[0], + action: { [weak state] in + state?.updateColor(presetColors[0], animated: true) + } + ), + availableSize: CGSize(width: 33.0, height: 33.0), + transition: context.transition + ) + context.add(swatch1Button + .position(CGPoint(x: offsetX, y: context.availableSize.height - environment.safeInsets.bottom - swatch1Button.size.height / 2.0 - 57.0)) + .appear(Transition.Appear { _, view, transition in + transition.animateScale(view: view, from: 0.1, to: 1.0) + transition.animateAlpha(view: view, from: 0.0, to: 1.0) + }) + .disappear(Transition.Disappear { view, transition, completion in + transition.setScale(view: view, scale: 0.1) + transition.setAlpha(view: view, alpha: 0.0, completion: { _ in + completion() + }) + }) + ) + offsetX += delta + + let swatch2Button = swatch2Button.update( + component: ColorSwatchComponent( + type: .pallete(state.currentColor == presetColors[1]), + color: presetColors[1], + action: { [weak state] in + state?.updateColor(presetColors[1], animated: true) + } + ), + availableSize: CGSize(width: 33.0, height: 33.0), + transition: context.transition + ) + context.add(swatch2Button + .position(CGPoint(x: offsetX, y: context.availableSize.height - environment.safeInsets.bottom - swatch2Button.size.height / 2.0 - 57.0)) + .appear(Transition.Appear { _, view, transition in + transition.animateScale(view: view, from: 0.1, to: 1.0, delay: 0.025) + transition.animateAlpha(view: view, from: 0.0, to: 1.0, delay: 0.025) + }) + .disappear(Transition.Disappear { view, transition, completion in + transition.setScale(view: view, scale: 0.1) + transition.setAlpha(view: view, alpha: 0.0, completion: { _ in + completion() + }) + }) + ) + offsetX += delta + + let swatch3Button = swatch3Button.update( + component: ColorSwatchComponent( + type: .pallete(state.currentColor == presetColors[2]), + color: presetColors[2], + action: { [weak state] in + state?.updateColor(presetColors[2], animated: true) + } + ), + availableSize: CGSize(width: 33.0, height: 33.0), + transition: context.transition + ) + context.add(swatch3Button + .position(CGPoint(x: offsetX, y: context.availableSize.height - environment.safeInsets.bottom - swatch3Button.size.height / 2.0 - 57.0)) + .appear(Transition.Appear { _, view, transition in + transition.animateScale(view: view, from: 0.1, to: 1.0, delay: 0.05) + transition.animateAlpha(view: view, from: 0.0, to: 1.0, delay: 0.05) + }) + .disappear(Transition.Disappear { view, transition, completion in + transition.setScale(view: view, scale: 0.1) + transition.setAlpha(view: view, alpha: 0.0, completion: { _ in + completion() + }) + }) + ) + offsetX += delta + + let swatch4Button = swatch4Button.update( + component: ColorSwatchComponent( + type: .pallete(state.currentColor == presetColors[3]), + color: presetColors[3], + action: { [weak state] in + state?.updateColor(presetColors[3], animated: true) + } + ), + availableSize: CGSize(width: 33.0, height: 33.0), + transition: context.transition + ) + context.add(swatch4Button + .position(CGPoint(x: offsetX, y: context.availableSize.height - environment.safeInsets.bottom - swatch4Button.size.height / 2.0 - 57.0)) + .appear(Transition.Appear { _, view, transition in + transition.animateScale(view: view, from: 0.1, to: 1.0, delay: 0.075) + transition.animateAlpha(view: view, from: 0.0, to: 1.0, delay: 0.075) + }) + .disappear(Transition.Disappear { view, transition, completion in + transition.setScale(view: view, scale: 0.1) + transition.setAlpha(view: view, alpha: 0.0, completion: { _ in + completion() + }) + }) + ) + offsetX += delta + + let swatch5Button = swatch5Button.update( + component: ColorSwatchComponent( + type: .pallete(state.currentColor == presetColors[4]), + color: presetColors[4], + action: { [weak state] in + state?.updateColor(presetColors[4], animated: true) + } + ), + availableSize: CGSize(width: 33.0, height: 33.0), + transition: context.transition + ) + context.add(swatch5Button + .position(CGPoint(x: offsetX, y: context.availableSize.height - environment.safeInsets.bottom - swatch5Button.size.height / 2.0 - 57.0)) + .appear(Transition.Appear { _, view, transition in + transition.animateScale(view: view, from: 0.1, to: 1.0, delay: 0.1) + transition.animateAlpha(view: view, from: 0.0, to: 1.0, delay: 0.1) + }) + .disappear(Transition.Disappear { view, transition, completion in + transition.setScale(view: view, scale: 0.1) + transition.setAlpha(view: view, alpha: 0.0, completion: { _ in + completion() + }) + }) + ) + offsetX += delta + delay += 0.025 + + let swatch6Button = swatch6Button.update( + component: ColorSwatchComponent( + type: .pallete(state.currentColor == presetColors[5]), + color: presetColors[5], + action: { [weak state] in + state?.updateColor(presetColors[5], animated: true) + } + ), + availableSize: CGSize(width: 33.0, height: 33.0), + transition: context.transition + ) + context.add(swatch6Button + .position(CGPoint(x: offsetX, y: context.availableSize.height - environment.safeInsets.bottom - swatch6Button.size.height / 2.0 - 57.0)) + .appear(Transition.Appear { _, view, transition in + transition.animateScale(view: view, from: 0.1, to: 1.0, delay: 0.125) + transition.animateAlpha(view: view, from: 0.0, to: 1.0, delay: 0.125) + }) + .disappear(Transition.Disappear { view, transition, completion in + transition.setScale(view: view, scale: 0.1) + transition.setAlpha(view: view, alpha: 0.0, completion: { _ in + completion() + }) + }) + ) + offsetX += delta + + let swatch7Button = swatch7Button.update( + component: ColorSwatchComponent( + type: .pallete(state.currentColor == presetColors[6]), + color: presetColors[6], + action: { [weak state] in + state?.updateColor(presetColors[6], animated: true) + } + ), + availableSize: CGSize(width: 33.0, height: 33.0), + transition: context.transition + ) + context.add(swatch7Button + .position(CGPoint(x: offsetX, y: context.availableSize.height - environment.safeInsets.bottom - swatch7Button.size.height / 2.0 - 57.0)) + .appear(Transition.Appear { _, view, transition in + transition.animateScale(view: view, from: 0.1, to: 1.0, delay: 0.15) + transition.animateAlpha(view: view, from: 0.0, to: 1.0, delay: 0.15) + }) + .disappear(Transition.Disappear { view, transition, completion in + transition.setScale(view: view, scale: 0.1) + transition.setAlpha(view: view, alpha: 0.0, completion: { _ in + completion() + }) + }) + ) + } else { + let tools = tools.update( + component: ToolsComponent( + state: state.drawingState, + isFocused: state.toolIsFocused, + tag: toolsTag, + toolPressed: { [weak state] tool in + if let state = state { + if state.drawingState.selectedTool == tool, tool != .lasso { + state.updateToolIsFocused(!state.toolIsFocused) + } else { + state.updateSelectedTool(tool) + } + } + }, + toolResized: { [weak state] _, size in + state?.updateBrushSize(size) + if state?.selectedEntity == nil { + previewBrushSize.invoke(size) + } + }, + sizeReleased: { + previewBrushSize.invoke(nil) + } + ), + availableSize: CGSize(width: context.availableSize.width - environment.safeInsets.left - environment.safeInsets.right, height: 120.0), + transition: context.transition + ) + context.add(tools + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - environment.safeInsets.bottom - tools.size.height / 2.0 - 41.0)) + .appear(Transition.Appear({ _, view, transition in + if let view = view as? ToolsComponent.View, !transition.animation.isImmediate { + view.animateIn(completion: {}) + } + })) + .disappear(Transition.Disappear({ view, transition, completion in + if let view = view as? ToolsComponent.View, !transition.animation.isImmediate { + view.animateOut(completion: completion) + } else { + completion() + } + })) + ) + } + + if let textEntity = state.selectedEntity as? DrawingTextEntity, let entityView = textEntity.currentEntityView as? DrawingTextEntityView, entityView.isEditing { + let topInset = environment.safeInsets.top + 31.0 + let textSize = textSize.update( + component: TextSizeSliderComponent( + value: textEntity.fontSize, + updated: { [weak state] size in + state?.updateBrushSize(size) + } + ), + availableSize: CGSize(width: 30.0, height: 240.0), + transition: context.transition + ) + context.add(textSize + .position(CGPoint(x: textSize.size.width / 2.0, y: topInset + (context.availableSize.height - topInset - environment.inputHeight) / 2.0)) + .appear(Transition.Appear { _, view, transition in + transition.animateAlpha(view: view, from: 0.0, to: 1.0) + + transition.animatePosition(view: view, from: CGPoint(x: -33.0, y: 0.0), to: CGPoint(), additive: true) + }) + .disappear(Transition.Disappear { view, transition, completion in + transition.setAlpha(view: view, alpha: 0.0, completion: { _ in + completion() + }) + transition.animatePosition(view: view, from: CGPoint(), to: CGPoint(x: -33.0, y: 0.0), additive: true) + }) + ) + + let textCancelButton = textCancelButton.update( + component: Button( + content: AnyComponent( + Text(text: "Cancel", font: Font.regular(17.0), color: .white) + ), + action: { [weak state] in + if let entity = state?.selectedEntity as? DrawingTextEntity, let entityView = entity.currentEntityView as? DrawingTextEntityView { + entityView.endEditing(reset: true) + } + } + ), + availableSize: CGSize(width: 100.0, height: 30.0), + transition: context.transition + ) + context.add(textCancelButton + .position(CGPoint(x: environment.safeInsets.left + textCancelButton.size.width / 2.0 + 13.0, y: environment.safeInsets.top + 31.0)) + .appear(.default(scale: true, alpha: true)) + .disappear(.default(scale: true, alpha: true)) + ) + + let textDoneButton = textDoneButton.update( + component: Button( + content: AnyComponent( + Text(text: "Done", font: Font.semibold(17.0), color: .white) + ), + action: { [weak state] in + if let entity = state?.selectedEntity as? DrawingTextEntity, let entityView = entity.currentEntityView as? DrawingTextEntityView { + entityView.endEditing() + } + } + ), + availableSize: CGSize(width: 100.0, height: 30.0), + transition: context.transition + ) + context.add(textDoneButton + .position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - textDoneButton.size.width / 2.0 - 13.0, y: environment.safeInsets.top + 31.0)) + .appear(.default(scale: true, alpha: true)) + .disappear(.default(scale: true, alpha: true)) + ) + } else { + let undoButton = undoButton.update( + component: Button( + content: AnyComponent( + Image(image: state.image(.undo)) + ), + isEnabled: state.drawingViewState.canUndo, + action: { + performAction.invoke(.undo) + } + ).minSize(CGSize(width: 44.0, height: 44.0)).tagged(undoButtonTag), + availableSize: CGSize(width: 24.0, height: 24.0), + transition: context.transition + ) + context.add(undoButton + .position(CGPoint(x: environment.safeInsets.left + undoButton.size.width / 2.0 + 2.0, y: environment.safeInsets.top + 31.0)) + .appear(.default(scale: true, alpha: true)) + .disappear(.default(scale: true, alpha: true)) + ) + + if state.drawingViewState.canRedo { + let redoButton = redoButton.update( + component: Button( + content: AnyComponent( + Image(image: state.image(.redo)) + ), + action: { + performAction.invoke(.redo) + } + ).minSize(CGSize(width: 44.0, height: 44.0)), + availableSize: CGSize(width: 24.0, height: 24.0), + transition: context.transition + ) + context.add(redoButton + .position(CGPoint(x: environment.safeInsets.left + undoButton.size.width + 2.0 + redoButton.size.width / 2.0, y: environment.safeInsets.top + 31.0)) + .appear(.default(scale: true, alpha: true)) + .disappear(.default(scale: true, alpha: true)) + ) + } + + let clearAllButton = clearAllButton.update( + component: Button( + content: AnyComponent( + Text(text: "Clear All", font: Font.regular(17.0), color: .white) + ), + isEnabled: state.drawingViewState.canClear, + action: { + performAction.invoke(.clear) + } + ).tagged(clearAllButtonTag), + availableSize: CGSize(width: 100.0, height: 30.0), + transition: context.transition + ) + context.add(clearAllButton + .position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - clearAllButton.size.width / 2.0 - 13.0, y: environment.safeInsets.top + 31.0)) + .appear(.default(scale: true, alpha: true)) + .disappear(.default(scale: true, alpha: true)) + ) + + if state.drawingViewState.canZoomOut { + let zoomOutButton = zoomOutButton.update( + component: Button( + content: AnyComponent( + ZoomOutButtonContent( + title: "Zoom Out", + image: state.image(.zoomOut) + ) + ), + action: { + performAction.invoke(.zoomOut) + } + ).minSize(CGSize(width: 44.0, height: 44.0)), + availableSize: CGSize(width: 120.0, height: 33.0), + transition: .immediate + ) + context.add(zoomOutButton + .position(CGPoint(x: context.availableSize.width / 2.0, y: environment.safeInsets.top + 32.0 - UIScreenPixel)) + .appear(.default(scale: true, alpha: true)) + .disappear(.default(scale: true, alpha: true)) + ) + } + } + + var isEditingSize = false + if state.toolIsFocused { + isEditingSize = true + } else if let entity = state.selectedEntity { + if entity is DrawingSimpleShapeEntity || entity is DrawingVectorEntity || entity is DrawingBubbleEntity { + isEditingSize = true + } + } + + if !state.toolIsFocused { + var color: DrawingColor? + if let entity = state.selectedEntity, !(entity is DrawingTextEntity) && presetColors.contains(entity.color) { + color = nil + } else { + color = state.currentColor + } + + if let _ = state.selectedEntity as? DrawingStickerEntity { + let stickerFlipButton = stickerFlipButton.update( + component: Button( + content: AnyComponent( + Image(image: state.image(.flip)) + ), + action: { [weak state] in + guard let state = state else { + return + } + if let entity = state.selectedEntity as? DrawingStickerEntity { + entity.mirrored = !entity.mirrored + entity.currentEntityView?.update(animated: true) + } + state.updated(transition: .easeInOut(duration: 0.2)) + } + ).minSize(CGSize(width: 44.0, height: 44.0)), + availableSize: CGSize(width: 33.0, height: 33.0), + transition: .immediate + ) + context.add(stickerFlipButton + .position(CGPoint(x: environment.safeInsets.left + stickerFlipButton.size.width / 2.0 + 3.0, y: context.availableSize.height - environment.safeInsets.bottom - stickerFlipButton.size.height / 2.0 - 51.0)) + .appear(.default(scale: true)) + .disappear(.default(scale: true)) + ) + } else { + if [.lasso, .eraser].contains(state.drawingState.selectedTool) { + + } else { + let colorButton = colorButton.update( + component: ColorSwatchComponent( + type: .main, + color: color, + tag: colorButtonTag, + action: { [weak state] in + if let state = state { + presentColorPicker(state.currentColor) + } + }, + holdAction: { + if let controller = controller() as? DrawingScreen, let buttonView = controller.node.componentHost.findTaggedView(tag: colorButtonTag) { + presentFastColorPicker(buttonView) + } + }, + pan: { point in + updateFastColorPickerPan(point) + }, + release: { + dismissFastColorPicker() + } + ), + availableSize: CGSize(width: 44.0, height: 44.0), + transition: context.transition + ) + context.add(colorButton + .position(CGPoint(x: environment.safeInsets.left + colorButton.size.width / 2.0 + 3.0, y: context.availableSize.height - environment.safeInsets.bottom - colorButton.size.height / 2.0 - 51.0)) + .appear(.default(scale: true)) + .disappear(.default(scale: true)) + ) + } + } + } + + var isModeControlEnabled = true + var modeRightInset: CGFloat = 57.0 + if isEditingSize { + if state.toolIsFocused { + let title: String + let image: UIImage? + var isEraser = false + if let mode = state.drawingState.toolState(for: state.drawingState.selectedTool).brushMode { + switch mode { + case .round: + title = "Round" + image = state.image(.round) + case .arrow: + title = "Arrow" + image = state.image(.arrow) + } + } else if let mode = state.drawingState.toolState(for: state.drawingState.selectedTool).eraserMode { + isEraser = true + switch mode { + case .bitmap: + title = "Eraser" + image = state.image(.round) + case .vector: + title = "Object" + image = state.image(.remove) + case .blur: + title = "Blur" + image = state.image(.blur) + } + } else { + title = "" + image = nil + } + + let brushModeButton = brushModeButton.update( + component: Button( + content: AnyComponent( + BrushButtonContent( + title: title, + image: image ?? UIImage() + ) + ), + action: { [weak state] in + guard let controller = controller() as? DrawingScreen else { + return + } + if let buttonView = controller.node.componentHost.findTaggedView(tag: brushModeButtonTag) as? Button.View { + if isEraser { + state?.presentEraserModePicker(buttonView) + } else { + state?.presentBrushModePicker(buttonView) + } + } + } + ).minSize(CGSize(width: 44.0, height: 44.0)).tagged(brushModeButtonTag), + availableSize: CGSize(width: 75.0, height: 33.0), + transition: .immediate + ) + context.add(brushModeButton + .position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - brushModeButton.size.width / 2.0 - 5.0, y: context.availableSize.height - environment.safeInsets.bottom - brushModeButton.size.height / 2.0 - 2.0 - UIScreenPixel)) + .appear(.default(alpha: true)) + .disappear(.default(alpha: true)) + ) + + modeRightInset += 35.0 + } else { + var isFilled = false + if let entity = state.selectedEntity as? DrawingSimpleShapeEntity, case .fill = entity.drawType { + isFilled = true + isModeControlEnabled = false + } else if let entity = state.selectedEntity as? DrawingBubbleEntity, case .fill = entity.drawType { + isFilled = true + isModeControlEnabled = false + } + + if let _ = state.selectedEntity as? DrawingBubbleEntity { + let flipButton = flipButton.update( + component: Button( + content: AnyComponent( + Image(image: state.image(.flip)) + ), + action: { [weak state] in + guard let state = state else { + return + } + if let entity = state.selectedEntity as? DrawingBubbleEntity { + var updatedTailPosition = entity.tailPosition + updatedTailPosition.x = 1.0 - updatedTailPosition.x + entity.tailPosition = updatedTailPosition + entity.currentEntityView?.update() + } + state.updated(transition: .easeInOut(duration: 0.2)) + } + ).minSize(CGSize(width: 44.0, height: 44.0)), + availableSize: CGSize(width: 33.0, height: 33.0), + transition: .immediate + ) + context.add(flipButton + .position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - flipButton.size.width / 2.0 - 3.0 - flipButton.size.width, y: context.availableSize.height - environment.safeInsets.bottom - flipButton.size.height / 2.0 - 2.0 - UIScreenPixel)) + .appear(.default(scale: true)) + .disappear(.default(scale: true)) + ) + modeRightInset += 35.0 + } + + let fillButton = fillButton.update( + component: Button( + content: AnyComponent( + Image(image: state.image(isFilled ? .fill : .stroke)) + ), + action: { [weak state] in + guard let state = state else { + return + } + if let entity = state.selectedEntity as? DrawingSimpleShapeEntity { + if case .fill = entity.drawType { + entity.drawType = .stroke + } else { + entity.drawType = .fill + } + entity.currentEntityView?.update() + } else if let entity = state.selectedEntity as? DrawingBubbleEntity { + if case .fill = entity.drawType { + entity.drawType = .stroke + } else { + entity.drawType = .fill + } + entity.currentEntityView?.update() + } else if let entity = state.selectedEntity as? DrawingVectorEntity { + if case .oneSidedArrow = entity.type { + entity.type = .twoSidedArrow + } else if case .twoSidedArrow = entity.type { + entity.type = .line + } else { + entity.type = .oneSidedArrow + } + entity.currentEntityView?.update() + } + state.updated(transition: .easeInOut(duration: 0.2)) + } + ).minSize(CGSize(width: 44.0, height: 44.0)).tagged(fillButtonTag), + availableSize: CGSize(width: 33.0, height: 33.0), + transition: .immediate + ) + context.add(fillButton + .position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - fillButton.size.width / 2.0 - 3.0, y: context.availableSize.height - environment.safeInsets.bottom - fillButton.size.height / 2.0 - 2.0 - UIScreenPixel)) + .appear(.default(scale: true)) + .disappear(.default(scale: true)) + ) + } + } else { + let addButton = addButton.update( + component: Button( + content: AnyComponent(ZStack([ + AnyComponentWithIdentity( + id: "background", + component: AnyComponent( + BlurredRectangle( + color: UIColor(rgb: 0x888888, alpha: 0.3), + radius: 16.5 + ) + ) + ), + AnyComponentWithIdentity( + id: "icon", + component: AnyComponent( + Image(image: state.image(.add)) + ) + ), + ])), + action: { [weak state] in + guard let controller = controller() as? DrawingScreen, let state = state else { + return + } + switch state.currentMode { + case .drawing: + if let buttonView = controller.node.componentHost.findTaggedView(tag: addButtonTag) as? Button.View { + state.presentShapePicker(buttonView) + } + case .sticker: + state.presentStickerPicker() + case .text: + state.addTextEntity() + } + } + ).minSize(CGSize(width: 44.0, height: 44.0)).tagged(addButtonTag), + availableSize: CGSize(width: 33.0, height: 33.0), + transition: .immediate + ) + context.add(addButton + .position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - addButton.size.width / 2.0 - 3.0, y: context.availableSize.height - environment.safeInsets.bottom - addButton.size.height / 2.0 - 51.0)) + .appear(.default(scale: true)) + .disappear(.default(scale: true)) + ) + + let doneButton = doneButton.update( + component: Button( + content: AnyComponent( + Image(image: state.image(.done)) + ), + action: { + apply.invoke(Void()) + } + ).minSize(CGSize(width: 44.0, height: 44.0)).tagged(doneButtonTag), + availableSize: CGSize(width: 33.0, height: 33.0), + transition: .immediate + ) + context.add(doneButton + .position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - doneButton.size.width / 2.0 - 3.0, y: context.availableSize.height - environment.safeInsets.bottom - doneButton.size.height / 2.0 - 2.0 - UIScreenPixel)) + .appear(Transition.Appear { _, view, transition in + transition.animateScale(view: view, from: 0.1, to: 1.0) + transition.animateAlpha(view: view, from: 0.0, to: 1.0) + + transition.animatePosition(view: view, from: CGPoint(x: 12.0, y: 0.0), to: CGPoint(), additive: true) + }) + .disappear(Transition.Disappear { view, transition, completion in + transition.setScale(view: view, scale: 0.1) + transition.setAlpha(view: view, alpha: 0.0, completion: { _ in + completion() + }) + transition.animatePosition(view: view, from: CGPoint(), to: CGPoint(x: 12.0, y: 0.0), additive: true) + }) + ) + } + + let selectedIndex: Int + switch state.currentMode { + case .drawing: + selectedIndex = 0 + case .sticker: + selectedIndex = 1 + case .text: + selectedIndex = 2 + } + + var selectedSize: CGFloat = 0.0 + if let entity = state.selectedEntity { + selectedSize = entity.lineWidth + } else { + selectedSize = state.drawingState.toolState(for: state.drawingState.selectedTool).size ?? 0.0 + } + + let modeAndSize = modeAndSize.update( + component: ModeAndSizeComponent( + values: ["Draw", "Sticker", "Text"], + sizeValue: selectedSize, + isEditing: isEditingSize, + isEnabled: isModeControlEnabled, + rightInset: modeRightInset - 57.0, + tag: modeTag, + selectedIndex: selectedIndex, + selectionChanged: { [weak state] index in + guard let state = state else { + return + } + switch index { + case 1: + state.presentStickerPicker() + case 2: + state.addTextEntity() + default: + state.updateCurrentMode(.drawing) + } + }, + sizeUpdated: { [weak state] size in + if let state = state { + state.updateBrushSize(size) + if state.selectedEntity == nil { + previewBrushSize.invoke(size) + } + } + }, + sizeReleased: { + previewBrushSize.invoke(nil) + } + ), + availableSize: CGSize(width: context.availableSize.width - 57.0 - modeRightInset, height: context.availableSize.height), + transition: context.transition + ) + context.add(modeAndSize + .position(CGPoint(x: context.availableSize.width / 2.0 - (modeRightInset - 57.0) / 2.0, y: context.availableSize.height - environment.safeInsets.bottom - modeAndSize.size.height / 2.0 - 9.0)) + .opacity(isModeControlEnabled ? 1.0 : 0.4) + ) + + var animatingOut = false + if let appearanceTransition = context.transition.userData(DrawingScreenTransition.self), case .animateOut = appearanceTransition { + animatingOut = true + } + + let deselectEntity = component.deselectEntity + let backButton = backButton.update( + component: Button( + content: AnyComponent( + LottieAnimationComponent( + animation: LottieAnimationComponent.AnimationItem( + name: "media_backToCancel", + mode: .animating(loop: false), + range: isEditingSize || animatingOut ? (0.5, 1.0) : (0.0, 0.5) + ), + colors: ["__allcolors__": .white], + size: CGSize(width: 33.0, height: 33.0) + ) + ), + action: { [weak state] in + if let state = state { + if state.toolIsFocused { + state.updateToolIsFocused(false) + } else if let selectedEntity = state.selectedEntity, !(selectedEntity is DrawingStickerEntity || selectedEntity is DrawingTextEntity) { + deselectEntity.invoke(Void()) + } else { + dismiss.invoke(Void()) + } + } + } + ).minSize(CGSize(width: 44.0, height: 44.0)), + availableSize: CGSize(width: 33.0, height: 33.0), + transition: .immediate + ) + context.add(backButton + .position(CGPoint(x: environment.safeInsets.left + backButton.size.width / 2.0 + 3.0, y: context.availableSize.height - environment.safeInsets.bottom - backButton.size.height / 2.0 - 2.0 - UIScreenPixel)) + ) + + return context.availableSize + } + } +} + +public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController { + fileprivate final class Node: ViewControllerTracingNode, FPSCounterDelegate { + private weak var controller: DrawingScreen? + private let context: AccountContext + private let updateState: ActionSlot + private let updateColor: ActionSlot + private let performAction: ActionSlot + private let updateToolState: ActionSlot + private let updateSelectedEntity: ActionSlot + private let insertEntity: ActionSlot + private let deselectEntity: ActionSlot + private let updatePlayback: ActionSlot + private let previewBrushSize: ActionSlot + private let apply: ActionSlot + private let dismiss: ActionSlot + + fileprivate let componentHost: ComponentView + + private let textEditAccessoryView: UIInputView + private let textEditAccessoryHost: ComponentView + + private var presentationData: PresentationData + private let hapticFeedback = HapticFeedback() + private var validLayout: ContainerViewLayout? + + private let fpsCounter = FPSCounter() + private var fpsLabel: UILabel? + + private var _drawingView: DrawingView? + var drawingView: DrawingView { + if self._drawingView == nil, let controller = self.controller { + self._drawingView = DrawingView(size: controller.size) + self._drawingView?.shouldBegin = { [weak self] _ in + if let strongSelf = self { + if strongSelf._entitiesView?.hasSelection == true { + strongSelf._entitiesView?.selectEntity(nil) + return false + } + return true + } else { + return false + } + } + self._drawingView?.stateUpdated = { [weak self] state in + if let strongSelf = self { + strongSelf.updateState.invoke(state) + } + } + self._drawingView?.requestMenu = { [weak self] elements, rect in + if let strongSelf = self, let drawingView = strongSelf._drawingView { + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + var actions: [ContextMenuAction] = [] + actions.append(ContextMenuAction(content: .text(title: presentationData.strings.Paint_Delete, accessibilityLabel: presentationData.strings.Paint_Delete), action: { [weak self] in + if let strongSelf = self { + strongSelf._drawingView?.removeElements(elements) + } + })) + actions.append(ContextMenuAction(content: .text(title: presentationData.strings.Paint_Duplicate, accessibilityLabel: presentationData.strings.Paint_Duplicate), action: { [weak self] in + if let strongSelf = self { + strongSelf._drawingView?.removeElements(elements) + } + })) + let strokeFrame = drawingView.lassoView.convert(rect, to: strongSelf.view).offsetBy(dx: 0.0, dy: -6.0) + let controller = ContextMenuController(actions: actions) + strongSelf.currentMenuController = controller + strongSelf.controller?.present( + controller, + in: .window(.root), + with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in + if let strongSelf = self { + return (strongSelf, strokeFrame, strongSelf, strongSelf.bounds) + } else { + return nil + } + }) + ) + } + } + self.performAction.connect { [weak self] action in + if let strongSelf = self { + strongSelf._drawingView?.performAction(action) + } + } + self.updateToolState.connect { [weak self] state in + if let strongSelf = self { + strongSelf._drawingView?.updateToolState(state) + } + } + self.previewBrushSize.connect { [weak self] size in + if let strongSelf = self { + strongSelf._drawingView?.setBrushSizePreview(size) + } + } + } + return self._drawingView! + } + + private weak var currentMenuController: ContextMenuController? + private var _entitiesView: DrawingEntitiesView? + var entitiesView: DrawingEntitiesView { + if self._entitiesView == nil, let controller = self.controller { + self._entitiesView = DrawingEntitiesView(context: self.context, size: controller.size, entities: []) + self._drawingView?.entitiesView = self._entitiesView + let entitiesLayer = self.entitiesView.layer + self._drawingView?.getFullImage = { [weak self, weak entitiesLayer] withDrawing in + if let strongSelf = self, let controller = strongSelf.controller, let currentImage = controller.getCurrentImage() { + if withDrawing { + let image = generateImage(controller.size, contextGenerator: { size, context in + let bounds = CGRect(origin: .zero, size: size) + if let cgImage = currentImage.cgImage { + context.draw(cgImage, in: bounds) + } + if let cgImage = strongSelf.drawingView.drawingImage?.cgImage { + context.draw(cgImage, in: bounds) + } + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + entitiesLayer?.render(in: context) + }, opaque: true, scale: 1.0) + return image + } else { + return currentImage + } + } else { + return nil + } + } + self._entitiesView?.selectionContainerView = self.selectionContainerView + self._entitiesView?.selectionChanged = { [weak self] entity in + if let strongSelf = self { + strongSelf.updateSelectedEntity.invoke(entity) + } + } + self._entitiesView?.requestedMenuForEntityView = { [weak self] entityView, isTopmost in + guard let strongSelf = self else { + return + } + if strongSelf.currentMenuController != nil { + if let entityView = entityView as? DrawingTextEntityView { + entityView.beginEditing(accessoryView: strongSelf.textEditAccessoryView) + } + return + } + var actions: [ContextMenuAction] = [] + actions.append(ContextMenuAction(content: .text(title: strongSelf.presentationData.strings.Paint_Delete, accessibilityLabel: strongSelf.presentationData.strings.Paint_Delete), action: { [weak self, weak entityView] in + if let strongSelf = self, let entityView = entityView { + strongSelf.entitiesView.remove(uuid: entityView.entity.uuid) + } + })) + if let entityView = entityView as? DrawingTextEntityView { + actions.append(ContextMenuAction(content: .text(title: strongSelf.presentationData.strings.Paint_Edit, accessibilityLabel: strongSelf.presentationData.strings.Paint_Edit), action: { [weak self, weak entityView] in + if let strongSelf = self, let entityView = entityView { + entityView.beginEditing(accessoryView: strongSelf.textEditAccessoryView) + strongSelf.entitiesView.selectEntity(entityView.entity) + } + })) + } + if !isTopmost { + actions.append(ContextMenuAction(content: .text(title: "Move Forward", accessibilityLabel: "Move Forward"), action: { [weak self, weak entityView] in + if let strongSelf = self, let entityView = entityView { + strongSelf.entitiesView.bringToFront(uuid: entityView.entity.uuid) + } + })) + } + actions.append(ContextMenuAction(content: .text(title: strongSelf.presentationData.strings.Paint_Duplicate, accessibilityLabel: strongSelf.presentationData.strings.Paint_Duplicate), action: { [weak self, weak entityView] in + if let strongSelf = self, let entityView = entityView { + let newEntity = strongSelf.entitiesView.duplicate(entityView.entity) + strongSelf.entitiesView.selectEntity(newEntity) + } + })) + let entityFrame = entityView.convert(entityView.selectionBounds, to: strongSelf.view).offsetBy(dx: 0.0, dy: -6.0) + let controller = ContextMenuController(actions: actions) + strongSelf.currentMenuController = controller + strongSelf.controller?.present( + controller, + in: .window(.root), + with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in + if let strongSelf = self { + return (strongSelf, entityFrame, strongSelf, strongSelf.bounds) + } else { + return nil + } + }) + ) + } + self.insertEntity.connect { [weak self] entity in + if let strongSelf = self, let entitiesView = strongSelf._entitiesView { + entitiesView.prepareNewEntity(entity) + entitiesView.add(entity) + entitiesView.selectEntity(entity) + + if let entityView = entitiesView.getView(for: entity.uuid) as? DrawingTextEntityView { + entityView.beginEditing(accessoryView: strongSelf.textEditAccessoryView) + } + } + } + self.deselectEntity.connect { [weak self] in + if let strongSelf = self, let entitiesView = strongSelf._entitiesView { + entitiesView.selectEntity(nil) + } + } + self.updatePlayback.connect { [weak self] play in + if let strongSelf = self, let entitiesView = strongSelf._entitiesView { + if play { + entitiesView.play() + } else { + entitiesView.pause() + } + } + } + } + return self._entitiesView! + } + + private var _selectionContainerView: DrawingSelectionContainerView? + var selectionContainerView: DrawingSelectionContainerView { + if self._selectionContainerView == nil { + self._selectionContainerView = DrawingSelectionContainerView(frame: .zero) + } + return self._selectionContainerView! + } + + private var _contentWrapperView: PortalSourceView? + var contentWrapperView: PortalSourceView { + if self._contentWrapperView == nil { + self._contentWrapperView = PortalSourceView() + } + return self._contentWrapperView! + } + + init(controller: DrawingScreen, context: AccountContext) { + self.controller = controller + self.context = context + self.updateState = ActionSlot() + self.updateColor = ActionSlot() + self.performAction = ActionSlot() + self.updateToolState = ActionSlot() + self.updateSelectedEntity = ActionSlot() + self.insertEntity = ActionSlot() + self.deselectEntity = ActionSlot() + self.updatePlayback = ActionSlot() + self.previewBrushSize = ActionSlot() + self.apply = ActionSlot() + self.dismiss = ActionSlot() + + self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + + self.componentHost = ComponentView() + + self.textEditAccessoryView = UIInputView(frame: CGRect(origin: .zero, size: CGSize(width: 100.0, height: 44.0)), inputViewStyle: .keyboard) + self.textEditAccessoryHost = ComponentView() + + super.init() + + self.apply.connect { [weak self] _ in + self?.controller?.requestApply() + } + self.dismiss.connect { [weak self] _ in + self?.controller?.requestDismiss() + } + } + + override func didLoad() { + super.didLoad() + + self.view.disablesInteractiveKeyboardGestureRecognizer = true + self.view.disablesInteractiveTransitionGestureRecognizer = true + + if self.fpsLabel == nil { + let fpsLabel = UILabel(frame: CGRect(origin: CGPoint(x: 30.0, y: 10.0), size: CGSize(width: 120.0, height: 44.0))) + fpsLabel.alpha = 0.1 + fpsLabel.textColor = .white +// self.view.addSubview(fpsLabel) + self.fpsLabel = fpsLabel + + self.fpsCounter.delegate = self + self.fpsCounter.startTracking() + } + } + + func fpsCounter(_ counter: FPSCounter, didUpdateFramesPerSecond fps: Int) { + self.fpsLabel?.text = "\(fps)" + } + + func presentEyedropper(dismissed: @escaping () -> Void) { + guard let controller = self.controller else { + return + } + self.entitiesView.pause() + + guard let currentImage = controller.getCurrentImage() else { + return + } + + let sourceImage = generateImage(controller.drawingView.imageSize, contextGenerator: { size, context in + let bounds = CGRect(origin: .zero, size: size) + if let cgImage = currentImage.cgImage { + context.draw(cgImage, in: bounds) + } + if let cgImage = controller.drawingView.drawingImage?.cgImage { + context.draw(cgImage, in: bounds) + } + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + controller.entitiesView.layer.render(in: context) + }, opaque: true, scale: 1.0) + guard let sourceImage = sourceImage else { + return + } + + let eyedropperView = EyedropperView(containerSize: controller.contentWrapperView.frame.size, drawingView: controller.drawingView, sourceImage: sourceImage) + eyedropperView.completed = { [weak self, weak controller] color in + if let strongSelf = self, let controller = controller { + strongSelf.updateColor.invoke(color) + controller.entitiesView.play() + dismissed() + } + } + eyedropperView.frame = controller.contentWrapperView.convert(controller.contentWrapperView.bounds, to: controller.view) + controller.view.addSubview(eyedropperView) + } + + func presentColorPicker(initialColor: DrawingColor, dismissed: @escaping () -> Void = {}) { + guard let controller = self.controller else { + return + } + self.hapticFeedback.impact(.medium) + let colorController = ColorPickerScreen(context: self.context, initialColor: initialColor, updated: { [weak self] color in + self?.updateColor.invoke(color) + }, openEyedropper: { [weak self] in + self?.presentEyedropper(dismissed: dismissed) + }, dismissed: { + dismissed() + }) + controller.present(colorController, in: .window(.root)) + } + + private var fastColorPickerView: ColorSpectrumPickerView? + func presentFastColorPicker(sourceView: UIView) { + guard self.fastColorPickerView == nil, let superview = sourceView.superview else { + return + } + + self.hapticFeedback.impact(.medium) + + let size = CGSize(width: min(350.0, superview.frame.width - 8.0 - 24.0), height: 296.0) + + let fastColorPickerView = ColorSpectrumPickerView(frame: CGRect(origin: CGPoint(x: sourceView.frame.minX + 5.0, y: sourceView.frame.maxY - size.height - 6.0), size: size)) + fastColorPickerView.selected = { [weak self] color in + self?.updateColor.invoke(color) + } + let _ = fastColorPickerView.updateLayout(size: size, selectedColor: nil) + sourceView.superview?.addSubview(fastColorPickerView) + + fastColorPickerView.animateIn() + + self.fastColorPickerView = fastColorPickerView + } + + func updateFastColorPickerPan(_ point: CGPoint) { + guard let fastColorPickerView = self.fastColorPickerView else { + return + } + fastColorPickerView.handlePan(point: point) + } + + func dismissFastColorPicker() { + guard let fastColorPickerView = self.fastColorPickerView else { + return + } + self.fastColorPickerView = nil + fastColorPickerView.animateOut(completion: { [weak fastColorPickerView] in + fastColorPickerView?.removeFromSuperview() + }) + } + + func animateIn() { + if let buttonView = self.componentHost.findTaggedView(tag: undoButtonTag) { + buttonView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + buttonView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3) + } + if let buttonView = self.componentHost.findTaggedView(tag: clearAllButtonTag) { + buttonView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + buttonView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3) + } + if let buttonView = self.componentHost.findTaggedView(tag: addButtonTag) { + buttonView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + buttonView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3) + } + } + + func animateOut(completion: @escaping () -> Void) { + if let layout = self.validLayout { + self.containerLayoutUpdated(layout: layout, animateOut: true, transition: .easeInOut(duration: 0.2)) + } + + if let buttonView = self.componentHost.findTaggedView(tag: undoButtonTag) { + buttonView.alpha = 0.0 + buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + buttonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3) + } + if let buttonView = self.componentHost.findTaggedView(tag: clearAllButtonTag) { + buttonView.alpha = 0.0 + buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + buttonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3) + } + if let view = self.componentHost.findTaggedView(tag: colorButtonTag) as? ColorSwatchComponent.View { + view.animateOut() + } + if let buttonView = self.componentHost.findTaggedView(tag: addButtonTag) { + buttonView.alpha = 0.0 + buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + buttonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3) + } + if let view = self.componentHost.findTaggedView(tag: toolsTag) as? ToolsComponent.View { + view.animateOut(completion: {}) + } + if let view = self.componentHost.findTaggedView(tag: modeTag) as? ModeAndSizeComponent.View { + view.animateOut() + } + if let buttonView = self.componentHost.findTaggedView(tag: doneButtonTag) { + buttonView.alpha = 0.0 + buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let result = super.hitTest(point, with: event) + if result == self.componentHost.view { + return nil + } + return result + } + + func containerLayoutUpdated(layout: ContainerViewLayout, animateOut: Bool = false, transition: Transition) { + let isFirstTime = self.validLayout == nil + self.validLayout = layout + + let environment = ViewControllerComponentContainer.Environment( + statusBarHeight: layout.statusBarHeight ?? 0.0, + navigationHeight: 0.0, + safeInsets: UIEdgeInsets(top: layout.intrinsicInsets.top + layout.safeInsets.top, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom + layout.safeInsets.bottom, right: layout.safeInsets.right), + inputHeight: layout.inputHeight ?? 0.0, + metrics: layout.metrics, + deviceMetrics: layout.deviceMetrics, + isVisible: true, + theme: self.presentationData.theme, + strings: self.presentationData.strings, + dateTimeFormat: self.presentationData.dateTimeFormat, + controller: { [weak self] in + return self?.controller + } + ) + + var transition = transition + if isFirstTime { + transition = transition.withUserData(DrawingScreenTransition.animateIn) + } else if animateOut { + transition = transition.withUserData(DrawingScreenTransition.animateOut) + } + + let componentSize = self.componentHost.update( + transition: transition, + component: AnyComponent( + DrawingScreenComponent( + context: self.context, + present: { [weak self] c in + self?.controller?.present(c, in: .window(.root)) + }, + updateState: self.updateState, + updateColor: self.updateColor, + performAction: self.performAction, + updateToolState: self.updateToolState, + updateSelectedEntity: self.updateSelectedEntity, + insertEntity: self.insertEntity, + deselectEntity: self.deselectEntity, + updatePlayback: self.updatePlayback, + previewBrushSize: self.previewBrushSize, + apply: self.apply, + dismiss: self.dismiss, + presentColorPicker: { [weak self] initialColor in + self?.presentColorPicker(initialColor: initialColor) + }, + presentFastColorPicker: { [weak self] sourceView in + self?.presentFastColorPicker(sourceView: sourceView) + }, + updateFastColorPickerPan: { [weak self] point in + self?.updateFastColorPickerPan(point) + }, + dismissFastColorPicker: { [weak self] in + self?.dismissFastColorPicker() + } + ) + ), + environment: { + environment + }, + forceUpdate: animateOut, + containerSize: layout.size + ) + if let componentView = self.componentHost.view { + if componentView.superview == nil { + self.view.insertSubview(componentView, at: 0) + componentView.clipsToBounds = true + } + + let componentFrame = CGRect(origin: .zero, size: componentSize) + transition.setFrame(view: componentView, frame: CGRect(origin: componentFrame.origin, size: CGSize(width: componentFrame.width, height: componentFrame.height))) + + if isFirstTime { + self.animateIn() + } + } + + if let entityView = self.entitiesView.selectedEntityView as? DrawingTextEntityView, let textEntity = entityView.entity as? DrawingTextEntity { + var isFirstTime = true + if let componentView = self.textEditAccessoryHost.view, componentView.superview != nil { + isFirstTime = false + } + let accessorySize = self.textEditAccessoryHost.update( + transition: isFirstTime ? .immediate : .easeInOut(duration: 0.2), + component: AnyComponent( + TextSettingsComponent( + color: textEntity.color, + style: DrawingTextStyle(style: textEntity.style), + alignment: DrawingTextAlignment(alignment: textEntity.alignment), + font: DrawingTextFont(font: textEntity.font), + presentColorPicker: { [weak self] in + guard let strongSelf = self, let entityView = strongSelf.entitiesView.selectedEntityView as? DrawingTextEntityView, let textEntity = entityView.entity as? DrawingTextEntity else { + return + } + entityView.suspendEditing() + self?.presentColorPicker(initialColor: textEntity.color, dismissed: { + entityView.resumeEditing() + }) + }, + presentFastColorPicker: { [weak self] buttonTag in + if let buttonView = self?.textEditAccessoryHost.findTaggedView(tag: buttonTag) { + self?.presentFastColorPicker(sourceView: buttonView) + } + }, + updateFastColorPickerPan: { [weak self] point in + self?.updateFastColorPickerPan(point) + }, + dismissFastColorPicker: { [weak self] in + self?.dismissFastColorPicker() + }, + toggleStyle: { [weak self] in + guard let strongSelf = self, let entityView = strongSelf.entitiesView.selectedEntityView as? DrawingTextEntityView, let textEntity = entityView.entity as? DrawingTextEntity else { + return + } + var nextStyle: DrawingTextEntity.Style + switch textEntity.style { + case .regular: + nextStyle = .filled + case .filled: + nextStyle = .semi + case .semi: + nextStyle = .stroke + case .stroke: + nextStyle = .regular + } + textEntity.style = nextStyle + entityView.update() + + if let layout = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout: layout, transition: .immediate) + } + }, + toggleAlignment: { [weak self] in + guard let strongSelf = self, let entityView = strongSelf.entitiesView.selectedEntityView as? DrawingTextEntityView, let textEntity = entityView.entity as? DrawingTextEntity else { + return + } + var nextAlignment: DrawingTextEntity.Alignment + switch textEntity.alignment { + case .left: + nextAlignment = .center + case .center: + nextAlignment = .right + case .right: + nextAlignment = .left + } + textEntity.alignment = nextAlignment + entityView.update() + + if let layout = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout: layout, transition: .immediate) + } + }, + updateFont: { [weak self] font in + guard let strongSelf = self, let entityView = strongSelf.entitiesView.selectedEntityView as? DrawingTextEntityView, let textEntity = entityView.entity as? DrawingTextEntity else { + return + } + textEntity.font = font.font + entityView.update() + + if let layout = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout: layout, transition: .immediate) + } + } + ) + ), + environment: {}, + forceUpdate: true, + containerSize: CGSize(width: layout.size.width, height: 44.0) + ) + if let componentView = self.textEditAccessoryHost.view { + if componentView.superview == nil { + self.textEditAccessoryView.addSubview(componentView) + } + UIView.performWithoutAnimation { + self.textEditAccessoryView.frame = CGRect(origin: .zero, size: accessorySize) + componentView.frame = CGRect(origin: .zero, size: accessorySize) + } + } + } + } + } + + fileprivate var node: Node { + return self.displayNode as! Node + } + + private let context: AccountContext + private let size: CGSize + + public var requestDismiss: (() -> Void)! + public var requestApply: (() -> Void)! + public var getCurrentImage: (() -> UIImage?)! + + public init(context: AccountContext, size: CGSize) { + self.context = context + self.size = size + + super.init(navigationBarPresentationData: nil) + + self.statusBar.statusBarStyle = .Hide + } + + public var drawingView: DrawingView { + return self.node.drawingView + } + + public var entitiesView: DrawingEntitiesView { + return self.node.entitiesView + } + + public var selectionContainerView: DrawingSelectionContainerView { + return self.node.selectionContainerView + } + + public var contentWrapperView: PortalSourceView { + return self.node.contentWrapperView + } + + required public init(coder: NSCoder) { + preconditionFailure() + } + + override public func loadDisplayNode() { + self.displayNode = Node(controller: self, context: self.context) + + super.displayNodeDidLoad() + } + + public func generateResultData() -> TGPaintingData! { + if self.drawingView.isEmpty && self.entitiesView.entities.isEmpty { + return nil + } + + let paintingImage = generateImage(self.drawingView.imageSize, contextGenerator: { size, context in + let bounds = CGRect(origin: .zero, size: size) + context.clear(bounds) + if let cgImage = self.drawingView.drawingImage?.cgImage { + context.draw(cgImage, in: bounds) + } + }, opaque: false, scale: 1.0) + + var hasAnimatedEntities = false + var legacyEntities: [TGPhotoPaintEntity] = [] + + for entity in self.entitiesView.entities { + if entity.isAnimated { + hasAnimatedEntities = true + } + if let entity = entity as? DrawingStickerEntity { + let coder = PostboxEncoder() + coder.encodeRootObject(entity.file) + + let baseSize = max(10.0, min(entity.referenceDrawingSize.width, entity.referenceDrawingSize.height) * 0.38) + if let stickerEntity = TGPhotoPaintStickerEntity(document: coder.makeData(), baseSize: CGSize(width: baseSize, height: baseSize), animated: entity.isAnimated) { + stickerEntity.position = entity.position + stickerEntity.scale = entity.scale + stickerEntity.angle = entity.rotation + legacyEntities.append(stickerEntity) + } + } else if let entity = entity as? DrawingTextEntity, let view = self.entitiesView.getView(for: entity.uuid) as? DrawingTextEntityView { + let textEntity = TGPhotoPaintStaticEntity() + textEntity.position = entity.position + textEntity.angle = entity.rotation + textEntity.renderImage = view.getRenderImage() + legacyEntities.append(textEntity) + } else if let _ = entity as? DrawingSimpleShapeEntity { + + } else if let _ = entity as? DrawingBubbleEntity { + + } else if let _ = entity as? DrawingVectorEntity { + + } + } + + let finalImage = generateImage(self.drawingView.imageSize, contextGenerator: { size, context in + let bounds = CGRect(origin: .zero, size: size) + context.clear(bounds) + if let cgImage = paintingImage?.cgImage { + context.draw(cgImage, in: bounds) + } + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + + //hide animated + self.entitiesView.layer.render(in: context) + }, opaque: false, scale: 1.0) + + var image = paintingImage + var stillImage: UIImage? + if hasAnimatedEntities { + stillImage = finalImage + } else { + image = finalImage + } + + return TGPaintingData(painting: nil, image: image, stillImage: stillImage, entities: legacyEntities, undoManager: nil) + } + + public func resultImage() -> UIImage! { + let image = generateImage(self.drawingView.imageSize, contextGenerator: { size, context in + let bounds = CGRect(origin: .zero, size: size) + context.clear(bounds) + if let cgImage = self.drawingView.drawingImage?.cgImage { + context.draw(cgImage, in: bounds) + } + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + self.entitiesView.layer.render(in: context) + }, opaque: false, scale: 1.0) + return image + } + + public func animateOut(_ completion: (() -> Void)!) { + self.selectionContainerView.alpha = 0.0 + + self.node.animateOut(completion: completion) + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + (self.displayNode as! Node).containerLayoutUpdated(layout: layout, transition: Transition(transition)) + } + + public func adapterContainerLayoutUpdatedSize(_ size: CGSize, intrinsicInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, statusBarHeight: CGFloat, inputHeight: CGFloat, animated: Bool) { + let layout = ContainerViewLayout( + size: size, + metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact), + deviceMetrics: DeviceMetrics(screenSize: size, scale: UIScreen.main.scale, statusBarHeight: statusBarHeight, onScreenNavigationHeight: nil), + intrinsicInsets: intrinsicInsets, + safeInsets: safeInsets, + additionalInsets: .zero, + statusBarHeight: statusBarHeight, + inputHeight: inputHeight, + inputHeightIsInteractivellyChanging: false, + inVoiceOver: false + ) + self.containerLayoutUpdated(layout, transition: animated ? .animated(duration: 0.3, curve: .easeInOut) : .immediate) + } +} diff --git a/submodules/DrawingUI/Sources/DrawingSimpleShapeEntity.swift b/submodules/DrawingUI/Sources/DrawingSimpleShapeEntity.swift new file mode 100644 index 0000000000..99d8ac9af9 --- /dev/null +++ b/submodules/DrawingUI/Sources/DrawingSimpleShapeEntity.swift @@ -0,0 +1,433 @@ +import Foundation +import UIKit +import Display +import AccountContext + +final class DrawingSimpleShapeEntity: DrawingEntity { + public enum ShapeType { + case rectangle + case ellipse + case star + } + + public enum DrawType { + case fill + case stroke + } + + let uuid: UUID + let isAnimated: Bool + + var shapeType: ShapeType + var drawType: DrawType + var color: DrawingColor + var lineWidth: CGFloat + + var referenceDrawingSize: CGSize + var position: CGPoint + var size: CGSize + var rotation: CGFloat + + init(shapeType: ShapeType, drawType: DrawType, color: DrawingColor, lineWidth: CGFloat) { + self.uuid = UUID() + self.isAnimated = false + + self.shapeType = shapeType + self.drawType = drawType + self.color = color + self.lineWidth = lineWidth + + self.referenceDrawingSize = .zero + self.position = .zero + self.size = CGSize(width: 1.0, height: 1.0) + self.rotation = 0.0 + } + + var center: CGPoint { + return self.position + } + + func duplicate() -> DrawingEntity { + let newEntity = DrawingSimpleShapeEntity(shapeType: self.shapeType, drawType: self.drawType, color: self.color, lineWidth: self.lineWidth) + newEntity.referenceDrawingSize = self.referenceDrawingSize + newEntity.position = self.position + newEntity.size = self.size + newEntity.rotation = self.rotation + return newEntity + } + + weak var currentEntityView: DrawingEntityView? + func makeView(context: AccountContext) -> DrawingEntityView { + let entityView = DrawingSimpleShapeEntityView(context: context, entity: self) + self.currentEntityView = entityView + return entityView + } +} + +final class DrawingSimpleShapeEntityView: DrawingEntityView { + private var shapeEntity: DrawingSimpleShapeEntity { + return self.entity as! DrawingSimpleShapeEntity + } + + private var currentShape: DrawingSimpleShapeEntity.ShapeType? + private var currentSize: CGSize? + + private let shapeLayer = SimpleShapeLayer() + + init(context: AccountContext, entity: DrawingSimpleShapeEntity) { + super.init(context: context, entity: entity) + + self.layer.addSublayer(self.shapeLayer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func update(animated: Bool) { + let shapeType = self.shapeEntity.shapeType + let size = self.shapeEntity.size + + self.center = self.shapeEntity.position + self.bounds = CGRect(origin: .zero, size: size) + self.transform = CGAffineTransformMakeRotation(self.shapeEntity.rotation) + + if shapeType != self.currentShape || size != self.currentSize { + self.currentShape = shapeType + self.currentSize = size + self.shapeLayer.frame = self.bounds + + switch shapeType { + case .rectangle: + self.shapeLayer.path = CGPath(rect: CGRect(origin: .zero, size: size), transform: nil) + case .ellipse: + self.shapeLayer.path = CGPath(ellipseIn: CGRect(origin: .zero, size: size), transform: nil) + case .star: + self.shapeLayer.path = CGPath.star(in: CGRect(origin: .zero, size: size), extrusion: size.width * 0.2, points: 5) + } + } + + switch self.shapeEntity.drawType { + case .fill: + self.shapeLayer.fillColor = self.shapeEntity.color.toCGColor() + self.shapeLayer.strokeColor = UIColor.clear.cgColor + case .stroke: + let minLineWidth = max(10.0, min(self.shapeEntity.referenceDrawingSize.width, self.shapeEntity.referenceDrawingSize.height) * 0.02) + let maxLineWidth = max(10.0, min(self.shapeEntity.referenceDrawingSize.width, self.shapeEntity.referenceDrawingSize.height) * 0.1) + let lineWidth = minLineWidth + (maxLineWidth - minLineWidth) * self.shapeEntity.lineWidth + + self.shapeLayer.fillColor = UIColor.clear.cgColor + self.shapeLayer.strokeColor = self.shapeEntity.color.toCGColor() + self.shapeLayer.lineWidth = lineWidth + } + + super.update(animated: animated) + } + + fileprivate var visualLineWidth: CGFloat { + return self.shapeLayer.lineWidth + } + + override func precisePoint(inside point: CGPoint) -> Bool { + if case .stroke = self.shapeEntity.drawType, var path = self.shapeLayer.path { + path = path.copy(strokingWithWidth: 20.0, lineCap: .square, lineJoin: .bevel, miterLimit: 0.0) + if path.contains(point) { + return true + } else { + return false + } + } else { + return super.precisePoint(inside: point) + } + } + + override func updateSelectionView() { + super.updateSelectionView() + + guard let selectionView = self.selectionView as? DrawingSimpleShapeEntititySelectionView else { + return + } + +// let scale = self.superview?.superview?.layer.value(forKeyPath: "transform.scale.x") as? CGFloat ?? 1.0 +// selectionView.scale = scale + + selectionView.transform = CGAffineTransformMakeRotation(self.shapeEntity.rotation) + } + + override func makeSelectionView() -> DrawingEntitySelectionView { + if let selectionView = self.selectionView { + return selectionView + } + let selectionView = DrawingSimpleShapeEntititySelectionView() + selectionView.entityView = self + return selectionView + } +} + +func gestureIsTracking(_ gestureRecognizer: UIPanGestureRecognizer) -> Bool { + return [.began, .changed].contains(gestureRecognizer.state) +} + +final class DrawingSimpleShapeEntititySelectionView: DrawingEntitySelectionView, UIGestureRecognizerDelegate { + private let leftHandle = SimpleShapeLayer() + private let topLeftHandle = SimpleShapeLayer() + private let topHandle = SimpleShapeLayer() + private let topRightHandle = SimpleShapeLayer() + private let rightHandle = SimpleShapeLayer() + private let bottomLeftHandle = SimpleShapeLayer() + private let bottomHandle = SimpleShapeLayer() + private let bottomRightHandle = SimpleShapeLayer() + + private var panGestureRecognizer: UIPanGestureRecognizer! + + override init(frame: CGRect) { + let handleBounds = CGRect(origin: .zero, size: entitySelectionViewHandleSize) + let handles = [ + self.leftHandle, + self.topLeftHandle, + self.topHandle, + self.topRightHandle, + self.rightHandle, + self.bottomLeftHandle, + self.bottomHandle, + self.bottomRightHandle + ] + + super.init(frame: frame) + + self.backgroundColor = .clear + self.isOpaque = false + + 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 + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + var scale: CGFloat = 1.0 { + didSet { + self.setNeedsLayout() + } + } + + override var selectionInset: CGFloat { + return 5.5 + } + + override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + + private var currentHandle: CALayer? + @objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { + guard let entityView = self.entityView, let entity = entityView.entity as? DrawingSimpleShapeEntity else { + return + } + let isAspectLocked = [.star].contains(entity.shapeType) + let location = gestureRecognizer.location(in: self) + + switch gestureRecognizer.state { + case .began: + 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) + + var updatedSize = entity.size + var updatedPosition = entity.position + + if self.currentHandle === self.leftHandle { + let deltaX = delta.x * cos(entity.rotation) + let deltaY = delta.x * sin(entity.rotation) + + updatedSize.width -= deltaX + updatedPosition.x -= deltaX * -0.5 + updatedPosition.y -= deltaY * -0.5 + + if isAspectLocked { + updatedSize.height -= delta.x + } + } else if self.currentHandle === self.rightHandle { + let deltaX = delta.x * cos(entity.rotation) + let deltaY = delta.x * sin(entity.rotation) + + updatedSize.width += deltaX + updatedPosition.x += deltaX * 0.5 + updatedPosition.y += deltaY * 0.5 + if isAspectLocked { + updatedSize.height += delta.x + } + } else if self.currentHandle === self.topHandle { + let deltaX = delta.y * sin(entity.rotation) + let deltaY = delta.y * cos(entity.rotation) + + updatedSize.height -= deltaY + updatedPosition.x += deltaX * 0.5 + updatedPosition.y += deltaY * 0.5 + if isAspectLocked { + updatedSize.width -= delta.y + } + } else if self.currentHandle === self.bottomHandle { + let deltaX = delta.y * sin(entity.rotation) + let deltaY = delta.y * cos(entity.rotation) + + updatedSize.height += deltaY + updatedPosition.x += deltaX * 0.5 + updatedPosition.y += deltaY * 0.5 + if isAspectLocked { + updatedSize.width += delta.y + } + } else if self.currentHandle === self.topLeftHandle { + var delta = delta + if isAspectLocked { + delta = CGPoint(x: delta.x, y: delta.x) + } + updatedSize.width -= delta.x + updatedPosition.x -= delta.x * -0.5 + updatedSize.height -= delta.y + updatedPosition.y += delta.y * 0.5 + } else if self.currentHandle === self.topRightHandle { + var delta = delta + if isAspectLocked { + delta = CGPoint(x: delta.x, y: -delta.x) + } + updatedSize.width += delta.x + updatedPosition.x += delta.x * 0.5 + updatedSize.height -= delta.y + updatedPosition.y += delta.y * 0.5 + } else if self.currentHandle === self.bottomLeftHandle { + var delta = delta + if isAspectLocked { + delta = CGPoint(x: delta.x, y: -delta.x) + } + updatedSize.width -= delta.x + updatedPosition.x -= delta.x * -0.5 + updatedSize.height += delta.y + updatedPosition.y += delta.y * 0.5 + } else if self.currentHandle === self.bottomRightHandle { + var delta = delta + if isAspectLocked { + delta = CGPoint(x: delta.x, y: delta.x) + } + updatedSize.width += delta.x + updatedPosition.x += delta.x * 0.5 + updatedSize.height += delta.y + updatedPosition.y += delta.y * 0.5 + } else if self.currentHandle === self.layer { + updatedPosition.x += delta.x + updatedPosition.y += delta.y + } + + entity.size = updatedSize + entity.position = updatedPosition + entityView.update() + + gestureRecognizer.setTranslation(.zero, in: entityView) + case .ended: + break + default: + break + } + } + + override func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) { + guard let entityView = self.entityView, let entity = entityView.entity as? DrawingSimpleShapeEntity else { + return + } + + switch gestureRecognizer.state { + case .began, .changed: + let scale = gestureRecognizer.scale + entity.size = CGSize(width: entity.size.width * scale, height: entity.size.height * scale) + entityView.update() + + gestureRecognizer.scale = 1.0 + default: + break + } + } + + override func handleRotate(_ gestureRecognizer: UIRotationGestureRecognizer) { + guard let entityView = self.entityView, let entity = entityView.entity as? DrawingSimpleShapeEntity 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() { + var inset = self.selectionInset + if let entityView = self.entityView as? DrawingSimpleShapeEntityView, let entity = entityView.entity as? DrawingSimpleShapeEntity, case .star = entity.shapeType { + inset -= entityView.visualLineWidth / 2.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.topLeftHandle, + self.topHandle, + self.topRightHandle, + self.rightHandle, + self.bottomLeftHandle, + self.bottomHandle, + self.bottomRightHandle + ] + + for handle in handles { + handle.path = handlePath + handle.bounds = bounds + handle.lineWidth = lineWidth + } + + self.topLeftHandle.position = CGPoint(x: inset, y: inset) + self.topHandle.position = CGPoint(x: self.bounds.midX, y: inset) + self.topRightHandle.position = CGPoint(x: self.bounds.maxX - inset, y: inset) + self.leftHandle.position = CGPoint(x: inset, y: self.bounds.midY) + self.rightHandle.position = CGPoint(x: self.bounds.maxX - inset, y: self.bounds.midY) + self.bottomLeftHandle.position = CGPoint(x: inset, y: self.bounds.maxY - inset) + self.bottomHandle.position = CGPoint(x: self.bounds.midX, y: self.bounds.maxY - inset) + self.bottomRightHandle.position = CGPoint(x: self.bounds.maxX - inset, y: self.bounds.maxY - inset) + } + + var isTracking: Bool { + return gestureIsTracking(self.panGestureRecognizer) + } +} diff --git a/submodules/DrawingUI/Sources/DrawingStickerEntity.swift b/submodules/DrawingUI/Sources/DrawingStickerEntity.swift new file mode 100644 index 0000000000..f591097401 --- /dev/null +++ b/submodules/DrawingUI/Sources/DrawingStickerEntity.swift @@ -0,0 +1,458 @@ +import Foundation +import UIKit +import Display +import SwiftSignalKit +import TelegramCore +import AnimatedStickerNode +import TelegramAnimatedStickerNode +import StickerResources +import AccountContext + +final class DrawingStickerEntity: DrawingEntity { + let uuid: UUID + let isAnimated: Bool + let file: TelegramMediaFile + + var referenceDrawingSize: CGSize + var position: CGPoint + var scale: CGFloat + var rotation: CGFloat + var mirrored: Bool + + var color: DrawingColor = DrawingColor.clear + var lineWidth: CGFloat = 0.0 + + init(file: TelegramMediaFile) { + self.uuid = UUID() + self.isAnimated = file.isAnimatedSticker + + self.file = file + + self.referenceDrawingSize = .zero + self.position = CGPoint() + self.scale = 1.0 + self.rotation = 0.0 + self.mirrored = false + } + + var center: CGPoint { + return self.position + } + + func duplicate() -> DrawingEntity { + let newEntity = DrawingStickerEntity(file: self.file) + newEntity.referenceDrawingSize = self.referenceDrawingSize + newEntity.position = self.position + newEntity.scale = self.scale + newEntity.rotation = self.rotation + newEntity.mirrored = self.mirrored + return newEntity + } + + weak var currentEntityView: DrawingEntityView? + func makeView(context: AccountContext) -> DrawingEntityView { + let entityView = DrawingStickerEntityView(context: context, entity: self) + self.currentEntityView = entityView + return entityView + } +} + +final class DrawingStickerEntityView: DrawingEntityView { + private var stickerEntity: DrawingStickerEntity { + return self.entity as! DrawingStickerEntity + } + + var started: ((Double) -> Void)? + + private var currentSize: CGSize? + private var dimensions: CGSize? + + private let imageNode: TransformImageNode + private var animationNode: AnimatedStickerNode? + + private var didSetUpAnimationNode = false + private let stickerFetchedDisposable = MetaDisposable() + private let cachedDisposable = MetaDisposable() + + private var isVisible = true + private var isPlaying = false + + init(context: AccountContext, entity: DrawingStickerEntity) { + self.imageNode = TransformImageNode() + + super.init(context: context, entity: entity) + + self.addSubview(self.imageNode.view) + + self.setup() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.stickerFetchedDisposable.dispose() + self.cachedDisposable.dispose() + } + + private var file: TelegramMediaFile { + return (self.entity as! DrawingStickerEntity).file + } + + private func setup() { + if let dimensions = self.file.dimensions { + if self.file.isAnimatedSticker || self.file.isVideoSticker { + if self.animationNode == nil { + let animationNode = DefaultAnimatedStickerNodeImpl() + animationNode.autoplay = false + self.animationNode = animationNode + animationNode.started = { [weak self, weak animationNode] in + self?.imageNode.isHidden = true + + if let animationNode = animationNode { + let _ = (animationNode.status + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] status in + self?.started?(status.duration) + }) + } + } + self.addSubnode(animationNode) + } + let dimensions = self.file.dimensions ?? PixelDimensions(width: 512, height: 512) + self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: self.context.account.postbox, file: self.file, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 256.0, height: 256.0)))) + self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: self.context.account, fileReference: stickerPackFileReference(self.file), resource: self.file.resource).start()) + } else { + if let animationNode = self.animationNode { + animationNode.visibility = false + self.animationNode = nil + animationNode.removeFromSupernode() + self.imageNode.isHidden = false + self.didSetUpAnimationNode = false + } + self.imageNode.setSignal(chatMessageSticker(account: self.context.account, file: self.file, small: false, synchronousLoad: false)) + self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: self.context.account, fileReference: stickerPackFileReference(self.file), resource: chatMessageStickerResource(file: self.file, small: false)).start()) + } + + self.dimensions = dimensions.cgSize + self.setNeedsLayout() + } + } + + override func play() { + self.isVisible = true + self.applyVisibility() + } + + override func pause() { + self.isVisible = false + self.applyVisibility() + } + + override func seek(to timestamp: Double) { + self.isVisible = false + self.isPlaying = false + self.animationNode?.seekTo(.timestamp(timestamp)) + } + + override func resetToStart() { + self.isVisible = false + self.isPlaying = false + self.animationNode?.seekTo(.timestamp(0.0)) + } + + override func updateVisibility(_ visibility: Bool) { + self.isVisible = visibility + self.applyVisibility() + } + + private func applyVisibility() { + let isPlaying = self.isVisible + if self.isPlaying != isPlaying { + self.isPlaying = isPlaying + + if isPlaying && !self.didSetUpAnimationNode { + self.didSetUpAnimationNode = true + let dimensions = self.file.dimensions ?? PixelDimensions(width: 512, height: 512) + let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 384.0, height: 384.0)) + let source = AnimatedStickerResourceSource(account: self.context.account, resource: self.file.resource, isVideo: self.file.isVideoSticker) + self.animationNode?.setup(source: source, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), playbackMode: .loop, mode: .direct(cachePathPrefix: nil)) + + self.cachedDisposable.set((source.cachedDataPath(width: 384, height: 384) + |> deliverOn(Queue.concurrentDefaultQueue())).start()) + } + self.animationNode?.visibility = isPlaying + } + } + + private var didApplyVisibility = false + override func layoutSubviews() { + super.layoutSubviews() + + let size = self.bounds.size + + if size.width > 0 && self.currentSize != size { + self.currentSize = size + + let sideSize: CGFloat = size.width + let boundingSize = CGSize(width: sideSize, height: sideSize) + + if let dimensions = self.dimensions { + let imageSize = dimensions.aspectFitted(boundingSize) + self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))() + self.imageNode.frame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: (size.height - imageSize.height) / 2.0), size: imageSize) + if let animationNode = self.animationNode { + animationNode.frame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: (size.height - imageSize.height) / 2.0), size: imageSize) + animationNode.updateLayout(size: imageSize) + + if !self.didApplyVisibility { + self.didApplyVisibility = true + self.applyVisibility() + } + } + } + } + } + + override func update(animated: Bool) { + guard let dimensions = self.stickerEntity.file.dimensions?.cgSize else { + return + } + self.center = self.stickerEntity.position + + let size = max(10.0, min(self.stickerEntity.referenceDrawingSize.width, self.stickerEntity.referenceDrawingSize.height) * 0.45) + + self.bounds = CGRect(origin: .zero, size: dimensions.fitted(CGSize(width: size, height: size))) + self.transform = CGAffineTransformScale(CGAffineTransformMakeRotation(self.stickerEntity.rotation), self.stickerEntity.scale, self.stickerEntity.scale) + + var transform = CATransform3DIdentity + + if self.stickerEntity.mirrored { + transform = CATransform3DRotate(transform, .pi, 0.0, 1.0, 0.0) + transform.m34 = -1.0 / self.imageNode.frame.width + } + + if animated { + UIView.animate(withDuration: 0.25, delay: 0.0) { + self.imageNode.transform = transform + self.animationNode?.transform = transform + } + } else { + self.imageNode.transform = transform + self.animationNode?.transform = transform + } + + super.update(animated: animated) + } + + override func updateSelectionView() { + guard let selectionView = self.selectionView as? DrawingStickerEntititySelectionView 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.stickerEntity.scale) * scale + selectionView.selectionInset * 2.0, height: (bounds.height * self.stickerEntity.scale) * scale + selectionView.selectionInset * 2.0)) + selectionView.transform = CGAffineTransformMakeRotation(self.stickerEntity.rotation) + + self.popIdentityTransformForMeasurement() + } + + override func makeSelectionView() -> DrawingEntitySelectionView { + if let selectionView = self.selectionView { + return selectionView + } + let selectionView = DrawingStickerEntititySelectionView() + selectionView.entityView = self + return selectionView + } +} + +final class DrawingStickerEntititySelectionView: 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 + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + var scale: CGFloat = 1.0 { + didSet { + self.setNeedsLayout() + } + } + + override var selectionInset: CGFloat { + return 18.0 + } + + override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + + private var currentHandle: CALayer? + @objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { + guard let entityView = self.entityView, let entity = entityView.entity as? DrawingStickerEntity else { + return + } + let location = gestureRecognizer.location(in: self) + + switch gestureRecognizer.state { + case .began: + 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) + + var updatedPosition = entity.position + var updatedScale = entity.scale + 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 *= 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 + } + + entity.position = updatedPosition + entity.scale = updatedScale + entity.rotation = updatedRotation + entityView.update() + + gestureRecognizer.setTranslation(.zero, in: entityView) + case .ended: + break + default: + break + } + } + + override func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) { + guard let entityView = self.entityView, let entity = entityView.entity as? DrawingStickerEntity else { + return + } + + switch gestureRecognizer.state { + case .began, .changed: + let scale = gestureRecognizer.scale + entity.scale = entity.scale * scale + entityView.update() + + gestureRecognizer.scale = 1.0 + default: + break + } + } + + override func handleRotate(_ gestureRecognizer: UIRotationGestureRecognizer) { + guard let entityView = self.entityView, let entity = entityView.entity as? DrawingStickerEntity 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) + + self.border.lineDashPattern = [12.0 / self.scale as NSNumber, 12.0 / self.scale as NSNumber] + self.border.lineWidth = 2.0 / self.scale + self.border.path = UIBezierPath(ovalIn: CGRect(origin: CGPoint(x: inset, y: inset), size: CGSize(width: self.bounds.width - inset * 2.0, height: self.bounds.height - inset * 2.0))).cgPath + } +} diff --git a/submodules/DrawingUI/Sources/DrawingTextEntity.swift b/submodules/DrawingUI/Sources/DrawingTextEntity.swift new file mode 100644 index 0000000000..9582748134 --- /dev/null +++ b/submodules/DrawingUI/Sources/DrawingTextEntity.swift @@ -0,0 +1,935 @@ +import Foundation +import UIKit +import Display +import AccountContext + +final class DrawingTextEntity: DrawingEntity { + enum Style { + 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 { + case sanFrancisco + case newYork + case monospaced + case round + + init(font: DrawingTextEntity.Font) { + switch font { + case .sanFrancisco: + self = .sanFrancisco + case .newYork: + self = .newYork + case .monospaced: + self = .monospaced + case .round: + self = .round + } + } + } + + enum Alignment { + case left + case center + case right + + init(font: DrawingTextEntity.Alignment) { + switch font { + case .left: + self = .left + case .center: + self = .center + case .right: + self = .right + } + } + + var alignment: NSTextAlignment { + switch self { + case .left: + return .left + case .center: + return .center + case .right: + return .right + } + } + } + + var uuid: UUID + let isAnimated: Bool + + var text: String + var style: Style + var font: Font + var alignment: Alignment + var fontSize: CGFloat + var color: DrawingColor + var lineWidth: CGFloat = 0.0 + + var referenceDrawingSize: CGSize + var position: CGPoint + var width: CGFloat + var rotation: CGFloat + + init(text: String, style: Style, font: Font, alignment: Alignment, fontSize: CGFloat, color: DrawingColor) { + self.uuid = UUID() + self.isAnimated = false + + 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.rotation = 0.0 + } + + var center: CGPoint { + return self.position + } + + 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.rotation = self.rotation + return newEntity + } + + weak var currentEntityView: DrawingEntityView? + func makeView(context: AccountContext) -> DrawingEntityView { + let entityView = DrawingTextEntityView(context: context, entity: self) + self.currentEntityView = entityView + return entityView + } +} + +final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate { + private var textEntity: DrawingTextEntity { + return self.entity as! DrawingTextEntity + } + + private let textView: DrawingTextView + + init(context: AccountContext, entity: DrawingTextEntity) { + self.textView = DrawingTextView(frame: .zero) + self.textView.clipsToBounds = false + + self.textView.backgroundColor = .clear + self.textView.text = entity.text + 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.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() + } + + func beginEditing(accessoryView: UIView?) { + self._isEditing = true + if !self.textEntity.text.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.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.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.isEmpty { + self.containerView?.remove(uuid: self.textEntity.uuid) + } + } + + self.textView.text = self.textEntity.text + + 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) { + self.sizeToFit() + self.textView.setNeedsDisplay() + } + + 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 +// CGFloat correction = _textView.font.pointSize * _font.sizeCorrection; +// rect.origin.y += correction; +// rect.size.height -= correction; + rect = rect.offsetBy(dx: 0.0, dy: 10.0) // CGRectOffset(rect, 0.0f, 10.0f); + self.textView.frame = rect + } + + override func update(animated: Bool = false) { + if !self.isEditing { + self.center = self.textEntity.position + self.transform = CGAffineTransformMakeRotation(self.textEntity.rotation) + } + + let minFontSize = max(10.0, min(self.textEntity.referenceDrawingSize.width, self.textEntity.referenceDrawingSize.height) * 0.05) + let maxFontSize = max(10.0, min(self.textEntity.referenceDrawingSize.width, self.textEntity.referenceDrawingSize.height) * 0.45) + let fontSize = minFontSize + (maxFontSize - minFontSize) * self.textEntity.fontSize + + switch self.textEntity.font { + case .sanFrancisco: + self.textView.font = Font.with(size: fontSize, design: .regular, weight: .semibold) + case .newYork: + self.textView.font = Font.with(size: fontSize, design: .serif, weight: .semibold) + case .monospaced: + self.textView.font = Font.with(size: fontSize, design: .monospace, weight: .semibold) + case .round: + self.textView.font = Font.with(size: fontSize, design: .round, weight: .semibold) + } + self.textView.textAlignment = self.textEntity.alignment.alignment + + 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.sizeToFit() + + super.update(animated: animated) + } + + override func updateSelectionView() { + super.updateSelectionView() + + guard let selectionView = self.selectionView as? DrawingTextEntititySelectionView else { + return + } + +// let scale = self.superview?.superview?.layer.value(forKeyPath: "transform.scale.x") as? CGFloat ?? 1.0 +// selectionView.scale = scale + + selectionView.transform = CGAffineTransformMakeRotation(self.textEntity.rotation) + } + + 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.drawHierarchy(in: rect, afterScreenUpdates: false) + let image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return image + } +} + +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 + } + + 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 { + return true + } + + 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: + 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) + + var updatedFontSize = entity.fontSize + 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 + updatedFontSize = max(0.0, min(1.0, updatedFontSize * 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 + } + + entity.fontSize = updatedFontSize + entity.position = updatedPosition + entity.rotation = updatedRotation + entityView.update() + + gestureRecognizer.setTranslation(.zero, in: entityView) + case .ended: + break + default: + break + } + } + + override func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) { + guard let entityView = self.entityView, let entity = entityView.entity as? DrawingTextEntity else { + return + } + + switch gestureRecognizer.state { + case .began, .changed: + let scale = gestureRecognizer.scale + entity.fontSize = max(0.0, min(1.0, entity.fontSize * scale)) + entityView.update() + + gestureRecognizer.scale = 1.0 + default: + break + } + } + + override func handleRotate(_ gestureRecognizer: UIRotationGestureRecognizer) { + guard let entityView = self.entityView, 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) + + self.border.lineDashPattern = [12.0 / self.scale as NSNumber, 12.0 / self.scale as NSNumber] + self.border.lineWidth = 2.0 / self.scale + self.border.path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: inset, y: inset), size: CGSize(width: self.bounds.width - inset * 2.0, height: self.bounds.height - inset * 2.0)), cornerRadius: 12.0 / self.scale).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(.normal) + 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() + } +} + +private class DrawingTextView: UITextView { + 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 font: UIFont? { + get { + return super.font + } + set { + super.font = newValue + if let font = newValue { + self.drawingLayoutManager.textContainers.first?.lineFragmentPadding = floor(font.pointSize * 0.24) + } + + self.fixTypingAttributes() + } + } + + 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() + } + + private 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 + } +} diff --git a/submodules/DrawingUI/Sources/DrawingTools.swift b/submodules/DrawingUI/Sources/DrawingTools.swift new file mode 100644 index 0000000000..b4d28cb97a --- /dev/null +++ b/submodules/DrawingUI/Sources/DrawingTools.swift @@ -0,0 +1,1231 @@ +import Foundation +import UIKit +import Display + +protocol DrawingRenderLayer: CALayer { + +} + +//final class PenTool: DrawingElement { +// class RenderLayer: SimpleLayer, DrawingRenderLayer { +// var lineWidth: CGFloat = 0.0 +// +// func setup(size: CGSize, color: DrawingColor, lineWidth: CGFloat, strokeWidth: CGFloat, shadowRadius: CGFloat) { +// self.contentsScale = 1.0 +// self.lineWidth = lineWidth +// +// +// } +// } +// +// let uuid = UUID() +// +// let drawingSize: CGSize +// let color: DrawingColor +// let lineWidth: CGFloat +// let arrow: Bool +// +// var path: BezierPath? +// var boundingBox: CGRect? +// +// var renderPath: CGPath? +// let renderStrokeWidth: CGFloat +// let renderShadowRadius: CGFloat +// let renderLineWidth: CGFloat +// +// var translation = CGPoint() +// +// private var currentRenderLayer: DrawingRenderLayer? +// +// var bounds: CGRect { +// return self.path?.path.bounds.offsetBy(dx: self.translation.x, dy: self.translation.y) ?? .zero +// } +// +// var points: [Polyline.Point] { +// guard let linePath = self.path else { +// return [] +// } +// var points: [Polyline.Point] = [] +// for element in linePath.elements { +// if case .moveTo = element.type { +// points.append(element.startPoint.offsetBy(self.translation)) +// } else { +// points.append(element.endPoint.offsetBy(self.translation)) +// } +// } +// return points +// } +// +// func containsPoint(_ point: CGPoint) -> Bool { +// return self.renderPath?.contains(point.offsetBy(CGPoint(x: -self.translation.x, y: -self.translation.y))) ?? false +// } +// +// func hasPointsInsidePath(_ path: UIBezierPath) -> Bool { +// if let linePath = self.path { +// let pathBoundingBox = path.bounds +// if self.bounds.intersects(pathBoundingBox) { +// for element in linePath.elements { +// if case .moveTo = element.type { +// if path.contains(element.startPoint.location.offsetBy(self.translation)) { +// return true +// } +// } else { +// if path.contains(element.startPoint.location.offsetBy(self.translation)) { +// return true +// } +// if path.contains(element.endPoint.location.offsetBy(self.translation)) { +// return true +// } +// if case .cubicCurve = element.type { +// if path.contains(element.controlPoints[0].offsetBy(self.translation)) { +// return true +// } +// if path.contains(element.controlPoints[1].offsetBy(self.translation)) { +// return true +// } +// } else if case .quadCurve = element.type { +// if path.contains(element.controlPoints[0].offsetBy(self.translation)) { +// return true +// } +// } +// } +// } +// } +// } +// return false +// } +// +// required init(drawingSize: CGSize, color: DrawingColor, lineWidth: CGFloat, arrow: Bool) { +// self.drawingSize = drawingSize +// self.color = color +// self.lineWidth = lineWidth +// self.arrow = arrow +// +// let strokeWidth = min(drawingSize.width, drawingSize.height) * 0.008 +// let shadowRadius = min(drawingSize.width, drawingSize.height) * 0.03 +// +// let minLineWidth = max(1.0, min(drawingSize.width, drawingSize.height) * 0.003) +// let maxLineWidth = max(10.0, min(drawingSize.width, drawingSize.height) * 0.09) +// let lineWidth = minLineWidth + (maxLineWidth - minLineWidth) * lineWidth +// +// self.renderStrokeWidth = strokeWidth +// self.renderShadowRadius = shadowRadius +// self.renderLineWidth = lineWidth +// } +// +// func setupRenderLayer() -> DrawingRenderLayer? { +// let layer = RenderLayer() +// layer.setup(size: self.drawingSize, color: self.color, lineWidth: self.renderLineWidth, strokeWidth: self.renderStrokeWidth, shadowRadius: self.renderShadowRadius) +// self.currentRenderLayer = layer +// return layer +// } +// +// func updatePath(_ path: DrawingGesturePipeline.DrawingResult, state: DrawingGesturePipeline.DrawingGestureState) { +// guard case let .smoothCurve(bezierPath) = path else { +// return +// } +// +// self.path = bezierPath +// +//// if self.arrow && polyline.isComplete, polyline.points.count > 2 { +//// let lastPoint = lastPosition +//// var secondPoint = polyline.points[polyline.points.count - 2] +//// if secondPoint.location.distance(to: lastPoint) < self.renderArrowLineWidth { +//// secondPoint = polyline.points[polyline.points.count - 3] +//// } +//// let angle = lastPoint.angle(to: secondPoint.location) +//// let point1 = lastPoint.pointAt(distance: self.renderArrowLength, angle: angle - CGFloat.pi * 0.15) +//// let point2 = lastPoint.pointAt(distance: self.renderArrowLength, angle: angle + CGFloat.pi * 0.15) +//// +//// let arrowPath = UIBezierPath() +//// arrowPath.move(to: point2) +//// arrowPath.addLine(to: lastPoint) +//// arrowPath.addLine(to: point1) +//// let arrowThickPath = arrowPath.cgPath.copy(strokingWithWidth: self.renderArrowLineWidth, lineCap: .round, lineJoin: .round, miterLimit: 0.0) +//// +//// combinedPath.usesEvenOddFillRule = false +//// combinedPath.append(UIBezierPath(cgPath: arrowThickPath)) +//// } +// +// +// let cgPath = bezierPath.path.cgPath.copy(strokingWithWidth: self.renderLineWidth, lineCap: .round, lineJoin: .round, miterLimit: 0.0) +// self.renderPath = cgPath +// +// if let currentRenderLayer = self.currentRenderLayer as? RenderLayer { +// currentRenderLayer.updatePath(cgPath) +// } +// } +// +// func draw(in context: CGContext, size: CGSize) { +// guard let path = self.renderPath else { +// return +// } +// context.saveGState() +// +// context.translateBy(x: self.translation.x, y: self.translation.y) +// +// context.setShouldAntialias(true) +// +// context.setBlendMode(.normal) +// +// context.addPath(path) +// context.setFillColor(UIColor.white.cgColor) +// context.setStrokeColor(UIColor.white.cgColor) +// context.setLineWidth(self.renderStrokeWidth * 0.5) +// context.setShadow(offset: .zero, blur: self.renderShadowRadius * 3.0, color: self.color.toCGColor()) +// context.drawPath(using: .fillStroke) +// +// context.addPath(path) +// context.setShadow(offset: .zero, blur: 0.0, color: UIColor.clear.cgColor) +// context.setLineCap(.round) +// context.setLineWidth(self.renderStrokeWidth) +// context.setStrokeColor(UIColor.white.mixedWith(self.color.toUIColor(), alpha: 0.25).cgColor) +// context.strokePath() +// +// context.addPath(path) +// context.setFillColor(UIColor.white.cgColor) +// +// context.fillPath() +// +// context.restoreGState() +// } +//} + +//final class PenTool: DrawingElement { +// class RenderLayer: SimpleLayer, DrawingRenderLayer { +// var lineWidth: CGFloat = 0.0 +// +// let fillLayer = SimpleShapeLayer() +// +// func setup(size: CGSize, color: DrawingColor, lineWidth: CGFloat) { +// self.contentsScale = 1.0 +// +// let minLineWidth = max(1.0, min(size.width, size.height) * 0.003) +// let maxLineWidth = max(10.0, min(size.width, size.height) * 0.055) +// let lineWidth = minLineWidth + (maxLineWidth - minLineWidth) * lineWidth +// +// self.lineWidth = lineWidth +// +// let bounds = CGRect(origin: .zero, size: size) +// self.frame = bounds +// +// self.fillLayer.frame = bounds +// self.fillLayer.contentsScale = 1.0 +// self.fillLayer.fillColor = color.toCGColor() +// self.fillLayer.strokeColor = color.toCGColor() +// self.fillLayer.lineWidth = 1.0 +// +// self.addSublayer(self.fillLayer) +// } +// +// func updatePath(_ path: CGPath) { +// self.fillLayer.path = path +// } +// } +// +// let uuid = UUID() +// +// let drawingSize: CGSize +// let color: DrawingColor +// let lineWidth: CGFloat +// let arrow: Bool +// +// var polyline: Polyline? +//// var path = BezierPath() +// let renderArrowLength: CGFloat +// let renderArrowLineWidth: CGFloat +// var renderLineWidth: CGFloat = 0.0 +// +// var translation = CGPoint() +// +// private var currentRenderLayer: DrawingRenderLayer? +// +// var bounds: CGRect { +// return .zero +// } +// +// var points: [CGPoint] { +// guard let polyline = self.polyline else { +// return [] +// } +// var points: [CGPoint] = [] +// for point in polyline.points { +// points.append(point.location) +// } +// return points +// } +// +// func containsPoint(_ point: CGPoint) -> Bool { +// return false +// } +// +// func hasPointsInsidePath(_ path: UIBezierPath) -> Bool { +// if let polyline = self.polyline { +// let pathBoundingBox = path.bounds +// if self.bounds.intersects(pathBoundingBox) { +// for point in polyline.points { +// if path.contains(point.location) { +// return true +// } +// } +// } +// } +// return false +// } +// +// required init(drawingSize: CGSize, color: DrawingColor, lineWidth: CGFloat, arrow: Bool) { +// self.drawingSize = drawingSize +// self.color = color +// self.lineWidth = lineWidth +// self.arrow = arrow +// +// self.renderArrowLength = min(drawingSize.width, drawingSize.height) * 0.04 +// self.renderArrowLineWidth = min(drawingSize.width, drawingSize.height) * 0.01 +// } +// +// func setupRenderLayer() -> DrawingRenderLayer? { +// let layer = RenderLayer() +// layer.setup(size: self.drawingSize, color: self.color, lineWidth: self.lineWidth) +// self.currentRenderLayer = layer +// return layer +// } +// +// func updatePath(_ path: DrawingGesturePipeline.DrawingResult, state: DrawingGesturePipeline.DrawingGestureState) { +// guard case let .polyline(polyline) = path else { +// return +// } +// +// self.polyline = polyline +// +// struct LineSegment { +// var firstPoint: CGPoint +// var secondPoint: CGPoint +// } +// +// func lineSegmentPerpendicularTo(_ pp: LineSegment, fraction: CGFloat) -> LineSegment { +// let x0: CGFloat = pp.firstPoint.x +// let y0: CGFloat = pp.firstPoint.y +// +// let x1: CGFloat = pp.secondPoint.x +// let y1: CGFloat = pp.secondPoint.y +// +// let dx = x1 - x0 +// let dy = y1 - y0 +// +// var xa: CGFloat +// var ya: CGFloat +// var xb: CGFloat +// var yb: CGFloat +// +// xa = x1 + fraction * 0.5 * dy +// ya = y1 - fraction * 0.5 * dx +// xb = x1 - fraction * 0.5 * dy +// yb = y1 + fraction * 0.5 * dx +// +// return LineSegment(firstPoint: CGPoint(x: xa, y: ya), secondPoint: CGPoint(x: xb, y: yb)) +// } +// +// +// func len_sq(_ p1: CGPoint, p2: CGPoint) -> CGFloat { +// let dx: CGFloat = p2.x - p1.x +// let dy: CGFloat = p2.y - p1.y +// return dx * dx + dy * dy +// } +// +// func clamp(_ value: T, min: T, max: T) -> T { +// if (value < min) { +// return min +// } +// if (value > max) { +// return max +// } +// return value +// } +// +// var ls: [LineSegment] = [] +// var isFirst = true +// +// let combinedPath = UIBezierPath() +// var segmentPath = UIBezierPath() +// +// var pts: [CGPoint] = [] +// var ctr: Int = 0 +// +// let ff: CGFloat = 0.2 +// let lower: CGFloat = 0.01 +// let upper: CGFloat = 1.0 +// +// var lastUsedIndex = 0 +// var index = 0 +// for point in polyline.points { +// pts.insert(point.location, at: ctr) +// ctr += 1 +// +// if ctr == 5 { +// pts[3] = CGPoint(x: (pts[2].x + pts[4].x) / 2.0, y: (pts[2].y + pts[4].y) / 2.0) +// +// if isFirst { +// isFirst = false +// let segment = LineSegment(firstPoint: pts[0], secondPoint: pts[0]) +// ls.append(segment) +// segmentPath.move(to: pts[0]) +// } +// +// let frac1: CGFloat = ff/clamp(len_sq(pts[0], p2: pts[1]), min: lower, max: upper) +// let frac2: CGFloat = ff/clamp(len_sq(pts[1], p2: pts[2]), min: lower, max: upper) +// let frac3: CGFloat = ff/clamp(len_sq(pts[2], p2: pts[3]), min: lower, max: upper) +// +// ls.insert(lineSegmentPerpendicularTo(LineSegment(firstPoint: pts[0], secondPoint: pts[1]), fraction: frac1), at: 1) +// ls.insert(lineSegmentPerpendicularTo(LineSegment(firstPoint: pts[1], secondPoint: pts[2]), fraction: frac2), at: 2) +// ls.insert(lineSegmentPerpendicularTo(LineSegment(firstPoint: pts[2], secondPoint: pts[3]), fraction: frac3), at: 3) +// +// segmentPath.move(to: ls[0].firstPoint) +// segmentPath.addCurve(to: ls[3].firstPoint, controlPoint1: ls[1].firstPoint, controlPoint2: ls[2].firstPoint) +// segmentPath.addLine(to: ls[3].secondPoint) +// segmentPath.addCurve(to: ls[0].secondPoint, controlPoint1: ls[2].secondPoint, controlPoint2: ls[1].secondPoint) +// segmentPath.close() +// combinedPath.append(segmentPath) +// +// let last = ls[3] +// ls.removeAll() +// ls.append(last) +// +// pts[0] = pts[3] +// pts[1] = pts[4] +// ctr = 2 +// +// combinedPath.append(segmentPath) +// segmentPath = UIBezierPath() +// +// lastUsedIndex = index +// } +// index += 1 +// } +// +// var lastPosition = polyline.points.last?.location ?? CGPoint() +// if let lastPoint = polyline.points.last, ls.count > 0 { +// if lastUsedIndex < polyline.points.count - 1 { +// let frac1: CGFloat = ff/clamp(len_sq(pts[0], p2: pts[1]), min: lower, max: upper) +// let frac2: CGFloat = ff/clamp(len_sq(pts[1], p2: lastPoint.location), min: lower, max: upper) +// ls.insert(lineSegmentPerpendicularTo(LineSegment(firstPoint: pts[0], secondPoint: pts[1]), fraction: frac1), at: 1) +// ls.insert(lineSegmentPerpendicularTo(LineSegment(firstPoint: pts[1], secondPoint: lastPoint.location), fraction: frac2), at: 2) +// +// segmentPath.move(to: ls[0].firstPoint) +// segmentPath.addQuadCurve(to: ls[2].firstPoint, controlPoint: ls[1].firstPoint) +// segmentPath.addLine(to: ls[2].secondPoint) +// segmentPath.addQuadCurve(to: ls[0].secondPoint, controlPoint: ls[1].secondPoint) +// segmentPath.addLine(to: ls[0].secondPoint) +// segmentPath.close() +// combinedPath.append(segmentPath) +// +// let diameter = ls[2].firstPoint.distance(to: ls[2].secondPoint) +// combinedPath.append(UIBezierPath(ovalIn: CGRect(origin: CGPoint(x: lastPoint.x - diameter * 0.5, y: lastPoint.y - diameter * 0.5), size: CGSize(width: diameter, height: diameter)))) +// +// lastPosition = lastPoint.location +// } else { +// let diameter = ls[0].firstPoint.distance(to: ls[0].secondPoint) +// let center = ls[0].firstPoint.point(to: ls[0].secondPoint, t: 0.5) +// combinedPath.append(UIBezierPath(ovalIn: CGRect(origin: CGPoint(x: center.x - diameter * 0.5, y: center.y - diameter * 0.5), size: CGSize(width: diameter, height: diameter)))) +// +// lastPosition = center +// } +// } +// +// if self.arrow && polyline.isComplete, polyline.points.count > 2 { +// let lastPoint = lastPosition +// var secondPoint = polyline.points[polyline.points.count - 2] +// if secondPoint.location.distance(to: lastPoint) < self.renderArrowLineWidth { +// secondPoint = polyline.points[polyline.points.count - 3] +// } +// let angle = lastPoint.angle(to: secondPoint.location) +// let point1 = lastPoint.pointAt(distance: self.renderArrowLength, angle: angle - CGFloat.pi * 0.15) +// let point2 = lastPoint.pointAt(distance: self.renderArrowLength, angle: angle + CGFloat.pi * 0.15) +// +// let arrowPath = UIBezierPath() +// arrowPath.move(to: point2) +// arrowPath.addLine(to: lastPoint) +// arrowPath.addLine(to: point1) +// let arrowThickPath = arrowPath.cgPath.copy(strokingWithWidth: self.renderArrowLineWidth, lineCap: .round, lineJoin: .round, miterLimit: 0.0) +// +// combinedPath.usesEvenOddFillRule = false +// combinedPath.append(UIBezierPath(cgPath: arrowThickPath)) +// } +// +// if let currentRenderLayer = self.currentRenderLayer as? RenderLayer { +// currentRenderLayer.updatePath(combinedPath.cgPath) +// } +// } +// +// func draw(in context: CGContext, size: CGSize) { +// let renderLayer: DrawingRenderLayer? +// if let currentRenderLayer = self.currentRenderLayer { +// renderLayer = currentRenderLayer +// } else { +// renderLayer = self.setupRenderLayer() +//// (renderLayer as? RenderLayer)?.updatePath(self.path.path.cgPath) +// } +// renderLayer?.render(in: context) +// } +//} + +final class MarkerTool: DrawingElement { + let uuid = UUID() + + let drawingSize: CGSize + let color: DrawingColor + let lineWidth: CGFloat + let arrow: Bool + + let renderLineWidth: CGFloat + var renderPath = UIBezierPath() + var renderAngle: CGFloat = 0.0 + + var translation = CGPoint() + + var bounds: CGRect { + return self.renderPath.bounds + } + + var points: [Polyline.Point] = [] + + weak var metalView: DrawingMetalView? + + func containsPoint(_ point: CGPoint) -> Bool { + return self.renderPath.contains(point) + } + + func hasPointsInsidePath(_ path: UIBezierPath) -> Bool { + let pathBoundingBox = path.bounds + if self.bounds.intersects(pathBoundingBox) { + for point in self.points { + if path.contains(point.location) { + return true + } + } + } + return false + } + + required init(drawingSize: CGSize, color: DrawingColor, lineWidth: CGFloat, arrow: Bool) { + self.drawingSize = drawingSize + self.color = color + self.lineWidth = lineWidth + self.arrow = arrow + + let minLineWidth = max(10.0, min(drawingSize.width, drawingSize.height) * 0.01) + let maxLineWidth = max(20.0, min(drawingSize.width, drawingSize.height) * 0.09) + let lineWidth = minLineWidth + (maxLineWidth - minLineWidth) * lineWidth + + self.renderLineWidth = lineWidth + } + + func setupRenderLayer() -> DrawingRenderLayer? { + return nil + } + + func updatePath(_ path: DrawingGesturePipeline.DrawingResult, state: DrawingGesturePipeline.DrawingGestureState) { + guard case let .location(point) = path else { + return + } + + if self.points.isEmpty { + self.renderPath.move(to: point.location) + } else { + self.renderPath.addLine(to: point.location) + } + self.points.append(point) + + self.metalView?.updated(point, state: state, brush: .marker, color: self.color, size: self.renderLineWidth) + } + + func draw(in context: CGContext, size: CGSize) { + guard !self.points.isEmpty else { + return + } + + self.metalView?.drawInContext(context) + } +} + +final class NeonTool: DrawingElement { + class RenderLayer: SimpleLayer, DrawingRenderLayer { + var lineWidth: CGFloat = 0.0 + + let shadowLayer = SimpleShapeLayer() + let borderLayer = SimpleShapeLayer() + let fillLayer = SimpleShapeLayer() + + func setup(size: CGSize, color: DrawingColor, lineWidth: CGFloat, strokeWidth: CGFloat, shadowRadius: CGFloat) { + self.contentsScale = 1.0 + self.lineWidth = lineWidth + + let bounds = CGRect(origin: .zero, size: size) + self.frame = bounds + + self.shadowLayer.frame = bounds + self.shadowLayer.backgroundColor = UIColor.clear.cgColor + self.shadowLayer.contentsScale = 1.0 + self.shadowLayer.lineWidth = strokeWidth * 0.5 + self.shadowLayer.lineCap = .round + self.shadowLayer.lineJoin = .round + self.shadowLayer.fillColor = UIColor.white.cgColor + self.shadowLayer.strokeColor = UIColor.white.cgColor + self.shadowLayer.shadowColor = color.toCGColor() + self.shadowLayer.shadowRadius = shadowRadius + self.shadowLayer.shadowOpacity = 1.0 + self.shadowLayer.shadowOffset = .zero + + + self.borderLayer.frame = bounds + self.borderLayer.contentsScale = 1.0 + self.borderLayer.lineWidth = strokeWidth + self.borderLayer.lineCap = .round + self.borderLayer.lineJoin = .round + self.borderLayer.fillColor = UIColor.clear.cgColor + self.borderLayer.strokeColor = UIColor.white.mixedWith(color.toUIColor(), alpha: 0.25).cgColor + + + self.fillLayer.frame = bounds + self.fillLayer.contentsScale = 1.0 + self.fillLayer.fillColor = UIColor.white.cgColor + + self.addSublayer(self.shadowLayer) + self.addSublayer(self.borderLayer) + self.addSublayer(self.fillLayer) + } + + func updatePath(_ path: CGPath) { + self.shadowLayer.path = path + self.borderLayer.path = path + self.fillLayer.path = path + } + } + + let uuid = UUID() + + let drawingSize: CGSize + let color: DrawingColor + let lineWidth: CGFloat + let arrow: Bool + + var path: BezierPath? + var boundingBox: CGRect? + + var renderPath: CGPath? + let renderStrokeWidth: CGFloat + let renderShadowRadius: CGFloat + let renderLineWidth: CGFloat + + var translation = CGPoint() + + private var currentRenderLayer: DrawingRenderLayer? + + var bounds: CGRect { + return self.path?.path.bounds.offsetBy(dx: self.translation.x, dy: self.translation.y) ?? .zero + } + + var points: [Polyline.Point] { + guard let linePath = self.path else { + return [] + } + var points: [Polyline.Point] = [] + for element in linePath.elements { + if case .moveTo = element.type { + points.append(element.startPoint.offsetBy(self.translation)) + } else { + points.append(element.endPoint.offsetBy(self.translation)) + } + } + return points + } + + func containsPoint(_ point: CGPoint) -> Bool { + return self.renderPath?.contains(point.offsetBy(CGPoint(x: -self.translation.x, y: -self.translation.y))) ?? false + } + + func hasPointsInsidePath(_ path: UIBezierPath) -> Bool { + if let linePath = self.path { + let pathBoundingBox = path.bounds + if self.bounds.intersects(pathBoundingBox) { + for element in linePath.elements { + if case .moveTo = element.type { + if path.contains(element.startPoint.location.offsetBy(self.translation)) { + return true + } + } else { + if path.contains(element.startPoint.location.offsetBy(self.translation)) { + return true + } + if path.contains(element.endPoint.location.offsetBy(self.translation)) { + return true + } + if case .cubicCurve = element.type { + if path.contains(element.controlPoints[0].offsetBy(self.translation)) { + return true + } + if path.contains(element.controlPoints[1].offsetBy(self.translation)) { + return true + } + } else if case .quadCurve = element.type { + if path.contains(element.controlPoints[0].offsetBy(self.translation)) { + return true + } + } + } + } + } + } + return false + } + + required init(drawingSize: CGSize, color: DrawingColor, lineWidth: CGFloat, arrow: Bool) { + self.drawingSize = drawingSize + self.color = color + self.lineWidth = lineWidth + self.arrow = arrow + + let strokeWidth = min(drawingSize.width, drawingSize.height) * 0.008 + let shadowRadius = min(drawingSize.width, drawingSize.height) * 0.03 + + let minLineWidth = max(1.0, min(drawingSize.width, drawingSize.height) * 0.003) + let maxLineWidth = max(10.0, min(drawingSize.width, drawingSize.height) * 0.09) + let lineWidth = minLineWidth + (maxLineWidth - minLineWidth) * lineWidth + + self.renderStrokeWidth = strokeWidth + self.renderShadowRadius = shadowRadius + self.renderLineWidth = lineWidth + } + + func setupRenderLayer() -> DrawingRenderLayer? { + let layer = RenderLayer() + layer.setup(size: self.drawingSize, color: self.color, lineWidth: self.renderLineWidth, strokeWidth: self.renderStrokeWidth, shadowRadius: self.renderShadowRadius) + self.currentRenderLayer = layer + return layer + } + + func updatePath(_ path: DrawingGesturePipeline.DrawingResult, state: DrawingGesturePipeline.DrawingGestureState) { + guard case let .smoothCurve(bezierPath) = path else { + return + } + + self.path = bezierPath + +// if self.arrow && polyline.isComplete, polyline.points.count > 2 { +// let lastPoint = lastPosition +// var secondPoint = polyline.points[polyline.points.count - 2] +// if secondPoint.location.distance(to: lastPoint) < self.renderArrowLineWidth { +// secondPoint = polyline.points[polyline.points.count - 3] +// } +// let angle = lastPoint.angle(to: secondPoint.location) +// let point1 = lastPoint.pointAt(distance: self.renderArrowLength, angle: angle - CGFloat.pi * 0.15) +// let point2 = lastPoint.pointAt(distance: self.renderArrowLength, angle: angle + CGFloat.pi * 0.15) +// +// let arrowPath = UIBezierPath() +// arrowPath.move(to: point2) +// arrowPath.addLine(to: lastPoint) +// arrowPath.addLine(to: point1) +// let arrowThickPath = arrowPath.cgPath.copy(strokingWithWidth: self.renderArrowLineWidth, lineCap: .round, lineJoin: .round, miterLimit: 0.0) +// +// combinedPath.usesEvenOddFillRule = false +// combinedPath.append(UIBezierPath(cgPath: arrowThickPath)) +// } + + + let cgPath = bezierPath.path.cgPath.copy(strokingWithWidth: self.renderLineWidth, lineCap: .round, lineJoin: .round, miterLimit: 0.0) + self.renderPath = cgPath + + if let currentRenderLayer = self.currentRenderLayer as? RenderLayer { + currentRenderLayer.updatePath(cgPath) + } + } + + func draw(in context: CGContext, size: CGSize) { + guard let path = self.renderPath else { + return + } + context.saveGState() + + context.translateBy(x: self.translation.x, y: self.translation.y) + + context.setShouldAntialias(true) + + context.setBlendMode(.normal) + + context.addPath(path) + context.setFillColor(UIColor.white.cgColor) + context.setStrokeColor(UIColor.white.cgColor) + context.setLineWidth(self.renderStrokeWidth * 0.5) + context.setShadow(offset: .zero, blur: self.renderShadowRadius * 1.9, color: self.color.toCGColor()) + context.drawPath(using: .fillStroke) + + context.addPath(path) + context.setShadow(offset: .zero, blur: 0.0, color: UIColor.clear.cgColor) + context.setLineCap(.round) + context.setLineWidth(self.renderStrokeWidth) + context.setStrokeColor(UIColor.white.mixedWith(self.color.toUIColor(), alpha: 0.25).cgColor) + context.strokePath() + + context.addPath(path) + context.setFillColor(UIColor.white.cgColor) + + context.fillPath() + + context.restoreGState() + } +} + +final class PencilTool: DrawingElement { + let uuid = UUID() + + let drawingSize: CGSize + let color: DrawingColor + let lineWidth: CGFloat + let arrow: Bool + + var translation = CGPoint() + + let renderLineWidth: CGFloat + var renderPath = UIBezierPath() + var renderAngle: CGFloat = 0.0 + + var bounds: CGRect { + return self.renderPath.bounds + } + + var points: [Polyline.Point] = [] + + weak var metalView: DrawingMetalView? + + func containsPoint(_ point: CGPoint) -> Bool { + return self.renderPath.contains(point.offsetBy(dx: -self.translation.x, dy: -self.translation.y)) + } + + func hasPointsInsidePath(_ path: UIBezierPath) -> Bool { + let pathBoundingBox = path.bounds + if self.bounds.intersects(pathBoundingBox) { + for point in self.points { + if path.contains(point.location.offsetBy(self.translation)) { + return true + } + } + } + return false + } + + required init(drawingSize: CGSize, color: DrawingColor, lineWidth: CGFloat, arrow: Bool) { + self.drawingSize = drawingSize + self.color = color + self.lineWidth = lineWidth + self.arrow = arrow + + let minLineWidth = max(10.0, min(drawingSize.width, drawingSize.height) * 0.01) + let maxLineWidth = max(20.0, min(drawingSize.width, drawingSize.height) * 0.09) + let lineWidth = minLineWidth + (maxLineWidth - minLineWidth) * lineWidth + + self.renderLineWidth = lineWidth + } + + func setupRenderLayer() -> DrawingRenderLayer? { + return nil + } + + func updatePath(_ path: DrawingGesturePipeline.DrawingResult, state: DrawingGesturePipeline.DrawingGestureState) { + guard case let .location(point) = path else { + return + } + + if self.points.isEmpty { + self.renderPath.move(to: point.location) + } else { + self.renderPath.addLine(to: point.location) + } + self.points.append(point) + + self.metalView?.updated(point, state: state, brush: .pencil, color: self.color, size: self.renderLineWidth) + } + + func draw(in context: CGContext, size: CGSize) { + guard !self.points.isEmpty else { + return + } + + self.metalView?.drawInContext(context) + } +} + +final class FillTool: DrawingElement { + let uuid = UUID() + + let drawingSize: CGSize + let color: DrawingColor + let renderLineWidth: CGFloat = 0.0 + + var bounds: CGRect { + return .zero + } + + var points: [Polyline.Point] { + return [] + } + + var translation = CGPoint() + + required init(drawingSize: CGSize, color: DrawingColor, lineWidth: CGFloat, arrow: Bool) { + self.drawingSize = drawingSize + self.color = color + } + + func setupRenderLayer() -> DrawingRenderLayer? { + return nil + } + + func updatePath(_ path: DrawingGesturePipeline.DrawingResult, state: DrawingGesturePipeline.DrawingGestureState) { + } + + func draw(in context: CGContext, size: CGSize) { + context.setShouldAntialias(false) + + context.setBlendMode(.copy) + + context.setFillColor(self.color.toCGColor()) + context.fill(CGRect(origin: .zero, size: size)) + + context.setBlendMode(.normal) + } + + func containsPoint(_ point: CGPoint) -> Bool { + return false + } + + func hasPointsInsidePath(_ path: UIBezierPath) -> Bool { + return false + } +} + + +final class BlurTool: DrawingElement { + class RenderLayer: SimpleLayer, DrawingRenderLayer { + var lineWidth: CGFloat = 0.0 + + let blurLayer = SimpleLayer() + let fillLayer = SimpleShapeLayer() + + func setup(size: CGSize, color: DrawingColor, lineWidth: CGFloat, image: UIImage?) { + self.contentsScale = 1.0 + self.lineWidth = lineWidth + + let bounds = CGRect(origin: .zero, size: size) + self.frame = bounds + + self.blurLayer.frame = bounds + self.fillLayer.frame = bounds + + if self.blurLayer.contents == nil, let image = image { + self.blurLayer.contents = image.cgImage + } + self.blurLayer.mask = self.fillLayer + + self.fillLayer.frame = bounds + self.fillLayer.contentsScale = 1.0 + self.fillLayer.strokeColor = UIColor.white.cgColor + self.fillLayer.fillColor = UIColor.clear.cgColor + self.fillLayer.lineCap = .round + self.fillLayer.lineWidth = lineWidth + + self.addSublayer(self.blurLayer) + } + + func updatePath(_ path: CGPath) { + self.fillLayer.path = path + } + } + + var getFullImage: () -> UIImage? = { return nil } + + let uuid = UUID() + + let drawingSize: CGSize + let color: DrawingColor + let lineWidth: CGFloat + let arrow: Bool + + var path: BezierPath? + var boundingBox: CGRect? + + var renderPath: CGPath? + let renderLineWidth: CGFloat + + var translation = CGPoint() + + private var currentRenderLayer: DrawingRenderLayer? + + var bounds: CGRect { + return self.path?.path.bounds.offsetBy(dx: self.translation.x, dy: self.translation.y) ?? .zero + } + + var points: [Polyline.Point] { + guard let linePath = self.path else { + return [] + } + var points: [Polyline.Point] = [] + for element in linePath.elements { + if case .moveTo = element.type { + points.append(element.startPoint.offsetBy(self.translation)) + } else { + points.append(element.endPoint.offsetBy(self.translation)) + } + } + return points + } + + func containsPoint(_ point: CGPoint) -> Bool { + return self.renderPath?.contains(point.offsetBy(CGPoint(x: -self.translation.x, y: -self.translation.y))) ?? false + } + + func hasPointsInsidePath(_ path: UIBezierPath) -> Bool { + if let linePath = self.path { + let pathBoundingBox = path.bounds + if self.bounds.intersects(pathBoundingBox) { + for element in linePath.elements { + if case .moveTo = element.type { + if path.contains(element.startPoint.location.offsetBy(self.translation)) { + return true + } + } else { + if path.contains(element.startPoint.location.offsetBy(self.translation)) { + return true + } + if path.contains(element.endPoint.location.offsetBy(self.translation)) { + return true + } + if case .cubicCurve = element.type { + if path.contains(element.controlPoints[0].offsetBy(self.translation)) { + return true + } + if path.contains(element.controlPoints[1].offsetBy(self.translation)) { + return true + } + } else if case .quadCurve = element.type { + if path.contains(element.controlPoints[0].offsetBy(self.translation)) { + return true + } + } + } + } + } + } + return false + } + + required init(drawingSize: CGSize, color: DrawingColor, lineWidth: CGFloat, arrow: Bool) { + self.drawingSize = drawingSize + self.color = color + self.lineWidth = lineWidth + self.arrow = arrow + + let minLineWidth = max(1.0, min(drawingSize.width, drawingSize.height) * 0.003) + let maxLineWidth = max(10.0, min(drawingSize.width, drawingSize.height) * 0.09) + let lineWidth = minLineWidth + (maxLineWidth - minLineWidth) * lineWidth + + self.renderLineWidth = lineWidth + } + + func setupRenderLayer() -> DrawingRenderLayer? { + let layer = RenderLayer() + layer.setup(size: self.drawingSize, color: self.color, lineWidth: self.renderLineWidth, image: self.getFullImage()) + self.currentRenderLayer = layer + return layer + } + + func updatePath(_ path: DrawingGesturePipeline.DrawingResult, state: DrawingGesturePipeline.DrawingGestureState) { + guard case let .smoothCurve(bezierPath) = path else { + return + } + + self.path = bezierPath + + let renderPath = bezierPath.path.cgPath + self.renderPath = renderPath + + if let currentRenderLayer = self.currentRenderLayer as? RenderLayer { + currentRenderLayer.updatePath(renderPath) + } + } + + func draw(in context: CGContext, size: CGSize) { + context.translateBy(x: self.translation.x, y: self.translation.y) + + let renderLayer: DrawingRenderLayer? + if let currentRenderLayer = self.currentRenderLayer { + renderLayer = currentRenderLayer + } else { + renderLayer = self.setupRenderLayer() + } + renderLayer?.render(in: context) + } +} + + +final class EraserTool: DrawingElement { + class RenderLayer: SimpleLayer, DrawingRenderLayer { + var lineWidth: CGFloat = 0.0 + + let blurLayer = SimpleLayer() + let fillLayer = SimpleShapeLayer() + + func setup(size: CGSize, color: DrawingColor, lineWidth: CGFloat, image: UIImage?) { + self.contentsScale = 1.0 + self.lineWidth = lineWidth + + let bounds = CGRect(origin: .zero, size: size) + self.frame = bounds + + self.blurLayer.frame = bounds + self.fillLayer.frame = bounds + + if self.blurLayer.contents == nil, let image = image { + self.blurLayer.contents = image.cgImage + self.blurLayer.contentsGravity = .resize + } + self.blurLayer.mask = self.fillLayer + + self.fillLayer.frame = bounds + self.fillLayer.contentsScale = 1.0 + self.fillLayer.strokeColor = UIColor.white.cgColor + self.fillLayer.fillColor = UIColor.clear.cgColor + self.fillLayer.lineCap = .round + self.fillLayer.lineWidth = lineWidth + + self.addSublayer(self.blurLayer) + } + + func updatePath(_ path: CGPath) { + self.fillLayer.path = path + } + } + + var getFullImage: () -> UIImage? = { return nil } + + let uuid = UUID() + + let drawingSize: CGSize + let color: DrawingColor + let lineWidth: CGFloat + let arrow: Bool + + var path: BezierPath? + var boundingBox: CGRect? + + var renderPath: CGPath? + let renderLineWidth: CGFloat + + var translation = CGPoint() + + private var currentRenderLayer: DrawingRenderLayer? + + var bounds: CGRect { + return self.path?.path.bounds.offsetBy(dx: self.translation.x, dy: self.translation.y) ?? .zero + } + + var points: [Polyline.Point] { + guard let linePath = self.path else { + return [] + } + var points: [Polyline.Point] = [] + for element in linePath.elements { + if case .moveTo = element.type { + points.append(element.startPoint.offsetBy(self.translation)) + } else { + points.append(element.endPoint.offsetBy(self.translation)) + } + } + return points + } + + func containsPoint(_ point: CGPoint) -> Bool { + return false +// return self.renderPath?.contains(point.offsetBy(CGPoint(x: -self.translation.x, y: -self.translation.y))) ?? false + } + + func hasPointsInsidePath(_ path: UIBezierPath) -> Bool { + if let linePath = self.path { + let pathBoundingBox = path.bounds + if self.bounds.intersects(pathBoundingBox) { + for element in linePath.elements { + if case .moveTo = element.type { + if path.contains(element.startPoint.location.offsetBy(self.translation)) { + return true + } + } else { + if path.contains(element.startPoint.location.offsetBy(self.translation)) { + return true + } + if path.contains(element.endPoint.location.offsetBy(self.translation)) { + return true + } + if case .cubicCurve = element.type { + if path.contains(element.controlPoints[0].offsetBy(self.translation)) { + return true + } + if path.contains(element.controlPoints[1].offsetBy(self.translation)) { + return true + } + } else if case .quadCurve = element.type { + if path.contains(element.controlPoints[0].offsetBy(self.translation)) { + return true + } + } + } + } + } + } + return false + } + + required init(drawingSize: CGSize, color: DrawingColor, lineWidth: CGFloat, arrow: Bool) { + self.drawingSize = drawingSize + self.color = color + self.lineWidth = lineWidth + self.arrow = arrow + + let minLineWidth = max(1.0, min(drawingSize.width, drawingSize.height) * 0.003) + let maxLineWidth = max(10.0, min(drawingSize.width, drawingSize.height) * 0.09) + let lineWidth = minLineWidth + (maxLineWidth - minLineWidth) * lineWidth + + self.renderLineWidth = lineWidth + } + + func setupRenderLayer() -> DrawingRenderLayer? { + let layer = RenderLayer() + layer.setup(size: self.drawingSize, color: self.color, lineWidth: self.renderLineWidth, image: self.getFullImage()) + self.currentRenderLayer = layer + return layer + } + + func updatePath(_ path: DrawingGesturePipeline.DrawingResult, state: DrawingGesturePipeline.DrawingGestureState) { + guard case let .smoothCurve(bezierPath) = path else { + return + } + + self.path = bezierPath + + let renderPath = bezierPath.path.cgPath + self.renderPath = renderPath + + if let currentRenderLayer = self.currentRenderLayer as? RenderLayer { + currentRenderLayer.updatePath(renderPath) + } + } + + func draw(in context: CGContext, size: CGSize) { + context.translateBy(x: self.translation.x, y: self.translation.y) + + let renderLayer: DrawingRenderLayer? + if let currentRenderLayer = self.currentRenderLayer { + renderLayer = currentRenderLayer + } else { + renderLayer = self.setupRenderLayer() + } + renderLayer?.render(in: context) + } +} diff --git a/submodules/DrawingUI/Sources/DrawingUtils.swift b/submodules/DrawingUI/Sources/DrawingUtils.swift new file mode 100644 index 0000000000..a47b8c680d --- /dev/null +++ b/submodules/DrawingUI/Sources/DrawingUtils.swift @@ -0,0 +1,950 @@ +import Foundation +import UIKit +import QuartzCore +import simd + +struct DrawingColor: Equatable { + public static var clear = DrawingColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0) + + public var red: CGFloat + public var green: CGFloat + public var blue: CGFloat + public var alpha: CGFloat + + public var position: CGPoint? + + var isClear: Bool { + return self.red.isZero && self.green.isZero && self.blue.isZero && self.alpha.isZero + } + + public init( + red: CGFloat, + green: CGFloat, + blue: CGFloat, + alpha: CGFloat = 1.0, + position: CGPoint? = nil + ) { + self.red = red + self.green = green + self.blue = blue + self.alpha = alpha + self.position = position + } + + public init(color: UIColor) { + var red: CGFloat = 0.0 + var green: CGFloat = 0.0 + var blue: CGFloat = 0.0 + var alpha: CGFloat = 1.0 + if color.getRed(&red, green: &green, blue: &blue, alpha: &alpha) { + self.init(red: red, green: green, blue: blue, alpha: alpha) + } else if color.getWhite(&red, alpha: &alpha) { + self.init(red: red, green: red, blue: red, alpha: alpha) + } else { + self.init(red: 0.0, green: 0.0, blue: 0.0) + } + } + + public init(rgb: UInt32) { + self.init(color: UIColor(rgb: rgb)) + } + + func withUpdatedRed(_ red: CGFloat) -> DrawingColor { + return DrawingColor( + red: red, + green: self.green, + blue: self.blue, + alpha: self.alpha + ) + } + + func withUpdatedGreen(_ green: CGFloat) -> DrawingColor { + return DrawingColor( + red: self.red, + green: green, + blue: self.blue, + alpha: self.alpha + ) + } + + func withUpdatedBlue(_ blue: CGFloat) -> DrawingColor { + return DrawingColor( + red: self.red, + green: self.green, + blue: blue, + alpha: self.alpha + ) + } + + func withUpdatedAlpha(_ alpha: CGFloat) -> DrawingColor { + return DrawingColor( + red: self.red, + green: self.green, + blue: self.blue, + alpha: alpha, + position: self.position + ) + } + + func withUpdatedPosition(_ position: CGPoint) -> DrawingColor { + return DrawingColor( + red: self.red, + green: self.green, + blue: self.blue, + alpha: self.alpha, + position: position + ) + } + + func toUIColor() -> UIColor { + return UIColor( + red: self.red, + green: self.green, + blue: self.blue, + alpha: self.alpha + ) + } + + func toCGColor() -> CGColor { + return self.toUIColor().cgColor + } + + func toFloat4() -> vector_float4 { + return [ + simd_float1(self.red), + simd_float1(self.green), + simd_float1(self.blue), + simd_float1(self.alpha) + ] + } + + public static func ==(lhs: DrawingColor, rhs: DrawingColor) -> Bool { + if lhs.red != rhs.red { + return false + } + if lhs.green != rhs.green { + return false + } + if lhs.blue != rhs.blue { + return false + } + if lhs.alpha != rhs.alpha { + return false + } + return true + } +} + +extension UIBezierPath { + convenience init(roundRect rect: CGRect, topLeftRadius: CGFloat = 0.0, topRightRadius: CGFloat = 0.0, bottomLeftRadius: CGFloat = 0.0, bottomRightRadius: CGFloat = 0.0) { + self.init() + + let path = CGMutablePath() + + let topLeft = rect.origin + let topRight = CGPoint(x: rect.maxX, y: rect.minY) + let bottomRight = CGPoint(x: rect.maxX, y: rect.maxY) + let bottomLeft = CGPoint(x: rect.minX, y: rect.maxY) + + if topLeftRadius != .zero { + path.move(to: CGPoint(x: topLeft.x+topLeftRadius, y: topLeft.y)) + } else { + path.move(to: CGPoint(x: topLeft.x, y: topLeft.y)) + } + + if topRightRadius != .zero { + path.addLine(to: CGPoint(x: topRight.x-topRightRadius, y: topRight.y)) + path.addCurve(to: CGPoint(x: topRight.x, y: topRight.y+topRightRadius), control1: CGPoint(x: topRight.x, y: topRight.y), control2:CGPoint(x: topRight.x, y: topRight.y + topRightRadius)) + } else { + path.addLine(to: CGPoint(x: topRight.x, y: topRight.y)) + } + + if bottomRightRadius != .zero { + path.addLine(to: CGPoint(x: bottomRight.x, y: bottomRight.y-bottomRightRadius)) + path.addCurve(to: CGPoint(x: bottomRight.x-bottomRightRadius, y: bottomRight.y), control1: CGPoint(x: bottomRight.x, y: bottomRight.y), control2: CGPoint(x: bottomRight.x-bottomRightRadius, y: bottomRight.y)) + } else { + path.addLine(to: CGPoint(x: bottomRight.x, y: bottomRight.y)) + } + + if bottomLeftRadius != .zero { + path.addLine(to: CGPoint(x: bottomLeft.x+bottomLeftRadius, y: bottomLeft.y)) + path.addCurve(to: CGPoint(x: bottomLeft.x, y: bottomLeft.y-bottomLeftRadius), control1: CGPoint(x: bottomLeft.x, y: bottomLeft.y), control2: CGPoint(x: bottomLeft.x, y: bottomLeft.y-bottomLeftRadius)) + } else { + path.addLine(to: CGPoint(x: bottomLeft.x, y: bottomLeft.y)) + } + + if topLeftRadius != .zero { + path.addLine(to: CGPoint(x: topLeft.x, y: topLeft.y+topLeftRadius)) + path.addCurve(to: CGPoint(x: topLeft.x+topLeftRadius, y: topLeft.y) , control1: CGPoint(x: topLeft.x, y: topLeft.y) , control2: CGPoint(x: topLeft.x+topLeftRadius, y: topLeft.y)) + } else { + path.addLine(to: CGPoint(x: topLeft.x, y: topLeft.y)) + } + + path.closeSubpath() + self.cgPath = path + } +} + +extension CGPoint { + func isEqual(to point: CGPoint, epsilon: CGFloat) -> Bool { + if x - epsilon <= point.x && point.x <= x + epsilon && y - epsilon <= point.y && point.y <= y + epsilon { + return true + } + return false + } + + static public func + (lhs: CGPoint, rhs: CGPoint) -> CGPoint { + return CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y) + } + + static public func - (lhs: CGPoint, rhs: CGPoint) -> CGPoint { + return CGPoint(x: lhs.x - rhs.x, y: lhs.y - rhs.y) + } + + static public func * (lhs: CGPoint, rhs: CGFloat) -> CGPoint { + return CGPoint(x: lhs.x * rhs, y: lhs.y * rhs) + } + + static public func / (lhs: CGPoint, rhs: CGFloat) -> CGPoint { + return CGPoint(x: lhs.x / rhs, y: lhs.y / rhs) + } + + var length: CGFloat { + return sqrt(self.x * self.x + self.y * self.y) + } + + static func middle(p1: CGPoint, p2: CGPoint) -> CGPoint { + return CGPoint(x: (p1.x + p2.x) * 0.5, y: (p1.y + p2.y) * 0.5) + } + + func distance(to point: CGPoint) -> CGFloat { + return sqrt(pow((point.x - self.x), 2) + pow((point.y - self.y), 2)) + } + + func distanceSquared(to point: CGPoint) -> CGFloat { + return pow((point.x - self.x), 2) + pow((point.y - self.y), 2) + } + + func angle(to point: CGPoint) -> CGFloat { + return atan2((point.y - self.y), (point.x - self.x)) + } + + func pointAt(distance: CGFloat, angle: CGFloat) -> CGPoint { + return CGPoint(x: distance * cos(angle) + self.x, y: distance * sin(angle) + self.y) + } + + func point(to point: CGPoint, t: CGFloat) -> CGPoint { + return CGPoint(x: self.x + t * (point.x - self.x), y: self.y + t * (point.y - self.y)) + } + + func perpendicularPointOnLine(start: CGPoint, end: CGPoint) -> CGPoint { + let l2 = start.distanceSquared(to: end) + if l2.isZero { + return start + } + let t = ((self.x - start.x) * (end.x - start.x) + (self.y - start.y) * (end.y - start.y)) / l2 + return CGPoint(x: start.x + t * (end.x - start.x), y: start.y + t * (end.y - start.y)) + } + + func linearBezierPoint(to: CGPoint, t: CGFloat) -> CGPoint { + let dx = to.x - x; + let dy = to.y - y; + + let px = x + (t * dx); + let py = y + (t * dy); + + return CGPoint(x: px, y: py) + } + + fileprivate func _cubicBezier(_ t: CGFloat, _ start: CGFloat, _ c1: CGFloat, _ c2: CGFloat, _ end: CGFloat) -> CGFloat { + let _t = 1 - t; + let _t2 = _t * _t; + let _t3 = _t * _t * _t ; + let t2 = t * t; + let t3 = t * t * t; + + return _t3 * start + + 3.0 * _t2 * t * c1 + + 3.0 * _t * t2 * c2 + + t3 * end; + } + + func cubicBezierPoint(to: CGPoint, controlPoint1 c1: CGPoint, controlPoint2 c2: CGPoint, t: CGFloat) -> CGPoint { + let x = _cubicBezier(t, self.x, c1.x, c2.x, to.x); + let y = _cubicBezier(t, self.y, c1.y, c2.y, to.y); + + return CGPoint(x: x, y: y); + } + + fileprivate func _quadBezier(_ t: CGFloat, _ start: CGFloat, _ c1: CGFloat, _ end: CGFloat) -> CGFloat { + let _t = 1 - t; + let _t2 = _t * _t; + let t2 = t * t; + + return _t2 * start + + 2 * _t * t * c1 + + t2 * end; + } + + func quadBezierPoint(to: CGPoint, controlPoint: CGPoint, t: CGFloat) -> CGPoint { + let x = _quadBezier(t, self.x, controlPoint.x, to.x); + let y = _quadBezier(t, self.y, controlPoint.y, to.y); + + return CGPoint(x: x, y: y); + } +} + + +extension CGPath { + static func star(in rect: CGRect, extrusion: CGFloat, points: Int = 5) -> CGPath { + func pointFrom(angle: CGFloat, radius: CGFloat, offset: CGPoint) -> CGPoint { + return CGPoint(x: radius * cos(angle) + offset.x, y: radius * sin(angle) + offset.y) + } + + let path = CGMutablePath() + + let center = rect.center.offsetBy(dx: 0.0, dy: rect.height * 0.05) + var angle: CGFloat = -CGFloat(.pi / 2.0) + let angleIncrement = CGFloat(.pi * 2.0 / Double(points)) + let radius = rect.width / 2.0 + + var firstPoint = true + for _ in 1 ... points { + let point = center.pointAt(distance: radius, angle: angle) + let nextPoint = center.pointAt(distance: radius, angle: angle + angleIncrement) + let midPoint = center.pointAt(distance: extrusion, angle: angle + angleIncrement * 0.5) + + if firstPoint { + firstPoint = false + path.move(to: point) + } + path.addLine(to: midPoint) + path.addLine(to: nextPoint) + + angle += angleIncrement + } + path.closeSubpath() + + return path + } + + static func arrow(from point: CGPoint, controlPoint: CGPoint, width: CGFloat, height: CGFloat, isOpen: Bool) -> CGPath { + let angle = atan2(point.y - controlPoint.y, point.x - controlPoint.x) + let angleAdjustment = atan2(width, -height) + let distance = hypot(width, height) + + let path = CGMutablePath() + path.move(to: point) + path.addLine(to: point.pointAt(distance: distance, angle: angle - angleAdjustment)) + if isOpen { + path.addLine(to: point) + } + path.addLine(to: point.pointAt(distance: distance, angle: angle + angleAdjustment)) + if isOpen { + path.addLine(to: point) + } else { + path.closeSubpath() + } + return path + } + + static func curve(start: CGPoint, end: CGPoint, mid: CGPoint, lineWidth: CGFloat?, arrowSize: CGSize?, twoSided: Bool = false) -> CGPath { + let linePath = CGMutablePath() + + let controlPoints = configureControlPoints(data: [start, mid, end]) + var lineStart = start + if let arrowSize = arrowSize, twoSided { + lineStart = start.pointAt(distance: -arrowSize.height * 0.5, angle: controlPoints[0].ctrl1.angle(to: start)) + } + linePath.move(to: lineStart) + linePath.addCurve(to: mid, control1: controlPoints[0].ctrl1, control2: controlPoints[0].ctrl2) + + var lineEnd = end + if let arrowSize = arrowSize { + lineEnd = end.pointAt(distance: -arrowSize.height * 0.5, angle: controlPoints[1].ctrl1.angle(to: end)) + } + linePath.addCurve(to: lineEnd, control1: controlPoints[1].ctrl1, control2: controlPoints[1].ctrl2) + + let path: CGMutablePath + if let lineWidth = lineWidth, let mutablePath = linePath.copy(strokingWithWidth: lineWidth, lineCap: .square, lineJoin: .round, miterLimit: 0.0).mutableCopy() { + path = mutablePath + } else { + path = linePath + } + + if let arrowSize = arrowSize { + let arrowPath = arrow(from: end, controlPoint: controlPoints[1].ctrl1, width: arrowSize.width, height: arrowSize.height, isOpen: false) + path.addPath(arrowPath) + + if twoSided { + let secondArrowPath = arrow(from: start, controlPoint: controlPoints[0].ctrl1, width: arrowSize.width, height: arrowSize.height, isOpen: false) + path.addPath(secondArrowPath) + } + } + + return path + } + + static func bubble(in rect: CGRect, cornerRadius: CGFloat, smallCornerRadius: CGFloat, tailPosition: CGPoint, tailWidth: CGFloat) -> CGPath { + let r1 = min(cornerRadius, min(rect.width, rect.height) / 3.0) + let r2 = min(smallCornerRadius, min(rect.width, rect.height) / 10.0) + + let ax = tailPosition.x * rect.width + let ay = tailPosition.y + + let width = min(max(tailWidth, ay / 2.0), rect.width / 4.0) + let angle = atan2(ay, width) + let h = r2 / tan(angle / 2.0) + + let r1a = min(r1, min(rect.maxX - ax, ax - rect.minX) * 0.5) + let r2a = min(r2, min(rect.maxX - ax, ax - rect.minX) * 0.2) + + let path = CGMutablePath() + path.addArc(center: CGPoint(x: rect.minX + r1, y: rect.minY + r1), radius: r1, startAngle: .pi, endAngle: .pi * 3.0 / 2.0, clockwise: false) + path.addArc(center: CGPoint(x: rect.maxX - r1, y: rect.minY + r1), radius: r1, startAngle: -.pi / 2.0, endAngle: 0.0, clockwise: false) + + if ax > rect.width / 2.0 { + if ax < rect.width - 1 { + path.addArc(center: CGPoint(x: rect.maxX - r1a, y: rect.maxY - r1a), radius: r1a, startAngle: 0.0, endAngle: .pi / 2.0, clockwise: false) + path.addArc(center: CGPoint(x: rect.minX + ax + r2a, y: rect.maxY + r2a), radius: r2a, startAngle: .pi * 3.0 / 2.0, endAngle: .pi, clockwise: true) + } + path.addLine(to: CGPoint(x: rect.minX + ax, y: rect.maxY + ay)) + path.addArc(center: CGPoint(x: rect.minX + ax - width - r2, y: rect.maxY + h), radius: h, startAngle: -(CGFloat.pi / 2 - angle), endAngle: CGFloat.pi * 3 / 2, clockwise: true) + path.addArc(center: CGPoint(x: rect.minX + r1, y: rect.maxY - r1), radius: r1, startAngle: CGFloat.pi / 2, endAngle: CGFloat.pi, clockwise: false) + } else { + path.addArc(center: CGPoint(x: rect.maxX - r1, y: rect.maxY - r1), radius: r1, startAngle: 0, endAngle: CGFloat.pi / 2, clockwise: false) + path.addArc(center: CGPoint(x: rect.minX + ax + width + r2, y: rect.maxY + h), radius: h, startAngle: CGFloat.pi * 3 / 2, endAngle: CGFloat.pi * 3 / 2 - angle, clockwise: true) + path.addLine(to: CGPoint(x: rect.minX + ax, y: rect.maxY + ay)) + if ax > 1 { + path.addArc(center: CGPoint(x: rect.minX + ax - r2a, y: rect.maxY + r2a), radius: r2a, startAngle: 0, endAngle: CGFloat.pi * 3 / 2, clockwise: true) + path.addArc(center: CGPoint(x: rect.minX + r1a, y: rect.maxY - r1a), radius: r1a, startAngle: CGFloat.pi / 2, endAngle: CGFloat.pi, clockwise: false) + } + } + + path.closeSubpath() + + return path + } +} + +private func configureControlPoints(data: [CGPoint]) -> [(ctrl1: CGPoint, ctrl2: CGPoint)] { + let segments = data.count - 1 + + if segments == 1 { + let p0 = data[0] + let p3 = data[1] + + return [(p0, p3)] + } else if segments > 1 { + var ad: [CGFloat] = [] + var d: [CGFloat] = [] + var bd: [CGFloat] = [] + + var rhsArray: [CGPoint] = [] + + for i in 0 ..< segments { + var rhsXValue: CGFloat = 0.0 + var rhsYValue: CGFloat = 0.0 + + let p0 = data[i] + let p3 = data[i + 1] + + if i == 0 { + bd.append(0.0) + d.append(2.0) + ad.append(1.0) + + rhsXValue = p0.x + 2.0 * p3.x + rhsYValue = p0.y + 2.0 * p3.y + } else if i == segments - 1 { + bd.append(2.0) + d.append(7.0) + ad.append(0.0) + + rhsXValue = 8.0 * p0.x + p3.x + rhsYValue = 8.0 * p0.y + p3.y + } else { + bd.append(1.0) + d.append(4.0) + ad.append(1.0) + + rhsXValue = 4.0 * p0.x + 2.0 * p3.x + rhsYValue = 4.0 * p0.y + 2.0 * p3.y + } + + rhsArray.append(CGPoint(x: rhsXValue, y: rhsYValue)) + } + + var firstControlPoints: [CGPoint?] = [] + var secondControlPoints: [CGPoint?] = [] + + var controlPoints : [(CGPoint, CGPoint)] = [] + + var solutionSet1 = [CGPoint?]() + solutionSet1 = Array(repeating: nil, count: segments) + + ad[0] = ad[0] / d[0] + rhsArray[0].x = rhsArray[0].x / d[0] + rhsArray[0].y = rhsArray[0].y / d[0] + + if segments > 2 { + for i in 1...segments - 2 { + let rhsValueX = rhsArray[i].x + let prevRhsValueX = rhsArray[i - 1].x + + let rhsValueY = rhsArray[i].y + let prevRhsValueY = rhsArray[i - 1].y + + ad[i] = ad[i] / (d[i] - bd[i] * ad[i - 1]); + + let exp1x = (rhsValueX - (bd[i] * prevRhsValueX)) + let exp1y = (rhsValueY - (bd[i] * prevRhsValueY)) + let exp2 = (d[i] - bd[i] * ad[i - 1]) + + rhsArray[i].x = exp1x / exp2 + rhsArray[i].y = exp1y / exp2 + } + } + + let lastElementIndex = segments - 1 + let exp1 = (rhsArray[lastElementIndex].x - bd[lastElementIndex] * rhsArray[lastElementIndex - 1].x) + let exp1y = (rhsArray[lastElementIndex].y - bd[lastElementIndex] * rhsArray[lastElementIndex - 1].y) + let exp2 = (d[lastElementIndex] - bd[lastElementIndex] * ad[lastElementIndex - 1]) + rhsArray[lastElementIndex].x = exp1 / exp2 + rhsArray[lastElementIndex].y = exp1y / exp2 + + solutionSet1[lastElementIndex] = rhsArray[lastElementIndex] + + for i in (0..= self.notificationDelay { + self.notifyUpdateForElapsedTime(elapsedTime) + self.lastNotificationTime = 0.0 + self.numberOfFrames = 0 + } + } + + private func notifyUpdateForElapsedTime(_ elapsedTime: CFAbsoluteTime) { + let fps = Int(round(Double(self.numberOfFrames) / elapsedTime)) + self.delegate?.fpsCounter(self, didUpdateFramesPerSecond: fps) + } +} + + +/// The delegate protocol for the FPSCounter class. +/// +/// Implement this protocol if you want to receive updates from a `FPSCounter`. +/// +protocol FPSCounterDelegate: NSObjectProtocol { + + /// Called in regular intervals while the counter is tracking FPS. + /// + /// - Parameters: + /// - counter: The FPSCounter that sent the update + /// - fps: The current FPS of the application + /// + func fpsCounter(_ counter: FPSCounter, didUpdateFramesPerSecond fps: Int) +} + +class BezierPath { + struct Element { + enum ElementType { + case moveTo + case addLine + case cubicCurve + case quadCurve + } + + let type: ElementType + + var startPoint: Polyline.Point + var endPoint: Polyline.Point + var controlPoints: [CGPoint] + + var lengthRange: ClosedRange? + var calculatedLength: CGFloat? + + func point(at t: CGFloat) -> CGPoint { + switch self.type { + case .addLine: + return self.startPoint.location.linearBezierPoint(to: self.endPoint.location, t: t) + case .cubicCurve: + return self.startPoint.location.cubicBezierPoint(to: self.endPoint.location, controlPoint1: self.controlPoints[0], controlPoint2: self.controlPoints[1], t: t) + case .quadCurve: + return self.startPoint.location.quadBezierPoint(to: self.endPoint.location, controlPoint: self.controlPoints[0], t: t) + default: + return .zero + } + } + } + + let path = UIBezierPath() + + var elements: [Element] = [] + var elementCount: Int { + return self.elements.count + } + + func element(at t: CGFloat) -> (element: Element, innerT: CGFloat)? { + let t = min(max(0.0, t), 1.0) + + for element in elements { + if let lengthRange = element.lengthRange, lengthRange.contains(t) { + let innerT = (t - lengthRange.lowerBound) / (lengthRange.upperBound - lengthRange.lowerBound) + return (element, innerT) + } + } + return nil + } + + var points: [Int: CGPoint] = [:] + func point(at t: CGFloat) -> CGPoint? { + if let (element, innerT) = self.element(at: t) { + return element.point(at: innerT) + } else { + return nil + } + } + + func append(_ element: Element) { + self.elements.append(element) + switch element.type { + case .moveTo: + self.move(to: element.startPoint.location) + case .addLine: + self.addLine(to: element.endPoint.location) + case .cubicCurve: + self.addCurve(to: element.endPoint.location, controlPoint1: element.controlPoints[0], controlPoint2: element.controlPoints[1]) + case .quadCurve: + self.addQuadCurve(to: element.endPoint.location, controlPoint: element.controlPoints[0]) + } + } + + private func move(to point: CGPoint) { + self.path.move(to: point) + } + + private func addLine(to point: CGPoint) { + self.path.addLine(to: point) + } + + private func addQuadCurve(to point: CGPoint, controlPoint: CGPoint) { + self.path.addQuadCurve(to: point, controlPoint: controlPoint) + } + + private func addCurve(to point: CGPoint, controlPoint1: CGPoint, controlPoint2: CGPoint) { + self.path.addCurve(to: point, controlPoint1: controlPoint1, controlPoint2: controlPoint2) + } + + private func close() { + self.path.close() + } + + func trimming(to elementIndex: Int) -> BezierPath { + let outputPath = BezierPath() + for element in self.elements[0 ... elementIndex] { + outputPath.append(element) + } + return outputPath + } + + func closedCopy() -> BezierPath { + let outputPath = BezierPath() + for element in self.elements { + outputPath.append(element) + } + outputPath.close() + return outputPath + } +} + +func concaveHullPath(points: [CGPoint]) -> CGPath { + let hull = getHull(points, concavity: 1000.0) + let hullPath = CGMutablePath() + var moved = true + for point in hull { + if moved { + hullPath.move(to: point) + moved = false + } else { + hullPath.addLine(to: point) + } + } + hullPath.closeSubpath() + + return hullPath +} + +func expandPath(_ path: CGPath, width: CGFloat) -> CGPath { + let expandedPath = path.copy(strokingWithWidth: width * 2.0, lineCap: .round, lineJoin: .round, miterLimit: 0.0) + + class UserInfo { + let outputPath = CGMutablePath() + var passedFirst = false + } + var userInfo = UserInfo() + + withUnsafeMutablePointer(to: &userInfo) { userInfoPointer in + expandedPath.apply(info: userInfoPointer) { (userInfo, nextElementPointer) in + let element = nextElementPointer.pointee + let userInfoPointer = userInfo!.assumingMemoryBound(to: UserInfo.self) + let userInfo = userInfoPointer.pointee + + if !userInfo.passedFirst { + if case .closeSubpath = element.type { + userInfo.passedFirst = true + } + } else { + switch element.type { + case .moveToPoint: + userInfo.outputPath.move(to: element.points[0]) + case .addLineToPoint: + userInfo.outputPath.addLine(to: element.points[0]) + case .addQuadCurveToPoint: + userInfo.outputPath.addQuadCurve(to: element.points[1], control: element.points[0]) + case .addCurveToPoint: + userInfo.outputPath.addCurve(to: element.points[2], control1: element.points[0], control2: element.points[1]) + case .closeSubpath: + userInfo.outputPath.closeSubpath() + @unknown default: + userInfo.outputPath.closeSubpath() + } + } + } + } + return userInfo.outputPath +} + +class Matrix { + private(set) var m: [Float] + + private init() { + m = [ + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ] + } + + @discardableResult + func translation(x: Float, y: Float, z: Float) -> Matrix { + m[12] = x + m[13] = y + m[14] = z + return self + } + + @discardableResult + func scaling(x: Float, y: Float, z: Float) -> Matrix { + m[0] = x + m[5] = y + m[10] = z + return self + } + + static var identity = Matrix() +} + +struct Vertex { + var position: vector_float4 + var texCoord: vector_float2 + + init(position: CGPoint, texCoord: CGPoint) { + self.position = position.toFloat4() + self.texCoord = texCoord.toFloat2() + } +} + +struct Point { + var position: vector_float4 + var color: vector_float4 + var angle: Float + var size: Float + + init(x: CGFloat, y: CGFloat, color: DrawingColor, size: CGFloat, angle: CGFloat = 0) { + self.position = vector_float4(Float(x), Float(y), 0, 1) + self.size = Float(size) + self.color = color.toFloat4() + self.angle = Float(angle) + } +} + +extension CGPoint { + func toFloat4(z: CGFloat = 0, w: CGFloat = 1) -> vector_float4 { + return [Float(x), Float(y), Float(z) ,Float(w)] + } + + func toFloat2() -> vector_float2 { + return [Float(x), Float(y)] + } + + func offsetBy(_ offset: CGPoint) -> CGPoint { + return self.offsetBy(dx: offset.x, dy: offset.y) + } +} diff --git a/submodules/DrawingUI/Sources/DrawingVectorEntity.swift b/submodules/DrawingUI/Sources/DrawingVectorEntity.swift new file mode 100644 index 0000000000..5e9d319af2 --- /dev/null +++ b/submodules/DrawingUI/Sources/DrawingVectorEntity.swift @@ -0,0 +1,327 @@ +import Foundation +import UIKit +import Display +import AccountContext + +final class DrawingVectorEntity: DrawingEntity { + public enum VectorType { + case line + case oneSidedArrow + case twoSidedArrow + } + + let uuid: UUID + let isAnimated: Bool + + var type: VectorType + var color: DrawingColor + var lineWidth: CGFloat + + var drawingSize: CGSize + var referenceDrawingSize: CGSize + var start: CGPoint + var mid: (CGFloat, CGFloat) + var end: CGPoint + + var _cachedMidPoint: (start: CGPoint, end: CGPoint, midLength: CGFloat, midHeight: CGFloat, midPoint: CGPoint)? + var midPoint: CGPoint { + if let (start, end, midLength, midHeight, midPoint) = self._cachedMidPoint, start == self.start, end == self.end, midLength == self.mid.0, midHeight == self.mid.1 { + return midPoint + } else { + let midPoint = midPointPositionFor(start: self.start, end: self.end, length: self.mid.0, height: self.mid.1) + self._cachedMidPoint = (self.start, self.end, self.mid.0, self.mid.1, midPoint) + return midPoint + } + } + + init(type: VectorType, color: DrawingColor, lineWidth: CGFloat) { + self.uuid = UUID() + self.isAnimated = false + + self.type = type + self.color = color + self.lineWidth = lineWidth + + self.drawingSize = .zero + self.referenceDrawingSize = .zero + self.start = CGPoint() + self.mid = (0.5, 0.0) + self.end = CGPoint() + } + + var center: CGPoint { + return self.start + } + + func duplicate() -> DrawingEntity { + let newEntity = DrawingVectorEntity(type: self.type, color: self.color, lineWidth: self.lineWidth) + newEntity.drawingSize = self.drawingSize + newEntity.referenceDrawingSize = self.referenceDrawingSize + newEntity.start = self.start + newEntity.mid = self.mid + newEntity.end = self.end + return newEntity + } + + weak var currentEntityView: DrawingEntityView? + func makeView(context: AccountContext) -> DrawingEntityView { + let entityView = DrawingVectorEntityView(context: context, entity: self) + self.currentEntityView = entityView + return entityView + } +} + +final class DrawingVectorEntityView: DrawingEntityView { + private var vectorEntity: DrawingVectorEntity { + return self.entity as! DrawingVectorEntity + } + + fileprivate let shapeLayer = SimpleShapeLayer() + + init(context: AccountContext, entity: DrawingVectorEntity) { + super.init(context: context, entity: entity) + + self.layer.addSublayer(self.shapeLayer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override var selectionBounds: CGRect { + return self.shapeLayer.path?.boundingBox ?? self.bounds + } + + override func update(animated: Bool) { + self.center = CGPoint(x: self.vectorEntity.drawingSize.width * 0.5, y: self.vectorEntity.drawingSize.height * 0.5) + self.bounds = CGRect(origin: .zero, size: self.vectorEntity.drawingSize) + + let minLineWidth = max(10.0, min(self.vectorEntity.referenceDrawingSize.width, self.vectorEntity.referenceDrawingSize.height) * 0.02) + let maxLineWidth = max(10.0, min(self.vectorEntity.referenceDrawingSize.width, self.vectorEntity.referenceDrawingSize.height) * 0.1) + let lineWidth = minLineWidth + (maxLineWidth - minLineWidth) * self.vectorEntity.lineWidth + + self.shapeLayer.path = CGPath.curve( + start: self.vectorEntity.start, + end: self.vectorEntity.end, + mid: self.vectorEntity.midPoint, + lineWidth: lineWidth, + arrowSize: self.vectorEntity.type == .line ? nil : CGSize(width: lineWidth * 1.5, height: lineWidth * 3.0), + twoSided: self.vectorEntity.type == .twoSidedArrow + ) + self.shapeLayer.fillColor = self.vectorEntity.color.toCGColor() + + super.update(animated: animated) + } + + override func updateSelectionView() { + guard let selectionView = self.selectionView as? DrawingVectorEntititySelectionView else { + return + } + + let scale = self.superview?.superview?.layer.value(forKeyPath: "transform.scale.x") as? CGFloat ?? 1.0 + + let drawingSize = self.vectorEntity.drawingSize + selectionView.bounds = CGRect(origin: .zero, size: drawingSize) + selectionView.center = CGPoint(x: drawingSize.width * 0.5 * scale, y: drawingSize.height * 0.5 * scale) + selectionView.transform = CGAffineTransform(scaleX: scale, y: scale) + selectionView.scale = scale + } + + override func precisePoint(inside point: CGPoint) -> Bool { + if let path = self.shapeLayer.path { + if path.contains(point) { + return true + } else { + return false + } + } else { + return super.precisePoint(inside: point) + } + } + + override func makeSelectionView() -> DrawingEntitySelectionView { + if let selectionView = self.selectionView { + return selectionView + } + let selectionView = DrawingVectorEntititySelectionView() + selectionView.entityView = self + return selectionView + } +} + +private func midPointPositionFor(start: CGPoint, end: CGPoint, length: CGFloat, height: CGFloat) -> CGPoint { + let distance = end.distance(to: start) + let angle = start.angle(to: end) + let p1 = start.pointAt(distance: distance * length, angle: angle) + let p2 = p1.pointAt(distance: distance * height, angle: angle + .pi * 0.5) + return p2 +} + +final class DrawingVectorEntititySelectionView: DrawingEntitySelectionView, UIGestureRecognizerDelegate { + private let startHandle = SimpleShapeLayer() + private let midHandle = SimpleShapeLayer() + private let endHandle = SimpleShapeLayer() + + private var panGestureRecognizer: UIPanGestureRecognizer! + + var scale: CGFloat = 1.0 { + didSet { + self.setNeedsLayout() + } + } + + override init(frame: CGRect) { + let handleBounds = CGRect(origin: .zero, size: entitySelectionViewHandleSize) + self.startHandle.bounds = handleBounds + self.startHandle.fillColor = UIColor(rgb: 0x0a60ff).cgColor + self.startHandle.strokeColor = UIColor(rgb: 0xffffff).cgColor + self.startHandle.rasterizationScale = UIScreen.main.scale + self.startHandle.shouldRasterize = true + + self.midHandle.bounds = handleBounds + self.midHandle.fillColor = UIColor(rgb: 0x00ff00).cgColor + self.midHandle.strokeColor = UIColor(rgb: 0xffffff).cgColor + self.midHandle.rasterizationScale = UIScreen.main.scale + self.midHandle.shouldRasterize = true + + self.endHandle.bounds = handleBounds + self.endHandle.fillColor = UIColor(rgb: 0x0a60ff).cgColor + self.endHandle.strokeColor = UIColor(rgb: 0xffffff).cgColor + self.endHandle.rasterizationScale = UIScreen.main.scale + self.endHandle.shouldRasterize = true + + super.init(frame: frame) + + self.backgroundColor = .clear + self.isOpaque = false + + self.layer.addSublayer(self.startHandle) + self.layer.addSublayer(self.midHandle) + self.layer.addSublayer(self.endHandle) + + let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:))) + panGestureRecognizer.delegate = self + self.addGestureRecognizer(panGestureRecognizer) + self.panGestureRecognizer = panGestureRecognizer + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + + private var currentHandle: CALayer? + @objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { + guard let entityView = self.entityView, let entity = entityView.entity as? DrawingVectorEntity else { + return + } + let location = gestureRecognizer.location(in: self) + + switch gestureRecognizer.state { + case .began: + 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) + + var updatedStart = entity.start + var updatedMid = entity.mid + var updatedEnd = entity.end + + if self.currentHandle === self.startHandle { + updatedStart.x += delta.x + updatedStart.y += delta.y + } else if self.currentHandle === self.endHandle { + updatedEnd.x += delta.x + updatedEnd.y += delta.y + } else if self.currentHandle === self.midHandle { + var updatedMidPoint = entity.midPoint + updatedMidPoint.x += delta.x + updatedMidPoint.y += delta.y + + let distance = updatedStart.distance(to: updatedEnd) + let pointOnLine = updatedMidPoint.perpendicularPointOnLine(start: updatedStart, end: updatedEnd) + + let angle = updatedStart.angle(to: updatedEnd) + let midAngle = updatedStart.angle(to: updatedMidPoint) + var height = updatedMidPoint.distance(to: pointOnLine) / distance + var deltaAngle = midAngle - angle + if deltaAngle > .pi { + deltaAngle = angle - 2 * .pi + } else if deltaAngle < -.pi { + deltaAngle = angle + 2 * .pi + } + if deltaAngle < 0.0 { + height *= -1.0 + } + let length = updatedStart.distance(to: pointOnLine) / distance + updatedMid = (length, height) + } else if self.currentHandle === self.layer { + updatedStart.x += delta.x + updatedStart.y += delta.y + updatedEnd.x += delta.x + updatedEnd.y += delta.y + } + + entity.start = updatedStart + entity.mid = updatedMid + entity.end = updatedEnd + entityView.update() + + gestureRecognizer.setTranslation(.zero, in: entityView) + case .ended: + break + default: + break + } + } + + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + if self.startHandle.frame.contains(point) || self.midHandle.frame.contains(point) || self.endHandle.frame.contains(point) { + return true + } else if let entityView = self.entityView as? DrawingVectorEntityView, let path = entityView.shapeLayer.path { + return path.contains(self.convert(point, to: entityView)) + } + return false + } + + override func layoutSubviews() { + guard let entityView = self.entityView as? DrawingVectorEntityView, let entity = entityView.entity as? DrawingVectorEntity else { + return + } + + 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 + + self.startHandle.path = handlePath + self.startHandle.position = entity.start + self.startHandle.bounds = bounds + self.startHandle.lineWidth = lineWidth + + self.midHandle.path = handlePath + self.midHandle.position = entity.midPoint + self.midHandle.bounds = bounds + self.midHandle.lineWidth = lineWidth + + self.endHandle.path = handlePath + self.endHandle.position = entity.end + self.endHandle.bounds = bounds + self.endHandle.lineWidth = lineWidth + } + + var isTracking: Bool { + return gestureIsTracking(self.panGestureRecognizer) + } +} diff --git a/submodules/DrawingUI/Sources/DrawingView.swift b/submodules/DrawingUI/Sources/DrawingView.swift new file mode 100644 index 0000000000..c4a2bdadd7 --- /dev/null +++ b/submodules/DrawingUI/Sources/DrawingView.swift @@ -0,0 +1,964 @@ +import Foundation +import UIKit +import Display +import SwiftSignalKit +import ComponentFlow +import LegacyComponents +import AppBundle +import ImageBlur + +protocol DrawingElement: AnyObject { + var uuid: UUID { get } + var bounds: CGRect { get } + var points: [Polyline.Point] { get } + + var translation: CGPoint { get set } + + var renderLineWidth: CGFloat { get } + + func containsPoint(_ point: CGPoint) -> Bool + func hasPointsInsidePath(_ path: UIBezierPath) -> Bool + + init(drawingSize: CGSize, color: DrawingColor, lineWidth: CGFloat, arrow: Bool) + + func setupRenderLayer() -> DrawingRenderLayer? + func updatePath(_ path: DrawingGesturePipeline.DrawingResult, state: DrawingGesturePipeline.DrawingGestureState) + + func draw(in: CGContext, size: CGSize) +} + +enum DrawingCommand { + enum DrawingElementTransform { + case move(offset: CGPoint) + } + + case addStroke(DrawingElement) + case updateStrokes([UUID], DrawingElementTransform) + case removeStroke(DrawingElement) + case addEntity(DrawingEntity) + case updateEntity(UUID, DrawingEntity) + case removeEntity(DrawingEntity) + case updateEntityZOrder(UUID, Int32) +} + +public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDrawingView { + public var zoomOut: () -> Void = {} + + struct NavigationState { + let canUndo: Bool + let canRedo: Bool + let canClear: Bool + let canZoomOut: Bool + } + + enum Action { + case undo + case redo + case clear + case zoomOut + } + + enum Tool { + case pen + case marker + case neon + case pencil + case eraser + case lasso + case objectRemover + case blur + } + + var tool: Tool = .pen + var toolColor: DrawingColor = DrawingColor(color: .white) + var toolBrushSize: CGFloat = 0.15 + var toolHasArrow: Bool = false + + var stateUpdated: (NavigationState) -> Void = { _ in } + + var shouldBegin: (CGPoint) -> Bool = { _ in return true } + var requestMenu: ([UUID], CGRect) -> Void = { _, _ in } + var getFullImage: (Bool) -> UIImage? = { _ in return nil } + + private var elements: [DrawingElement] = [] + private var redoElements: [DrawingElement] = [] + fileprivate var uncommitedElement: DrawingElement? + + private(set) var drawingImage: UIImage? + private let renderer: UIGraphicsImageRenderer + + private var currentDrawingView: UIView + private var currentDrawingLayer: DrawingRenderLayer? + + private var selectionImage: UIImage? + private var pannedSelectionView: UIView + + var lassoView: DrawingLassoView + private var metalView: DrawingMetalView + + private let brushSizePreviewLayer: SimpleShapeLayer + + let imageSize: CGSize + private var zoomScale: CGFloat = 1.0 + + private var drawingGesturePipeline: DrawingGesturePipeline? + private var longPressGestureRecognizer: UILongPressGestureRecognizer? + + private var loadedTemplates: [UnistrokeTemplate] = [] + private var previousStrokePoint: CGPoint? + private var strokeRecognitionTimer: SwiftSignalKit.Timer? + + private func loadTemplates() { + func load(_ name: String) { + if let url = getAppBundle().url(forResource: name, withExtension: "json"), + let data = try? Data(contentsOf: url), + let json = try? JSONSerialization.jsonObject(with: data, options: .fragmentsAllowed) as? [String: Any], + let points = json["points"] as? [Any] + { + var strokePoints: [CGPoint] = [] + for point in points { + let x = (point as! [Any]).first as! Double + let y = (point as! [Any]).last as! Double + strokePoints.append(CGPoint(x: x, y: y)) + } + let template = UnistrokeTemplate(name: name, points: strokePoints) + self.loadedTemplates.append(template) + } + } + + load("shape_rectangle") + load("shape_circle") + load("shape_star") + load("shape_arrow") + } + + public init(size: CGSize) { + self.imageSize = size + + let format = UIGraphicsImageRendererFormat() + format.scale = 1.0 + self.renderer = UIGraphicsImageRenderer(size: size, format: format) + + self.currentDrawingView = UIView() + self.currentDrawingView.frame = CGRect(origin: .zero, size: size) + self.currentDrawingView.contentScaleFactor = 1.0 + self.currentDrawingView.backgroundColor = .clear + self.currentDrawingView.isUserInteractionEnabled = false + + self.pannedSelectionView = UIView() + self.pannedSelectionView.frame = CGRect(origin: .zero, size: size) + self.pannedSelectionView.contentScaleFactor = 1.0 + self.pannedSelectionView.backgroundColor = .clear + self.pannedSelectionView.isUserInteractionEnabled = false + + self.lassoView = DrawingLassoView(size: size) + self.lassoView.isHidden = true + + self.metalView = DrawingMetalView(size: size)! + + self.brushSizePreviewLayer = SimpleShapeLayer() + self.brushSizePreviewLayer.bounds = CGRect(origin: .zero, size: CGSize(width: 100.0, height: 100.0)) + self.brushSizePreviewLayer.strokeColor = UIColor(rgb: 0x919191).cgColor + self.brushSizePreviewLayer.fillColor = UIColor.white.cgColor + self.brushSizePreviewLayer.path = CGPath(ellipseIn: CGRect(origin: .zero, size: CGSize(width: 100.0, height: 100.0)), transform: nil) + self.brushSizePreviewLayer.opacity = 0.0 + self.brushSizePreviewLayer.shadowColor = UIColor.black.cgColor + self.brushSizePreviewLayer.shadowOpacity = 0.5 + self.brushSizePreviewLayer.shadowOffset = CGSize(width: 0.0, height: 3.0) + self.brushSizePreviewLayer.shadowRadius = 20.0 + + super.init(frame: CGRect(origin: .zero, size: size)) + + Queue.mainQueue().async { + self.loadTemplates() + } + + self.backgroundColor = .clear + self.contentScaleFactor = 1.0 + + self.addSubview(self.currentDrawingView) + self.addSubview(self.metalView) + self.lassoView.addSubview(self.pannedSelectionView) + self.addSubview(self.lassoView) + self.layer.addSublayer(self.brushSizePreviewLayer) + + let drawingGesturePipeline = DrawingGesturePipeline(view: self) + drawingGesturePipeline.gestureRecognizer?.shouldBegin = { [weak self] point in + if let strongSelf = self { + if !strongSelf.shouldBegin(point) { + return false + } + if !strongSelf.lassoView.isHidden && strongSelf.lassoView.point(inside: strongSelf.convert(point, to: strongSelf.lassoView), with: nil) { + return false + } + return true + } else { + return false + } + } + drawingGesturePipeline.onDrawing = { [weak self] state, path in + guard let strongSelf = self else { + return + } + if case .objectRemover = strongSelf.tool { + if case let .location(point) = path { + var elementsToRemove: [DrawingElement] = [] + for element in strongSelf.elements { + if element.containsPoint(point.location) { + elementsToRemove.append(element) + } + } + + for element in elementsToRemove { + strongSelf.removeElement(element) + } + } + } else if case .lasso = strongSelf.tool { + if case let .smoothCurve(bezierPath) = path { + let scale = strongSelf.bounds.width / strongSelf.imageSize.width + + switch state { + case .began: + strongSelf.lassoView.setup(scale: scale) + strongSelf.lassoView.updatePath(bezierPath) + case .changed: + strongSelf.lassoView.updatePath(bezierPath) + case .ended: + let closedPath = bezierPath.closedCopy() + + var selectedElements: [DrawingElement] = [] + var selectedPoints: [CGPoint] = [] + var maxLineWidth: CGFloat = 0.0 + for element in strongSelf.elements { + if element.hasPointsInsidePath(closedPath.path) { + maxLineWidth = max(maxLineWidth, element.renderLineWidth) + selectedElements.append(element) + selectedPoints.append(contentsOf: element.points.map { $0.location }) + } + } + + if selectedPoints.count > 0 { + strongSelf.lassoView.apply(scale: scale, points: selectedPoints, selectedElements: selectedElements.map { $0.uuid }, expand: maxLineWidth) + } else { + strongSelf.lassoView.reset() + } + case .cancelled: + strongSelf.lassoView.reset() + } + } + } else { + switch state { + case .began: + strongSelf.previousStrokePoint = nil + + if strongSelf.uncommitedElement != nil { + strongSelf.finishDrawing() + } + + guard let newElement = strongSelf.prepareNewElement() else { + return + } + if let renderLayer = newElement.setupRenderLayer() { + strongSelf.currentDrawingView.layer.addSublayer(renderLayer) + strongSelf.currentDrawingLayer = renderLayer + } + newElement.updatePath(path, state: state) + strongSelf.uncommitedElement = newElement + case .changed: + strongSelf.uncommitedElement?.updatePath(path, state: state) + + if case let .polyline(line) = path, let lastPoint = line.points.last { + if let previousStrokePoint = strongSelf.previousStrokePoint, line.points.count > 10 { + if lastPoint.location.distance(to: previousStrokePoint) > 10.0 { + strongSelf.previousStrokePoint = lastPoint.location + + strongSelf.strokeRecognitionTimer?.invalidate() + strongSelf.strokeRecognitionTimer = nil + } + + if strongSelf.strokeRecognitionTimer == nil { + strongSelf.strokeRecognitionTimer = SwiftSignalKit.Timer(timeout: 0.85, repeat: false, completion: { [weak self] in + guard let strongSelf = self else { + return + } + if let previousStrokePoint = strongSelf.previousStrokePoint, lastPoint.location.distance(to: previousStrokePoint) <= 10.0 { + let strokeRecognizer = Unistroke(points: line.points.map { $0.location }) + if let template = strokeRecognizer.match(templates: strongSelf.loadedTemplates, minThreshold: 0.5) { + + let edges = line.bounds + let bounds = CGRect(origin: edges.origin, size: CGSize(width: edges.width - edges.minX, height: edges.height - edges.minY)) + + var entity: DrawingEntity? + if template == "shape_rectangle" { + let shapeEntity = DrawingSimpleShapeEntity(shapeType: .rectangle, drawType: .stroke, color: strongSelf.toolColor, lineWidth: 0.25) + shapeEntity.referenceDrawingSize = strongSelf.imageSize + shapeEntity.position = bounds.center + shapeEntity.size = bounds.size + entity = shapeEntity + } else if template == "shape_circle" { + let shapeEntity = DrawingSimpleShapeEntity(shapeType: .ellipse, drawType: .stroke, color: strongSelf.toolColor, lineWidth: 0.25) + shapeEntity.referenceDrawingSize = strongSelf.imageSize + shapeEntity.position = bounds.center + shapeEntity.size = bounds.size + entity = shapeEntity + } else if template == "shape_star" { + let shapeEntity = DrawingSimpleShapeEntity(shapeType: .star, drawType: .stroke, color: strongSelf.toolColor, lineWidth: 0.25) + shapeEntity.referenceDrawingSize = strongSelf.imageSize + shapeEntity.position = bounds.center + shapeEntity.size = CGSize(width: max(bounds.width, bounds.height), height: max(bounds.width, bounds.height)) + entity = shapeEntity + } else if template == "shape_arrow" { + let arrowEntity = DrawingVectorEntity(type: .oneSidedArrow, color: strongSelf.toolColor, lineWidth: 0.2) + arrowEntity.referenceDrawingSize = strongSelf.imageSize + arrowEntity.start = line.points.first?.location ?? .zero + arrowEntity.end = line.points[line.points.count - 4].location + entity = arrowEntity + } + + if let entity = entity { + strongSelf.entitiesView?.add(entity) + strongSelf.cancelDrawing() + strongSelf.drawingGesturePipeline?.gestureRecognizer?.isEnabled = false + strongSelf.drawingGesturePipeline?.gestureRecognizer?.isEnabled = true + } + } + } + strongSelf.strokeRecognitionTimer?.invalidate() + strongSelf.strokeRecognitionTimer = nil + }, queue: Queue.mainQueue()) + strongSelf.strokeRecognitionTimer?.start() + } + } else { + strongSelf.previousStrokePoint = lastPoint.location + } + } + + case .ended: + strongSelf.strokeRecognitionTimer?.invalidate() + strongSelf.strokeRecognitionTimer = nil + strongSelf.uncommitedElement?.updatePath(path, state: state) + Queue.mainQueue().after(0.05) { + strongSelf.finishDrawing() + } + case .cancelled: + strongSelf.strokeRecognitionTimer?.invalidate() + strongSelf.strokeRecognitionTimer = nil + strongSelf.cancelDrawing() + } + } + } + self.drawingGesturePipeline = drawingGesturePipeline + + let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handleLongPress(_:))) + longPressGestureRecognizer.minimumPressDuration = 0.45 + longPressGestureRecognizer.allowableMovement = 2.0 + longPressGestureRecognizer.delegate = self + self.addGestureRecognizer(longPressGestureRecognizer) + self.longPressGestureRecognizer = longPressGestureRecognizer + + self.lassoView.requestMenu = { [weak self] elements, rect in + if let strongSelf = self { + strongSelf.requestMenu(elements, rect) + } + } + + self.lassoView.panBegan = { [weak self] elements in + if let strongSelf = self { + strongSelf.skipDrawing = Set(elements) + strongSelf.commit(reset: true) + strongSelf.updateSelectionImage() + } + } + + self.lassoView.panChanged = { [weak self] elements, offset in + if let strongSelf = self { + let offset = CGPoint(x: offset.x * -1.0, y: offset.y * -1.0) + strongSelf.lassoView.bounds = CGRect(origin: offset, size: strongSelf.lassoView.bounds.size) + } + } + + self.lassoView.panEnded = { [weak self] elements, offset in + if let strongSelf = self { + let elementsSet = Set(elements) + for element in strongSelf.elements { + if elementsSet.contains(element.uuid) { + element.translation = element.translation.offsetBy(offset) + } + } + strongSelf.skipDrawing = Set() + strongSelf.commit(reset: true) + strongSelf.selectionImage = nil + strongSelf.pannedSelectionView.layer.contents = nil + + strongSelf.lassoView.bounds = CGRect(origin: .zero, size: strongSelf.lassoView.bounds.size) + strongSelf.lassoView.translate(offset) + } + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.longPressTimer?.invalidate() + self.strokeRecognitionTimer?.invalidate() + } + + public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + if gestureRecognizer === self.longPressGestureRecognizer, !self.lassoView.isHidden { + return false + } + return true + } + + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + + private var longPressTimer: SwiftSignalKit.Timer? + private var fillCircleLayer: CALayer? + @objc func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { + let location = gestureRecognizer.location(in: self) + switch gestureRecognizer.state { + case .began: + self.longPressTimer?.invalidate() + self.longPressTimer = nil + + if self.longPressTimer == nil { + self.longPressTimer = SwiftSignalKit.Timer(timeout: 0.25, repeat: false, completion: { [weak self] in + if let strongSelf = self { + strongSelf.cancelDrawing() + + let newElement = FillTool(drawingSize: strongSelf.imageSize, color: strongSelf.toolColor, lineWidth: 0.0, arrow: false) + strongSelf.uncommitedElement = newElement + strongSelf.finishDrawing() + } + }, queue: Queue.mainQueue()) + self.longPressTimer?.start() + + let fillCircleLayer = SimpleShapeLayer() + fillCircleLayer.bounds = CGRect(origin: .zero, size: CGSize(width: 160.0, height: 160.0)) + fillCircleLayer.position = location + fillCircleLayer.path = UIBezierPath(ovalIn: CGRect(origin: .zero, size: CGSize(width: 160.0, height: 160.0))).cgPath + fillCircleLayer.fillColor = self.toolColor.toCGColor() + self.layer.addSublayer(fillCircleLayer) + self.fillCircleLayer = fillCircleLayer + + fillCircleLayer.animateScale(from: 0.01, to: 12.0, duration: 0.35, removeOnCompletion: false, completion: { [weak self] _ in + if let strongSelf = self { + if let fillCircleLayer = strongSelf.fillCircleLayer { + strongSelf.fillCircleLayer = nil + fillCircleLayer.removeFromSuperlayer() + } + } + }) + } + case .ended, .cancelled: + self.longPressTimer?.invalidate() + self.longPressTimer = nil + if let fillCircleLayer = self.fillCircleLayer { + self.fillCircleLayer = nil + fillCircleLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak fillCircleLayer] _ in + fillCircleLayer?.removeFromSuperlayer() + }) + } + default: + break + } + } + + private var skipDrawing = Set() + private func commit(reset: Bool = false) { + let currentImage = self.drawingImage + self.drawingImage = self.renderer.image { context in + if !reset { + context.cgContext.clear(CGRect(origin: .zero, size: self.imageSize)) + if let image = currentImage { + image.draw(at: .zero) + } + if let uncommitedElement = self.uncommitedElement { + uncommitedElement.draw(in: context.cgContext, size: self.imageSize) + } + } else { + context.cgContext.clear(CGRect(origin: .zero, size: self.imageSize)) + for element in self.elements { + if !self.skipDrawing.contains(element.uuid) { + element.draw(in: context.cgContext, size: self.imageSize) + } + } + } + } + self.layer.contents = self.drawingImage?.cgImage + + if let currentDrawingLayer = self.currentDrawingLayer { + self.currentDrawingLayer = nil + currentDrawingLayer.removeFromSuperlayer() + } + + self.metalView.clear() + } + + private func updateSelectionImage() { + self.selectionImage = self.renderer.image { context in + for element in self.elements { + if self.skipDrawing.contains(element.uuid) { + element.draw(in: context.cgContext, size: self.imageSize) + } + } + } + self.pannedSelectionView.layer.contents = self.selectionImage?.cgImage + } + + fileprivate func cancelDrawing() { + self.uncommitedElement = nil + + if let currentDrawingLayer = self.currentDrawingLayer { + self.currentDrawingLayer = nil + currentDrawingLayer.removeFromSuperlayer() + } + } + + fileprivate func finishDrawing() { + self.commit() + + self.redoElements.removeAll() + if let uncommitedElement = self.uncommitedElement { + self.elements.append(uncommitedElement) + self.uncommitedElement = nil + } + + self.updateInternalState() + } + + weak var entitiesView: DrawingEntitiesView? + func clear() { + self.entitiesView?.removeAll() + + self.uncommitedElement = nil + self.elements.removeAll() + self.redoElements.removeAll() + self.drawingImage = nil + self.commit(reset: true) + + self.updateInternalState() + + self.lassoView.reset() + } + + private func undo() { + guard let lastElement = self.elements.last else { + return + } + self.uncommitedElement = nil + self.redoElements.append(lastElement) + self.elements.removeLast() + self.commit(reset: true) + + self.updateInternalState() + } + + private func redo() { + guard let lastElement = self.redoElements.last else { + return + } + self.uncommitedElement = nil + self.elements.append(lastElement) + self.redoElements.removeLast() + self.uncommitedElement = lastElement + self.commit(reset: false) + self.uncommitedElement = nil + + self.updateInternalState() + } + + private var preparredEraserImage: UIImage? + + func updateToolState(_ state: DrawingToolState) { + switch state { + case let .pen(brushState): + self.drawingGesturePipeline?.mode = .polyline + self.tool = .pen + self.toolColor = brushState.color + self.toolBrushSize = brushState.size + self.toolHasArrow = brushState.mode == .arrow + case let .marker(brushState): + self.drawingGesturePipeline?.mode = .location + self.tool = .marker + self.toolColor = brushState.color + self.toolBrushSize = brushState.size + self.toolHasArrow = brushState.mode == .arrow + case let .neon(brushState): + self.drawingGesturePipeline?.mode = .smoothCurve + self.tool = .neon + self.toolColor = brushState.color + self.toolBrushSize = brushState.size + self.toolHasArrow = brushState.mode == .arrow + case let .pencil(brushState): + self.drawingGesturePipeline?.mode = .location + self.tool = .pencil + self.toolColor = brushState.color + self.toolBrushSize = brushState.size + self.toolHasArrow = brushState.mode == .arrow + case .lasso: + self.drawingGesturePipeline?.mode = .smoothCurve + self.tool = .lasso + case let .eraser(eraserState): + switch eraserState.mode { + case .bitmap: + self.tool = .eraser + self.drawingGesturePipeline?.mode = .smoothCurve + case .vector: + self.tool = .objectRemover + self.drawingGesturePipeline?.mode = .location + case .blur: + self.tool = .blur + self.drawingGesturePipeline?.mode = .smoothCurve + } + self.toolBrushSize = eraserState.size + } + + if [.eraser, .blur].contains(self.tool) { + Queue.concurrentDefaultQueue().async { + if let image = self.getFullImage(self.tool == .blur) { + if case .eraser = self.tool { + Queue.mainQueue().async { + self.preparredEraserImage = image + } + } else { +// let format = UIGraphicsImageRendererFormat() +// format.scale = 1.0 +// let size = image.size.fitted(CGSize(width: 256, height: 256)) +// let renderer = UIGraphicsImageRenderer(size: size, format: format) +// let scaledImage = renderer.image { _ in +// image.draw(in: CGRect(origin: .zero, size: size)) +// } + + let blurredImage = blurredImage(image, radius: 60.0) + Queue.mainQueue().async { + self.preparredEraserImage = blurredImage + } + } + } + } + } else { + self.preparredEraserImage = nil + } + + } + + func performAction(_ action: Action) { + switch action { + case .undo: + self.undo() + case .redo: + self.redo() + case .clear: + self.clear() + case .zoomOut: + self.zoomOut() + } + } + + private func updateInternalState() { + self.stateUpdated(NavigationState( + canUndo: !self.elements.isEmpty, + canRedo: !self.redoElements.isEmpty, + canClear: !self.elements.isEmpty, + canZoomOut: self.zoomScale > 1.0 + .ulpOfOne + )) + } + + public func updateZoomScale(_ scale: CGFloat) { + self.zoomScale = scale + self.updateInternalState() + } + + private func prepareNewElement() -> DrawingElement? { + let scale = 1.0 / self.zoomScale + let element: DrawingElement? + switch self.tool { + case .pen: + let penTool = PenTool( + drawingSize: self.imageSize, + color: self.toolColor, + lineWidth: self.toolBrushSize * scale, + arrow: self.toolHasArrow + ) + element = penTool + case .marker: + let markerTool = MarkerTool( + drawingSize: self.imageSize, + color: self.toolColor, + lineWidth: self.toolBrushSize * scale, + arrow: self.toolHasArrow + ) + markerTool.metalView = self.metalView + element = markerTool + case .neon: + element = NeonTool( + drawingSize: self.imageSize, + color: self.toolColor, + lineWidth: self.toolBrushSize * scale, + arrow: self.toolHasArrow + ) + case .pencil: + let pencilTool = PencilTool( + drawingSize: self.imageSize, + color: self.toolColor, + lineWidth: self.toolBrushSize * scale, + arrow: self.toolHasArrow + ) + pencilTool.metalView = self.metalView + element = pencilTool + case .blur: + let blurTool = BlurTool( + drawingSize: self.imageSize, + color: self.toolColor, + lineWidth: self.toolBrushSize * scale, + arrow: false) + blurTool.getFullImage = { [weak self] in + return self?.preparredEraserImage + } + element = blurTool + case .eraser: + let eraserTool = EraserTool( + drawingSize: self.imageSize, + color: self.toolColor, + lineWidth: self.toolBrushSize * scale, + arrow: false) + eraserTool.getFullImage = { [weak self] in + return self?.preparredEraserImage + } + element = eraserTool + default: + element = nil + } + return element + } + + func removeElement(_ element: DrawingElement) { + self.elements.removeAll(where: { $0 === element }) + self.commit(reset: true) + } + + func removeElements(_ elements: [UUID]) { + self.elements.removeAll(where: { elements.contains($0.uuid) }) + self.commit(reset: true) + + self.lassoView.reset() + } + + func setBrushSizePreview(_ size: CGFloat?) { + let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + if let size = size { + let minBrushSize = 2.0 + let maxBrushSize = 28.0 + let brushSize = minBrushSize + (maxBrushSize - minBrushSize) * size + + self.brushSizePreviewLayer.transform = CATransform3DMakeScale(brushSize / 100.0, brushSize / 100.0, 1.0) + transition.setAlpha(layer: self.brushSizePreviewLayer, alpha: 1.0) + } else { + transition.setAlpha(layer: self.brushSizePreviewLayer, alpha: 0.0) + } + } + + public override func layoutSubviews() { + super.layoutSubviews() + + let scale = self.scale + let transform = CGAffineTransformMakeScale(scale, scale) + self.currentDrawingView.transform = transform + self.currentDrawingView.frame = self.bounds + + self.drawingGesturePipeline?.transform = CGAffineTransformMakeScale(1.0 / scale, 1.0 / scale) + + self.lassoView.transform = transform + self.lassoView.frame = self.bounds + + self.metalView.transform = transform + self.metalView.frame = self.bounds + + self.brushSizePreviewLayer.position = CGPoint(x: self.bounds.width / 2.0, y: self.bounds.height / 2.0) + } + + public var isEmpty: Bool { + return self.elements.isEmpty + } + + public var scale: CGFloat { + return self.bounds.width / self.imageSize.width + } + + public var isTracking: Bool { + return self.uncommitedElement != nil + } +} + +class DrawingLassoView: UIView { + private var lassoBlackLayer: SimpleShapeLayer + private var lassoWhiteLayer: SimpleShapeLayer + + var requestMenu: ([UUID], CGRect) -> Void = { _, _ in } + + var panBegan: ([UUID]) -> Void = { _ in } + var panChanged: ([UUID], CGPoint) -> Void = { _, _ in } + var panEnded: ([UUID], CGPoint) -> Void = { _, _ in } + + private var currentScale: CGFloat = 1.0 + private var currentPoints: [CGPoint] = [] + private var selectedElements: [UUID] = [] + private var currentExpand: CGFloat = 0.0 + + init(size: CGSize) { + self.lassoBlackLayer = SimpleShapeLayer() + self.lassoBlackLayer.frame = CGRect(origin: .zero, size: size) + + self.lassoWhiteLayer = SimpleShapeLayer() + self.lassoWhiteLayer.frame = CGRect(origin: .zero, size: size) + + super.init(frame: CGRect(origin: .zero, size: size)) + + self.layer.addSublayer(self.lassoBlackLayer) + self.layer.addSublayer(self.lassoWhiteLayer) + + let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:))) + tapGestureRecognizer.numberOfTouchesRequired = 1 + self.addGestureRecognizer(tapGestureRecognizer) + + let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:))) + panGestureRecognizer.maximumNumberOfTouches = 1 + self.addGestureRecognizer(panGestureRecognizer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setup(scale: CGFloat) { + self.isHidden = false + + let dash: CGFloat = 5.0 / scale + + self.lassoBlackLayer.opacity = 0.5 + self.lassoBlackLayer.fillColor = UIColor.clear.cgColor + self.lassoBlackLayer.strokeColor = UIColor.black.cgColor + self.lassoBlackLayer.lineWidth = 2.0 / scale + self.lassoBlackLayer.lineJoin = .round + self.lassoBlackLayer.lineCap = .round + self.lassoBlackLayer.lineDashPattern = [dash as NSNumber, dash * 2.5 as NSNumber] + + let blackAnimation = CABasicAnimation(keyPath: "lineDashPhase") + blackAnimation.fromValue = dash * 3.5 + blackAnimation.toValue = 0 + blackAnimation.duration = 0.45 + blackAnimation.repeatCount = .infinity + self.lassoBlackLayer.add(blackAnimation, forKey: "lineDashPhase") + + self.lassoWhiteLayer.opacity = 0.5 + self.lassoWhiteLayer.fillColor = UIColor.clear.cgColor + self.lassoWhiteLayer.strokeColor = UIColor.white.cgColor + self.lassoWhiteLayer.lineWidth = 2.0 / scale + self.lassoWhiteLayer.lineJoin = .round + self.lassoWhiteLayer.lineCap = .round + self.lassoWhiteLayer.lineDashPattern = [dash as NSNumber, dash * 2.5 as NSNumber] + + let whiteAnimation = CABasicAnimation(keyPath: "lineDashPhase") + whiteAnimation.fromValue = dash * 3.5 + dash * 1.75 + whiteAnimation.toValue = dash * 1.75 + whiteAnimation.duration = 0.45 + whiteAnimation.repeatCount = .infinity + self.lassoWhiteLayer.add(whiteAnimation, forKey: "lineDashPhase") + } + + @objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { + guard let path = self.lassoBlackLayer.path else { + return + } + self.requestMenu(self.selectedElements, path.boundingBox) + } + + @objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { + let translation = gestureRecognizer.translation(in: self) + + switch gestureRecognizer.state { + case .began: + self.panBegan(self.selectedElements) + case .changed: + self.panChanged(self.selectedElements, translation) + case .ended: + self.panEnded(self.selectedElements, translation) + default: + break + } + } + + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + if let path = self.lassoBlackLayer.path { + return path.contains(point) + } else { + return false + } + } + + func updatePath(_ bezierPath: BezierPath) { + self.lassoBlackLayer.path = bezierPath.path.cgPath + self.lassoWhiteLayer.path = bezierPath.path.cgPath + } + + func translate(_ offset: CGPoint) { + let updatedPoints = self.currentPoints.map { $0.offsetBy(offset) } + + self.apply(scale: self.currentScale, points: updatedPoints, selectedElements: self.selectedElements, expand: self.currentExpand) + } + + func apply(scale: CGFloat, points: [CGPoint], selectedElements: [UUID], expand: CGFloat) { + self.currentScale = scale + self.currentPoints = points + self.selectedElements = selectedElements + self.currentExpand = expand + + let dash: CGFloat = 5.0 / scale + + let hullPath = concaveHullPath(points: points) + let expandedPath = expandPath(hullPath, width: expand) + self.lassoBlackLayer.path = expandedPath + self.lassoWhiteLayer.path = expandedPath + + self.lassoBlackLayer.removeAllAnimations() + self.lassoWhiteLayer.removeAllAnimations() + + let blackAnimation = CABasicAnimation(keyPath: "lineDashPhase") + blackAnimation.fromValue = 0 + blackAnimation.toValue = dash * 3.5 + blackAnimation.duration = 0.45 + blackAnimation.repeatCount = .infinity + self.lassoBlackLayer.add(blackAnimation, forKey: "lineDashPhase") + + self.lassoWhiteLayer.fillColor = UIColor.clear.cgColor + self.lassoWhiteLayer.strokeColor = UIColor.white.cgColor + self.lassoWhiteLayer.lineWidth = 2.0 / scale + self.lassoWhiteLayer.lineJoin = .round + self.lassoWhiteLayer.lineCap = .round + self.lassoWhiteLayer.lineDashPattern = [dash as NSNumber, dash * 2.5 as NSNumber] + + let whiteAnimation = CABasicAnimation(keyPath: "lineDashPhase") + whiteAnimation.fromValue = dash * 1.75 + whiteAnimation.toValue = dash * 3.5 + dash * 1.75 + whiteAnimation.duration = 0.45 + whiteAnimation.repeatCount = .infinity + self.lassoWhiteLayer.add(whiteAnimation, forKey: "lineDashPhase") + } + + func reset() { + self.bounds = CGRect(origin: .zero, size: self.bounds.size) + + self.selectedElements = [] + + self.isHidden = true + self.lassoBlackLayer.path = nil + self.lassoWhiteLayer.path = nil + self.lassoBlackLayer.removeAllAnimations() + self.lassoWhiteLayer.removeAllAnimations() + } +} diff --git a/submodules/DrawingUI/Sources/EyedropperView.swift b/submodules/DrawingUI/Sources/EyedropperView.swift new file mode 100644 index 0000000000..b22e95673d --- /dev/null +++ b/submodules/DrawingUI/Sources/EyedropperView.swift @@ -0,0 +1,233 @@ +import Foundation +import UIKit +import Display +import SwiftSignalKit + +private let size = CGSize(width: 148.0, height: 148.0) +private let outerWidth: CGFloat = 12.0 +private let ringWidth: CGFloat = 5.0 +private let selectionWidth: CGFloat = 4.0 + +private func generateShadowImage(size: CGSize) -> UIImage? { + let inset: CGFloat = 60.0 + let imageSize = CGSize(width: size.width + inset * 2.0, height: size.height + inset * 2.0) + return generateImage(imageSize, rotatedContext: { imageSize, context in + context.clear(CGRect(origin: .zero, size: imageSize)) + + context.setShadow(offset: CGSize(width: 0.0, height: 0.0), blur: 40.0, color: UIColor(rgb: 0x000000, alpha: 0.9).cgColor) + context.setFillColor(UIColor(rgb: 0x000000, alpha: 0.1).cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: inset, y: inset), size: size)) + }) +} + +private func generateGridImage(size: CGSize, light: Bool) -> UIImage? { + return generateImage(size, rotatedContext: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + + context.setFillColor(light ? UIColor.white.cgColor : UIColor(rgb: 0x505050).cgColor) + + let lineWidth: CGFloat = 1.0 + var offset: CGFloat = 7.0 + for _ in 0 ..< 8 { + context.fill(CGRect(origin: CGPoint(x: 0.0, y: offset), size: CGSize(width: size.width, height: lineWidth))) + context.fill(CGRect(origin: CGPoint(x: offset, y: 0.0), size: CGSize(width: lineWidth, height: size.height))) + + offset += 14.0 + } + }) +} + +final class EyedropperView: UIView { + private weak var drawingView: DrawingView? + + private let containerView: UIView + private let shadowLayer: SimpleLayer + private let clipView: UIView + private let zoomedView: UIImageView + + private let gridLayer: SimpleLayer + + private let outerColorLayer: SimpleLayer + private let ringLayer: SimpleLayer + private let selectionLayer: SimpleLayer + + private let sourceImage: (data: Data, size: CGSize, bytesPerRow: Int, info: CGBitmapInfo)? + + var completed: (DrawingColor) -> Void = { _ in } + + init(containerSize: CGSize, drawingView: DrawingView, sourceImage: UIImage) { + self.drawingView = drawingView + + self.zoomedView = UIImageView(image: sourceImage) + self.zoomedView.isOpaque = true + self.zoomedView.layer.magnificationFilter = .nearest + + if let cgImage = sourceImage.cgImage, let pixelData = cgImage.dataProvider?.data as? Data { + self.sourceImage = (pixelData, sourceImage.size, cgImage.bytesPerRow, cgImage.bitmapInfo) + } else { + self.sourceImage = nil + } + + let bounds = CGRect(origin: .zero, size: size) + + self.containerView = UIView() + self.containerView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((containerSize.width - size.width) / 2.0), y: floorToScreenPixels((containerSize.height - size.height) / 2.0)), size: size) + + self.shadowLayer = SimpleLayer() + self.shadowLayer.contents = generateShadowImage(size: size)?.cgImage + self.shadowLayer.frame = bounds.insetBy(dx: -60.0, dy: -60.0) + + let clipFrame = bounds.insetBy(dx: outerWidth + ringWidth, dy: outerWidth + ringWidth) + self.clipView = UIView() + self.clipView.clipsToBounds = true + self.clipView.frame = bounds.insetBy(dx: outerWidth + ringWidth, dy: outerWidth + ringWidth) + self.clipView.layer.cornerRadius = size.width / 2.0 - outerWidth - ringWidth + if #available(iOS 13.0, *) { + self.clipView.layer.cornerCurve = .circular + } + self.clipView.addSubview(self.zoomedView) + + self.gridLayer = SimpleLayer() + self.gridLayer.opacity = 0.6 + + self.gridLayer.frame = self.clipView.bounds + self.gridLayer.contents = generateGridImage(size: clipFrame.size, light: true)?.cgImage + + self.outerColorLayer = SimpleLayer() + self.outerColorLayer.rasterizationScale = UIScreen.main.scale + self.outerColorLayer.shouldRasterize = true + self.outerColorLayer.frame = bounds + self.outerColorLayer.cornerRadius = self.outerColorLayer.frame.width / 2.0 + self.outerColorLayer.borderWidth = outerWidth + + self.ringLayer = SimpleLayer() + self.ringLayer.rasterizationScale = UIScreen.main.scale + self.ringLayer.shouldRasterize = true + self.ringLayer.borderColor = UIColor.white.cgColor + self.ringLayer.frame = bounds.insetBy(dx: outerWidth, dy: outerWidth) + self.ringLayer.cornerRadius = self.ringLayer.frame.width / 2.0 + self.ringLayer.borderWidth = ringWidth + + self.selectionLayer = SimpleLayer() + self.selectionLayer.borderColor = UIColor.white.cgColor + self.selectionLayer.borderWidth = selectionWidth + self.selectionLayer.cornerRadius = 2.0 + self.selectionLayer.frame = CGRect(origin: CGPoint(x: clipFrame.minX + 48.0, y: clipFrame.minY + 48.0), size: CGSize(width: 17.0, height: 17.0)).insetBy(dx: -UIScreenPixel, dy: -UIScreenPixel) + + super.init(frame: .zero) + + self.addSubview(self.containerView) + self.clipView.layer.addSublayer(self.gridLayer) + + self.containerView.layer.addSublayer(self.shadowLayer) + self.containerView.addSubview(self.clipView) + self.containerView.layer.addSublayer(self.ringLayer) + self.containerView.layer.addSublayer(self.outerColorLayer) + self.containerView.layer.addSublayer(self.selectionLayer) + + self.containerView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) + self.containerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + + let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:))) + self.addGestureRecognizer(panGestureRecognizer) + + Queue.mainQueue().justDispatch { + self.updateColor() + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private var gridIsLight = true + private var currentColor: DrawingColor? + func setColor(_ color: UIColor) { + self.currentColor = DrawingColor(color: color) + self.outerColorLayer.borderColor = color.cgColor + self.selectionLayer.backgroundColor = color.cgColor + + if color.lightness > 0.9 { + self.ringLayer.borderColor = UIColor(rgb: 0x999999).cgColor + if self.gridIsLight { + self.gridIsLight = false + self.gridLayer.contents = generateGridImage(size: self.clipView.frame.size, light: false)?.cgImage + } + } else { + self.ringLayer.borderColor = UIColor.white.cgColor + if !self.gridIsLight { + self.gridIsLight = true + self.gridLayer.contents = generateGridImage(size: self.clipView.frame.size, light: true)?.cgImage + } + } + } + + private func getColorAt(_ point: CGPoint) -> UIColor? { + guard var sourceImage = self.sourceImage, point.x >= 0 && point.x < sourceImage.size.width && point.y >= 0 && point.y < sourceImage.size.height else { + return UIColor.black + } + + let x = Int(point.x) + let y = Int(point.y) + + var color: UIColor? + sourceImage.data.withUnsafeMutableBytes { buffer in + guard let bytes = buffer.assumingMemoryBound(to: UInt8.self).baseAddress else { + return + } + + let srcLine = bytes.advanced(by: y * sourceImage.bytesPerRow) + let srcPixel = srcLine + x * 4 + let r = srcPixel.pointee + let g = srcPixel.advanced(by: 1).pointee + let b = srcPixel.advanced(by: 2).pointee + + if sourceImage.info.contains(.byteOrder32Little) { + color = UIColor(red: CGFloat(b) / 255.0, green: CGFloat(g) / 255.0, blue: CGFloat(r) / 255.0, alpha: 1.0) + } else { + color = UIColor(red: CGFloat(r) / 255.0, green: CGFloat(g) / 255.0, blue: CGFloat(b) / 255.0, alpha: 1.0) + } + } + return color + } + + private func updateColor() { + guard let drawingView = self.drawingView else { + return + } + var point = self.convert(self.containerView.center, to: drawingView) + point.x /= drawingView.scale + point.y /= drawingView.scale + + let scale: CGFloat = 15.0 + self.zoomedView.transform = CGAffineTransformMakeScale(scale, scale) + self.zoomedView.center = CGPoint(x: self.clipView.frame.width / 2.0 + (self.zoomedView.bounds.width / 2.0 - point.x) * scale, y: self.clipView.frame.height / 2.0 + (self.zoomedView.bounds.height / 2.0 - point.y) * scale) + + if let color = self.getColorAt(point) { + self.setColor(color) + } + } + + @objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { + switch gestureRecognizer.state { + case .changed: + let translation = gestureRecognizer.translation(in: self) + self.containerView.center = self.containerView.center.offsetBy(dx: translation.x, dy: translation.y) + gestureRecognizer.setTranslation(.zero, in: self) + + self.updateColor() + case .ended, .cancelled: + if let color = currentColor { + self.containerView.alpha = 0.0 + self.containerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in + self?.removeFromSuperview() + }) + self.containerView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2) + + self.completed(color) + } + default: + break + } + } +} diff --git a/submodules/DrawingUI/Sources/ModeAndSizeComponent.swift b/submodules/DrawingUI/Sources/ModeAndSizeComponent.swift new file mode 100644 index 0000000000..599616a585 --- /dev/null +++ b/submodules/DrawingUI/Sources/ModeAndSizeComponent.swift @@ -0,0 +1,266 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import LegacyComponents +import TelegramCore +import Postbox +import SegmentedControlNode + +private func generateMaskPath(size: CGSize, leftRadius: CGFloat, rightRadius: CGFloat) -> UIBezierPath { + let path = UIBezierPath() + path.addArc(withCenter: CGPoint(x: leftRadius, y: size.height / 2.0), radius: leftRadius, startAngle: .pi * 0.5, endAngle: -.pi * 0.5, clockwise: true) + path.addArc(withCenter: CGPoint(x: size.width - rightRadius, y: size.height / 2.0), radius: rightRadius, startAngle: -.pi * 0.5, endAngle: .pi * 0.5, clockwise: true) + path.close() + return path +} + +private func generateKnobImage() -> UIImage? { + let side: CGFloat = 28.0 + let margin: CGFloat = 10.0 + + let image = generateImage(CGSize(width: side + margin * 2.0, height: side + margin * 2.0), opaque: false, rotatedContext: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + + context.setShadow(offset: CGSize(width: 0.0, height: 0.0), blur: 9.0, color: UIColor(rgb: 0x000000, alpha: 0.3).cgColor) + context.setFillColor(UIColor.white.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: margin, y: margin), size: CGSize(width: side, height: side))) + }) + return image?.stretchableImage(withLeftCapWidth: Int(margin + side * 0.5), topCapHeight: Int(margin + side * 0.5)) +} + +class ModeAndSizeComponent: Component { + let values: [String] + let sizeValue: CGFloat + let isEditing: Bool + let isEnabled: Bool + let rightInset: CGFloat + let tag: AnyObject? + let selectedIndex: Int + let selectionChanged: (Int) -> Void + let sizeUpdated: (CGFloat) -> Void + let sizeReleased: () -> Void + + init(values: [String], sizeValue: CGFloat, isEditing: Bool, isEnabled: Bool, rightInset: CGFloat, tag: AnyObject?, selectedIndex: Int, selectionChanged: @escaping (Int) -> Void, sizeUpdated: @escaping (CGFloat) -> Void, sizeReleased: @escaping () -> Void) { + self.values = values + self.sizeValue = sizeValue + self.isEditing = isEditing + self.isEnabled = isEnabled + self.rightInset = rightInset + self.tag = tag + self.selectedIndex = selectedIndex + self.selectionChanged = selectionChanged + self.sizeUpdated = sizeUpdated + self.sizeReleased = sizeReleased + } + + static func ==(lhs: ModeAndSizeComponent, rhs: ModeAndSizeComponent) -> Bool { + if lhs.values != rhs.values { + return false + } + if lhs.sizeValue != rhs.sizeValue { + return false + } + if lhs.isEditing != rhs.isEditing { + return false + } + if lhs.isEnabled != rhs.isEnabled { + return false + } + if lhs.rightInset != rhs.rightInset { + return false + } + if lhs.selectedIndex != rhs.selectedIndex { + return false + } + return true + } + + final class View: UIView, UIGestureRecognizerDelegate, ComponentTaggedView { + private let backgroundNode: NavigationBackgroundNode + private let node: SegmentedControlNode + + private var knob: UIImageView + + private let maskLayer = SimpleShapeLayer() + + private var isEditing: Bool? + private var isControlEnabled: Bool? + private var sliderWidth: CGFloat = 0.0 + + fileprivate var updated: (CGFloat) -> Void = { _ in } + fileprivate var released: () -> Void = { } + + private var component: ModeAndSizeComponent? + public func matches(tag: Any) -> Bool { + if let component = self.component, let componentTag = component.tag { + let tag = tag as AnyObject + if componentTag === tag { + return true + } + } + return false + } + + init() { + self.backgroundNode = NavigationBackgroundNode(color: UIColor(rgb: 0x888888, alpha: 0.3)) + self.node = SegmentedControlNode(theme: SegmentedControlTheme(backgroundColor: .clear, foregroundColor: UIColor(rgb: 0x6f7075, alpha: 0.6), shadowColor: .black, textColor: UIColor(rgb: 0xffffff), dividerColor: UIColor(rgb: 0x505155, alpha: 0.6)), items: [], selectedIndex: 0, cornerRadius: 16.0) + + self.knob = UIImageView(image: generateKnobImage()) + + super.init(frame: CGRect()) + + self.layer.allowsGroupOpacity = true + + self.addSubview(self.backgroundNode.view) + self.addSubview(self.node.view) + self.addSubview(self.knob) + + self.backgroundNode.layer.mask = self.maskLayer + + let pressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handlePress(_:))) + pressGestureRecognizer.minimumPressDuration = 0.01 + pressGestureRecognizer.delegate = self + self.addGestureRecognizer(pressGestureRecognizer) + + let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:))) + panGestureRecognizer.delegate = self + self.addGestureRecognizer(panGestureRecognizer) + } + + required init?(coder aDecoder: NSCoder) { + preconditionFailure() + } + + @objc func handlePress(_ gestureRecognizer: UILongPressGestureRecognizer) { + let location = gestureRecognizer.location(in: self).offsetBy(dx: -12.0, dy: 0.0) + guard self.frame.width > 0.0, case .began = gestureRecognizer.state else { + return + } + let value = max(0.0, min(1.0, location.x / (self.frame.width - 24.0))) + self.updated(value) + } + + @objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { + switch gestureRecognizer.state { + case .changed: + let location = gestureRecognizer.location(in: self).offsetBy(dx: -12.0, dy: 0.0) + guard self.frame.width > 0.0 else { + return + } + let value = max(0.0, min(1.0, location.x / (self.frame.width - 24.0))) + self.updated(value) + case .ended, .cancelled: + self.released() + default: + break + } + } + + override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + if let isEditing = self.isEditing, let isControlEnabled = self.isControlEnabled { + return isEditing && isControlEnabled + } else { + return false + } + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + + func animateIn() { + self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + } + + func animateOut() { + self.node.alpha = 0.0 + self.node.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + + self.backgroundNode.alpha = 0.0 + self.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + } + + func update(component: ModeAndSizeComponent, availableSize: CGSize, transition: Transition) -> CGSize { + self.component = component + + self.updated = component.sizeUpdated + self.released = component.sizeReleased + + let previousIsEditing = self.isEditing + self.isEditing = component.isEditing + self.isControlEnabled = component.isEnabled + + if component.isEditing { + self.sliderWidth = availableSize.width + } + + self.node.items = component.values.map { SegmentedControlItem(title: $0) } + self.node.setSelectedIndex(component.selectedIndex, animated: !transition.animation.isImmediate) + let selectionChanged = component.selectionChanged + self.node.selectedIndexChanged = { [weak self] index in + self?.window?.endEditing(true) + selectionChanged(index) + } + + let nodeSize = self.node.updateLayout(.stretchToFill(width: availableSize.width + component.rightInset), transition: transition.containedViewLayoutTransition) + let size = CGSize(width: availableSize.width, height: nodeSize.height) + transition.setFrame(view: self.node.view, frame: CGRect(origin: CGPoint(), size: nodeSize)) + + var isDismissingEditing = false + if component.isEditing != previousIsEditing && !component.isEditing { + isDismissingEditing = true + } + + self.knob.alpha = component.isEditing ? 1.0 : 0.0 + if !isDismissingEditing { + self.knob.frame = CGRect(origin: CGPoint(x: -12.0 + floorToScreenPixels((self.sliderWidth + 24.0 - self.knob.frame.size.width) * component.sizeValue), y: floorToScreenPixels((size.height - self.knob.frame.size.height) / 2.0)), size: self.knob.frame.size) + } + + if component.isEditing != previousIsEditing { + let containedTransition = transition.containedViewLayoutTransition + let maskPath: UIBezierPath + if component.isEditing { + maskPath = generateMaskPath(size: size, leftRadius: 2.0, rightRadius: 11.5) + let selectionFrame = self.node.animateSelection(to: self.knob.center, transition: containedTransition) + containedTransition.animateFrame(layer: self.knob.layer, from: selectionFrame.insetBy(dx: -9.0, dy: -9.0)) + + self.knob.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } else { + maskPath = generateMaskPath(size: size, leftRadius: 16.0, rightRadius: 16.0) + if previousIsEditing != nil { + let selectionFrame = self.node.animateSelection(from: self.knob.center, transition: containedTransition) + containedTransition.animateFrame(layer: self.knob.layer, from: self.knob.frame, to: selectionFrame.insetBy(dx: -9.0, dy: -9.0)) + self.knob.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + } + } + transition.setShapeLayerPath(layer: self.maskLayer, path: maskPath.cgPath) + } + + transition.setFrame(layer: self.maskLayer, frame: CGRect(origin: .zero, size: nodeSize)) + + transition.setFrame(view: self.backgroundNode.view, frame: CGRect(origin: CGPoint(), size: size)) + self.backgroundNode.update(size: size, transition: transition.containedViewLayoutTransition) + + if let screenTransition = transition.userData(DrawingScreenTransition.self) { + switch screenTransition { + case .animateIn: + self.animateIn() + case .animateOut: + self.animateOut() + } + } + + return size + } + } + + func makeView() -> View { + return View() + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, transition: transition) + } +} diff --git a/submodules/DrawingUI/Sources/PenTool.swift b/submodules/DrawingUI/Sources/PenTool.swift new file mode 100644 index 0000000000..141e086184 --- /dev/null +++ b/submodules/DrawingUI/Sources/PenTool.swift @@ -0,0 +1,428 @@ +import Foundation +import UIKit +import Display + +final class PenTool: DrawingElement { + class RenderLayer: SimpleLayer, DrawingRenderLayer { + func setup(size: CGSize) { + self.shouldRasterize = true + self.contentsScale = 1.0 + + let bounds = CGRect(origin: .zero, size: size) + self.frame = bounds + } + + private var line: StrokeLine? + fileprivate func draw(line: StrokeLine, rect: CGRect) { + self.line = line + self.setNeedsDisplay(rect.insetBy(dx: -50.0, dy: -50.0)) + } + + override func draw(in ctx: CGContext) { + self.line?.drawInContext(ctx) + } + } + + let uuid = UUID() + + let drawingSize: CGSize + let color: DrawingColor + let lineWidth: CGFloat + let arrow: Bool + + var path: Polyline? + var boundingBox: CGRect? + + private var renderLine: StrokeLine + var didSetupArrow = false + private var renderLineArrow1: StrokeLine? + private var renderLineArrow2: StrokeLine? + let renderLineWidth: CGFloat + + var translation = CGPoint() + + private var currentRenderLayer: DrawingRenderLayer? + + var bounds: CGRect { + return self.path?.bounds.offsetBy(dx: self.translation.x, dy: self.translation.y) ?? .zero + } + + var points: [Polyline.Point] { + guard let linePath = self.path else { + return [] + } + var points: [Polyline.Point] = [] + for point in linePath.points { + points.append(point.offsetBy(self.translation)) + } + return points + } + + func containsPoint(_ point: CGPoint) -> Bool { + return false + // return self.renderPath?.contains(point.offsetBy(CGPoint(x: -self.translation.x, y: -self.translation.y))) ?? false + } + + func hasPointsInsidePath(_ path: UIBezierPath) -> Bool { + if let linePath = self.path { + let pathBoundingBox = path.bounds + if self.bounds.intersects(pathBoundingBox) { + for point in linePath.points { + if path.contains(point.location.offsetBy(self.translation)) { + return true + } + } + } + } + return false + } + + required init(drawingSize: CGSize, color: DrawingColor, lineWidth: CGFloat, arrow: Bool) { + self.drawingSize = drawingSize + self.color = color + self.lineWidth = lineWidth + self.arrow = arrow + + let minLineWidth = max(1.0, min(drawingSize.width, drawingSize.height) * 0.003) + let maxLineWidth = max(10.0, min(drawingSize.width, drawingSize.height) * 0.09) + let lineWidth = minLineWidth + (maxLineWidth - minLineWidth) * lineWidth + + self.renderLineWidth = lineWidth + + self.renderLine = StrokeLine(color: color.toUIColor(), minLineWidth: minLineWidth, lineWidth: lineWidth) + if arrow { + self.renderLineArrow1 = StrokeLine(color: color.toUIColor(), minLineWidth: minLineWidth, lineWidth: lineWidth * 0.8) + self.renderLineArrow2 = StrokeLine(color: color.toUIColor(), minLineWidth: minLineWidth, lineWidth: lineWidth * 0.8) + } + } + + func setupRenderLayer() -> DrawingRenderLayer? { + let layer = RenderLayer() + layer.setup(size: self.drawingSize) + self.currentRenderLayer = layer + return layer + } + + func updatePath(_ path: DrawingGesturePipeline.DrawingResult, state: DrawingGesturePipeline.DrawingGestureState) { + guard case let .polyline(line) = path, let point = line.points.last else { + return + } + self.path = line + + let rect = self.renderLine.draw(at: point) + if let currentRenderLayer = self.currentRenderLayer as? RenderLayer { + currentRenderLayer.draw(line: self.renderLine, rect: rect) + } + // self.path = bezierPath + + // if self.arrow && polyline.isComplete, polyline.points.count > 2 { + // let lastPoint = lastPosition + // var secondPoint = polyline.points[polyline.points.count - 2] + // if secondPoint.location.distance(to: lastPoint) < self.renderArrowLineWidth { + // secondPoint = polyline.points[polyline.points.count - 3] + // } + // let angle = lastPoint.angle(to: secondPoint.location) + // let point1 = lastPoint.pointAt(distance: self.renderArrowLength, angle: angle - CGFloat.pi * 0.15) + // let point2 = lastPoint.pointAt(distance: self.renderArrowLength, angle: angle + CGFloat.pi * 0.15) + // + // let arrowPath = UIBezierPath() + // arrowPath.move(to: point2) + // arrowPath.addLine(to: lastPoint) + // arrowPath.addLine(to: point1) + // let arrowThickPath = arrowPath.cgPath.copy(strokingWithWidth: self.renderArrowLineWidth, lineCap: .round, lineJoin: .round, miterLimit: 0.0) + // + // combinedPath.usesEvenOddFillRule = false + // combinedPath.append(UIBezierPath(cgPath: arrowThickPath)) + // } + + // let cgPath = bezierPath.path.cgPath.copy(strokingWithWidth: self.renderLineWidth, lineCap: .round, lineJoin: .round, miterLimit: 0.0) + // self.renderPath = cgPath + + // if let currentRenderLayer = self.currentRenderLayer as? RenderLayer { + // currentRenderLayer.updatePath(cgPath) + // } + } + + func draw(in context: CGContext, size: CGSize) { + context.saveGState() + + context.translateBy(x: self.translation.x, y: self.translation.y) + + context.setShouldAntialias(true) + + if self.arrow, let path = self.path, let lastPoint = path.points.last { + var lastPointWithVelocity: Polyline.Point? + for point in path.points.reversed() { + if point.velocity > 0.0 { + lastPointWithVelocity = point + break + } + } + if !self.didSetupArrow, let lastPointWithVelocity = lastPointWithVelocity { + let w = self.renderLineWidth + var dist: CGFloat = 18.0 * sqrt(w) + let spread: CGFloat = .pi * max(0.05, 0.03 * sqrt(w)) + + let suffix = path.points.suffix(100).reversed() + + var p0 = suffix.first! + + var p2 = suffix.last! + var d: CGFloat = 0 + for p in suffix { + d += hypot(p0.location.x - p.location.x, p0.location.y - p.location.y) + if d >= dist { + p2 = p + break + } + p0 = p + } + + p0 = suffix.first! + dist = min(dist, hypot(p0.location.x - p2.location.x, p0.location.y - p2.location.y)) + + var i = 0 + for spread in [-spread, spread] { + var points: [CGPoint] = [] + points.append(lastPoint.location) + + p0 = suffix.first! + var prev = p0.location + d = 0 + for p in suffix { + let d1 = hypot(p0.location.x - p.location.x, p0.location.y - p.location.y) + d += d1 + if d >= dist { + break + } + let d2 = d1 / cos(spread) + let angle = atan2(p.location.y - p0.location.y, p.location.x - p0.location.x) + let cur = CGPoint(x: prev.x + d2 * cos(angle + spread), y: prev.y + d2 * sin(angle + spread)) + + points.append( + cur + ) + + p0 = p + prev = cur + } + + for point in points { + if i == 0 { + let _ = self.renderLineArrow1?.draw(at: Polyline.Point(location: point, force: 0.0, altitudeAngle: 0.0, azimuth: 0.0, velocity: lastPointWithVelocity.velocity, touchPoint: lastPointWithVelocity.touchPoint)) + } else if i == 1 { + let _ = self.renderLineArrow2?.draw(at: Polyline.Point(location: point, force: 0.0, altitudeAngle: 0.0, azimuth: 0.0, velocity: lastPointWithVelocity.velocity, touchPoint: lastPointWithVelocity.touchPoint)) + } + } + i += 1 + } + self.didSetupArrow = true + } + self.renderLineArrow1?.drawInContext(context) + self.renderLineArrow2?.drawInContext(context) + } + + self.renderLine.drawInContext(context) + + context.restoreGState() + } +} + +private class StrokeLine { + struct Segment { + let a: CGPoint + let b: CGPoint + let c: CGPoint + let d: CGPoint + let abWidth: CGFloat + let cdWidth: CGFloat + } + + struct Point { + let position: CGPoint + let width: CGFloat + + init(position: CGPoint, width: CGFloat) { + self.position = position + self.width = width + } + } + + private(set) var points: [Point] = [] + private var smoothPoints: [Point] = [] + private var segments: [Segment] = [] + private var lastWidth: CGFloat? + + private let minLineWidth: CGFloat + let lineWidth: CGFloat + + let color: UIColor + + init(color: UIColor, minLineWidth: CGFloat, lineWidth: CGFloat) { + self.color = color + self.minLineWidth = minLineWidth + self.lineWidth = lineWidth + } + + func draw(at point: Polyline.Point) -> CGRect { + let width = extractLineWidth(from: point.velocity) + self.lastWidth = width + + let point = Point(position: point.location, width: width) + return appendPoint(point) + } + + func drawInContext(_ context: CGContext) { + self.drawSegments(self.segments, inContext: context) + } + + func extractLineWidth(from velocity: CGFloat) -> CGFloat { + let minValue = self.minLineWidth + let maxValue = self.lineWidth + + var size = max(minValue, min(maxValue + 1 - (velocity / 150), maxValue)) + if let lastWidth = self.lastWidth { + size = size * 0.2 + lastWidth * 0.8 + } + return size + } + + func appendPoint(_ point: Point) -> CGRect { + self.points.append(point) + + guard self.points.count > 2 else { return .null } + + let index = self.points.count - 1 + let point0 = self.points[index - 2] + let point1 = self.points[index - 1] + let point2 = self.points[index] + + let newSmoothPoints = smoothPoints( + fromPoint0: point0, + point1: point1, + point2: point2 + ) + + let lastOldSmoothPoint = smoothPoints.last + smoothPoints.append(contentsOf: newSmoothPoints) + + guard smoothPoints.count > 1 else { return .null } + + let newSegments: ([Segment], CGRect) = { + guard let lastOldSmoothPoint = lastOldSmoothPoint else { + return segments(fromSmoothPoints: newSmoothPoints) + } + return segments(fromSmoothPoints: [lastOldSmoothPoint] + newSmoothPoints) + }() + segments.append(contentsOf: newSegments.0) + + return newSegments.1 + } + + func smoothPoints(fromPoint0 point0: Point, point1: Point, point2: Point) -> [Point] { + var smoothPoints = [Point]() + + let midPoint1 = (point0.position + point1.position) * 0.5 + let midPoint2 = (point1.position + point2.position) * 0.5 + + let segmentDistance = 2.0 + let distance = midPoint1.distance(to: midPoint2) + let numberOfSegments = min(128, max(floor(distance/segmentDistance), 32)) + + let step = 1.0 / numberOfSegments + for t in stride(from: 0, to: 1, by: step) { + let position = midPoint1 * pow(1 - t, 2) + point1.position * 2 * (1 - t) * t + midPoint2 * t * t + let size = pow(1 - t, 2) * ((point0.width + point1.width) * 0.5) + 2 * (1 - t) * t * point1.width + t * t * ((point1.width + point2.width) * 0.5) + let point = Point(position: position, width: size) + smoothPoints.append(point) + } + + let finalPoint = Point(position: midPoint2, width: (point1.width + point2.width) * 0.5) + smoothPoints.append(finalPoint) + + return smoothPoints + } + + func segments(fromSmoothPoints smoothPoints: [Point]) -> ([Segment], CGRect) { + var segments = [Segment]() + var updateRect = CGRect.null + for i in 1 ..< smoothPoints.count { + let previousPoint = smoothPoints[i - 1].position + let previousWidth = smoothPoints[i - 1].width + let currentPoint = smoothPoints[i].position + let currentWidth = smoothPoints[i].width + let direction = currentPoint - previousPoint + + guard !currentPoint.isEqual(to: previousPoint, epsilon: 0.0001) else { + continue + } + + var perpendicular = CGPoint(x: -direction.y, y: direction.x) + let length = perpendicular.length + if length > 0.0 { + perpendicular = perpendicular / length + } + + let a = previousPoint + perpendicular * previousWidth / 2 + let b = previousPoint - perpendicular * previousWidth / 2 + let c = currentPoint + perpendicular * currentWidth / 2 + let d = currentPoint - perpendicular * currentWidth / 2 + + let ab: CGPoint = { + let center = (b + a)/2 + let radius = center - b + return .init(x: center.x - radius.y, y: center.y + radius.x) + }() + let cd: CGPoint = { + let center = (c + d)/2 + let radius = center - c + return .init(x: center.x + radius.y, y: center.y - radius.x) + }() + + let minX = min(a.x, b.x, c.x, d.x, ab.x, cd.x) + let minY = min(a.y, b.y, c.y, d.y, ab.y, cd.y) + let maxX = max(a.x, b.x, c.x, d.x, ab.x, cd.x) + let maxY = max(a.y, b.y, c.y, d.y, ab.y, cd.y) + + updateRect = updateRect.union(CGRect(x: minX, y: minY, width: maxX - minX, height: maxY - minY)) + + segments.append(Segment(a: a, b: b, c: c, d: d, abWidth: previousWidth, cdWidth: currentWidth)) + } + return (segments, updateRect) + } + + func drawSegments(_ segments: [Segment], inContext context: CGContext) { + for segment in segments { + context.beginPath() + + context.setStrokeColor(color.cgColor) + context.setFillColor(color.cgColor) + + context.move(to: segment.b) + + let abStartAngle = atan2(segment.b.y - segment.a.y, segment.b.x - segment.a.x) + context.addArc( + center: (segment.a + segment.b)/2, + radius: segment.abWidth/2, + startAngle: abStartAngle, + endAngle: abStartAngle + .pi, + clockwise: true + ) + context.addLine(to: segment.c) + + let cdStartAngle = atan2(segment.c.y - segment.d.y, segment.c.x - segment.d.x) + context.addArc( + center: (segment.c + segment.d)/2, + radius: segment.cdWidth/2, + startAngle: cdStartAngle, + endAngle: cdStartAngle + .pi, + clockwise: true + ) + context.closePath() + + context.fillPath() + context.strokePath() + } + } +} + diff --git a/submodules/DrawingUI/Sources/StickerPickerScreen.swift b/submodules/DrawingUI/Sources/StickerPickerScreen.swift new file mode 100644 index 0000000000..ff8707f5c5 --- /dev/null +++ b/submodules/DrawingUI/Sources/StickerPickerScreen.swift @@ -0,0 +1,861 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramPresentationData +import AccountContext +import ComponentFlow +import ViewControllerComponent +import EntityKeyboard +import PagerComponent + +private final class StickerSelectionComponent: Component { + public typealias EnvironmentType = Empty + + public let theme: PresentationTheme + public let strings: PresentationStrings + public let deviceMetrics: DeviceMetrics + public let stickerContent: EmojiPagerContentComponent + public let backgroundColor: UIColor + public let separatorColor: UIColor + + public init( + theme: PresentationTheme, + strings: PresentationStrings, + deviceMetrics: DeviceMetrics, + stickerContent: EmojiPagerContentComponent, + backgroundColor: UIColor, + separatorColor: UIColor + ) { + self.theme = theme + self.strings = strings + self.deviceMetrics = deviceMetrics + self.stickerContent = stickerContent + self.backgroundColor = backgroundColor + self.separatorColor = separatorColor + } + + public static func ==(lhs: StickerSelectionComponent, rhs: StickerSelectionComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings != rhs.strings { + return false + } + if lhs.deviceMetrics != rhs.deviceMetrics { + return false + } + if lhs.stickerContent != rhs.stickerContent { + return false + } + if lhs.backgroundColor != rhs.backgroundColor { + return false + } + if lhs.separatorColor != rhs.separatorColor { + return false + } + return true + } + + public final class View: UIView { + private let keyboardView: ComponentView + private let keyboardClippingView: UIView + private let panelHostView: PagerExternalTopPanelContainer + private let panelBackgroundView: BlurredBackgroundView + private let panelSeparatorView: UIView + + private var component: StickerSelectionComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.keyboardView = ComponentView() + self.keyboardClippingView = UIView() + self.panelHostView = PagerExternalTopPanelContainer() + self.panelBackgroundView = BlurredBackgroundView(color: .clear, enableBlur: true) + self.panelSeparatorView = UIView() + + super.init(frame: frame) + + self.addSubview(self.keyboardClippingView) + self.addSubview(self.panelBackgroundView) + self.addSubview(self.panelSeparatorView) + self.addSubview(self.panelHostView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + func update(component: StickerSelectionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.backgroundColor = component.backgroundColor + let panelBackgroundColor = component.backgroundColor.withMultipliedAlpha(0.85) + self.panelBackgroundView.updateColor(color: panelBackgroundColor, transition: .immediate) + self.panelSeparatorView.backgroundColor = component.separatorColor + + self.component = component + self.state = state + + let topPanelHeight: CGFloat = 42.0 + + let keyboardSize = self.keyboardView.update( + transition: transition,//.withUserData(EmojiPagerContentComponent.SynchronousLoadBehavior(isDisabled: true)), + component: AnyComponent(EntityKeyboardComponent( + theme: component.theme, + strings: component.strings, + isContentInFocus: true, + containerInsets: UIEdgeInsets(top: topPanelHeight - 34.0, left: 0.0, bottom: 0.0, right: 0.0), + topPanelInsets: UIEdgeInsets(top: 0.0, left: 4.0, bottom: 0.0, right: 4.0), + emojiContent: nil, + stickerContent: component.stickerContent, + gifContent: nil, + hasRecentGifs: false, + availableGifSearchEmojies: [], + defaultToEmojiTab: false, + externalTopPanelContainer: self.panelHostView, + topPanelExtensionUpdated: { _, _ in }, + hideInputUpdated: { _, _, _ in }, + hideTopPanelUpdated: { _, _ in }, + switchToTextInput: {}, + switchToGifSubject: { _ in }, + reorderItems: { _, _ in }, + makeSearchContainerNode: { _ in return nil }, + deviceMetrics: component.deviceMetrics, + hiddenInputHeight: 0.0, + inputHeight: 0.0, + displayBottomPanel: false, + isExpanded: true + )), + environment: {}, + containerSize: availableSize + ) + if let keyboardComponentView = self.keyboardView.view { + if keyboardComponentView.superview == nil { + self.keyboardClippingView.addSubview(keyboardComponentView) + } + + if panelBackgroundColor.alpha < 0.01 { + self.keyboardClippingView.clipsToBounds = true + } else { + self.keyboardClippingView.clipsToBounds = false + } + + transition.setFrame(view: self.keyboardClippingView, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight), size: CGSize(width: availableSize.width, height: availableSize.height - topPanelHeight))) + + transition.setFrame(view: keyboardComponentView, frame: CGRect(origin: CGPoint(x: 0.0, y: -topPanelHeight), size: keyboardSize)) + transition.setFrame(view: self.panelHostView, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight - 34.0), size: CGSize(width: keyboardSize.width, height: 0.0))) + + transition.setFrame(view: self.panelBackgroundView, frame: CGRect(origin: CGPoint(), size: CGSize(width: keyboardSize.width, height: topPanelHeight))) + self.panelBackgroundView.update(size: self.panelBackgroundView.bounds.size, transition: transition.containedViewLayoutTransition) + + transition.setFrame(view: self.panelSeparatorView, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight), size: CGSize(width: keyboardSize.width, height: UIScreenPixel))) + transition.setAlpha(view: self.panelSeparatorView, alpha: 1.0) + } + + return availableSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +public class StickerPickerScreen: ViewController { + final class Node: ViewControllerTracingNode, UIScrollViewDelegate, UIGestureRecognizerDelegate { + private var presentationData: PresentationData + private weak var controller: StickerPickerScreen? + private let theme: PresentationTheme + + let dim: ASDisplayNode + let wrappingView: UIView + let containerView: UIView + let scrollView: UIScrollView + let hostView: ComponentHostView + + private var stickerContent: EmojiPagerContentComponent? + private let stickerContentDisposable = MetaDisposable() + private var scheduledEmojiContentAnimationHint: EmojiPagerContentComponent.ContentAnimation? + + private(set) var isExpanded = false + private var panGestureRecognizer: UIPanGestureRecognizer? + private var panGestureArguments: (topInset: CGFloat, offset: CGFloat, scrollView: UIScrollView?, listNode: ListView?)? + + private var currentIsVisible: Bool = false + private var currentLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)? + + fileprivate var temporaryDismiss = false + + init(context: AccountContext, controller: StickerPickerScreen, theme: PresentationTheme) { + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.controller = controller + self.theme = theme + + self.dim = ASDisplayNode() + self.dim.alpha = 0.0 + self.dim.backgroundColor = UIColor(white: 0.0, alpha: 0.25) + + self.wrappingView = UIView() + self.containerView = UIView() + self.scrollView = UIScrollView() + self.hostView = ComponentHostView() + + super.init() + + self.scrollView.delegate = self + self.scrollView.showsVerticalScrollIndicator = false + + self.containerView.clipsToBounds = true + self.containerView.backgroundColor = .clear + + self.addSubnode(self.dim) + + self.view.addSubview(self.wrappingView) + self.wrappingView.addSubview(self.containerView) + self.containerView.addSubview(self.scrollView) + self.scrollView.addSubview(self.hostView) + + self.stickerContentDisposable.set(( + EmojiPagerContentComponent.stickerInputData( + context: context, + animationCache: context.animationCache, + animationRenderer: context.animationRenderer, + stickerNamespaces: [Namespaces.ItemCollection.CloudStickerPacks], + stickerOrderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudAllPremiumStickers], + chatPeerId: context.account.peerId + ) + |> deliverOnMainQueue).start(next: { [weak self] content in + if let strongSelf = self { + strongSelf.updateContent(content) + } + }) + ) + } + + deinit { + self.stickerContentDisposable.dispose() + } + + + func updateContent(_ content: EmojiPagerContentComponent) { + self.stickerContent = content + content.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction( + performItemAction: { [weak self] _, item, _, _, _, _ in + guard let strongSelf = self, let file = item.itemFile else { + return + } + strongSelf.controller?.completion(file) + strongSelf.controller?.dismiss(animated: true) + }, + deleteBackwards: {}, + openStickerSettings: { +// guard let controllerInteraction = controllerInteraction else { +// return +// } +// let controller = installedStickerPacksController(context: context, mode: .modal) +// controller.navigationPresentation = .modal +// controllerInteraction.navigationController()?.pushViewController(controller) + }, + openFeatured: { +// guard let controllerInteraction = controllerInteraction else { +// return +// } +// +// controllerInteraction.navigationController()?.pushViewController(FeaturedStickersScreen( +// context: context, +// highlightedPackId: nil, +// sendSticker: { [weak controllerInteraction] fileReference, sourceNode, sourceRect in +// guard let controllerInteraction = controllerInteraction else { +// return false +// } +// return controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect, nil, []) +// } +// )) + }, + openSearch: {}, + addGroupAction: { [weak self] groupId, isPremiumLocked in + guard let strongSelf = self, let controller = strongSelf.controller, let collectionId = groupId.base as? ItemCollectionId else { + return + } + let context = controller.context + + if isPremiumLocked { +// let controller = PremiumIntroScreen(context: context, source: .stickers) +// controllerInteraction.navigationController()?.pushViewController(controller) + + return + } + + let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedStickerPacks) + let _ = (context.account.postbox.combinedView(keys: [viewKey]) + |> take(1) + |> deliverOnMainQueue).start(next: { views in + guard let view = views.views[viewKey] as? OrderedItemListView else { + return + } + for featuredStickerPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) { + if featuredStickerPack.info.id == collectionId { + let _ = (context.engine.stickers.loadedStickerPack(reference: .id(id: featuredStickerPack.info.id.id, accessHash: featuredStickerPack.info.accessHash), forceActualized: false) + |> mapToSignal { result -> Signal in + switch result { + case let .result(info, items, installed): + if installed { + return .complete() + } else { + return context.engine.stickers.addStickerPackInteractively(info: info, items: items) + } + case .fetching: + break + case .none: + break + } + return .complete() + } + |> deliverOnMainQueue).start(completed: { + }) + + break + } + } + }) + }, + clearGroup: { [weak self] groupId in + guard let strongSelf = self, let controller = strongSelf.controller else { + return + } + if groupId == AnyHashable("popular") { + let presentationData = controller.context.sharedContext.currentPresentationData.with { $0 } + let actionSheet = ActionSheetController(theme: ActionSheetControllerTheme(presentationTheme: presentationData.theme, fontSize: presentationData.listsFontSize)) + var items: [ActionSheetItem] = [] + let context = controller.context + items.append(ActionSheetTextItem(title: presentationData.strings.Chat_ClearReactionsAlertText, parseMarkdown: true)) + items.append(ActionSheetButtonItem(title: presentationData.strings.Chat_ClearReactionsAlertAction, color: .destructive, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + guard let strongSelf = self else { + return + } + + strongSelf.scheduledEmojiContentAnimationHint = EmojiPagerContentComponent.ContentAnimation(type: .groupRemoved(id: "popular")) + let _ = context.engine.stickers.clearRecentlyUsedReactions().start() + })) + actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + context.sharedContext.mainWindow?.presentInGlobalOverlay(actionSheet) + } + }, + pushController: { c in }, + presentController: { c in }, + presentGlobalOverlayController: { c in }, + navigationController: { [weak self] in + return self?.controller?.navigationController as? NavigationController + }, + requestUpdate: { _ in }, + updateSearchQuery: { _, _ in }, + chatPeerId: nil, + peekBehavior: nil, + customLayout: nil, + externalBackground: nil, + externalExpansionView: nil, + useOpaqueTheme: false + ) + + if let (layout, navigationHeight) = self.currentLayout { + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate) + } + } + + override func didLoad() { + super.didLoad() + + let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) + panRecognizer.delegate = self + panRecognizer.delaysTouchesBegan = false + panRecognizer.cancelsTouchesInView = true + self.panGestureRecognizer = panRecognizer + self.wrappingView.addGestureRecognizer(panRecognizer) + + self.dim.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) + + self.controller?.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate) + } + + @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.controller?.completion(nil) + self.controller?.dismiss(animated: true) + } + } + + override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + if let (layout, _) = self.currentLayout { + if case .regular = layout.metrics.widthClass { + return false + } + } + return true + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + let contentOffset = self.scrollView.contentOffset.y + self.controller?.navigationBar?.updateBackgroundAlpha(min(30.0, contentOffset) / 30.0, transition: .immediate) + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + if gestureRecognizer is UIPanGestureRecognizer && otherGestureRecognizer is UIPanGestureRecognizer { + return true + } + return false + } + + private var isDismissing = false + func animateIn() { + ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear).updateAlpha(node: self.dim, alpha: 1.0) + + let targetPosition = self.containerView.center + let startPosition = targetPosition.offsetBy(dx: 0.0, dy: self.bounds.height) + + self.containerView.center = startPosition + let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) + transition.animateView(allowUserInteraction: true, { + self.containerView.center = targetPosition + }, completion: { _ in + }) + } + + func animateOut(completion: @escaping () -> Void = {}) { + self.isDismissing = true + + let positionTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut) + positionTransition.updatePosition(layer: self.containerView.layer, position: CGPoint(x: self.containerView.center.x, y: self.bounds.height + self.containerView.bounds.height / 2.0), completion: { [weak self] _ in + self?.controller?.dismiss(animated: false, completion: completion) + }) + let alphaTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut) + alphaTransition.updateAlpha(node: self.dim, alpha: 0.0) + + if !self.temporaryDismiss { + self.controller?.updateModalStyleOverlayTransitionFactor(0.0, transition: positionTransition) + } + } + + func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: Transition) { + self.currentLayout = (layout, navigationHeight) + + self.dim.frame = CGRect(origin: CGPoint(x: 0.0, y: -layout.size.height), size: CGSize(width: layout.size.width, height: layout.size.height * 3.0)) + + var effectiveExpanded = self.isExpanded + if case .regular = layout.metrics.widthClass { + effectiveExpanded = true + } + + let isLandscape = layout.orientation == .landscape + let edgeTopInset = isLandscape ? 0.0 : self.defaultTopInset + let topInset: CGFloat + if let (panInitialTopInset, panOffset, _, _) = self.panGestureArguments { + if effectiveExpanded { + topInset = min(edgeTopInset, panInitialTopInset + max(0.0, panOffset)) + } else { + topInset = max(0.0, panInitialTopInset + min(0.0, panOffset)) + } + } else { + topInset = effectiveExpanded ? 0.0 : edgeTopInset + } + transition.setFrame(view: self.wrappingView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: layout.size), completion: nil) + + let modalProgress = isLandscape ? 0.0 : (1.0 - topInset / self.defaultTopInset) + self.controller?.updateModalStyleOverlayTransitionFactor(modalProgress, transition: transition.containedViewLayoutTransition) + + let clipFrame: CGRect + if layout.metrics.widthClass == .compact { + self.dim.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.25) + if isLandscape { + self.containerView.layer.cornerRadius = 0.0 + } else { + self.containerView.layer.cornerRadius = 10.0 + } + + if #available(iOS 11.0, *) { + if layout.safeInsets.bottom.isZero { + self.containerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + } else { + self.containerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner] + } + } + + if isLandscape { + clipFrame = CGRect(origin: CGPoint(), size: layout.size) + } else { + let coveredByModalTransition: CGFloat = 0.0 + var containerTopInset: CGFloat = 10.0 + if let statusBarHeight = layout.statusBarHeight { + containerTopInset += statusBarHeight + } + + let unscaledFrame = CGRect(origin: CGPoint(x: 0.0, y: containerTopInset - coveredByModalTransition * 10.0), size: CGSize(width: layout.size.width, height: layout.size.height - containerTopInset)) + let maxScale: CGFloat = (layout.size.width - 16.0 * 2.0) / layout.size.width + let containerScale = 1.0 * (1.0 - coveredByModalTransition) + maxScale * coveredByModalTransition + let maxScaledTopInset: CGFloat = containerTopInset - 10.0 + let scaledTopInset: CGFloat = containerTopInset * (1.0 - coveredByModalTransition) + maxScaledTopInset * coveredByModalTransition + let containerFrame = unscaledFrame.offsetBy(dx: 0.0, dy: scaledTopInset - (unscaledFrame.midY - containerScale * unscaledFrame.height / 2.0)) + + clipFrame = CGRect(x: containerFrame.minX, y: containerFrame.minY, width: containerFrame.width, height: containerFrame.height) + } + } else { + self.dim.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.4) + self.containerView.layer.cornerRadius = 10.0 + + let verticalInset: CGFloat = 44.0 + + let maxSide = max(layout.size.width, layout.size.height) + let minSide = min(layout.size.width, layout.size.height) + let containerSize = CGSize(width: min(layout.size.width - 20.0, floor(maxSide / 2.0)), height: min(layout.size.height, minSide) - verticalInset * 2.0) + clipFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - containerSize.width) / 2.0), y: floor((layout.size.height - containerSize.height) / 2.0)), size: containerSize) + } + + transition.setFrame(view: self.containerView, frame: clipFrame) + transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: clipFrame.size), completion: nil) + + if let stickerContent = self.stickerContent { + var stickersTransition: Transition = transition + if let scheduledEmojiContentAnimationHint = self.scheduledEmojiContentAnimationHint { + self.scheduledEmojiContentAnimationHint = nil + let contentAnimation = scheduledEmojiContentAnimationHint + stickersTransition = Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(contentAnimation) + } + + var contentSize = self.hostView.update( + transition: stickersTransition, + component: AnyComponent( + StickerSelectionComponent( + theme: self.theme, + strings: self.presentationData.strings, + deviceMetrics: layout.deviceMetrics, + stickerContent: stickerContent, + backgroundColor: self.theme.list.itemBlocksBackgroundColor, + separatorColor: self.theme.list.blocksBackgroundColor + ) + ), + environment: {}, + forceUpdate: true, + containerSize: CGSize(width: clipFrame.size.width, height: clipFrame.height) + ) + contentSize.height = max(layout.size.height - navigationHeight, contentSize.height) + transition.setFrame(view: self.hostView, frame: CGRect(origin: CGPoint(), size: contentSize), completion: nil) + self.scrollView.contentSize = contentSize + } + } + + private var didPlayAppearAnimation = false + func updateIsVisible(isVisible: Bool) { + if self.currentIsVisible == isVisible { + return + } + self.currentIsVisible = isVisible + + guard let currentLayout = self.currentLayout else { + return + } + self.containerLayoutUpdated(layout: currentLayout.layout, navigationHeight: currentLayout.navigationHeight, transition: .immediate) + + if !self.didPlayAppearAnimation { + self.didPlayAppearAnimation = true + self.animateIn() + } + } + + private var defaultTopInset: CGFloat { + guard let (layout, _) = self.currentLayout else{ + return 210.0 + } + if case .compact = layout.metrics.widthClass { + var factor: CGFloat = 0.2488 + if layout.size.width <= 320.0 { + factor = 0.15 + } + return floor(max(layout.size.width, layout.size.height) * factor) + } else { + return 210.0 + } + } + + private func findScrollView(view: UIView?) -> (UIScrollView, ListView?)? { + if let view = view { + if let view = view as? UIScrollView, view.contentSize.width < view.contentSize.height { + return (view, nil) + } + if let node = view.asyncdisplaykit_node as? ListView { + return (node.scroller, node) + } + return findScrollView(view: view.superview) + } else { + return nil + } + } + + @objc func panGesture(_ recognizer: UIPanGestureRecognizer) { + guard let (layout, navigationHeight) = self.currentLayout else { + return + } + + let isLandscape = layout.orientation == .landscape + let edgeTopInset = isLandscape ? 0.0 : defaultTopInset + + switch recognizer.state { + case .began: + let point = recognizer.location(in: self.view) + let currentHitView = self.hitTest(point, with: nil) + + var scrollViewAndListNode = self.findScrollView(view: currentHitView) + if scrollViewAndListNode?.0.frame.height == self.frame.width { + scrollViewAndListNode = nil + } + let scrollView = scrollViewAndListNode?.0 + let listNode = scrollViewAndListNode?.1 + + let topInset: CGFloat + if self.isExpanded { + topInset = 0.0 + } else { + topInset = edgeTopInset + } + + self.panGestureArguments = (topInset, 0.0, scrollView, listNode) + case .changed: + guard let (topInset, panOffset, scrollView, listNode) = self.panGestureArguments else { + return + } + let visibleContentOffset = listNode?.visibleContentOffset() + let contentOffset = scrollView?.contentOffset.y ?? 0.0 + + var translation = recognizer.translation(in: self.view).y + + var currentOffset = topInset + translation + + let epsilon = 1.0 + if case let .known(value) = visibleContentOffset, value <= epsilon { + if let scrollView = scrollView { + scrollView.bounces = false + scrollView.setContentOffset(CGPoint(x: 0.0, y: 0.0), animated: false) + } + } else if let scrollView = scrollView, contentOffset <= -scrollView.contentInset.top + epsilon { + scrollView.bounces = false + scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) + } else if let scrollView = scrollView { + translation = panOffset + currentOffset = topInset + translation + if self.isExpanded { + recognizer.setTranslation(CGPoint(), in: self.view) + } else if currentOffset > 0.0 { + scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) + } + } + + self.panGestureArguments = (topInset, translation, scrollView, listNode) + + if !self.isExpanded { + if currentOffset > 0.0, let scrollView = scrollView { + scrollView.panGestureRecognizer.setTranslation(CGPoint(), in: scrollView) + } + } + + var bounds = self.bounds + if self.isExpanded { + bounds.origin.y = -max(0.0, translation - edgeTopInset) + } else { + bounds.origin.y = -translation + } + bounds.origin.y = min(0.0, bounds.origin.y) + self.bounds = bounds + + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate) + case .ended: + guard let (currentTopInset, panOffset, scrollView, listNode) = self.panGestureArguments else { + return + } + self.panGestureArguments = nil + + let visibleContentOffset = listNode?.visibleContentOffset() + let contentOffset = scrollView?.contentOffset.y ?? 0.0 + + let translation = recognizer.translation(in: self.view).y + var velocity = recognizer.velocity(in: self.view) + + if self.isExpanded { + if case let .known(value) = visibleContentOffset, value > 0.1 { + velocity = CGPoint() + } else if case .unknown = visibleContentOffset { + velocity = CGPoint() + } else if contentOffset > 0.1 { + velocity = CGPoint() + } + } + + var bounds = self.bounds + if self.isExpanded { + bounds.origin.y = -max(0.0, translation - edgeTopInset) + } else { + bounds.origin.y = -translation + } + bounds.origin.y = min(0.0, bounds.origin.y) + + scrollView?.bounces = true + + let offset = currentTopInset + panOffset + let topInset: CGFloat = edgeTopInset + + var dismissing = false + if bounds.minY < -60 || (bounds.minY < 0.0 && velocity.y > 300.0) || (self.isExpanded && bounds.minY.isZero && velocity.y > 1800.0) { + self.controller?.dismiss(animated: true, completion: nil) + dismissing = true + } else if self.isExpanded { + if velocity.y > 300.0 || offset > topInset / 2.0 { + self.isExpanded = false + if let listNode = listNode { + listNode.scroller.setContentOffset(CGPoint(), animated: false) + } else if let scrollView = scrollView { + scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) + } + + let distance = topInset - offset + let initialVelocity: CGFloat = distance.isZero ? 0.0 : abs(velocity.y / distance) + let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity)) + + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(transition)) + } else { + self.isExpanded = true + + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(.animated(duration: 0.3, curve: .easeInOut))) + } + } else if (velocity.y < -300.0 || offset < topInset / 2.0) { + if velocity.y > -2200.0 && velocity.y < -300.0, let listNode = listNode { + DispatchQueue.main.async { + listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + } + } + + let initialVelocity: CGFloat = offset.isZero ? 0.0 : abs(velocity.y / offset) + let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity)) + self.isExpanded = true + + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(transition)) + } else { + if let listNode = listNode { + listNode.scroller.setContentOffset(CGPoint(), animated: false) + } else if let scrollView = scrollView { + scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) + } + + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(.animated(duration: 0.3, curve: .easeInOut))) + } + + if !dismissing { + var bounds = self.bounds + let previousBounds = bounds + bounds.origin.y = 0.0 + self.bounds = bounds + self.layer.animateBounds(from: previousBounds, to: self.bounds, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) + } + case .cancelled: + self.panGestureArguments = nil + + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(.animated(duration: 0.3, curve: .easeInOut))) + default: + break + } + } + + func update(isExpanded: Bool, transition: ContainedViewLayoutTransition) { + guard isExpanded != self.isExpanded else { + return + } + self.isExpanded = isExpanded + + guard let (layout, navigationHeight) = self.currentLayout else { + return + } + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(transition)) + } + } + + var node: Node { + return self.displayNode as! Node + } + + private let context: AccountContext + private let theme: PresentationTheme + + private var currentLayout: ContainerViewLayout? + + public var pushController: (ViewController) -> Void = { _ in } + public var presentController: (ViewController) -> Void = { _ in } + + var completion: (TelegramMediaFile?) -> Void = { _ in } + + public init(context: AccountContext) { + self.context = context + self.theme = defaultDarkColorPresentationTheme + + super.init(navigationBarPresentationData: nil) + + self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) + self.statusBar.statusBarStyle = .Ignore + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func cancelPressed() { + self.dismiss(animated: true, completion: nil) + } + + override open func loadDisplayNode() { + self.displayNode = Node(context: self.context, controller: self, theme: self.theme) + self.displayNodeDidLoad() + } + + public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { + self.view.endEditing(true) + if flag { + self.node.animateOut(completion: { + super.dismiss(animated: false, completion: {}) + completion?() + }) + } else { + super.dismiss(animated: false, completion: {}) + completion?() + } + } + + override open func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + self.node.updateIsVisible(isVisible: true) + } + + override open func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + + self.node.updateIsVisible(isVisible: false) + } + + override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + self.currentLayout = layout + super.containerLayoutUpdated(layout, transition: transition) + + let navigationHeight: CGFloat = 56.0 + + self.node.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(transition)) + } +} diff --git a/submodules/DrawingUI/Sources/TextSettingsComponent.swift b/submodules/DrawingUI/Sources/TextSettingsComponent.swift new file mode 100644 index 0000000000..c2707e73a5 --- /dev/null +++ b/submodules/DrawingUI/Sources/TextSettingsComponent.swift @@ -0,0 +1,710 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import LegacyComponents +import TelegramCore +import Postbox + +enum DrawingTextStyle: Equatable { + 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 DrawingTextAlignment: Equatable { + case left + case center + case right + + init(alignment: DrawingTextEntity.Alignment) { + switch alignment { + case .left: + self = .left + case .center: + self = .center + case .right: + self = .right + } + } +} + +enum DrawingTextFont: Equatable, CaseIterable { + case sanFrancisco + case newYork + case monospaced + case round + + init(font: DrawingTextEntity.Font) { + switch font { + case .sanFrancisco: + self = .sanFrancisco + case .newYork: + self = .newYork + case .monospaced: + self = .monospaced + case .round: + self = .round + } + } + + var font: DrawingTextEntity.Font { + switch self { + case .sanFrancisco: + return .sanFrancisco + case .newYork: + return .newYork + case .monospaced: + return .monospaced + case .round: + return .round + } + } + + var title: String { + switch self { + case .sanFrancisco: + return "San Francisco" + case .newYork: + return "New York" + case .monospaced: + return "Monospaced" + case .round: + return "Rounded" + } + } + + var uiFont: UIFont { + switch self { + case .sanFrancisco: + return Font.semibold(13.0) + case .newYork: + return Font.with(size: 13.0, design: .serif, weight: .semibold) + case .monospaced: + return Font.with(size: 13.0, design: .monospace, weight: .semibold) + case .round: + return Font.with(size: 13.0, design: .round, weight: .semibold) + } + } +} + +final class TextAlignmentComponent: Component { + let alignment: DrawingTextAlignment + + init(alignment: DrawingTextAlignment) { + self.alignment = alignment + } + + static func == (lhs: TextAlignmentComponent, rhs: TextAlignmentComponent) -> Bool { + return lhs.alignment == rhs.alignment + } + + public final class View: UIView { + private let line1 = SimpleLayer() + private let line2 = SimpleLayer() + private let line3 = SimpleLayer() + private let line4 = SimpleLayer() + + override init(frame: CGRect) { + super.init(frame: frame) + + let lines = [self.line1, self.line2, self.line3, self.line4] + lines.forEach { line in + line.backgroundColor = UIColor.white.cgColor + line.cornerRadius = 1.0 + line.masksToBounds = true + self.layer.addSublayer(line) + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: TextAlignmentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let height = 2.0 - UIScreenPixel + let spacing: CGFloat = 3.0 + UIScreenPixel + let long = 21.0 + let short = 13.0 + + let size = CGSize(width: long, height: 18.0) + + switch component.alignment { + case .left: + transition.setFrame(layer: self.line1, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: long, height: height))) + transition.setFrame(layer: self.line2, frame: CGRect(origin: CGPoint(x: 0.0, y: height + spacing), size: CGSize(width: short, height: height))) + transition.setFrame(layer: self.line3, frame: CGRect(origin: CGPoint(x: 0.0, y: height + spacing + height + spacing), size: CGSize(width: long, height: height))) + transition.setFrame(layer: self.line4, frame: CGRect(origin: CGPoint(x: 0.0, y: height + spacing + height + spacing + height + spacing), size: CGSize(width: short, height: height))) + case .center: + transition.setFrame(layer: self.line1, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - long) / 2.0), y: 0.0), size: CGSize(width: long, height: height))) + transition.setFrame(layer: self.line2, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - short) / 2.0), y: height + spacing), size: CGSize(width: short, height: height))) + transition.setFrame(layer: self.line3, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - long) / 2.0), y: height + spacing + height + spacing), size: CGSize(width: long, height: height))) + transition.setFrame(layer: self.line4, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - short) / 2.0), y: height + spacing + height + spacing + height + spacing), size: CGSize(width: short, height: height))) + case .right: + transition.setFrame(layer: self.line1, frame: CGRect(origin: CGPoint(x: size.width - long, y: 0.0), size: CGSize(width: long, height: height))) + transition.setFrame(layer: self.line2, frame: CGRect(origin: CGPoint(x: size.width - short, y: height + spacing), size: CGSize(width: short, height: height))) + transition.setFrame(layer: self.line3, frame: CGRect(origin: CGPoint(x: size.width - long, y: height + spacing + height + spacing), size: CGSize(width: long, height: height))) + transition.setFrame(layer: self.line4, frame: CGRect(origin: CGPoint(x: size.width - short, y: height + spacing + height + spacing + height + spacing), size: CGSize(width: short, height: height))) + } + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +final class TextFontComponent: Component { + let values: [DrawingTextFont] + let selectedValue: DrawingTextFont + let updated: (DrawingTextFont) -> Void + + init(values: [DrawingTextFont], selectedValue: DrawingTextFont, updated: @escaping (DrawingTextFont) -> Void) { + self.values = values + self.selectedValue = selectedValue + self.updated = updated + } + + static func == (lhs: TextFontComponent, rhs: TextFontComponent) -> Bool { + return lhs.values == rhs.values && lhs.selectedValue == rhs.selectedValue + } + + public final class View: UIView { + private var buttons: [DrawingTextFont: HighlightableButton] = [:] + private let scrollView = UIScrollView() + private let scrollMask = UIView() + private let maskLeft = SimpleGradientLayer() + private let maskCenter = SimpleLayer() + private let maskRight = SimpleGradientLayer() + + private var updated: (DrawingTextFont) -> Void = { _ in } + + override init(frame: CGRect) { + if #available(iOS 11.0, *) { + self.scrollView.contentInsetAdjustmentBehavior = .never + } + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.showsVerticalScrollIndicator = false + self.scrollView.decelerationRate = .fast + + super.init(frame: frame) + + self.mask = self.scrollMask + + self.maskLeft.type = .axial + self.maskLeft.startPoint = CGPoint(x: 0.0, y: 0.5) + self.maskLeft.endPoint = CGPoint(x: 1.0, y: 0.5) + self.maskLeft.colors = [UIColor.white.withAlphaComponent(0.0).cgColor, UIColor.white.cgColor] + self.maskLeft.locations = [0.0, 1.0] + + self.maskCenter.backgroundColor = UIColor.white.cgColor + + self.maskRight.type = .axial + self.maskRight.startPoint = CGPoint(x: 0.0, y: 0.5) + self.maskRight.endPoint = CGPoint(x: 1.0, y: 0.5) + self.maskRight.colors = [UIColor.white.cgColor, UIColor.white.withAlphaComponent(0.0).cgColor] + self.maskRight.locations = [0.0, 1.0] + + self.scrollMask.layer.addSublayer(self.maskLeft) + self.scrollMask.layer.addSublayer(self.maskCenter) + self.scrollMask.layer.addSublayer(self.maskRight) + + self.addSubview(self.scrollView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func pressed(_ sender: HighlightableButton) { + for (font, button) in self.buttons { + if button === sender { + self.updated(font) + break + } + } + } + + private var previousValue: DrawingTextFont? + func update(component: TextFontComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.updated = component.updated + + var contentWidth: CGFloat = 0.0 + + for value in component.values { + contentWidth += 12.0 + let button: HighlightableButton + if let current = self.buttons[value] { + button = current + } else { + button = HighlightableButton() + button.setTitle(value.title, for: .normal) + button.titleLabel?.font = value.uiFont + button.sizeToFit() + button.frame = CGRect(origin: .zero, size: CGSize(width: button.frame.width + 16.0, height: 30.0)) + button.layer.cornerRadius = 11.0 + button.addTarget(self, action: #selector(self.pressed(_:)), for: .touchUpInside) + + self.buttons[value] = button + + self.scrollView.addSubview(button) + } + + if value == component.selectedValue { + button.layer.borderWidth = 1.0 - UIScreenPixel + button.layer.borderColor = UIColor.white.cgColor + } else { + button.layer.borderWidth = UIScreenPixel + button.layer.borderColor = UIColor.white.withAlphaComponent(0.5).cgColor + } + + button.frame = CGRect(origin: CGPoint(x: contentWidth, y: 0.0), size: button.frame.size) + contentWidth += button.frame.width + } + contentWidth += 12.0 + + if self.scrollView.contentSize.width != contentWidth { + self.scrollView.contentSize = CGSize(width: contentWidth, height: 30.0) + } + self.scrollView.frame = CGRect(origin: .zero, size: availableSize) + + self.scrollMask.frame = CGRect(origin: .zero, size: availableSize) + self.maskLeft.frame = CGRect(origin: .zero, size: CGSize(width: 12.0, height: 30.0)) + self.maskCenter.frame = CGRect(origin: CGPoint(x: 12.0, y: 0.0), size: CGSize(width: availableSize.width - 24.0, height: 30.0)) + self.maskRight.frame = CGRect(origin: CGPoint(x: availableSize.width - 12.0, y: 0.0), size: CGSize(width: 12.0, height: 30.0)) + + if component.selectedValue != self.previousValue { + self.previousValue = component.selectedValue + + if let button = self.buttons[component.selectedValue] { + self.scrollView.scrollRectToVisible(button.frame.insetBy(dx: -48.0, dy: 0.0), animated: true) + } + } + + return availableSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +final class TextSettingsComponent: CombinedComponent { + let color: DrawingColor? + let style: DrawingTextStyle + let alignment: DrawingTextAlignment + let font: DrawingTextFont + + let presentColorPicker: () -> Void + let presentFastColorPicker: (GenericComponentViewTag) -> Void + let updateFastColorPickerPan: (CGPoint) -> Void + let dismissFastColorPicker: () -> Void + let toggleStyle: () -> Void + let toggleAlignment: () -> Void + let updateFont: (DrawingTextFont) -> Void + + init( + color: DrawingColor?, + style: DrawingTextStyle, + alignment: DrawingTextAlignment, + font: DrawingTextFont, + presentColorPicker: @escaping () -> Void = {}, + presentFastColorPicker: @escaping (GenericComponentViewTag) -> Void = { _ in }, + updateFastColorPickerPan: @escaping (CGPoint) -> Void = { _ in }, + dismissFastColorPicker: @escaping () -> Void = {}, + toggleStyle: @escaping () -> Void, + toggleAlignment: @escaping () -> Void, + updateFont: @escaping (DrawingTextFont) -> Void + ) { + self.color = color + self.style = style + self.alignment = alignment + self.font = font + self.presentColorPicker = presentColorPicker + self.presentFastColorPicker = presentFastColorPicker + self.updateFastColorPickerPan = updateFastColorPickerPan + self.dismissFastColorPicker = dismissFastColorPicker + self.toggleStyle = toggleStyle + self.toggleAlignment = toggleAlignment + self.updateFont = updateFont + } + + static func ==(lhs: TextSettingsComponent, rhs: TextSettingsComponent) -> Bool { + if lhs.color != rhs.color { + return false + } + if lhs.style != rhs.style { + return false + } + if lhs.alignment != rhs.alignment { + return false + } + if lhs.font != rhs.font { + return false + } + return true + } + + final class State: ComponentState { + enum ImageKey: Hashable { + case regular + case filled + case semi + case stroke + } + private var cachedImages: [ImageKey: UIImage] = [:] + func image(_ key: ImageKey) -> UIImage { + if let image = self.cachedImages[key] { + return image + } else { + var image: UIImage + switch key { + case .regular: + image = UIImage(bundleImageName: "Media Editor/TextDefault")! + case .filled: + image = UIImage(bundleImageName: "Media Editor/TextFilled")! + case .semi: + image = UIImage(bundleImageName: "Media Editor/TextSemi")! + case .stroke: + image = UIImage(bundleImageName: "Media Editor/TextStroke")! + } + cachedImages[key] = image + return image + } + } + } + + func makeState() -> State { + State() + } + + static var body: Body { + let colorButton = Child(ColorSwatchComponent.self) + let colorButtonTag = GenericComponentViewTag() + + let alignmentButton = Child(Button.self) + let styleButton = Child(Button.self) + let font = Child(TextFontComponent.self) + + return { context in + let component = context.component + let state = context.state + + let toggleStyle = component.toggleStyle + let toggleAlignment = component.toggleAlignment + let updateFont = component.updateFont + + var offset: CGFloat = 6.0 + if let color = component.color { + let presentColorPicker = component.presentColorPicker + let presentFastColorPicker = component.presentFastColorPicker + let updateFastColorPickerPan = component.updateFastColorPickerPan + let dismissFastColorPicker = component.dismissFastColorPicker + + let colorButton = colorButton.update( + component: ColorSwatchComponent( + type: .main, + color: color, + tag: colorButtonTag, + action: { + presentColorPicker() + }, + holdAction: { + presentFastColorPicker(colorButtonTag) + }, + pan: { point in + updateFastColorPickerPan(point) + }, + release: { + dismissFastColorPicker() + } + ), + availableSize: CGSize(width: 44.0, height: 44.0), + transition: context.transition + ) + context.add(colorButton + .position(CGPoint(x: colorButton.size.width / 2.0, y: context.availableSize.height / 2.0)) + ) + offset += 44.0 + } + + let styleImage: UIImage + switch component.style { + case .regular: + styleImage = state.image(.regular) + case .filled: + styleImage = state.image(.filled) + case .semi: + styleImage = state.image(.semi) + case .stroke: + styleImage = state.image(.stroke) + } + + let styleButton = styleButton.update( + component: Button( + content: AnyComponent( + Image( + image: styleImage + ) + ), + action: { + toggleStyle() + } + ).minSize(CGSize(width: 44.0, height: 44.0)), + availableSize: CGSize(width: 30.0, height: 30.0), + transition: .easeInOut(duration: 0.2) + ) + context.add(styleButton + .position(CGPoint(x: offset + styleButton.size.width / 2.0, y: context.availableSize.height / 2.0)) + .update(Transition.Update { _, view, transition in + if let snapshot = view.snapshotView(afterScreenUpdates: false) { + transition.setAlpha(view: snapshot, alpha: 0.0, completion: { [weak snapshot] _ in + snapshot?.removeFromSuperview() + }) + snapshot.frame = view.frame + transition.animateAlpha(view: view, from: 0.0, to: 1.0) + view.superview?.addSubview(snapshot) + } + }) + ) + offset += 44.0 + + let alignmentButton = alignmentButton.update( + component: Button( + content: AnyComponent( + TextAlignmentComponent( + alignment: component.alignment + ) + ), + action: { + toggleAlignment() + } + ).minSize(CGSize(width: 44.0, height: 44.0)), + availableSize: context.availableSize, + transition: .easeInOut(duration: 0.2) + ) + context.add(alignmentButton + .position(CGPoint(x: offset + alignmentButton.size.width / 2.0, y: context.availableSize.height / 2.0)) + ) + offset += 44.0 + + let font = font.update( + component: TextFontComponent( + values: DrawingTextFont.allCases, + selectedValue: component.font, + updated: { font in + updateFont(font) + } + ), + availableSize: CGSize(width: context.availableSize.width - offset, height: 30.0), + transition: .easeInOut(duration: 0.2) + ) + context.add(font + .position(CGPoint(x: offset + font.size.width / 2.0, y: context.availableSize.height / 2.0)) + ) + offset += 44.0 + + return context.availableSize + } + } +} + +private func generateMaskPath(size: CGSize, topRadius: CGFloat, bottomRadius: CGFloat) -> UIBezierPath { + let path = UIBezierPath() + path.addArc(withCenter: CGPoint(x: size.width / 2.0, y: topRadius), radius: topRadius, startAngle: .pi, endAngle: 0, clockwise: true) + path.addArc(withCenter: CGPoint(x: size.width / 2.0, y: size.height - bottomRadius), radius: bottomRadius, startAngle: 0, endAngle: .pi, clockwise: true) + path.close() + return path +} + +private func generateKnobImage() -> UIImage? { + let side: CGFloat = 32.0 + let margin: CGFloat = 10.0 + + let image = generateImage(CGSize(width: side + margin * 2.0, height: side + margin * 2.0), opaque: false, rotatedContext: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + + context.setShadow(offset: CGSize(width: 0.0, height: 0.0), blur: 9.0, color: UIColor(rgb: 0x000000, alpha: 0.3).cgColor) + context.setFillColor(UIColor.white.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: margin, y: margin), size: CGSize(width: side, height: side))) + }) + return image?.stretchableImage(withLeftCapWidth: Int(margin + side * 0.5), topCapHeight: Int(margin + side * 0.5)) +} + +final class TextSizeSliderComponent: Component { + let value: CGFloat + let updated: (CGFloat) -> Void + + public init( + value: CGFloat, + updated: @escaping (CGFloat) -> Void + ) { + self.value = value + self.updated = updated + } + + public static func ==(lhs: TextSizeSliderComponent, rhs: TextSizeSliderComponent) -> Bool { + if lhs.value != rhs.value { + return false + } + return true + } + + final class View: UIView, UIGestureRecognizerDelegate { + private var validSize: CGSize? + private var component: TextSizeSliderComponent? + + private let backgroundNode = NavigationBackgroundNode(color: UIColor(rgb: 0x888888, alpha: 0.3)) + private let maskLayer = SimpleShapeLayer() + + private let knobContainer = SimpleLayer() + private let knob = SimpleLayer() + + fileprivate var updated: (CGFloat) -> Void = { _ in } + + init() { + super.init(frame: CGRect()) + + let pressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handlePress(_:))) + pressGestureRecognizer.minimumPressDuration = 0.01 + pressGestureRecognizer.delegate = self + self.addGestureRecognizer(pressGestureRecognizer) + self.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))) + } + + required init?(coder aDecoder: NSCoder) { + preconditionFailure() + } + + private var isTracking: Bool? + private var isPanning = false + private var isPressing = false + + @objc func handlePress(_ gestureRecognizer: UILongPressGestureRecognizer) { + guard self.frame.height > 0.0 else { + return + } + switch gestureRecognizer.state { + case .began: + self.isPressing = true + if let size = self.validSize, let component = self.component { + let _ = self.updateLayout(size: size, component: component, transition: .easeInOut(duration: 0.2)) + } + + let location = gestureRecognizer.location(in: self).offsetBy(dx: 0.0, dy: -12.0) + let value = 1.0 - max(0.0, min(1.0, location.y / (self.frame.height - 24.0))) + self.updated(value) + case .ended, .cancelled: + self.isPressing = false + if let size = self.validSize, let component = self.component { + let _ = self.updateLayout(size: size, component: component, transition: .easeInOut(duration: 0.2)) + } + + default: + break + } + } + + @objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { + guard self.frame.height > 0.0 else { + return + } + switch gestureRecognizer.state { + case .began, .changed: + self.isPanning = true + if let size = self.validSize, let component = self.component { + let _ = self.updateLayout(size: size, component: component, transition: .easeInOut(duration: 0.2)) + } + let location = gestureRecognizer.location(in: self).offsetBy(dx: 0.0, dy: -12.0) + let value = 1.0 - max(0.0, min(1.0, location.y / (self.frame.height - 24.0))) + self.updated(value) + case .ended, .cancelled: + self.isPanning = false + if let size = self.validSize, let component = self.component { + let _ = self.updateLayout(size: size, component: component, transition: .easeInOut(duration: 0.2)) + } + default: + break + } + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + + func updateLayout(size: CGSize, component: TextSizeSliderComponent, transition: Transition) -> CGSize { + self.component = component + + let previousSize = self.validSize + self.validSize = size + + if self.backgroundNode.view.superview == nil { + self.addSubview(self.backgroundNode.view) + } + if self.knobContainer.superlayer == nil { + self.layer.addSublayer(self.knobContainer) + } + if self.knob.superlayer == nil { + self.knob.contents = generateKnobImage()?.cgImage + self.knobContainer.addSublayer(self.knob) + } + + let isTracking = self.isPanning || self.isPressing + if self.isTracking != isTracking { + self.isTracking = isTracking + transition.setSublayerTransform(view: self, transform: isTracking ? CATransform3DMakeTranslation(8.0, 0.0, 0.0) : CATransform3DMakeTranslation(-size.width / 2.0, 0.0, 0.0)) + transition.setSublayerTransform(layer: self.knobContainer, transform: isTracking ? CATransform3DIdentity : CATransform3DMakeTranslation(4.0, 0.0, 0.0)) + } + + let knobSize = CGSize(width: 52.0, height: 52.0) + self.knob.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - knobSize.width) / 2.0), y: -12.0 + floorToScreenPixels((size.height + 24.0 - knobSize.height) * (1.0 - component.value))), size: knobSize) + + transition.setFrame(view: self.backgroundNode.view, frame: CGRect(origin: CGPoint(), size: size)) + self.backgroundNode.update(size: size, transition: transition.containedViewLayoutTransition) + + transition.setFrame(layer: self.knobContainer, frame: CGRect(origin: CGPoint(), size: size)) + + if previousSize != size { + transition.setFrame(layer: self.maskLayer, frame: CGRect(origin: .zero, size: size)) + self.maskLayer.path = generateMaskPath(size: size, topRadius: 15.0, bottomRadius: 3.0).cgPath + self.backgroundNode.layer.mask = self.maskLayer + } + + return size + } + } + + func makeView() -> View { + return View() + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + view.updated = self.updated + return view.updateLayout(size: availableSize, component: self, transition: transition) + } +} diff --git a/submodules/DrawingUI/Sources/ToolsComponent.swift b/submodules/DrawingUI/Sources/ToolsComponent.swift new file mode 100644 index 0000000000..a9d7b0c79b --- /dev/null +++ b/submodules/DrawingUI/Sources/ToolsComponent.swift @@ -0,0 +1,630 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import SwiftSignalKit +import LegacyComponents +import TelegramCore +import Postbox + +private let toolSize = CGSize(width: 40.0, height: 176.0) + +private class ToolView: UIView, UIGestureRecognizerDelegate { + let type: DrawingToolState.Key + + var isSelected = false + var isToolFocused = false + var isVisible = false + private var currentSize: CGFloat? + private var currentEraserMode: DrawingToolState.EraserState.Mode? + + private let tip: UIImageView + private let background: SimpleLayer + private let band: SimpleGradientLayer + private let eraserType: SimpleLayer + + var pressed: (DrawingToolState.Key) -> Void = { _ in } + var swiped: (DrawingToolState.Key, CGFloat) -> Void = { _, _ in } + var released: () -> Void = { } + + init(type: DrawingToolState.Key) { + self.type = type + self.tip = UIImageView() + self.tip.isUserInteractionEnabled = false + + self.background = SimpleLayer() + + self.band = SimpleGradientLayer() + self.band.cornerRadius = 2.0 + self.band.type = .axial + self.band.startPoint = CGPoint(x: 0.0, y: 0.5) + self.band.endPoint = CGPoint(x: 1.0, y: 0.5) + self.band.masksToBounds = true + + self.eraserType = SimpleLayer() + self.eraserType.opacity = 0.0 + self.eraserType.transform = CATransform3DMakeScale(0.001, 0.001, 1.0) + + let backgroundImage: UIImage? + let tipImage: UIImage? + + var tipAbove = true + var hasBand = true + var hasEraserType = false + + switch type { + case .pen: + backgroundImage = UIImage(bundleImageName: "Media Editor/ToolPen") + tipImage = UIImage(bundleImageName: "Media Editor/ToolPenTip")?.withRenderingMode(.alwaysTemplate) + case .marker: + backgroundImage = UIImage(bundleImageName: "Media Editor/ToolMarker") + tipImage = UIImage(bundleImageName: "Media Editor/ToolMarkerTip")?.withRenderingMode(.alwaysTemplate) + tipAbove = false + case .neon: + backgroundImage = UIImage(bundleImageName: "Media Editor/ToolNeon") + tipImage = UIImage(bundleImageName: "Media Editor/ToolNeonTip")?.withRenderingMode(.alwaysTemplate) + tipAbove = false + case .pencil: + backgroundImage = UIImage(bundleImageName: "Media Editor/ToolPencil") + tipImage = UIImage(bundleImageName: "Media Editor/ToolPencilTip")?.withRenderingMode(.alwaysTemplate) + case .lasso: + backgroundImage = UIImage(bundleImageName: "Media Editor/ToolLasso") + tipImage = nil + hasBand = false + case .eraser: + self.eraserType.contents = UIImage(bundleImageName: "Media Editor/EraserRemove")?.cgImage + backgroundImage = UIImage(bundleImageName: "Media Editor/ToolEraser") + tipImage = nil + hasBand = false + hasEraserType = true + } + + self.tip.image = tipImage + self.background.contents = backgroundImage?.cgImage + + super.init(frame: CGRect(origin: .zero, size: toolSize)) + + self.tip.frame = CGRect(origin: .zero, size: toolSize) + self.background.frame = CGRect(origin: .zero, size: toolSize) + + self.band.frame = CGRect(origin: CGPoint(x: 3.0, y: 64.0), size: CGSize(width: toolSize.width - 6.0, height: toolSize.width - 16.0)) + self.band.anchorPoint = CGPoint(x: 0.5, y: 0.0) + + self.eraserType.position = CGPoint(x: 20.0, y: 56.0) + self.eraserType.bounds = CGRect(origin: .zero, size: CGSize(width: 16.0, height: 16.0)) + + if tipAbove { + self.layer.addSublayer(self.background) + self.addSubview(self.tip) + } else { + self.addSubview(self.tip) + self.layer.addSublayer(self.background) + } + + if hasBand { + self.layer.addSublayer(self.band) + } + + if hasEraserType { + self.layer.addSublayer(self.eraserType) + } + + let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:))) + self.addGestureRecognizer(tapGestureRecognizer) + + let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:))) + self.addGestureRecognizer(panGestureRecognizer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + if gestureRecognizer is UIPanGestureRecognizer { + if self.isSelected && !self.isToolFocused { + return true + } else { + return false + } + } + return self.isVisible + } + + @objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { + self.pressed(self.type) + } + + @objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { + guard let size = self.currentSize else { + return + } + switch gestureRecognizer.state { + case .changed: + let translation = gestureRecognizer.translation(in: self) + gestureRecognizer.setTranslation(.zero, in: self) + + let updatedSize = max(0.0, min(1.0, size - translation.y / 200.0)) + self.swiped(self.type, updatedSize) + case .ended, .cancelled: + self.released() + default: + break + } + } + + func animateIn(animated: Bool, delay: Double = 0.0) { + let layout = { + self.bounds = CGRect(origin: .zero, size: self.bounds.size) + } + if animated { + UIView.animate(withDuration: 0.5, delay: delay, usingSpringWithDamping: 0.6, initialSpringVelocity: 0.0, animations: layout) + } else { + layout() + } + } + + func animateOut(animated: Bool, delay: Double = 0.0, completion: @escaping () -> Void = {}) { + let layout = { + self.bounds = CGRect(origin: CGPoint(x: 0.0, y: -140.0), size: self.bounds.size) + } + if animated { + UIView.animate(withDuration: 0.5, delay: delay, usingSpringWithDamping: 0.6, initialSpringVelocity: 0.0, animations: layout, completion: { _ in + completion() + }) + } else { + layout() + completion() + } + } + + func update(state: DrawingToolState) { + if let _ = self.tip.image { + let color = state.color?.toUIColor() + self.tip.tintColor = color + + self.currentSize = state.size + + guard let color = color else { + return + } + var locations: [NSNumber] = [0.0, 1.0] + var colors: [CGColor] = [] + switch self.type { + case .pen: + locations = [0.0, 0.15, 0.85, 1.0] + colors = [ + color.withMultipliedBrightnessBy(0.7).cgColor, + color.cgColor, + color.cgColor, + color.withMultipliedBrightnessBy(0.7).cgColor + ] + case .marker: + locations = [0.0, 0.15, 0.85, 1.0] + colors = [ + color.withMultipliedBrightnessBy(0.7).cgColor, + color.cgColor, + color.cgColor, + color.withMultipliedBrightnessBy(0.7).cgColor + ] + case .neon: + locations = [0.0, 0.15, 0.85, 1.0] + colors = [ + color.withMultipliedBrightnessBy(0.7).cgColor, + color.cgColor, + color.cgColor, + color.withMultipliedBrightnessBy(0.7).cgColor + ] + case .pencil: + locations = [0.0, 0.25, 0.25, 0.75, 0.75, 1.0] + colors = [ + color.withMultipliedBrightnessBy(0.85).cgColor, + color.withMultipliedBrightnessBy(0.85).cgColor, + color.withMultipliedBrightnessBy(1.15).cgColor, + color.withMultipliedBrightnessBy(1.15).cgColor, + color.withMultipliedBrightnessBy(0.85).cgColor, + color.withMultipliedBrightnessBy(0.85).cgColor + ] + default: + return + } + + self.band.transform = CATransform3DMakeScale(1.0, 0.08 + 0.92 * (state.size ?? 1.0), 1.0) + + self.band.locations = locations + self.band.colors = colors + } + + if case .eraser = self.type { + let previousEraserMode = self.currentEraserMode + self.currentEraserMode = state.eraserMode + + let transition = Transition(animation: Transition.Animation.curve(duration: 0.2, curve: .easeInOut)) + if [.vector, .blur].contains(state.eraserMode) { + if !self.eraserType.opacity.isZero && (previousEraserMode != self.currentEraserMode) { + let snapshot = SimpleShapeLayer() + snapshot.contents = self.eraserType.contents + snapshot.frame = self.eraserType.frame + self.layer.addSublayer(snapshot) + + snapshot.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak snapshot] _ in + snapshot?.removeFromSuperlayer() + }) + snapshot.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false) + + self.eraserType.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.eraserType.animateScale(from: 0.001, to: 1.0, duration: 0.2) + } else { + transition.setAlpha(layer: self.eraserType, alpha: 1.0) + transition.setScale(layer: self.eraserType, scale: 1.0) + } + + self.eraserType.contents = UIImage(bundleImageName: state.eraserMode == .vector ? "Media Editor/EraserRemove" : "Media Editor/BrushBlur")?.cgImage + } else { + transition.setAlpha(layer: self.eraserType, alpha: 0.0) + transition.setScale(layer: self.eraserType, scale: 0.001) + } + } + } +} + +final class ToolsComponent: Component { + let state: DrawingState + let isFocused: Bool + let tag: AnyObject? + let toolPressed: (DrawingToolState.Key) -> Void + let toolResized: (DrawingToolState.Key, CGFloat) -> Void + let sizeReleased: () -> Void + + init(state: DrawingState, isFocused: Bool, tag: AnyObject?, toolPressed: @escaping (DrawingToolState.Key) -> Void, toolResized: @escaping (DrawingToolState.Key, CGFloat) -> Void, sizeReleased: @escaping () -> Void) { + self.state = state + self.isFocused = isFocused + self.tag = tag + self.toolPressed = toolPressed + self.toolResized = toolResized + self.sizeReleased = sizeReleased + } + + static func == (lhs: ToolsComponent, rhs: ToolsComponent) -> Bool { + return lhs.state == rhs.state && lhs.isFocused == rhs.isFocused + } + + public final class View: UIView, ComponentTaggedView { + private let toolViews: [ToolView] + private let maskImageView: UIImageView + + private var isToolFocused: Bool? + + private var component: ToolsComponent? + public func matches(tag: Any) -> Bool { + if let component = self.component, let componentTag = component.tag { + let tag = tag as AnyObject + if componentTag === tag { + return true + } + } + return false + } + + override init(frame: CGRect) { + var toolViews: [ToolView] = [] + for type in DrawingToolState.Key.allCases { + toolViews.append(ToolView(type: type)) + } + self.toolViews = toolViews + + self.maskImageView = UIImageView() + self.maskImageView.image = generateGradientImage(size: CGSize(width: 1.0, height: 120.0), colors: [UIColor.white, UIColor.white, UIColor.white.withAlphaComponent(0.0)], locations: [0.0, 0.88, 1.0], direction: .vertical) + + super.init(frame: frame) + + self.mask = self.maskImageView + + toolViews.forEach { self.addSubview($0) } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let result = super.hitTest(point, with: event) + if result === self { + return nil + } + return result + } + + func animateIn(completion: @escaping () -> Void) { + var delay = 0.0 + for i in 0 ..< self.toolViews.count { + let view = self.toolViews[i] + view.animateOut(animated: false) + view.animateIn(animated: true, delay: delay) + delay += 0.025 + } + } + + func animateOut(completion: @escaping () -> Void) { + let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + var delay = 0.0 + for i in 0 ..< self.toolViews.count { + let view = self.toolViews[i] + view.animateOut(animated: true, delay: delay, completion: i == self.toolViews.count - 1 ? completion : {}) + delay += 0.025 + + transition.setPosition(view: view, position: CGPoint(x: view.center.x, y: toolSize.height / 2.0 - 30.0 + 34.0)) + } + } + + func update(component: ToolsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + + let wasFocused = self.isToolFocused + + self.isToolFocused = component.isFocused + + let toolPressed = component.toolPressed + let toolResized = component.toolResized + let toolSizeReleased = component.sizeReleased + + let spacing: CGFloat = 44.0 + let totalWidth = spacing * CGFloat(self.toolViews.count - 1) + + let left = (availableSize.width - totalWidth) / 2.0 + var xPositions: [CGFloat] = [] + + var selectedIndex = 0 + let isFocused = component.isFocused + + for i in 0 ..< self.toolViews.count { + xPositions.append(left + spacing * CGFloat(i)) + + if self.toolViews[i].type == component.state.selectedTool { + selectedIndex = i + } + } + + if isFocused { + let originalFocusedToolPosition = xPositions[selectedIndex] + xPositions[selectedIndex] = availableSize.width / 2.0 + + let delta = availableSize.width / 2.0 - originalFocusedToolPosition + + for i in 0 ..< xPositions.count { + if i != selectedIndex { + xPositions[i] += delta + } + } + } + + var offset: CGFloat = 100.0 + for i in 0 ..< self.toolViews.count { + let view = self.toolViews[i] + + var scale = 0.5 + var verticalOffset: CGFloat = 34.0 + if i == selectedIndex { + if isFocused { + scale = 1.0 + verticalOffset = 30.0 + } else { + verticalOffset = 18.0 + } + view.isSelected = true + view.isToolFocused = isFocused + view.isVisible = true + } else { + view.isSelected = false + view.isToolFocused = false + view.isVisible = !isFocused + } + view.isUserInteractionEnabled = view.isVisible + + let layout = { + view.center = CGPoint(x: xPositions[i], y: toolSize.height / 2.0 - 30.0 + verticalOffset) + view.transform = CGAffineTransform(scaleX: scale, y: scale) + } + if case .curve = transition.animation { + UIView.animate( + withDuration: 0.7, + delay: 0.0, + usingSpringWithDamping: 0.6, + initialSpringVelocity: 0.0, + options: .allowUserInteraction, + animations: layout) + } else { + layout() + } + + view.update(state: component.state.toolState(for: view.type)) + + view.pressed = { type in + toolPressed(type) + } + view.swiped = { type, size in + toolResized(type, size) + } + view.released = { + toolSizeReleased() + } + + offset += 44.0 + } + + + if wasFocused != nil && wasFocused != component.isFocused { + var animated = false + if case .curve = transition.animation { + animated = true + } + if isFocused { + var delay = 0.0 + for i in (selectedIndex + 1 ..< self.toolViews.count).reversed() { + let view = self.toolViews[i] + view.animateOut(animated: animated, delay: delay) + delay += 0.025 + } + delay = 0.0 + for i in (0 ..< selectedIndex) { + let view = self.toolViews[i] + view.animateOut(animated: animated, delay: delay) + delay += 0.025 + } + } else { + var delay = 0.0 + for i in (selectedIndex + 1 ..< self.toolViews.count) { + let view = self.toolViews[i] + view.animateIn(animated: animated, delay: delay) + delay += 0.025 + } + delay = 0.0 + for i in (0 ..< selectedIndex).reversed() { + let view = self.toolViews[i] + view.animateIn(animated: animated, delay: delay) + delay += 0.025 + } + } + } + + self.maskImageView.frame = CGRect(origin: .zero, size: availableSize) + + if let screenTransition = transition.userData(DrawingScreenTransition.self) { + switch screenTransition { + case .animateIn: + self.animateIn(completion: {}) + case .animateOut: + self.animateOut(completion: {}) + } + } + + return availableSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + + +final class BrushButtonContent: CombinedComponent { + let title: String + let image: UIImage + + init( + title: String, + image: UIImage + ) { + self.title = title + self.image = image + } + + static func ==(lhs: BrushButtonContent, rhs: BrushButtonContent) -> Bool { + if lhs.title != rhs.title { + return false + } + if lhs.image !== rhs.image { + return false + } + return true + } + + static var body: Body { + let title = Child(Text.self) + let image = Child(Image.self) + + return { context in + let component = context.component + + let title = title.update( + component: Text( + text: component.title, + font: Font.regular(17.0), + color: .white + ), + availableSize: context.availableSize, + transition: .immediate + ) + + let image = image.update( + component: Image(image: component.image), + availableSize: CGSize(width: 24.0, height: 24.0), + transition: .immediate + ) + context.add(image + .position(CGPoint(x: context.availableSize.width - image.size.width / 2.0, y: context.availableSize.height / 2.0)) + ) + + context.add(title + .position(CGPoint(x: context.availableSize.width - image.size.width - title.size.width / 2.0, y: context.availableSize.height / 2.0)) + ) + + return context.availableSize + } + } +} + +final class ZoomOutButtonContent: CombinedComponent { + let title: String + let image: UIImage + + init( + title: String, + image: UIImage + ) { + self.title = title + self.image = image + } + + static func ==(lhs: ZoomOutButtonContent, rhs: ZoomOutButtonContent) -> Bool { + if lhs.title != rhs.title { + return false + } + if lhs.image !== rhs.image { + return false + } + return true + } + + static var body: Body { + let title = Child(Text.self) + let image = Child(Image.self) + + return { context in + let component = context.component + + let title = title.update( + component: Text( + text: component.title, + font: Font.regular(17.0), + color: .white + ), + availableSize: context.availableSize, + transition: .immediate + ) + + let image = image.update( + component: Image(image: component.image), + availableSize: CGSize(width: 24.0, height: 24.0), + transition: .immediate + ) + + let spacing: CGFloat = 2.0 + let width = title.size.width + spacing + image.size.width + context.add(image + .position(CGPoint(x: image.size.width / 2.0, y: context.availableSize.height / 2.0)) + ) + + context.add(title + .position(CGPoint(x: image.size.width + spacing + title.size.width / 2.0, y: context.availableSize.height / 2.0)) + ) + + return CGSize(width: width, height: context.availableSize.height) + } + } +} diff --git a/submodules/DrawingUI/Sources/Unistroke.swift b/submodules/DrawingUI/Sources/Unistroke.swift new file mode 100644 index 0000000000..f73219e8b1 --- /dev/null +++ b/submodules/DrawingUI/Sources/Unistroke.swift @@ -0,0 +1,241 @@ +import Foundation +import UIKit + +private let pointsCount: Int = 64 +private let squareSize: Double = 250.0 +private let diagonal = sqrt(squareSize * squareSize + squareSize * squareSize) +private let halfDiagonal = diagonal * 0.5 +private let angleRange: Double = .pi / 4.0 +private let anglePrecision: Double = .pi / 90.0 + +class Unistroke { + let points: [CGPoint] + + init(points: [CGPoint]) { + var points = resample(points: points, totalPoints: pointsCount) + let radians = indicativeAngle(points: points) + points = rotate(points: points, byRadians: -radians) + points = scale(points: points, toSize: squareSize) + points = translate(points: points, to: .zero) + self.points = points + } + + func match(templates: [UnistrokeTemplate], minThreshold: Double = 0.8) -> String? { + var bestDistance = Double.infinity + var bestTemplate: UnistrokeTemplate? + for template in templates { + let templateDistance = distanceAtBestAngle(points: self.points, strokeTemplate: template.points, fromAngle: -angleRange, toAngle: angleRange, threshold: anglePrecision) + if templateDistance < bestDistance { + bestDistance = templateDistance + bestTemplate = template + } + } + + if let bestTemplate = bestTemplate { + bestDistance = 1.0 - bestDistance / halfDiagonal + if bestDistance < minThreshold { + return nil + } + return bestTemplate.name + } else { + return nil + } + } +} + +class UnistrokeTemplate : Unistroke { + var name: String + + init(name: String, points: [CGPoint]) { + self.name = name + super.init(points: points) + } +} + +private struct Edge { + var minX: Double + var minY: Double + var maxX: Double + var maxY: Double + + init(minX: Double, maxX: Double, minY: Double, maxY: Double) { + self.minX = minX + self.minY = minY + self.maxX = maxX + self.maxY = maxY + } + + mutating func addPoint(value: CGPoint) { + self.minX = min(self.minX,value.x) + self.maxX = max(self.maxX,value.x) + self.minY = min(self.minY,value.y) + self.maxY = max(self.maxY,value.y) + } + +} + +private extension Double { + func toRadians() -> Double { + let res = self * .pi / 180.0 + return res + } +} + +private func resample(points: [CGPoint], totalPoints: Int) -> [CGPoint] { + var initialPoints = points + let interval = pathLength(points: initialPoints) / Double(totalPoints - 1) + var totalLength: Double = 0.0 + var newPoints: [CGPoint] = [points[0]] + for i in 1 ..< initialPoints.count { + let currentLength = initialPoints[i - 1].distance(to: initialPoints[i]) + if totalLength + currentLength >= interval { + let newPoint = CGPoint( + x: initialPoints[i - 1].x + ((interval - totalLength) / currentLength) * (initialPoints[i].x - initialPoints[i - 1].x), + y: initialPoints[i - 1].y + ((interval - totalLength) / currentLength) * (initialPoints[i].y - initialPoints[i - 1].y) + ) + newPoints.append(newPoint) + initialPoints.insert(newPoint, at: i) + totalLength = 0.0 + } else { + totalLength += currentLength + } + } + if newPoints.count == totalPoints - 1 { + newPoints.append(points.last!) + } + return newPoints +} + +private func pathLength(points: [CGPoint]) -> Double { + var distance: Double = 0.0 + for index in 1 ..< points.count { + distance += points[index - 1].distance(to: points[index]) + } + return distance +} + +private func pathDistance(path1: [CGPoint], path2: [CGPoint]) -> Double { + var d: Double = 0.0 + for idx in 0 ..< min(path1.count, path2.count) { + d += path1[idx].distance(to: path2[idx]) + } + return d / Double(path1.count) +} + +private func centroid(points: [CGPoint]) -> CGPoint { + var centroidPoint: CGPoint = .zero + for point in points { + centroidPoint.x = centroidPoint.x + point.x + centroidPoint.y = centroidPoint.y + point.y + } + centroidPoint.x = (centroidPoint.x / Double(points.count)) + centroidPoint.y = (centroidPoint.y / Double(points.count)) + return centroidPoint +} + +private func boundingBox(points: [CGPoint]) -> CGRect { + var edge = Edge(minX: +Double.infinity, maxX: -Double.infinity, minY: +Double.infinity, maxY: -Double.infinity) + for point in points { + edge.addPoint(value: point) + } + return CGRect(x: edge.minX, y: edge.minY, width: (edge.maxX - edge.minX), height: (edge.maxY - edge.minY)) +} + +private func rotate(points: [CGPoint], byRadians radians: Double) -> [CGPoint] { + let centroid = centroid(points: points) + let cosinus = cos(radians) + let sinus = sin(radians) + var result: [CGPoint] = [] + for point in points { + result.append( + CGPoint( + x: (point.x - centroid.x) * cosinus - (point.y - centroid.y) * sinus + centroid.x, + y: (point.x - centroid.x) * sinus + (point.y - centroid.y) * cosinus + centroid.y + ) + ) + } + return result +} + +private func scale(points: [CGPoint], toSize size: Double) -> [CGPoint] { + let boundingBox = boundingBox(points: points) + var result: [CGPoint] = [] + for point in points { + result.append( + CGPoint( + x: point.x * (size / boundingBox.width), + y: point.y * (size / boundingBox.height) + ) + ) + } + return result +} + +private func translate(points: [CGPoint], to pt: CGPoint) -> [CGPoint] { + let centroidPoint = centroid(points: points) + var newPoints: [CGPoint] = [] + for point in points { + newPoints.append( + CGPoint( + x: point.x + pt.x - centroidPoint.x, + y: point.y + pt.y - centroidPoint.y + ) + ) + } + return newPoints +} + +private func vectorize(points: [CGPoint]) -> [Double] { + var sum: Double = 0.0 + var vector: [Double] = [] + for point in points { + vector.append(point.x) + vector.append(point.y) + sum += (point.x * point.x) + (point.y * point.y) + } + let magnitude = sqrt(sum) + for i in 0 ..< vector.count { + vector[i] = vector[i] / magnitude + } + return vector +} + +private func indicativeAngle(points: [CGPoint]) -> Double { + let centroid = centroid(points: points) + return atan2(centroid.y - points[0].y, centroid.x - points[0].x) +} + +private func distanceAtBestAngle(points: [CGPoint], strokeTemplate: [CGPoint], fromAngle: Double, toAngle: Double, threshold: Double) -> Double { + func distanceAtAngle(points: [CGPoint], strokeTemplate: [CGPoint], radians: Double) -> Double { + let rotatedPoints = rotate(points: points, byRadians: radians) + return pathDistance(path1: rotatedPoints, path2: strokeTemplate) + } + + let phi: Double = (0.5 * (-1.0 + sqrt(5.0))) + + var fromAngle = fromAngle + var toAngle = toAngle + + var x1 = phi * fromAngle + (1.0 - phi) * toAngle + var f1 = distanceAtAngle(points: points, strokeTemplate: strokeTemplate, radians: x1) + + var x2 = (1.0 - phi) * fromAngle + phi * toAngle + var f2 = distanceAtAngle(points: points, strokeTemplate: strokeTemplate, radians: x2) + + while abs(toAngle - fromAngle) > threshold { + if f1 < f2 { + toAngle = x2 + x2 = x1 + f2 = f1 + x1 = phi * fromAngle + (1.0 - phi) * toAngle + f1 = distanceAtAngle(points: points, strokeTemplate: strokeTemplate, radians: x1) + } else { + fromAngle = x1 + x1 = x2 + f1 = f2 + x2 = (1.0 - phi) * fromAngle + phi * toAngle + f2 = distanceAtAngle(points: points, strokeTemplate: strokeTemplate, radians: x2) + } + } + return min(f1, f2) +} diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoEditorTabController.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoEditorTabController.h index 0646c13aad..bd87dbf0e8 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoEditorTabController.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoEditorTabController.h @@ -4,6 +4,12 @@ @protocol TGMediaEditAdjustments; +@protocol TGPhotoEditorTabProtocol + + + +@end + @interface TGPhotoEditorTabController : TGViewController { bool _dismissing; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h index e2e38a1bf9..a5a523c8ac 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h @@ -11,6 +11,7 @@ @end + @protocol TGPhotoPaintStickerRenderView @property (nonatomic, copy) void(^started)(double); @@ -56,6 +57,65 @@ @end + +@protocol TGPhotoDrawingView + +@property (nonatomic, readonly) BOOL isTracking; + +@property (nonatomic, copy) void(^zoomOut)(void); + +- (void)updateZoomScale:(CGFloat)scale; + +@end + +@protocol TGPhotoDrawingEntitiesView + +@property (nonatomic, copy) void(^hasSelectionChanged)(bool); +@property (nonatomic, readonly) BOOL hasSelection; + +- (void)play; +- (void)pause; +- (void)seekTo:(double)timestamp; +- (void)resetToStart; +- (void)updateVisibility:(BOOL)visibility; +- (void)clearSelection; +- (void)onZoom; + +- (void)handlePinch:(UIPinchGestureRecognizer *)gestureRecognizer; +- (void)handleRotate:(UIRotationGestureRecognizer *)gestureRecognizer; + +@end + +@protocol TGPhotoDrawingInterfaceController + +@property (nonatomic, copy) void(^requestDismiss)(void); +@property (nonatomic, copy) void(^requestApply)(void); +@property (nonatomic, copy) UIImage *(^getCurrentImage)(void); + +- (TGPaintingData *)generateResultData; +- (void)animateOut:(void(^)(void))completion; + +- (void)adapterContainerLayoutUpdatedSize:(CGSize)size + intrinsicInsets:(UIEdgeInsets)intrinsicInsets + safeInsets:(UIEdgeInsets)safeInsets + statusBarHeight:(CGFloat)statusBarHeight + inputHeight:(CGFloat)inputHeight + animated:(BOOL)animated; + +@end + + +@protocol TGPhotoDrawingAdapter + +@property (nonatomic, readonly) id drawingView; +@property (nonatomic, readonly) id drawingEntitiesView; +@property (nonatomic, readonly) UIView * selectionContainerView; +@property (nonatomic, readonly) UIView * contentWrapperView; +@property (nonatomic, readonly) id interfaceController; + +@end + + @protocol TGPhotoPaintStickersContext - (int64_t)documentIdForDocument:(id)document; @@ -67,4 +127,6 @@ @property (nonatomic, copy) id(^captionPanelView)(void); +- (id)drawingAdapter:(CGSize)size; + @end diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintTextEntity.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintTextEntity.h index 2cde313c36..d7b28a8e1d 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintTextEntity.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintTextEntity.h @@ -23,3 +23,10 @@ typedef enum { - (instancetype)initWithText:(NSString *)text font:(TGPhotoPaintFont *)font swatch:(TGPaintSwatch *)swatch baseFontSize:(CGFloat)baseFontSize maxWidth:(CGFloat)maxWidth style:(TGPhotoPaintTextEntityStyle)style; @end + + +@interface TGPhotoPaintStaticEntity : TGPhotoPaintEntity + +@property (nonatomic, strong) UIImage *renderImage; + +@end diff --git a/submodules/LegacyComponents/Sources/TGPhotoDrawingController.h b/submodules/LegacyComponents/Sources/TGPhotoDrawingController.h new file mode 100644 index 0000000000..a0cd0459b3 --- /dev/null +++ b/submodules/LegacyComponents/Sources/TGPhotoDrawingController.h @@ -0,0 +1,19 @@ +#import "TGPhotoEditorTabController.h" + +#import + +@class PGPhotoEditor; +@class TGPhotoEditorPreviewView; + +@protocol TGPhotoPaintStickersContext; + +@interface TGPhotoDrawingController : TGPhotoEditorTabController + +@property (nonatomic, copy) void (^requestDismiss)(void); +@property (nonatomic, copy) void (^requestApply)(void); + +- (instancetype)initWithContext:(id)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView entitiesView:(TGPhotoEntitiesContainerView *)entitiesView stickersContext:(id)stickersContext; + +- (TGPaintingData *)paintingData; + +@end diff --git a/submodules/LegacyComponents/Sources/TGPhotoDrawingController.m b/submodules/LegacyComponents/Sources/TGPhotoDrawingController.m new file mode 100644 index 0000000000..4c956f2500 --- /dev/null +++ b/submodules/LegacyComponents/Sources/TGPhotoDrawingController.m @@ -0,0 +1,883 @@ +#import "TGPhotoDrawingController.h" +#import "TGPhotoPaintController.h" + +#import "LegacyComponentsInternal.h" + +#import + +#import +#import +#import +#import "TGPhotoEditorInterfaceAssets.h" +#import + +#import +#import + +#import +#import + +#import + +#import "TGPaintingWrapperView.h" +#import "TGPhotoEditorSparseView.h" +#import "TGPhotoEntitiesContainerView.h" + +#import "PGPhotoEditor.h" +#import "TGPhotoEditorPreviewView.h" + + + + +#import "TGPaintCanvas.h" +#import "TGPainting.h" + +@interface TGPhotoDrawingController () +{ + id _context; + id _stickersContext; + id _drawingAdapter; + + TGModernGalleryZoomableScrollView *_scrollView; + UIView *_scrollContentView; + UIView *_scrollContainerView; + + TGPaintingWrapperView *_paintingWrapperView; + UIView *_drawingView; + + UIPinchGestureRecognizer *_entityPinchGestureRecognizer; + UIRotationGestureRecognizer *_entityRotationGestureRecognizer; + + UIView *_entitiesOutsideContainerView; + UIView *_entitiesWrapperView; + UIView *_entitiesView; + + UIView *_selectionContainerView; + + TGPhotoEditorSparseView *_interfaceWrapperView; + UIViewController *_interfaceController; + + CGSize _previousSize; + CGFloat _keyboardHeight; + TGObserverProxy *_keyboardWillChangeFrameProxy; + + TGPainting *_painting; + + bool _skipEntitiesSetup; + bool _entitiesReady; + + TGPaintingData *_resultData; +} + +@property (nonatomic, weak) PGPhotoEditor *photoEditor; +@property (nonatomic, weak) TGPhotoEditorPreviewView *previewView; + +@end + +@implementation TGPhotoDrawingController + +- (instancetype)initWithContext:(id)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView entitiesView:(TGPhotoEntitiesContainerView *)entitiesView stickersContext:(id)stickersContext +{ + self = [super initWithContext:context]; + if (self != nil) + { + _context = context; + _stickersContext = stickersContext; + + CGSize size = TGScaleToSize(photoEditor.originalSize, [TGPhotoPaintController maximumPaintingSize]); + _drawingAdapter = [_stickersContext drawingAdapter:size]; + _interfaceController = (UIViewController *)_drawingAdapter.interfaceController; + + __weak TGPhotoDrawingController *weakSelf = self; + _interfaceController.requestDismiss = ^{ + __strong TGPhotoDrawingController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + strongSelf.requestDismiss(); + }; + _interfaceController.requestApply = ^{ + __strong TGPhotoDrawingController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + strongSelf.requestApply(); + }; + _interfaceController.getCurrentImage = ^UIImage *{ + __strong TGPhotoDrawingController *strongSelf = weakSelf; + if (strongSelf == nil) + return nil; + + return [strongSelf.photoEditor currentResultImage]; + }; + + self.photoEditor = photoEditor; + self.previewView = previewView; + + _keyboardWillChangeFrameProxy = [[TGObserverProxy alloc] initWithTarget:self targetSelector:@selector(keyboardWillChangeFrame:) name:UIKeyboardWillChangeFrameNotification]; + } + return self; +} + +- (void)loadView +{ + [super loadView]; + self.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + + _scrollView = [[TGModernGalleryZoomableScrollView alloc] initWithFrame:self.view.bounds hasDoubleTap:false]; + if (@available(iOS 11.0, *)) { + _scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; + } + _scrollView.contentInset = UIEdgeInsetsZero; + _scrollView.delegate = self; + _scrollView.showsHorizontalScrollIndicator = false; + _scrollView.showsVerticalScrollIndicator = false; + [self.view addSubview:_scrollView]; + + _scrollContentView = [[UIView alloc] initWithFrame:self.view.bounds]; + [_scrollView addSubview:_scrollContentView]; + + _scrollContainerView = _drawingAdapter.contentWrapperView; + _scrollContainerView.clipsToBounds = true; +// [_scrollContainerView addTarget:self action:@selector(containerPressed) forControlEvents:UIControlEventTouchUpInside]; + [_scrollContentView addSubview:_scrollContainerView]; + + _entityPinchGestureRecognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handlePinch:)]; + _entityPinchGestureRecognizer.delegate = self; + [_scrollContentView addGestureRecognizer:_entityPinchGestureRecognizer]; + + _entityRotationGestureRecognizer = [[UIRotationGestureRecognizer alloc] initWithTarget:self action:@selector(handleRotate:)]; + _entityRotationGestureRecognizer.delegate = self; + [_scrollContentView addGestureRecognizer:_entityRotationGestureRecognizer]; + + __weak TGPhotoDrawingController *weakSelf = self; + _paintingWrapperView = [[TGPaintingWrapperView alloc] init]; + _paintingWrapperView.clipsToBounds = true; + _paintingWrapperView.shouldReceiveTouch = ^bool + { + __strong TGPhotoDrawingController *strongSelf = weakSelf; + if (strongSelf == nil) + return false; + + return true; + }; + [_scrollContainerView addSubview:_paintingWrapperView]; + + _entitiesOutsideContainerView = [[TGPhotoEditorSparseView alloc] init]; + _entitiesOutsideContainerView.clipsToBounds = true; + [_scrollContainerView addSubview:_entitiesOutsideContainerView]; + + _entitiesWrapperView = [[TGPhotoEditorSparseView alloc] init]; + [_entitiesOutsideContainerView addSubview:_entitiesWrapperView]; + + if (_entitiesView == nil) { + _entitiesView = (UIView *)[_drawingAdapter drawingEntitiesView]; + } + if (!_skipEntitiesSetup) { + [_entitiesWrapperView addSubview:_entitiesView]; + } + + _selectionContainerView = [_drawingAdapter selectionContainerView]; + _selectionContainerView.clipsToBounds = false; + [_scrollContainerView addSubview:_selectionContainerView]; + + _interfaceWrapperView = [[TGPhotoEditorSparseView alloc] initWithFrame:CGRectZero]; + [self.view addSubview:_interfaceWrapperView]; + + TGPhotoEditorPreviewView *previewView = _previewView; + previewView.userInteractionEnabled = false; + previewView.hidden = true; + + [_interfaceWrapperView addSubview:_interfaceController.view]; + + if (![self _updateControllerInset:false]) + [self controllerInsetUpdated:UIEdgeInsetsZero]; +} + +- (void)setupCanvas +{ + __weak TGPhotoDrawingController *weakSelf = self; + if (_drawingView == nil) { + _drawingView = (UIView *)_drawingAdapter.drawingView; + _drawingView.zoomOut = ^{ + __strong TGPhotoDrawingController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf->_scrollView setZoomScale:strongSelf->_scrollView.normalZoomScale animated:true]; + }; + [_paintingWrapperView addSubview:_drawingView]; + } + + _entitiesView.hasSelectionChanged = ^(bool hasSelection) { + __strong TGPhotoDrawingController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf->_scrollView.pinchGestureRecognizer.enabled = !hasSelection; + }; + + [self.view setNeedsLayout]; +} + +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + + [self transitionIn]; +} + +- (void)containerPressed { + [_entitiesView clearSelection]; +} + +- (void)handlePinch:(UIPinchGestureRecognizer *)gestureRecognizer +{ + [_entitiesView handlePinch:gestureRecognizer]; +} + +- (void)handleRotate:(UIRotationGestureRecognizer *)gestureRecognizer +{ + [_entitiesView handleRotate:gestureRecognizer]; +} + +- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)__unused gestureRecognizer +{ + if (gestureRecognizer == _entityPinchGestureRecognizer && !_entitiesView.hasSelection) { + return false; + } + return !_drawingView.isTracking; +} + +- (BOOL)gestureRecognizer:(UIGestureRecognizer *)__unused gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)__unused otherGestureRecognizer +{ + return true; +} + +#pragma mark - Tab Bar + +- (TGPhotoEditorTab)availableTabs +{ + return 0; +} + +- (TGPhotoEditorTab)activeTab +{ + return 0; +} + +#pragma mark - Undo & Redo + + +- (TGPaintingData *)_prepareResultData +{ + TGPaintingData *resultData = _resultData; + if (_resultData == nil) { + resultData = [_interfaceController generateResultData]; + _resultData = resultData; + } + return resultData; +} + +- (UIImage *)image +{ + TGPaintingData *paintingData = [self _prepareResultData]; + return paintingData.image; +} + +- (TGPaintingData *)paintingData +{ + return [self _prepareResultData]; +} + +#pragma mark - Scroll View + +- (CGSize)fittedContentSize +{ + return [TGPhotoPaintController fittedContentSize:_photoEditor.cropRect orientation:_photoEditor.cropOrientation originalSize:_photoEditor.originalSize]; +} + ++ (CGSize)fittedContentSize:(CGRect)cropRect orientation:(UIImageOrientation)orientation originalSize:(CGSize)originalSize { + CGSize fittedOriginalSize = TGScaleToSize(originalSize, [TGPhotoPaintController maximumPaintingSize]); + CGFloat scale = fittedOriginalSize.width / originalSize.width; + + CGSize size = CGSizeMake(cropRect.size.width * scale, cropRect.size.height * scale); + if (orientation == UIImageOrientationLeft || orientation == UIImageOrientationRight) + size = CGSizeMake(size.height, size.width); + + return CGSizeMake(floor(size.width), floor(size.height)); +} + +- (CGRect)fittedCropRect:(bool)originalSize +{ + return [TGPhotoPaintController fittedCropRect:_photoEditor.cropRect originalSize:_photoEditor.originalSize keepOriginalSize:originalSize]; +} + ++ (CGRect)fittedCropRect:(CGRect)cropRect originalSize:(CGSize)originalSize keepOriginalSize:(bool)keepOriginalSize { + CGSize fittedOriginalSize = TGScaleToSize(originalSize, [TGPhotoPaintController maximumPaintingSize]); + CGFloat scale = fittedOriginalSize.width / originalSize.width; + + CGSize size = fittedOriginalSize; + if (!keepOriginalSize) + size = CGSizeMake(cropRect.size.width * scale, cropRect.size.height * scale); + + return CGRectMake(-cropRect.origin.x * scale, -cropRect.origin.y * scale, size.width, size.height); +} + +- (CGPoint)fittedCropCenterScale:(CGFloat)scale +{ + return [TGPhotoPaintController fittedCropRect:_photoEditor.cropRect centerScale:scale]; +} + ++ (CGPoint)fittedCropRect:(CGRect)cropRect centerScale:(CGFloat)scale +{ + CGSize size = CGSizeMake(cropRect.size.width * scale, cropRect.size.height * scale); + CGRect rect = CGRectMake(cropRect.origin.x * scale, cropRect.origin.y * scale, size.width, size.height); + + return CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect)); +} + +- (void)resetScrollView +{ + CGSize fittedContentSize = [self fittedContentSize]; + CGRect fittedCropRect = [self fittedCropRect:false]; + _entitiesWrapperView.frame = CGRectMake(0.0f, 0.0f, fittedContentSize.width, fittedContentSize.height); + + CGFloat scale = _entitiesOutsideContainerView.bounds.size.width / fittedCropRect.size.width; + _entitiesWrapperView.transform = CGAffineTransformMakeScale(scale, scale); + _entitiesWrapperView.frame = CGRectMake(0.0f, 0.0f, _entitiesOutsideContainerView.bounds.size.width, _entitiesOutsideContainerView.bounds.size.height); + + CGSize contentSize = [self contentSize]; + _scrollView.minimumZoomScale = 1.0f; + _scrollView.maximumZoomScale = 1.0f; + _scrollView.normalZoomScale = 1.0f; + _scrollView.zoomScale = 1.0f; + _scrollView.contentSize = contentSize; + [self contentView].frame = CGRectMake(0.0f, 0.0f, contentSize.width, contentSize.height); + + [self adjustZoom]; + _scrollView.zoomScale = _scrollView.normalZoomScale; +} + +- (void)scrollViewWillBeginZooming:(UIScrollView *)__unused scrollView withView:(UIView *)__unused view +{ +} + +- (void)scrollViewDidZoom:(UIScrollView *)__unused scrollView +{ + [self adjustZoom]; + [_entitiesView onZoom]; +} + +- (void)scrollViewDidEndZooming:(UIScrollView *)__unused scrollView withView:(UIView *)__unused view atScale:(CGFloat)__unused scale +{ + [self adjustZoom]; + + if (_scrollView.zoomScale < _scrollView.normalZoomScale - FLT_EPSILON) + { + [TGHacks setAnimationDurationFactor:0.5f]; + [_scrollView setZoomScale:_scrollView.normalZoomScale animated:true]; + [TGHacks setAnimationDurationFactor:1.0f]; + } +} + +- (UIView *)contentView +{ + return _scrollContentView; +} + +- (CGSize)contentSize +{ + return _scrollView.frame.size; +} + +- (UIView *)viewForZoomingInScrollView:(UIScrollView *)__unused scrollView +{ + return [self contentView]; +} + +- (void)adjustZoom +{ + CGSize contentSize = [self contentSize]; + CGSize boundsSize = _scrollView.frame.size; + if (contentSize.width < FLT_EPSILON || contentSize.height < FLT_EPSILON || boundsSize.width < FLT_EPSILON || boundsSize.height < FLT_EPSILON) + return; + + CGFloat scaleWidth = boundsSize.width / contentSize.width; + CGFloat scaleHeight = boundsSize.height / contentSize.height; + CGFloat minScale = MIN(scaleWidth, scaleHeight); + CGFloat maxScale = MAX(scaleWidth, scaleHeight); + maxScale = MAX(maxScale, minScale * 3.0f); + + if (ABS(maxScale - minScale) < 0.01f) + maxScale = minScale; + + _scrollView.contentInset = UIEdgeInsetsZero; + + if (_scrollView.minimumZoomScale != 0.05f) + _scrollView.minimumZoomScale = 0.05f; + if (_scrollView.normalZoomScale != minScale) + _scrollView.normalZoomScale = minScale; + if (_scrollView.maximumZoomScale != maxScale) + _scrollView.maximumZoomScale = maxScale; + + CGRect contentFrame = [self contentView].frame; + + if (boundsSize.width > contentFrame.size.width) + contentFrame.origin.x = (boundsSize.width - contentFrame.size.width) / 2.0f; + else + contentFrame.origin.x = 0; + + if (boundsSize.height > contentFrame.size.height) + contentFrame.origin.y = (boundsSize.height - contentFrame.size.height) / 2.0f; + else + contentFrame.origin.y = 0; + + [self contentView].frame = contentFrame; + + _scrollView.scrollEnabled = ABS(_scrollView.zoomScale - _scrollView.normalZoomScale) > FLT_EPSILON; + + [_drawingView updateZoomScale:_scrollView.zoomScale]; +} + +#pragma mark - Transitions + +- (void)transitionIn +{ +// _portraitSettingsView.layer.shouldRasterize = true; +// _landscapeSettingsView.layer.shouldRasterize = true; +// +// [UIView animateWithDuration:0.3f animations:^ +// { +// _portraitToolsWrapperView.alpha = 1.0f; +// _landscapeToolsWrapperView.alpha = 1.0f; +// +// _portraitActionsView.alpha = 1.0f; +// _landscapeActionsView.alpha = 1.0f; +// } completion:^(__unused BOOL finished) +// { +// _portraitSettingsView.layer.shouldRasterize = false; +// _landscapeSettingsView.layer.shouldRasterize = false; +// }]; + + if (self.presentedForAvatarCreation) { + _drawingView.hidden = true; + } +} + ++ (CGRect)photoContainerFrameForParentViewFrame:(CGRect)parentViewFrame toolbarLandscapeSize:(CGFloat)toolbarLandscapeSize orientation:(UIInterfaceOrientation)orientation panelSize:(CGFloat)panelSize hasOnScreenNavigation:(bool)hasOnScreenNavigation +{ + CGRect frame = [TGPhotoEditorTabController photoContainerFrameForParentViewFrame:parentViewFrame toolbarLandscapeSize:toolbarLandscapeSize orientation:orientation panelSize:panelSize hasOnScreenNavigation:hasOnScreenNavigation]; + + switch (orientation) + { + case UIInterfaceOrientationLandscapeLeft: + frame.origin.x -= TGPhotoPaintTopPanelSize; + break; + + case UIInterfaceOrientationLandscapeRight: + frame.origin.x += TGPhotoPaintTopPanelSize; + break; + + default: + frame.origin.y += TGPhotoPaintTopPanelSize; + break; + } + + return frame; +} + +- (CGRect)_targetFrameForTransitionInFromFrame:(CGRect)fromFrame +{ + CGSize referenceSize = [self referenceViewSize]; + CGRect containerFrame = [TGPhotoPaintController photoContainerFrameForParentViewFrame:CGRectMake(0, 0, referenceSize.width, referenceSize.height) toolbarLandscapeSize:self.toolbarLandscapeSize orientation:self.effectiveOrientation panelSize:TGPhotoPaintTopPanelSize + TGPhotoPaintBottomPanelSize hasOnScreenNavigation:self.hasOnScreenNavigation]; + + CGSize fittedSize = TGScaleToSize(fromFrame.size, containerFrame.size); + CGRect toFrame = CGRectMake(containerFrame.origin.x + (containerFrame.size.width - fittedSize.width) / 2, containerFrame.origin.y + (containerFrame.size.height - fittedSize.height) / 2, fittedSize.width, fittedSize.height); + + return toFrame; +} + +- (void)_finishedTransitionInWithView:(UIView *)transitionView +{ + if ([transitionView isKindOfClass:[TGPhotoEditorPreviewView class]]) { + + } else { + [transitionView removeFromSuperview]; + } + + [self setupCanvas]; + _entitiesView.hidden = false; + + TGPhotoEditorPreviewView *previewView = _previewView; + [previewView setPaintingHidden:true]; + previewView.hidden = false; + [_scrollContainerView insertSubview:previewView belowSubview:_paintingWrapperView]; + [self updateContentViewLayout]; + [previewView performTransitionInIfNeeded]; + + CGRect rect = [self fittedCropRect:true]; + _entitiesView.frame = CGRectMake(0, 0, rect.size.width, rect.size.height); + _entitiesView.transform = CGAffineTransformMakeRotation(_photoEditor.cropRotation); + + CGSize fittedOriginalSize = TGScaleToSize(_photoEditor.originalSize, [TGPhotoPaintController maximumPaintingSize]); + CGSize rotatedSize = TGRotatedContentSize(fittedOriginalSize, _photoEditor.cropRotation); + CGPoint centerPoint = CGPointMake(rotatedSize.width / 2.0f, rotatedSize.height / 2.0f); + + CGFloat scale = fittedOriginalSize.width / _photoEditor.originalSize.width; + CGPoint offset = TGPaintSubtractPoints(centerPoint, [self fittedCropCenterScale:scale]); + + CGPoint boundsCenter = TGPaintCenterOfRect(_entitiesWrapperView.bounds); + _entitiesView.center = TGPaintAddPoints(boundsCenter, offset); + + if (!_skipEntitiesSetup || _entitiesReady) { + [_entitiesWrapperView addSubview:_entitiesView]; + } + _entitiesReady = true; + [self resetScrollView]; +} + +- (void)prepareForCustomTransitionOut +{ + _previewView.hidden = true; + _drawingView.hidden = true; + _entitiesOutsideContainerView.hidden = true; +} + +- (void)transitionOutSwitching:(bool)__unused switching completion:(void (^)(void))completion +{ + TGPhotoEditorPreviewView *previewView = self.previewView; + previewView.interactionEnded = nil; + + [_interfaceController animateOut:^{ + completion(); + }]; +} + +- (CGRect)transitionOutSourceFrameForReferenceFrame:(CGRect)referenceFrame orientation:(UIInterfaceOrientation)orientation +{ + CGRect containerFrame = [TGPhotoPaintController photoContainerFrameForParentViewFrame:self.view.frame toolbarLandscapeSize:self.toolbarLandscapeSize orientation:orientation panelSize:TGPhotoPaintTopPanelSize + TGPhotoPaintBottomPanelSize hasOnScreenNavigation:self.hasOnScreenNavigation]; + + CGSize fittedSize = TGScaleToSize(referenceFrame.size, containerFrame.size); + return CGRectMake(containerFrame.origin.x + (containerFrame.size.width - fittedSize.width) / 2, containerFrame.origin.y + (containerFrame.size.height - fittedSize.height) / 2, fittedSize.width, fittedSize.height); +} + +- (void)_animatePreviewViewTransitionOutToFrame:(CGRect)targetFrame saving:(bool)saving parentView:(UIView *)parentView completion:(void (^)(void))completion +{ + _dismissing = true; + +// [_entitySelectionView removeFromSuperview]; +// _entitySelectionView = nil; + + TGPhotoEditorPreviewView *previewView = self.previewView; + [previewView prepareForTransitionOut]; + + UIInterfaceOrientation orientation = self.effectiveOrientation; + CGRect containerFrame = [TGPhotoPaintController photoContainerFrameForParentViewFrame:self.view.frame toolbarLandscapeSize:self.toolbarLandscapeSize orientation:orientation panelSize:TGPhotoPaintTopPanelSize + TGPhotoPaintBottomPanelSize hasOnScreenNavigation:self.hasOnScreenNavigation]; + CGRect referenceFrame = CGRectMake(0, 0, self.photoEditor.rotatedCropSize.width, self.photoEditor.rotatedCropSize.height); + CGRect rect = CGRectOffset([self transitionOutSourceFrameForReferenceFrame:referenceFrame orientation:orientation], -containerFrame.origin.x, -containerFrame.origin.y); + previewView.frame = rect; + + UIView *snapshotView = nil; + POPSpringAnimation *snapshotAnimation = nil; + NSMutableArray *animations = [[NSMutableArray alloc] init]; + + if (saving && CGRectIsNull(targetFrame) && parentView != nil) + { + snapshotView = [previewView snapshotViewAfterScreenUpdates:false]; + snapshotView.frame = [_scrollContainerView convertRect:previewView.frame toView:parentView]; + + UIView *canvasSnapshotView = [_paintingWrapperView resizableSnapshotViewFromRect:[_paintingWrapperView convertRect:previewView.bounds fromView:previewView] afterScreenUpdates:false withCapInsets:UIEdgeInsetsZero]; + canvasSnapshotView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + canvasSnapshotView.transform = _entitiesOutsideContainerView.transform; + canvasSnapshotView.frame = snapshotView.bounds; + [snapshotView addSubview:canvasSnapshotView]; + + UIView *entitiesSnapshotView = [_entitiesWrapperView resizableSnapshotViewFromRect:[_entitiesWrapperView convertRect:previewView.bounds fromView:previewView] afterScreenUpdates:false withCapInsets:UIEdgeInsetsZero]; + entitiesSnapshotView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + entitiesSnapshotView.transform = _entitiesOutsideContainerView.transform; + entitiesSnapshotView.frame = snapshotView.bounds; + [snapshotView addSubview:entitiesSnapshotView]; + + CGSize fittedSize = TGScaleToSize(previewView.frame.size, self.view.frame.size); + targetFrame = CGRectMake((self.view.frame.size.width - fittedSize.width) / 2, (self.view.frame.size.height - fittedSize.height) / 2, fittedSize.width, fittedSize.height); + + [parentView addSubview:snapshotView]; + + snapshotAnimation = [TGPhotoEditorAnimation prepareTransitionAnimationForPropertyNamed:kPOPViewFrame]; + snapshotAnimation.fromValue = [NSValue valueWithCGRect:snapshotView.frame]; + snapshotAnimation.toValue = [NSValue valueWithCGRect:targetFrame]; + [animations addObject:snapshotAnimation]; + } + + targetFrame = CGRectOffset(targetFrame, -containerFrame.origin.x, -containerFrame.origin.y); + CGPoint targetCenter = TGPaintCenterOfRect(targetFrame); + + POPSpringAnimation *previewAnimation = [TGPhotoEditorAnimation prepareTransitionAnimationForPropertyNamed:kPOPViewFrame]; + previewAnimation.fromValue = [NSValue valueWithCGRect:previewView.frame]; + previewAnimation.toValue = [NSValue valueWithCGRect:targetFrame]; + [animations addObject:previewAnimation]; + + POPSpringAnimation *previewAlphaAnimation = [TGPhotoEditorAnimation prepareTransitionAnimationForPropertyNamed:kPOPViewAlpha]; + previewAlphaAnimation.fromValue = @(previewView.alpha); + previewAlphaAnimation.toValue = @(0.0f); + [animations addObject:previewAnimation]; + + POPSpringAnimation *entitiesAnimation = [TGPhotoEditorAnimation prepareTransitionAnimationForPropertyNamed:kPOPViewCenter]; + entitiesAnimation.fromValue = [NSValue valueWithCGPoint:_entitiesOutsideContainerView.center]; + entitiesAnimation.toValue = [NSValue valueWithCGPoint:targetCenter]; + [animations addObject:entitiesAnimation]; + + CGFloat targetEntitiesScale = targetFrame.size.width / _entitiesOutsideContainerView.frame.size.width; + POPSpringAnimation *entitiesScaleAnimation = [TGPhotoEditorAnimation prepareTransitionAnimationForPropertyNamed:kPOPViewScaleXY]; + entitiesScaleAnimation.fromValue = [NSValue valueWithCGSize:CGSizeMake(1.0f, 1.0f)]; + entitiesScaleAnimation.toValue = [NSValue valueWithCGSize:CGSizeMake(targetEntitiesScale, targetEntitiesScale)]; + [animations addObject:entitiesScaleAnimation]; + + POPSpringAnimation *entitiesAlphaAnimation = [TGPhotoEditorAnimation prepareTransitionAnimationForPropertyNamed:kPOPViewAlpha]; + entitiesAlphaAnimation.fromValue = @(_drawingView.alpha); + entitiesAlphaAnimation.toValue = @(0.0f); + [animations addObject:entitiesAlphaAnimation]; + + POPSpringAnimation *paintingAnimation = [TGPhotoEditorAnimation prepareTransitionAnimationForPropertyNamed:kPOPViewCenter]; + paintingAnimation.fromValue = [NSValue valueWithCGPoint:_paintingWrapperView.center]; + paintingAnimation.toValue = [NSValue valueWithCGPoint:targetCenter]; + [animations addObject:paintingAnimation]; + + CGFloat targetPaintingScale = targetFrame.size.width / _paintingWrapperView.frame.size.width; + POPSpringAnimation *paintingScaleAnimation = [TGPhotoEditorAnimation prepareTransitionAnimationForPropertyNamed:kPOPViewScaleXY]; + paintingScaleAnimation.fromValue = [NSValue valueWithCGSize:CGSizeMake(1.0f, 1.0f)]; + paintingScaleAnimation.toValue = [NSValue valueWithCGSize:CGSizeMake(targetPaintingScale, targetPaintingScale)]; + [animations addObject:paintingScaleAnimation]; + + POPSpringAnimation *paintingAlphaAnimation = [TGPhotoEditorAnimation prepareTransitionAnimationForPropertyNamed:kPOPViewAlpha]; + paintingAlphaAnimation.fromValue = @(_paintingWrapperView.alpha); + paintingAlphaAnimation.toValue = @(0.0f); + [animations addObject:paintingAlphaAnimation]; + + [TGPhotoEditorAnimation performBlock:^(__unused bool allFinished) + { + [snapshotView removeFromSuperview]; + + if (completion != nil) + completion(); + } whenCompletedAllAnimations:animations]; + + if (snapshotAnimation != nil) + [snapshotView pop_addAnimation:snapshotAnimation forKey:@"frame"]; + [previewView pop_addAnimation:previewAnimation forKey:@"frame"]; + [previewView pop_addAnimation:previewAlphaAnimation forKey:@"alpha"]; + + [_entitiesOutsideContainerView pop_addAnimation:entitiesAnimation forKey:@"frame"]; + [_entitiesOutsideContainerView pop_addAnimation:entitiesScaleAnimation forKey:@"scale"]; + [_entitiesOutsideContainerView pop_addAnimation:entitiesAlphaAnimation forKey:@"alpha"]; + + [_paintingWrapperView pop_addAnimation:paintingAnimation forKey:@"frame"]; + [_paintingWrapperView pop_addAnimation:paintingScaleAnimation forKey:@"scale"]; + [_paintingWrapperView pop_addAnimation:paintingAlphaAnimation forKey:@"alpha"]; + + if (saving) + { + _entitiesOutsideContainerView.hidden = true; + _paintingWrapperView.hidden = true; + previewView.hidden = true; + } +} + +- (CGRect)transitionOutReferenceFrame +{ + TGPhotoEditorPreviewView *previewView = _previewView; + return [previewView convertRect:previewView.bounds toView:self.view]; +} + +- (UIView *)transitionOutReferenceView +{ + return _previewView; +} + +- (UIView *)snapshotView +{ + TGPhotoEditorPreviewView *previewView = self.previewView; + return [previewView originalSnapshotView]; +} + +- (id)currentResultRepresentation +{ + return TGPaintCombineCroppedImages(self.photoEditor.currentResultImage, [self image], true, _photoEditor.originalSize, _photoEditor.cropRect, _photoEditor.cropOrientation, _photoEditor.cropRotation, false); +} + +#pragma mark - Layout + +- (void)viewWillLayoutSubviews +{ + [super viewWillLayoutSubviews]; + + [self updateLayout:[[LegacyComponentsGlobals provider] applicationStatusBarOrientation]]; +} + +- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration +{ + [super willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration]; + + [self updateLayout:toInterfaceOrientation]; +} + +- (void)updateContentViewLayout +{ + CGAffineTransform rotationTransform = CGAffineTransformMakeRotation(TGRotationForOrientation(_photoEditor.cropOrientation)); + _entitiesOutsideContainerView.transform = rotationTransform; + _entitiesOutsideContainerView.frame = self.previewView.frame; + [self resetScrollView]; +} + +- (void)keyboardWillChangeFrame:(NSNotification *)notification +{ + UIView *parentView = self.view; + + NSTimeInterval duration = notification.userInfo[UIKeyboardAnimationDurationUserInfoKey] == nil ? 0.3 : [notification.userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue]; + int curve = [notification.userInfo[UIKeyboardAnimationCurveUserInfoKey] intValue]; + CGRect screenKeyboardFrame = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; + CGRect keyboardFrame = [parentView convertRect:screenKeyboardFrame fromView:nil]; + + CGFloat keyboardHeight = (keyboardFrame.size.height <= FLT_EPSILON || keyboardFrame.size.width <= FLT_EPSILON) ? 0.0f : (parentView.frame.size.height - keyboardFrame.origin.y); + keyboardHeight = MAX(keyboardHeight, 0.0f); + + _keyboardHeight = keyboardHeight; + + CGSize referenceSize = [self referenceViewSize]; + CGFloat screenSide = MAX(referenceSize.width, referenceSize.height) + 2 * TGPhotoPaintBottomPanelSize; + + CGRect containerFrame = [TGPhotoPaintController photoContainerFrameForParentViewFrame:CGRectMake(0, 0, referenceSize.width, referenceSize.height) toolbarLandscapeSize:self.toolbarLandscapeSize orientation:self.effectiveOrientation panelSize:TGPhotoPaintTopPanelSize + TGPhotoPaintBottomPanelSize hasOnScreenNavigation:self.hasOnScreenNavigation]; + + CGFloat topInset = [self controllerStatusBarHeight] + 31.0; + CGFloat visibleArea = self.view.frame.size.height - _keyboardHeight - topInset; + CGFloat yCenter = visibleArea / 2.0f; + CGFloat offset = yCenter - _previewView.center.y - containerFrame.origin.y + topInset; + CGFloat offsetHeight = _keyboardHeight > FLT_EPSILON ? offset : 0.0f; + + [UIView animateWithDuration:duration delay:0.0 options:curve animations:^ + { + _interfaceWrapperView.frame = CGRectMake((referenceSize.width - screenSide) / 2, (referenceSize.height - screenSide) / 2, _interfaceWrapperView.frame.size.width, _interfaceWrapperView.frame.size.height); + _scrollContainerView.frame = CGRectMake(containerFrame.origin.x, containerFrame.origin.y + offsetHeight, containerFrame.size.width, containerFrame.size.height); + } completion:nil]; + + [self updateInterfaceLayoutAnimated:true]; +} + +- (void)updateInterfaceLayoutAnimated:(BOOL)animated { + if (_interfaceController == nil) + return; + + CGSize size = [self referenceViewSize]; + _interfaceController.view.frame = CGRectMake((_interfaceWrapperView.frame.size.width - size.width) / 2.0, (_interfaceWrapperView.frame.size.height - size.height) / 2.0, size.width, size.height); + [_interfaceController adapterContainerLayoutUpdatedSize:[self referenceViewSize] + intrinsicInsets:_context.safeAreaInset + safeInsets:UIEdgeInsetsMake(0.0, _context.safeAreaInset.left, 0.0, _context.safeAreaInset.right) + statusBarHeight:[_context statusBarFrame].size.height + inputHeight:_keyboardHeight + animated:animated]; + +} + +- (void)updateLayout:(UIInterfaceOrientation)orientation +{ + if ([self inFormSheet] || [UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) + { + orientation = UIInterfaceOrientationPortrait; + } + + CGSize referenceSize = [self referenceViewSize]; + CGFloat screenSide = MAX(referenceSize.width, referenceSize.height) + 2 * TGPhotoPaintBottomPanelSize; + + bool sizeUpdated = false; + if (!CGSizeEqualToSize(referenceSize, _previousSize)) { + sizeUpdated = true; + _previousSize = referenceSize; + } + + UIEdgeInsets safeAreaInset = [TGViewController safeAreaInsetForOrientation:orientation hasOnScreenNavigation:self.hasOnScreenNavigation]; + UIEdgeInsets screenEdges = UIEdgeInsetsMake((screenSide - referenceSize.height) / 2, (screenSide - referenceSize.width) / 2, (screenSide + referenceSize.height) / 2, (screenSide + referenceSize.width) / 2); + screenEdges.top += safeAreaInset.top; + screenEdges.left += safeAreaInset.left; + screenEdges.bottom -= safeAreaInset.bottom; + screenEdges.right -= safeAreaInset.right; + + CGRect containerFrame = [TGPhotoPaintController photoContainerFrameForParentViewFrame:CGRectMake(0, 0, referenceSize.width, referenceSize.height) toolbarLandscapeSize:self.toolbarLandscapeSize orientation:orientation panelSize:TGPhotoPaintTopPanelSize + TGPhotoPaintBottomPanelSize hasOnScreenNavigation:self.hasOnScreenNavigation]; + + PGPhotoEditor *photoEditor = self.photoEditor; + TGPhotoEditorPreviewView *previewView = self.previewView; + + CGSize fittedSize = TGScaleToSize(photoEditor.rotatedCropSize, containerFrame.size); + CGRect previewFrame = CGRectMake((containerFrame.size.width - fittedSize.width) / 2, (containerFrame.size.height - fittedSize.height) / 2, fittedSize.width, fittedSize.height); + + CGFloat topInset = [self controllerStatusBarHeight] + 31.0; + CGFloat visibleArea = self.view.frame.size.height - _keyboardHeight - topInset; + CGFloat yCenter = visibleArea / 2.0f; + CGFloat offset = yCenter - _previewView.center.y - containerFrame.origin.y + topInset; + CGFloat offsetHeight = _keyboardHeight > FLT_EPSILON ? offset : 0.0f; + + _interfaceWrapperView.frame = CGRectMake((referenceSize.width - screenSide) / 2, (referenceSize.height - screenSide) / 2, screenSide, screenSide); + [self updateInterfaceLayoutAnimated:false]; + + if (_dismissing || (previewView.superview != _scrollContainerView && previewView.superview != self.view)) + return; + + if (previewView.superview == self.view) + { + previewFrame = CGRectMake(containerFrame.origin.x + (containerFrame.size.width - fittedSize.width) / 2, containerFrame.origin.y + (containerFrame.size.height - fittedSize.height) / 2, fittedSize.width, fittedSize.height); + } + + UIImageOrientation cropOrientation = _photoEditor.cropOrientation; + CGRect cropRect = _photoEditor.cropRect; + CGSize originalSize = _photoEditor.originalSize; + CGFloat rotation = _photoEditor.cropRotation; + + CGAffineTransform rotationTransform = CGAffineTransformMakeRotation(TGRotationForOrientation(cropOrientation)); + _entitiesOutsideContainerView.transform = rotationTransform; + _entitiesOutsideContainerView.frame = previewFrame; + + _scrollView.frame = self.view.bounds; + + if (sizeUpdated) { + [self resetScrollView]; + } + [self adjustZoom]; + + _paintingWrapperView.transform = CGAffineTransformMakeRotation(TGRotationForOrientation(cropOrientation)); + _paintingWrapperView.frame = previewFrame; + + CGFloat originalWidth = TGOrientationIsSideward(cropOrientation, NULL) ? previewFrame.size.height : previewFrame.size.width; + CGFloat ratio = originalWidth / cropRect.size.width; + CGRect originalFrame = CGRectMake(-cropRect.origin.x * ratio, -cropRect.origin.y * ratio, originalSize.width * ratio, originalSize.height * ratio); + + previewView.frame = previewFrame; + + if ([self presentedForAvatarCreation]) { + CGAffineTransform transform = CGAffineTransformMakeRotation(TGRotationForOrientation(photoEditor.cropOrientation)); + if (photoEditor.cropMirrored) + transform = CGAffineTransformScale(transform, -1.0f, 1.0f); + previewView.transform = transform; + } + + CGSize fittedOriginalSize = CGSizeMake(originalSize.width * ratio, originalSize.height * ratio); + CGSize rotatedSize = TGRotatedContentSize(fittedOriginalSize, rotation); + CGPoint centerPoint = CGPointMake(rotatedSize.width / 2.0f, rotatedSize.height / 2.0f); + + CGFloat scale = fittedOriginalSize.width / _photoEditor.originalSize.width; + CGPoint centerOffset = TGPaintSubtractPoints(centerPoint, [self fittedCropCenterScale:scale]); + + _drawingView.transform = CGAffineTransformIdentity; + _drawingView.frame = originalFrame; + _drawingView.transform = CGAffineTransformMakeRotation(rotation); + _drawingView.center = TGPaintAddPoints(TGPaintCenterOfRect(_paintingWrapperView.bounds), centerOffset); + + _selectionContainerView.transform = CGAffineTransformRotate(rotationTransform, rotation); + _selectionContainerView.frame = previewFrame; + + _scrollContainerView.frame = CGRectMake(containerFrame.origin.x, containerFrame.origin.y + offsetHeight, containerFrame.size.width, containerFrame.size.height); +} + +- (UIRectEdge)preferredScreenEdgesDeferringSystemGestures +{ + return UIRectEdgeTop | UIRectEdgeBottom; +} + +@end diff --git a/submodules/LegacyComponents/Sources/TGPhotoEditorController.m b/submodules/LegacyComponents/Sources/TGPhotoEditorController.m index 2293be6c68..831cd61133 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoEditorController.m +++ b/submodules/LegacyComponents/Sources/TGPhotoEditorController.m @@ -39,6 +39,7 @@ #import "TGPhotoCropController.h" #import "TGPhotoToolsController.h" #import "TGPhotoPaintController.h" +#import "TGPhotoDrawingController.h" #import "TGPhotoQualityController.h" #import "TGPhotoAvatarPreviewController.h" @@ -710,7 +711,7 @@ startPosition = self.trimStartValue; CMTime targetTime = CMTimeMakeWithSeconds(startPosition, NSEC_PER_SEC); - [_player.currentItem seekToTime:targetTime toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero]; + [_player.currentItem seekToTime:targetTime toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero completionHandler:nil]; [self _setupPlaybackStartedObserver]; @@ -1232,7 +1233,7 @@ [self savePaintingData]; bool resetTransform = false; - if ([self presentedForAvatarCreation] && tab == TGPhotoEditorCropTab && [currentController isKindOfClass:[TGPhotoPaintController class]]) { + if ([self presentedForAvatarCreation] && tab == TGPhotoEditorCropTab && [currentController isKindOfClass:[TGPhotoDrawingController class]]) { resetTransform = true; } @@ -1560,10 +1561,26 @@ case TGPhotoEditorPaintTab: { - TGPhotoPaintController *paintController = [[TGPhotoPaintController alloc] initWithContext:_context photoEditor:_photoEditor previewView:_previewView entitiesView:_fullEntitiesView]; - paintController.stickersContext = _stickersContext; - paintController.toolbarLandscapeSize = TGPhotoEditorToolbarSize; - paintController.controlVideoPlayback = ^(bool play) { + [_portraitToolbarView setAllButtonsHidden:true animated:false]; + [_landscapeToolbarView setAllButtonsHidden:true animated:false]; + + [_containerView.superview bringSubviewToFront:_containerView]; + + TGPhotoDrawingController *drawingController = [[TGPhotoDrawingController alloc] initWithContext:_context photoEditor:_photoEditor previewView:_previewView entitiesView:_fullEntitiesView stickersContext:_stickersContext]; + drawingController.requestDismiss = ^{ + __strong TGPhotoEditorController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + [strongSelf dismissEditor]; + }; + drawingController.requestApply = ^{ + __strong TGPhotoEditorController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + [strongSelf applyEditor]; + }; + drawingController.toolbarLandscapeSize = TGPhotoEditorToolbarSize; + drawingController.controlVideoPlayback = ^(bool play) { __strong TGPhotoEditorController *strongSelf = weakSelf; if (strongSelf == nil) return; @@ -1573,7 +1590,7 @@ [strongSelf stopVideoPlayback:false]; } }; - paintController.beginTransitionIn = ^UIView *(CGRect *referenceFrame, UIView **parentView, bool *noTransitionView) + drawingController.beginTransitionIn = ^UIView *(CGRect *referenceFrame, UIView **parentView, bool *noTransitionView) { __strong TGPhotoEditorController *strongSelf = weakSelf; if (strongSelf == nil) @@ -1585,7 +1602,7 @@ return transitionReferenceView; }; - paintController.finishedTransitionIn = ^ + drawingController.finishedTransitionIn = ^ { __strong TGPhotoEditorController *strongSelf = weakSelf; if (strongSelf == nil) @@ -1599,8 +1616,17 @@ if (isInitialAppearance) [strongSelf startVideoPlayback:true]; }; + drawingController.finishedTransitionOut = ^{ + __strong TGPhotoEditorController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf->_containerView.superview insertSubview:strongSelf->_containerView atIndex:2]; + [strongSelf->_portraitToolbarView setAllButtonsHidden:false animated:true]; + [strongSelf->_landscapeToolbarView setAllButtonsHidden:false animated:true]; + }; - controller = paintController; + controller = drawingController; } break; @@ -1680,7 +1706,7 @@ _currentTabController.switchingFromTab = switchingFromTab; _currentTabController.initialAppearance = isInitialAppearance; - if (![_currentTabController isKindOfClass:[TGPhotoPaintController class]]) + if (![_currentTabController isKindOfClass:[TGPhotoDrawingController class]]) _currentTabController.availableTabs = _availableTabs; if ([self presentedForAvatarCreation] && self.navigationController == nil) @@ -1877,57 +1903,57 @@ }; TGPaintingData *paintingData = nil; - if ([_currentTabController isKindOfClass:[TGPhotoPaintController class]]) - paintingData = [(TGPhotoPaintController *)_currentTabController paintingData]; + if ([_currentTabController isKindOfClass:[TGPhotoDrawingController class]]) + paintingData = [(TGPhotoDrawingController *)_currentTabController paintingData]; PGPhotoEditorValues *editorValues = paintingData == nil ? [_photoEditor exportAdjustments] : [_photoEditor exportAdjustmentsWithPaintingData:paintingData]; - if ((_initialAdjustments == nil && (![editorValues isDefaultValuesForAvatar:[self presentedForAvatarCreation]] || editorValues.cropOrientation != UIImageOrientationUp)) || (_initialAdjustments != nil && ![editorValues isEqual:_initialAdjustments])) - { - TGMenuSheetController *controller = [[TGMenuSheetController alloc] initWithContext:_context dark:false]; - controller.dismissesByOutsideTap = true; - controller.narrowInLandscape = true; - __weak TGMenuSheetController *weakController = controller; - - NSArray *items = @ - [ - [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"PhotoEditor.DiscardChanges") type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^ - { - __strong TGMenuSheetController *strongController = weakController; - if (strongController == nil) - return; - - [strongController dismissAnimated:true manual:false completion:^ - { - dismiss(); - }]; - }], - [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Common.Cancel") type:TGMenuSheetButtonTypeCancel fontSize:20.0 action:^ - { - __strong TGMenuSheetController *strongController = weakController; - if (strongController != nil) - [strongController dismissAnimated:true]; - }] - ]; - - [controller setItemViews:items]; - controller.sourceRect = ^ - { - __strong TGPhotoEditorController *strongSelf = weakSelf; - if (strongSelf == nil) - return CGRectZero; - - if (UIInterfaceOrientationIsPortrait(strongSelf.effectiveOrientation)) - return [strongSelf.view convertRect:strongSelf->_portraitToolbarView.cancelButtonFrame fromView:strongSelf->_portraitToolbarView]; - else - return [strongSelf.view convertRect:strongSelf->_landscapeToolbarView.cancelButtonFrame fromView:strongSelf->_landscapeToolbarView]; - }; - [controller presentInViewController:self sourceView:self.view animated:true]; - } - else - { +// if ((_initialAdjustments == nil && (![editorValues isDefaultValuesForAvatar:[self presentedForAvatarCreation]] || editorValues.cropOrientation != UIImageOrientationUp)) || (_initialAdjustments != nil && ![editorValues isEqual:_initialAdjustments])) +// { +// TGMenuSheetController *controller = [[TGMenuSheetController alloc] initWithContext:_context dark:false]; +// controller.dismissesByOutsideTap = true; +// controller.narrowInLandscape = true; +// __weak TGMenuSheetController *weakController = controller; +// +// NSArray *items = @ +// [ +// [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"PhotoEditor.DiscardChanges") type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^ +// { +// __strong TGMenuSheetController *strongController = weakController; +// if (strongController == nil) +// return; +// +// [strongController dismissAnimated:true manual:false completion:^ +// { +// dismiss(); +// }]; +// }], +// [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Common.Cancel") type:TGMenuSheetButtonTypeCancel fontSize:20.0 action:^ +// { +// __strong TGMenuSheetController *strongController = weakController; +// if (strongController != nil) +// [strongController dismissAnimated:true]; +// }] +// ]; +// +// [controller setItemViews:items]; +// controller.sourceRect = ^ +// { +// __strong TGPhotoEditorController *strongSelf = weakSelf; +// if (strongSelf == nil) +// return CGRectZero; +// +// if (UIInterfaceOrientationIsPortrait(strongSelf.effectiveOrientation)) +// return [strongSelf.view convertRect:strongSelf->_portraitToolbarView.cancelButtonFrame fromView:strongSelf->_portraitToolbarView]; +// else +// return [strongSelf.view convertRect:strongSelf->_landscapeToolbarView.cancelButtonFrame fromView:strongSelf->_landscapeToolbarView]; +// }; +// [controller presentInViewController:self sourceView:self.view animated:true]; +// } +// else +// { dismiss(); - } +// } } - (void)doneButtonPressed @@ -1940,10 +1966,10 @@ } - (void)savePaintingData { - if (![_currentTabController isKindOfClass:[TGPhotoPaintController class]]) + if (![_currentTabController isKindOfClass:[TGPhotoDrawingController class]]) return; - TGPhotoPaintController *paintController = (TGPhotoPaintController *)_currentTabController; + TGPhotoDrawingController *paintController = (TGPhotoDrawingController *)_currentTabController; TGPaintingData *paintingData = [paintController paintingData]; _photoEditor.paintingData = paintingData; @@ -1967,7 +1993,7 @@ NSTimeInterval trimStartValue = 0.0; NSTimeInterval trimEndValue = 0.0; - if ([_currentTabController isKindOfClass:[TGPhotoPaintController class]]) + if ([_currentTabController isKindOfClass:[TGPhotoDrawingController class]]) { [self savePaintingData]; } @@ -2244,8 +2270,8 @@ [progressWindow performSelector:@selector(showAnimated) withObject:nil afterDelay:0.5]; TGPaintingData *paintingData = nil; - if ([_currentTabController isKindOfClass:[TGPhotoPaintController class]]) - paintingData = [(TGPhotoPaintController *)_currentTabController paintingData]; + if ([_currentTabController isKindOfClass:[TGPhotoDrawingController class]]) + paintingData = [(TGPhotoDrawingController *)_currentTabController paintingData]; PGPhotoEditorValues *editorValues = paintingData == nil ? [_photoEditor exportAdjustments] : [_photoEditor exportAdjustmentsWithPaintingData:paintingData]; @@ -2266,8 +2292,8 @@ [progressWindow performSelector:@selector(showAnimated) withObject:nil afterDelay:0.5]; TGPaintingData *paintingData = nil; - if ([_currentTabController isKindOfClass:[TGPhotoPaintController class]]) - paintingData = [(TGPhotoPaintController *)_currentTabController paintingData]; + if ([_currentTabController isKindOfClass:[TGPhotoDrawingController class]]) + paintingData = [(TGPhotoDrawingController *)_currentTabController paintingData]; PGPhotoEditorValues *editorValues = paintingData == nil ? [_photoEditor exportAdjustments] : [_photoEditor exportAdjustmentsWithPaintingData:paintingData]; diff --git a/submodules/LegacyComponents/Sources/TGPhotoPaintController.m b/submodules/LegacyComponents/Sources/TGPhotoPaintController.m index 7d882e38f3..18333b75ee 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoPaintController.m +++ b/submodules/LegacyComponents/Sources/TGPhotoPaintController.m @@ -54,7 +54,7 @@ const CGFloat TGPhotoPaintTopPanelSize = 44.0f; const CGFloat TGPhotoPaintBottomPanelSize = 79.0f; const CGSize TGPhotoPaintingLightMaxSize = { 1280.0f, 1280.0f }; -const CGSize TGPhotoPaintingMaxSize = { 1920.0f, 1920.0f }; +const CGSize TGPhotoPaintingMaxSize = { 2560.0f, 2560.0f }; const CGFloat TGPhotoPaintStickerKeyboardSize = 260.0f; diff --git a/submodules/LegacyComponents/Sources/TGPhotoPaintTextEntity.m b/submodules/LegacyComponents/Sources/TGPhotoPaintTextEntity.m index ead1ac53be..d3463c9ff0 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoPaintTextEntity.m +++ b/submodules/LegacyComponents/Sources/TGPhotoPaintTextEntity.m @@ -50,3 +50,8 @@ } @end + + +@implementation TGPhotoPaintStaticEntity + +@end diff --git a/submodules/LegacyMediaPickerUI/BUILD b/submodules/LegacyMediaPickerUI/BUILD index a534328596..53d93d060d 100644 --- a/submodules/LegacyMediaPickerUI/BUILD +++ b/submodules/LegacyMediaPickerUI/BUILD @@ -28,6 +28,7 @@ swift_library( "//submodules/StickerResources:StickerResources", "//submodules/TextFormat:TextFormat", "//submodules/AttachmentUI:AttachmentUI", + "//submodules/DrawingUI:DrawingUI", ], visibility = [ "//visibility:public", diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift index 638082caa8..560aeed2aa 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift @@ -8,6 +8,7 @@ import AnimatedStickerNode import TelegramAnimatedStickerNode import YuvConversion import StickerResources +import DrawingUI protocol LegacyPaintEntity { var position: CGPoint { get } @@ -472,4 +473,27 @@ public final class LegacyPaintStickersContext: NSObject, TGPhotoPaintStickersCon return nil } } + + class LegacyDrawingAdapter: NSObject, TGPhotoDrawingAdapter { + let drawingView: TGPhotoDrawingView! + let drawingEntitiesView: TGPhotoDrawingEntitiesView! + let selectionContainerView: UIView + let contentWrapperView: UIView! + let interfaceController: TGPhotoDrawingInterfaceController! + + init(context: AccountContext, size: CGSize) { + let interfaceController = DrawingScreen(context: context, size: size) + self.interfaceController = interfaceController + self.drawingView = interfaceController.drawingView + self.drawingEntitiesView = interfaceController.entitiesView + self.selectionContainerView = interfaceController.selectionContainerView + self.contentWrapperView = interfaceController.contentWrapperView + + super.init() + } + } + + public func drawingAdapter(_ size: CGSize) -> TGPhotoDrawingAdapter! { + return LegacyDrawingAdapter(context: self.context, size: size) + } } diff --git a/submodules/PaymentMethodUI/Sources/AddPaymentMethodSheetScreen.swift b/submodules/PaymentMethodUI/Sources/AddPaymentMethodSheetScreen.swift index fa1a4287be..27c1013e2d 100644 --- a/submodules/PaymentMethodUI/Sources/AddPaymentMethodSheetScreen.swift +++ b/submodules/PaymentMethodUI/Sources/AddPaymentMethodSheetScreen.swift @@ -196,7 +196,7 @@ private final class AddPaymentMethodSheetComponent: CombinedComponent { }) } )), - backgroundColor: .white, + backgroundColor: .color(.white), animateOut: animateOut ), environment: { @@ -204,6 +204,7 @@ private final class AddPaymentMethodSheetComponent: CombinedComponent { SheetComponentEnvironment( isDisplaying: environment.value.isVisible, isCentered: false, + hasInputHeight: !environment.inputHeight.isZero, dismiss: { animated in if animated { animateOut.invoke(Action { _ in diff --git a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift index efee1944ae..c1b2de1310 100644 --- a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift @@ -1117,7 +1117,7 @@ private final class DemoSheetComponent: CombinedComponent { }) } )), - backgroundColor: environment.theme.actionSheet.opaqueItemBackgroundColor, + backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), animateOut: animateOut ), environment: { @@ -1125,6 +1125,7 @@ private final class DemoSheetComponent: CombinedComponent { SheetComponentEnvironment( isDisplaying: environment.value.isVisible, isCentered: environment.metrics.widthClass == .regular, + hasInputHeight: !environment.inputHeight.isZero, dismiss: { animated in if animated { animateOut.invoke(Action { _ in diff --git a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift index fadd9ff184..5654bdd90b 100644 --- a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift @@ -1003,7 +1003,7 @@ private final class LimitSheetComponent: CombinedComponent { }) } )), - backgroundColor: environment.theme.actionSheet.opaqueItemBackgroundColor, + backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), animateOut: animateOut ), environment: { @@ -1011,6 +1011,7 @@ private final class LimitSheetComponent: CombinedComponent { SheetComponentEnvironment( isDisplaying: environment.value.isVisible, isCentered: environment.metrics.widthClass == .regular, + hasInputHeight: !environment.inputHeight.isZero, dismiss: { animated in if animated { animateOut.invoke(Action { _ in diff --git a/submodules/SegmentedControlNode/Sources/SegmentedControlNode.swift b/submodules/SegmentedControlNode/Sources/SegmentedControlNode.swift index c5b9d906c6..2cd2bbdf12 100644 --- a/submodules/SegmentedControlNode/Sources/SegmentedControlNode.swift +++ b/submodules/SegmentedControlNode/Sources/SegmentedControlNode.swift @@ -53,8 +53,9 @@ public extension SegmentedControlTheme { } } -private func generateSelectionImage(theme: SegmentedControlTheme) -> UIImage? { - return generateImage(CGSize(width: 20.0, height: 20.0), rotatedContext: { size, context in +private func generateSelectionImage(theme: SegmentedControlTheme, cornerRadius: CGFloat) -> UIImage? { + let cornerRadius = cornerRadius - 1.0 + return generateImage(CGSize(width: 4.0 + cornerRadius * 2.0, height: 4.0 + cornerRadius * 2.0), rotatedContext: { size, context in let bounds = CGRect(origin: CGPoint(), size: size) context.clear(bounds) @@ -62,8 +63,8 @@ private func generateSelectionImage(theme: SegmentedControlTheme) -> UIImage? { context.setShadow(offset: CGSize(width: 0.0, height: -3.0), blur: 6.0, color: theme.shadowColor.withAlphaComponent(0.12).cgColor) } context.setFillColor(theme.foregroundColor.cgColor) - context.fillEllipse(in: CGRect(x: 2.0, y: 2.0, width: 16.0, height: 16.0)) - })?.stretchableImage(withLeftCapWidth: 10, topCapHeight: 10) + context.fillEllipse(in: CGRect(x: 2.0, y: 2.0, width: cornerRadius * 2.0, height: cornerRadius * 2.0)) + })?.stretchableImage(withLeftCapWidth: Int(2 + cornerRadius), topCapHeight: Int(2 + cornerRadius)) } public struct SegmentedControlItem: Equatable { @@ -119,7 +120,12 @@ public final class SegmentedControlNode: ASDisplayNode, UIGestureRecognizerDeleg let dividersCount = self._items.count > 2 ? self._items.count - 1 : 0 if self.dividerNodes.count != dividersCount { self.dividerNodes.forEach { $0.removeFromSupernode() } - self.dividerNodes = (0 ..< dividersCount).map { _ in ASDisplayNode() } + self.dividerNodes = (0 ..< dividersCount).map { _ in + let node = ASDisplayNode() + node.backgroundColor = self.theme.dividerColor + return node + } + self.dividerNodes.forEach(self.addSubnode(_:)) } if let layout = self.validLayout { @@ -164,7 +170,7 @@ public final class SegmentedControlNode: ASDisplayNode, UIGestureRecognizerDeleg f(true) } - public init(theme: SegmentedControlTheme, items: [SegmentedControlItem], selectedIndex: Int) { + public init(theme: SegmentedControlTheme, items: [SegmentedControlItem], selectedIndex: Int, cornerRadius: CGFloat = 9.0) { self.theme = theme self._items = items self._selectedIndex = selectedIndex @@ -196,7 +202,7 @@ public final class SegmentedControlNode: ASDisplayNode, UIGestureRecognizerDeleg super.init() self.clipsToBounds = true - self.cornerRadius = 9.0 + self.cornerRadius = cornerRadius self.addSubnode(self.selectionNode) self.itemNodes.forEach(self.addSubnode(_:)) @@ -204,7 +210,7 @@ public final class SegmentedControlNode: ASDisplayNode, UIGestureRecognizerDeleg self.dividerNodes.forEach(self.addSubnode(_:)) self.backgroundColor = self.theme.backgroundColor - self.selectionNode.image = generateSelectionImage(theme: self.theme) + self.selectionNode.image = generateSelectionImage(theme: self.theme, cornerRadius: cornerRadius) } override public func didLoad() { @@ -286,6 +292,26 @@ public final class SegmentedControlNode: ASDisplayNode, UIGestureRecognizerDeleg } } + public func animateSelection(to point: CGPoint, transition: ContainedViewLayoutTransition) -> CGRect { + self.isUserInteractionEnabled = false + self.alpha = 0.0 + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + + let selectionFrame = self.selectionNode.frame + transition.animateFrame(node: self.selectionNode, from: self.selectionNode.frame, to: CGRect(origin: CGPoint(x: point.x - self.selectionNode.frame.height / 2.0, y: self.selectionNode.frame.minY), size: CGSize(width: self.selectionNode.frame.height, height: self.selectionNode.frame.height))) + return selectionFrame + } + + public func animateSelection(from point: CGPoint, transition: ContainedViewLayoutTransition) -> CGRect { + self.isUserInteractionEnabled = true + self.alpha = 1.0 + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + + let selectionFrame = self.selectionNode.frame + transition.animateFrame(node: self.selectionNode, from: CGRect(origin: CGPoint(x: point.x - self.selectionNode.frame.height / 2.0, y: self.selectionNode.frame.minY), size: CGSize(width: self.selectionNode.frame.height, height: self.selectionNode.frame.height)), to: self.selectionNode.frame) + return selectionFrame + } + public func updateTheme(_ theme: SegmentedControlTheme) { guard theme != self.theme else { return @@ -293,7 +319,7 @@ public final class SegmentedControlNode: ASDisplayNode, UIGestureRecognizerDeleg self.theme = theme self.backgroundColor = self.theme.backgroundColor - self.selectionNode.image = generateSelectionImage(theme: self.theme) + self.selectionNode.image = generateSelectionImage(theme: self.theme, cornerRadius: self.cornerRadius) for itemNode in self.itemNodes { if let title = itemNode.attributedTitle(for: .normal)?.string { diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 87854ad68c..25872973c1 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -304,6 +304,7 @@ swift_library( "//submodules/TelegramUI/Components/NotificationPeerExceptionController", "//submodules/TelegramUI/Components/ChatListHeaderComponent", "//submodules/MediaPasteboardUI:MediaPasteboardUI", + "//submodules/DrawingUI:DrawingUI", ] + select({ "@build_bazel_rules_apple//apple:ios_armv7": [], "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, diff --git a/submodules/TelegramUI/Components/EntityKeyboard/BUILD b/submodules/TelegramUI/Components/EntityKeyboard/BUILD index 5298ee819c..da95fdb49f 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/BUILD +++ b/submodules/TelegramUI/Components/EntityKeyboard/BUILD @@ -40,6 +40,7 @@ swift_library( "//submodules/Components/SolidRoundedButtonComponent:SolidRoundedButtonComponent", "//submodules/Components/LottieAnimationComponent:LottieAnimationComponent", "//submodules/LocalizedPeerData:LocalizedPeerData", + "//submodules/TelegramNotices:TelegramNotices", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index 6f0f559657..ee85986a3a 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -23,6 +23,7 @@ import AudioToolbox import SolidRoundedButtonComponent import EmojiTextAttachmentView import EmojiStatusComponent +import TelegramNotices private let premiumBadgeIcon: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat List/PeerPremiumIcon"), color: .white) private let featuredBadgeIcon: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/PanelBadgeAdd"), color: .white) @@ -7059,6 +7060,469 @@ public final class EmojiPagerContentComponent: Component { } return emojiItems } + + public static func stickerInputData( + context: AccountContext, + animationCache: AnimationCache, + animationRenderer: MultiAnimationRenderer, + stickerNamespaces: [ItemCollectionId.Namespace], + stickerOrderedItemListCollectionIds: [Int32], + chatPeerId: EnginePeer.Id? + ) -> Signal { + let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) + let isPremiumDisabled = premiumConfiguration.isPremiumDisabled + + struct PeerSpecificPackData: Equatable { + var info: StickerPackCollectionInfo + var items: [StickerPackItem] + var peer: EnginePeer + + static func ==(lhs: PeerSpecificPackData, rhs: PeerSpecificPackData) -> Bool { + if lhs.info.id != rhs.info.id { + return false + } + if lhs.items != rhs.items { + return false + } + if lhs.peer != rhs.peer { + return false + } + + return true + } + } + + let peerSpecificPack: Signal + if let chatPeerId = chatPeerId { + peerSpecificPack = combineLatest( + context.engine.peers.peerSpecificStickerPack(peerId: chatPeerId), + context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: chatPeerId)) + ) + |> map { packData, peer -> PeerSpecificPackData? in + guard let peer = peer else { + return nil + } + + guard let (info, items) = packData.packInfo else { + return nil + } + + return PeerSpecificPackData(info: info, items: items.compactMap { $0 as? StickerPackItem }, peer: peer) + } + |> distinctUntilChanged + } else { + peerSpecificPack = .single(nil) + } + + let strings = context.sharedContext.currentPresentationData.with({ $0 }).strings + + return combineLatest( + context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: stickerOrderedItemListCollectionIds, namespaces: stickerNamespaces, aroundIndex: nil, count: 10000000), + hasPremium(context: context, chatPeerId: chatPeerId, premiumIfSavedMessages: false), + context.account.viewTracker.featuredStickerPacks(), + context.engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: Namespaces.CachedItemCollection.featuredStickersConfiguration, id: ValueBoxKey(length: 0))), + ApplicationSpecificNotice.dismissedTrendingStickerPacks(accountManager: context.sharedContext.accountManager), + peerSpecificPack + ) + |> map { view, hasPremium, featuredStickerPacks, featuredStickersConfiguration, dismissedTrendingStickerPacks, peerSpecificPack -> EmojiPagerContentComponent in + struct ItemGroup { + var supergroupId: AnyHashable + var id: AnyHashable + var title: String + var subtitle: String? + var actionButtonTitle: String? + var isPremiumLocked: Bool + var isFeatured: Bool + var displayPremiumBadges: Bool + var headerItem: EntityKeyboardAnimationData? + var items: [EmojiPagerContentComponent.Item] + } + var itemGroups: [ItemGroup] = [] + var itemGroupIndexById: [AnyHashable: Int] = [:] + + var savedStickers: OrderedItemListView? + var recentStickers: OrderedItemListView? + var cloudPremiumStickers: OrderedItemListView? + for orderedView in view.orderedItemListsViews { + if orderedView.collectionId == Namespaces.OrderedItemList.CloudRecentStickers { + recentStickers = orderedView + } else if orderedView.collectionId == Namespaces.OrderedItemList.CloudSavedStickers { + savedStickers = orderedView + } else if orderedView.collectionId == Namespaces.OrderedItemList.CloudAllPremiumStickers { + cloudPremiumStickers = orderedView + } + } + + var installedCollectionIds = Set() + for (id, _, _) in view.collectionInfos { + installedCollectionIds.insert(id) + } + + let dismissedTrendingStickerPacksSet = Set(dismissedTrendingStickerPacks ?? []) + let featuredStickerPacksSet = Set(featuredStickerPacks.map(\.info.id.id)) + + if dismissedTrendingStickerPacksSet != featuredStickerPacksSet { + let featuredStickersConfiguration = featuredStickersConfiguration?.get(FeaturedStickersConfiguration.self) + for featuredStickerPack in featuredStickerPacks { + if installedCollectionIds.contains(featuredStickerPack.info.id) { + continue + } + + guard let item = featuredStickerPack.topItems.first else { + continue + } + + let animationData: EntityKeyboardAnimationData + + if let thumbnail = featuredStickerPack.info.thumbnail { + let type: EntityKeyboardAnimationData.ItemType + if item.file.isAnimatedSticker { + type = .lottie + } else if item.file.isVideoEmoji || item.file.isVideoSticker { + type = .video + } else { + type = .still + } + + animationData = EntityKeyboardAnimationData( + id: .stickerPackThumbnail(featuredStickerPack.info.id), + type: type, + resource: .stickerPackThumbnail(stickerPack: .id(id: featuredStickerPack.info.id.id, accessHash: featuredStickerPack.info.accessHash), resource: thumbnail.resource), + dimensions: thumbnail.dimensions.cgSize, + immediateThumbnailData: featuredStickerPack.info.immediateThumbnailData, + isReaction: false + ) + } else { + animationData = EntityKeyboardAnimationData(file: item.file) + } + + let resultItem = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: item.file, + subgroupId: nil, + icon: .none, + accentTint: false + ) + + let supergroupId = "featuredTop" + let groupId: AnyHashable = supergroupId + let isPremiumLocked: Bool = item.file.isPremiumSticker && !hasPremium + if isPremiumLocked && isPremiumDisabled { + continue + } + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + } else { + itemGroupIndexById[groupId] = itemGroups.count + + let trendingIsPremium = featuredStickersConfiguration?.isPremium ?? false + let title = trendingIsPremium ? strings.Stickers_TrendingPremiumStickers : strings.StickerPacksSettings_FeaturedPacks + + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: title, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, headerItem: nil, items: [resultItem])) + } + } + } + + if let savedStickers = savedStickers { + for item in savedStickers.items { + guard let item = item.contents.get(SavedStickerItem.self) else { + continue + } + if isPremiumDisabled && item.file.isPremiumSticker { + continue + } + + let animationData = EntityKeyboardAnimationData(file: item.file) + let resultItem = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: item.file, + subgroupId: nil, + icon: .none, + accentTint: false + ) + + let groupId = "saved" + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + } else { + itemGroupIndexById[groupId] = itemGroups.count + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: strings.EmojiInput_SectionTitleFavoriteStickers, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, headerItem: nil, items: [resultItem])) + } + } + } + + if let recentStickers = recentStickers { + for item in recentStickers.items { + guard let item = item.contents.get(RecentMediaItem.self) else { + continue + } + if isPremiumDisabled && item.media.isPremiumSticker { + continue + } + + let animationData = EntityKeyboardAnimationData(file: item.media) + let resultItem = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: item.media, + subgroupId: nil, + icon: .none, + accentTint: false + ) + + let groupId = "recent" + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + } else { + itemGroupIndexById[groupId] = itemGroups.count + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: strings.Stickers_FrequentlyUsed, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, headerItem: nil, items: [resultItem])) + } + } + } + + var premiumStickers: [StickerPackItem] = [] + if hasPremium { + for entry in view.entries { + guard let item = entry.item as? StickerPackItem else { + continue + } + + if item.file.isPremiumSticker { + premiumStickers.append(item) + } + } + + if let cloudPremiumStickers = cloudPremiumStickers, !cloudPremiumStickers.items.isEmpty { + premiumStickers.append(contentsOf: cloudPremiumStickers.items.compactMap { item -> StickerPackItem? in guard let item = item.contents.get(RecentMediaItem.self) else { + return nil + } + return StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: 0), file: item.media, indexKeys: []) + }) + } + } + + if !premiumStickers.isEmpty { + var processedIds = Set() + for item in premiumStickers { + if isPremiumDisabled && item.file.isPremiumSticker { + continue + } + if processedIds.contains(item.file.fileId) { + continue + } + processedIds.insert(item.file.fileId) + + let animationData = EntityKeyboardAnimationData(file: item.file) + let resultItem = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: item.file, + subgroupId: nil, + icon: .none, + accentTint: false + ) + + let groupId = "premium" + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + } else { + itemGroupIndexById[groupId] = itemGroups.count + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: strings.EmojiInput_SectionTitlePremiumStickers, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, headerItem: nil, items: [resultItem])) + } + } + } + + var avatarPeer: EnginePeer? + if let peerSpecificPack = peerSpecificPack { + avatarPeer = peerSpecificPack.peer + + var processedIds = Set() + for item in peerSpecificPack.items { + if isPremiumDisabled && item.file.isPremiumSticker { + continue + } + if processedIds.contains(item.file.fileId) { + continue + } + processedIds.insert(item.file.fileId) + + let animationData = EntityKeyboardAnimationData(file: item.file) + let resultItem = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: item.file, + subgroupId: nil, + icon: .none, + accentTint: false + ) + + let groupId = "peerSpecific" + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + } else { + itemGroupIndexById[groupId] = itemGroups.count + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: peerSpecificPack.peer.compactDisplayTitle, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, headerItem: nil, items: [resultItem])) + } + } + } + + for entry in view.entries { + guard let item = entry.item as? StickerPackItem else { + continue + } + let animationData = EntityKeyboardAnimationData(file: item.file) + let resultItem = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: item.file, + subgroupId: nil, + icon: .none, + accentTint: false + ) + let groupId = entry.index.collectionId + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + } else { + itemGroupIndexById[groupId] = itemGroups.count + + var title = "" + var headerItem: EntityKeyboardAnimationData? + inner: for (id, info, _) in view.collectionInfos { + if id == groupId, let info = info as? StickerPackCollectionInfo { + title = info.title + + if let thumbnail = info.thumbnail { + let type: EntityKeyboardAnimationData.ItemType + if item.file.isAnimatedSticker { + type = .lottie + } else if item.file.isVideoEmoji || item.file.isVideoSticker { + type = .video + } else { + type = .still + } + + headerItem = EntityKeyboardAnimationData( + id: .stickerPackThumbnail(info.id), + type: type, + resource: .stickerPackThumbnail(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), resource: thumbnail.resource), + dimensions: thumbnail.dimensions.cgSize, + immediateThumbnailData: info.immediateThumbnailData, + isReaction: false + ) + } + + break inner + } + } + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: title, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: true, headerItem: headerItem, items: [resultItem])) + } + } + + for featuredStickerPack in featuredStickerPacks { + if installedCollectionIds.contains(featuredStickerPack.info.id) { + continue + } + + for item in featuredStickerPack.topItems { + let animationData = EntityKeyboardAnimationData(file: item.file) + let resultItem = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: item.file, + subgroupId: nil, + icon: .none, + accentTint: false + ) + + let supergroupId = featuredStickerPack.info.id + let groupId: AnyHashable = supergroupId + let isPremiumLocked: Bool = item.file.isPremiumSticker && !hasPremium + if isPremiumLocked && isPremiumDisabled { + continue + } + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + } else { + itemGroupIndexById[groupId] = itemGroups.count + + let subtitle: String = strings.StickerPack_StickerCount(Int32(featuredStickerPack.info.count)) + var headerItem: EntityKeyboardAnimationData? + + if let thumbnailFileId = featuredStickerPack.info.thumbnailFileId, let file = featuredStickerPack.topItems.first(where: { $0.file.fileId.id == thumbnailFileId }) { + headerItem = EntityKeyboardAnimationData(file: file.file) + } else if let thumbnail = featuredStickerPack.info.thumbnail { + let info = featuredStickerPack.info + let type: EntityKeyboardAnimationData.ItemType + if item.file.isAnimatedSticker { + type = .lottie + } else if item.file.isVideoEmoji || item.file.isVideoSticker { + type = .video + } else { + type = .still + } + + headerItem = EntityKeyboardAnimationData( + id: .stickerPackThumbnail(info.id), + type: type, + resource: .stickerPackThumbnail(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), resource: thumbnail.resource), + dimensions: thumbnail.dimensions.cgSize, + immediateThumbnailData: info.immediateThumbnailData, + isReaction: false + ) + } + + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: featuredStickerPack.info.title, subtitle: subtitle, actionButtonTitle: strings.Stickers_Install, isPremiumLocked: isPremiumLocked, isFeatured: true, displayPremiumBadges: false, headerItem: headerItem, items: [resultItem])) + } + } + } + + return EmojiPagerContentComponent( + id: "stickers", + context: context, + avatarPeer: avatarPeer, + animationCache: animationCache, + animationRenderer: animationRenderer, + inputInteractionHolder: EmojiPagerContentComponent.InputInteractionHolder(), + itemGroups: itemGroups.map { group -> EmojiPagerContentComponent.ItemGroup in + var hasClear = false + var isEmbedded = false + if group.id == AnyHashable("recent") { + hasClear = true + } else if group.id == AnyHashable("featuredTop") { + hasClear = true + isEmbedded = true + } + + return EmojiPagerContentComponent.ItemGroup( + supergroupId: group.supergroupId, + groupId: group.id, + title: group.title, + subtitle: group.subtitle, + actionButtonTitle: group.actionButtonTitle, + isFeatured: group.isFeatured, + isPremiumLocked: group.isPremiumLocked, + isEmbedded: isEmbedded, + hasClear: hasClear, + collapsedLineCount: nil, + displayPremiumBadges: group.displayPremiumBadges, + headerItem: group.headerItem, + items: group.items + ) + }, + itemLayoutType: .detailed, + itemContentUniqueId: nil, + warpContentsOnEdges: false, + displaySearchWithPlaceholder: nil, + searchInitiallyHidden: false, + searchIsPlaceholderOnly: true, + emptySearchResults: nil, + enableLongPress: false, + selectedItems: Set() + ) + } + } } func generateTopicIcon(backgroundColors: [UIColor], strokeColors: [UIColor], title: String) -> UIImage? { diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift index 58c999ae4b..9ad2a68437 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift @@ -90,7 +90,7 @@ public final class EntityKeyboardComponent: Component { public let isContentInFocus: Bool public let containerInsets: UIEdgeInsets public let topPanelInsets: UIEdgeInsets - public let emojiContent: EmojiPagerContentComponent + public let emojiContent: EmojiPagerContentComponent? public let stickerContent: EmojiPagerContentComponent? public let gifContent: GifPagerContentComponent? public let hasRecentGifs: Bool @@ -116,7 +116,7 @@ public final class EntityKeyboardComponent: Component { isContentInFocus: Bool, containerInsets: UIEdgeInsets, topPanelInsets: UIEdgeInsets, - emojiContent: EmojiPagerContentComponent, + emojiContent: EmojiPagerContentComponent?, stickerContent: EmojiPagerContentComponent?, gifContent: GifPagerContentComponent?, hasRecentGifs: Bool, @@ -304,24 +304,26 @@ public final class EntityKeyboardComponent: Component { } )) )) - for emoji in component.availableGifSearchEmojies { - topGifItems.append(EntityKeyboardTopPanelComponent.Item( - id: emoji.emoji, - isReorderable: false, - content: AnyComponent(EntityKeyboardAnimationTopPanelComponent( - context: component.emojiContent.context, - item: EntityKeyboardAnimationData(file: emoji.file), - isFeatured: false, - isPremiumLocked: false, - animationCache: component.emojiContent.animationCache, - animationRenderer: component.emojiContent.animationRenderer, - theme: component.theme, - title: emoji.title, - pressed: { [weak self] in - self?.component?.switchToGifSubject(.emojiSearch(emoji.emoji)) - } + if let emojiContent = component.emojiContent { + for emoji in component.availableGifSearchEmojies { + topGifItems.append(EntityKeyboardTopPanelComponent.Item( + id: emoji.emoji, + isReorderable: false, + content: AnyComponent(EntityKeyboardAnimationTopPanelComponent( + context: emojiContent.context, + item: EntityKeyboardAnimationData(file: emoji.file), + isFeatured: false, + isPremiumLocked: false, + animationCache: emojiContent.animationCache, + animationRenderer: emojiContent.animationRenderer, + theme: component.theme, + title: emoji.title, + pressed: { [weak self] in + self?.component?.switchToGifSubject(.emojiSearch(emoji.emoji)) + } + )) )) - )) + } } let defaultActiveGifItemId: AnyHashable switch gifContent.subject { @@ -480,101 +482,104 @@ public final class EntityKeyboardComponent: Component { } let emojiContentItemIdUpdated = ActionSlot<(AnyHashable, AnyHashable?, Transition)>() - contents.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(component.emojiContent))) - var topEmojiItems: [EntityKeyboardTopPanelComponent.Item] = [] - for itemGroup in component.emojiContent.itemGroups { - if !itemGroup.items.isEmpty { - if let id = itemGroup.groupId.base as? String { - if id == "recent" { - let iconMapping: [String: EntityKeyboardIconTopPanelComponent.Icon] = [ - "recent": .recent, - ] - let titleMapping: [String: String] = [ - "recent": component.strings.Stickers_Recent, - ] - if let icon = iconMapping[id], let title = titleMapping[id] { + if let emojiContent = component.emojiContent { + contents.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(emojiContent))) + var topEmojiItems: [EntityKeyboardTopPanelComponent.Item] = [] + for itemGroup in emojiContent.itemGroups { + if !itemGroup.items.isEmpty { + if let id = itemGroup.groupId.base as? String { + if id == "recent" { + let iconMapping: [String: EntityKeyboardIconTopPanelComponent.Icon] = [ + "recent": .recent, + ] + let titleMapping: [String: String] = [ + "recent": component.strings.Stickers_Recent, + ] + if let icon = iconMapping[id], let title = titleMapping[id] { + topEmojiItems.append(EntityKeyboardTopPanelComponent.Item( + id: itemGroup.supergroupId, + isReorderable: false, + content: AnyComponent(EntityKeyboardIconTopPanelComponent( + icon: icon, + theme: component.theme, + useAccentColor: false, + title: title, + pressed: { [weak self] in + self?.scrollToItemGroup(contentId: "emoji", groupId: itemGroup.supergroupId, subgroupId: nil) + } + )) + )) + } + } else if id == "static" { topEmojiItems.append(EntityKeyboardTopPanelComponent.Item( id: itemGroup.supergroupId, isReorderable: false, - content: AnyComponent(EntityKeyboardIconTopPanelComponent( - icon: icon, + content: AnyComponent(EntityKeyboardStaticStickersPanelComponent( theme: component.theme, - useAccentColor: false, - title: title, + title: component.strings.EmojiInput_PanelTitleEmoji, + pressed: { [weak self] subgroupId in + guard let strongSelf = self else { + return + } + strongSelf.scrollToItemGroup(contentId: "emoji", groupId: itemGroup.supergroupId, subgroupId: subgroupId.rawValue) + } + )) + )) + } + } else { + if let animationData = itemGroup.items[0].animationData { + topEmojiItems.append(EntityKeyboardTopPanelComponent.Item( + id: itemGroup.supergroupId, + isReorderable: !itemGroup.isFeatured, + content: AnyComponent(EntityKeyboardAnimationTopPanelComponent( + context: emojiContent.context, + item: itemGroup.headerItem ?? animationData, + isFeatured: itemGroup.isFeatured, + isPremiumLocked: itemGroup.isPremiumLocked, + animationCache: emojiContent.animationCache, + animationRenderer: emojiContent.animationRenderer, + theme: component.theme, + title: itemGroup.title ?? "", pressed: { [weak self] in self?.scrollToItemGroup(contentId: "emoji", groupId: itemGroup.supergroupId, subgroupId: nil) } )) )) } - } else if id == "static" { - topEmojiItems.append(EntityKeyboardTopPanelComponent.Item( - id: itemGroup.supergroupId, - isReorderable: false, - content: AnyComponent(EntityKeyboardStaticStickersPanelComponent( - theme: component.theme, - title: component.strings.EmojiInput_PanelTitleEmoji, - pressed: { [weak self] subgroupId in - guard let strongSelf = self else { - return - } - strongSelf.scrollToItemGroup(contentId: "emoji", groupId: itemGroup.supergroupId, subgroupId: subgroupId.rawValue) - } - )) - )) - } - } else { - if let animationData = itemGroup.items[0].animationData { - topEmojiItems.append(EntityKeyboardTopPanelComponent.Item( - id: itemGroup.supergroupId, - isReorderable: !itemGroup.isFeatured, - content: AnyComponent(EntityKeyboardAnimationTopPanelComponent( - context: component.emojiContent.context, - item: itemGroup.headerItem ?? animationData, - isFeatured: itemGroup.isFeatured, - isPremiumLocked: itemGroup.isPremiumLocked, - animationCache: component.emojiContent.animationCache, - animationRenderer: component.emojiContent.animationRenderer, - theme: component.theme, - title: itemGroup.title ?? "", - pressed: { [weak self] in - self?.scrollToItemGroup(contentId: "emoji", groupId: itemGroup.supergroupId, subgroupId: nil) - } - )) - )) } } } + contentTopPanels.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(EntityKeyboardTopPanelComponent( + id: "emoji", + theme: component.theme, + items: topEmojiItems, + containerSideInset: component.containerInsets.left + component.topPanelInsets.left, + activeContentItemIdUpdated: emojiContentItemIdUpdated, + activeContentItemMapping: ["popular": "recent"], + reorderItems: { [weak self] items in + guard let strongSelf = self else { + return + } + strongSelf.reorderPacks(category: .emoji, items: items) + } + )))) + contentIcons.append(PagerComponentContentIcon(id: "emoji", imageName: "Chat/Input/Media/EntityInputEmojiIcon")) + contentAccessoryLeftButtons.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(Button( + content: AnyComponent(BundleIconComponent( + name: "Chat/Input/Media/EntityInputGlobeIcon", + tintColor: component.theme.chat.inputMediaPanel.panelIconColor, + maxSize: nil + )), + action: { [weak self] in + guard let strongSelf = self, let component = strongSelf.component else { + return + } + component.switchToTextInput() + } + ).minSize(CGSize(width: 38.0, height: 38.0))))) } - contentTopPanels.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(EntityKeyboardTopPanelComponent( - id: "emoji", - theme: component.theme, - items: topEmojiItems, - containerSideInset: component.containerInsets.left + component.topPanelInsets.left, - activeContentItemIdUpdated: emojiContentItemIdUpdated, - activeContentItemMapping: ["popular": "recent"], - reorderItems: { [weak self] items in - guard let strongSelf = self else { - return - } - strongSelf.reorderPacks(category: .emoji, items: items) - } - )))) - contentIcons.append(PagerComponentContentIcon(id: "emoji", imageName: "Chat/Input/Media/EntityInputEmojiIcon")) - contentAccessoryLeftButtons.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(Button( - content: AnyComponent(BundleIconComponent( - name: "Chat/Input/Media/EntityInputGlobeIcon", - tintColor: component.theme.chat.inputMediaPanel.panelIconColor, - maxSize: nil - )), - action: { [weak self] in - guard let strongSelf = self, let component = strongSelf.component else { - return - } - component.switchToTextInput() - } - ).minSize(CGSize(width: 38.0, height: 38.0))))) - let deleteBackwards = component.emojiContent.inputInteractionHolder.inputInteraction?.deleteBackwards + + let deleteBackwards = component.emojiContent?.inputInteractionHolder.inputInteraction?.deleteBackwards contentAccessoryRightButtons.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(Button( content: AnyComponent(BundleIconComponent( name: "Chat/Input/Media/EntityInputClearIcon", @@ -620,7 +625,7 @@ public final class EntityKeyboardComponent: Component { theme: component.theme, containerInsets: component.containerInsets, deleteBackwards: { [weak self] in - self?.component?.emojiContent.inputInteractionHolder.inputInteraction?.deleteBackwards() + self?.component?.emojiContent?.inputInteractionHolder.inputInteraction?.deleteBackwards() AudioServicesPlaySystemSound(0x451) } )) : nil, @@ -665,7 +670,8 @@ public final class EntityKeyboardComponent: Component { ) transition.setFrame(view: self.pagerView, frame: CGRect(origin: CGPoint(), size: pagerSize)) - if let searchComponent = self.searchComponent { + let accountContext = component.emojiContent?.context ?? component.stickerContent?.context + if let searchComponent = self.searchComponent, let accountContext = accountContext { var animateIn = false let searchView: ComponentHostView var searchViewTransition = transition @@ -686,7 +692,7 @@ public final class EntityKeyboardComponent: Component { component: AnyComponent(searchComponent), environment: { EntitySearchContentEnvironment( - context: component.emojiContent.context, + context: accountContext, theme: component.theme, deviceMetrics: component.deviceMetrics, inputHeight: component.inputHeight diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Add.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/Add.imageset/Contents.json new file mode 100644 index 0000000000..3c11391605 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Add.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "add.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Add.imageset/add.png b/submodules/TelegramUI/Images.xcassets/Media Editor/Add.imageset/add.png new file mode 100644 index 0000000000..39b8cd220d Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/Add.imageset/add.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/BrushArrow.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/BrushArrow.imageset/Contents.json new file mode 100644 index 0000000000..8118e6a70b --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/BrushArrow.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "arrowTip.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/BrushArrow.imageset/arrowTip.png b/submodules/TelegramUI/Images.xcassets/Media Editor/BrushArrow.imageset/arrowTip.png new file mode 100644 index 0000000000..fc129d11fa Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/BrushArrow.imageset/arrowTip.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/BrushBlur.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/BrushBlur.imageset/Contents.json new file mode 100644 index 0000000000..db896561d2 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/BrushBlur.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "blurTip.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/BrushBlur.imageset/blurTip.png b/submodules/TelegramUI/Images.xcassets/Media Editor/BrushBlur.imageset/blurTip.png new file mode 100644 index 0000000000..bf79edd1e9 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/BrushBlur.imageset/blurTip.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/BrushRemove.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/BrushRemove.imageset/Contents.json new file mode 100644 index 0000000000..9c0eb53ecf --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/BrushRemove.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "xmarkTip.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/BrushRemove.imageset/xmarkTip.png b/submodules/TelegramUI/Images.xcassets/Media Editor/BrushRemove.imageset/xmarkTip.png new file mode 100644 index 0000000000..4599befbfd Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/BrushRemove.imageset/xmarkTip.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/BrushRound.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/BrushRound.imageset/Contents.json new file mode 100644 index 0000000000..7ef95b8810 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/BrushRound.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "roundTip.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/BrushRound.imageset/roundTip.png b/submodules/TelegramUI/Images.xcassets/Media Editor/BrushRound.imageset/roundTip.png new file mode 100644 index 0000000000..dbc11b4eea Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/BrushRound.imageset/roundTip.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/Contents.json new file mode 100644 index 0000000000..6e965652df --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Done.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/Done.imageset/Contents.json new file mode 100644 index 0000000000..efa7620e6b --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Done.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ic_editor_check (2).pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Done.imageset/ic_editor_check (2).pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/Done.imageset/ic_editor_check (2).pdf new file mode 100644 index 0000000000..b3af47d0bd Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/Done.imageset/ic_editor_check (2).pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/EraserRemove.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/EraserRemove.imageset/Contents.json new file mode 100644 index 0000000000..058c5b2ec5 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/EraserRemove.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "EraserRemove.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/EraserRemove.imageset/EraserRemove.png b/submodules/TelegramUI/Images.xcassets/Media Editor/EraserRemove.imageset/EraserRemove.png new file mode 100644 index 0000000000..b5456be6f0 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/EraserRemove.imageset/EraserRemove.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Eyedropper.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/Eyedropper.imageset/Contents.json new file mode 100644 index 0000000000..38e7441369 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Eyedropper.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "eyedropper.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Eyedropper.imageset/eyedropper.png b/submodules/TelegramUI/Images.xcassets/Media Editor/Eyedropper.imageset/eyedropper.png new file mode 100644 index 0000000000..abc70d9b76 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/Eyedropper.imageset/eyedropper.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Fill.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/Fill.imageset/Contents.json new file mode 100644 index 0000000000..0a2222bdd6 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Fill.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "shapeFill.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Fill.imageset/shapeFill.png b/submodules/TelegramUI/Images.xcassets/Media Editor/Fill.imageset/shapeFill.png new file mode 100644 index 0000000000..ef2f0f598a Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/Fill.imageset/shapeFill.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Flip.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/Flip.imageset/Contents.json new file mode 100644 index 0000000000..a80e618bcd --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Flip.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "shapeFlip.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Flip.imageset/shapeFlip.png b/submodules/TelegramUI/Images.xcassets/Media Editor/Flip.imageset/shapeFlip.png new file mode 100644 index 0000000000..14f33e51d6 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/Flip.imageset/shapeFlip.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Pencil.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/Pencil.imageset/Contents.json new file mode 100644 index 0000000000..1921e5967b --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Pencil.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Pencil.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Pencil.imageset/Pencil.png b/submodules/TelegramUI/Images.xcassets/Media Editor/Pencil.imageset/Pencil.png new file mode 100644 index 0000000000..17c0a20afa Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/Pencil.imageset/Pencil.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Redo.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/Redo.imageset/Contents.json new file mode 100644 index 0000000000..71cf2c0e13 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Redo.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "undo.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Redo.imageset/undo.png b/submodules/TelegramUI/Images.xcassets/Media Editor/Redo.imageset/undo.png new file mode 100644 index 0000000000..1dc7913d2b Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/Redo.imageset/undo.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/RoundSpectrum.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/RoundSpectrum.imageset/Contents.json new file mode 100644 index 0000000000..12886b8bc3 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/RoundSpectrum.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Round.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/RoundSpectrum.imageset/Round.png b/submodules/TelegramUI/Images.xcassets/Media Editor/RoundSpectrum.imageset/Round.png new file mode 100644 index 0000000000..f902e548ad Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/RoundSpectrum.imageset/Round.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeArrow.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeArrow.imageset/Contents.json new file mode 100644 index 0000000000..a318c3b3bf --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeArrow.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "shapeArrow.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeArrow.imageset/shapeArrow.png b/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeArrow.imageset/shapeArrow.png new file mode 100644 index 0000000000..3b9bcccbee Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeArrow.imageset/shapeArrow.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeBubble.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeBubble.imageset/Contents.json new file mode 100644 index 0000000000..f299d75d3b --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeBubble.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "shapeBubble.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeBubble.imageset/shapeBubble.png b/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeBubble.imageset/shapeBubble.png new file mode 100644 index 0000000000..fe5c8de653 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeBubble.imageset/shapeBubble.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeEllipse.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeEllipse.imageset/Contents.json new file mode 100644 index 0000000000..5331f04cdc --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeEllipse.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "shapeEllipse.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeEllipse.imageset/shapeEllipse.png b/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeEllipse.imageset/shapeEllipse.png new file mode 100644 index 0000000000..c62655e6d4 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeEllipse.imageset/shapeEllipse.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeRectangle.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeRectangle.imageset/Contents.json new file mode 100644 index 0000000000..76d78cbfdf --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeRectangle.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "shapeRectangle.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeRectangle.imageset/shapeRectangle.png b/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeRectangle.imageset/shapeRectangle.png new file mode 100644 index 0000000000..052b65140c Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeRectangle.imageset/shapeRectangle.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeStar.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeStar.imageset/Contents.json new file mode 100644 index 0000000000..14f839bcbd --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeStar.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "shapeStar.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeStar.imageset/shapeStar.png b/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeStar.imageset/shapeStar.png new file mode 100644 index 0000000000..c182350cd1 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeStar.imageset/shapeStar.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Spectrum.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/Spectrum.imageset/Contents.json new file mode 100644 index 0000000000..8543bdd0ca --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Spectrum.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "spectrum.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Spectrum.imageset/spectrum.png b/submodules/TelegramUI/Images.xcassets/Media Editor/Spectrum.imageset/spectrum.png new file mode 100644 index 0000000000..6879c454d3 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/Spectrum.imageset/spectrum.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Stroke.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/Stroke.imageset/Contents.json new file mode 100644 index 0000000000..e626e85320 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Stroke.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "shapeStroke.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Stroke.imageset/shapeStroke.png b/submodules/TelegramUI/Images.xcassets/Media Editor/Stroke.imageset/shapeStroke.png new file mode 100644 index 0000000000..4c977158d8 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/Stroke.imageset/shapeStroke.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/TextDefault.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/TextDefault.imageset/Contents.json new file mode 100644 index 0000000000..2154ff497f --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/TextDefault.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "default.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/TextDefault.imageset/default.png b/submodules/TelegramUI/Images.xcassets/Media Editor/TextDefault.imageset/default.png new file mode 100644 index 0000000000..d617482831 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/TextDefault.imageset/default.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/TextFilled.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/TextFilled.imageset/Contents.json new file mode 100644 index 0000000000..cfb66f3a39 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/TextFilled.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "filled.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/TextFilled.imageset/filled.png b/submodules/TelegramUI/Images.xcassets/Media Editor/TextFilled.imageset/filled.png new file mode 100644 index 0000000000..714e1b38de Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/TextFilled.imageset/filled.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/TextSemi.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/TextSemi.imageset/Contents.json new file mode 100644 index 0000000000..8e12193fcc --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/TextSemi.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "semi.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/TextSemi.imageset/semi.png b/submodules/TelegramUI/Images.xcassets/Media Editor/TextSemi.imageset/semi.png new file mode 100644 index 0000000000..0110b1fbdd Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/TextSemi.imageset/semi.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/TextStroke.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/TextStroke.imageset/Contents.json new file mode 100644 index 0000000000..0010a83b52 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/TextStroke.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "stroke.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/TextStroke.imageset/stroke.png b/submodules/TelegramUI/Images.xcassets/Media Editor/TextStroke.imageset/stroke.png new file mode 100644 index 0000000000..03e0cdf7a3 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/TextStroke.imageset/stroke.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolEraser.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolEraser.imageset/Contents.json new file mode 100644 index 0000000000..1ff77c3d76 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolEraser.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "eraser.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolEraser.imageset/eraser.png b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolEraser.imageset/eraser.png new file mode 100644 index 0000000000..c9691ac63d Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolEraser.imageset/eraser.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolLasso.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolLasso.imageset/Contents.json new file mode 100644 index 0000000000..3ee8a92135 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolLasso.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "lasso.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolLasso.imageset/lasso.png b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolLasso.imageset/lasso.png new file mode 100644 index 0000000000..f24e755242 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolLasso.imageset/lasso.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolMarker.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolMarker.imageset/Contents.json new file mode 100644 index 0000000000..c2a56d01f6 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolMarker.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "brush.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolMarker.imageset/brush.png b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolMarker.imageset/brush.png new file mode 100644 index 0000000000..88326aca61 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolMarker.imageset/brush.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolMarkerTip.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolMarkerTip.imageset/Contents.json new file mode 100644 index 0000000000..c2a56d01f6 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolMarkerTip.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "brush.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolMarkerTip.imageset/brush.png b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolMarkerTip.imageset/brush.png new file mode 100644 index 0000000000..ebad6e5c01 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolMarkerTip.imageset/brush.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolNeon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolNeon.imageset/Contents.json new file mode 100644 index 0000000000..1aaea262c9 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolNeon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "neon.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolNeon.imageset/neon.png b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolNeon.imageset/neon.png new file mode 100644 index 0000000000..a4e196b6e8 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolNeon.imageset/neon.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolNeonTip.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolNeonTip.imageset/Contents.json new file mode 100644 index 0000000000..1aaea262c9 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolNeonTip.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "neon.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolNeonTip.imageset/neon.png b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolNeonTip.imageset/neon.png new file mode 100644 index 0000000000..44e4b6eed6 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolNeonTip.imageset/neon.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolPen.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolPen.imageset/Contents.json new file mode 100644 index 0000000000..ccf7e066b7 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolPen.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "pen.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolPen.imageset/pen.png b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolPen.imageset/pen.png new file mode 100644 index 0000000000..33f522cd3c Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolPen.imageset/pen.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolPenTip.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolPenTip.imageset/Contents.json new file mode 100644 index 0000000000..ccf7e066b7 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolPenTip.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "pen.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolPenTip.imageset/pen.png b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolPenTip.imageset/pen.png new file mode 100644 index 0000000000..372abdb7e3 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolPenTip.imageset/pen.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolPencil.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolPencil.imageset/Contents.json new file mode 100644 index 0000000000..397f131a40 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolPencil.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "pencil.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolPencil.imageset/pencil.png b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolPencil.imageset/pencil.png new file mode 100644 index 0000000000..3a1e8cdf91 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolPencil.imageset/pencil.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolPencilTip.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolPencilTip.imageset/Contents.json new file mode 100644 index 0000000000..397f131a40 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolPencilTip.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "pencil.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolPencilTip.imageset/pencil.png b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolPencilTip.imageset/pencil.png new file mode 100644 index 0000000000..35f34c997d Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolPencilTip.imageset/pencil.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Undo.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/Undo.imageset/Contents.json new file mode 100644 index 0000000000..71cf2c0e13 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Undo.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "undo.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Undo.imageset/undo.png b/submodules/TelegramUI/Images.xcassets/Media Editor/Undo.imageset/undo.png new file mode 100644 index 0000000000..c2b1063a39 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/Undo.imageset/undo.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ZoomOut.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/ZoomOut.imageset/Contents.json new file mode 100644 index 0000000000..71d9667c0e --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/ZoomOut.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "zoomOut.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ZoomOut.imageset/zoomOut.png b/submodules/TelegramUI/Images.xcassets/Media Editor/ZoomOut.imageset/zoomOut.png new file mode 100644 index 0000000000..7cb307c9ba Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/ZoomOut.imageset/zoomOut.png differ diff --git a/submodules/TelegramUI/Resources/Animations/media_backToCancel.json b/submodules/TelegramUI/Resources/Animations/media_backToCancel.json new file mode 100644 index 0000000000..eaad629275 --- /dev/null +++ b/submodules/TelegramUI/Resources/Animations/media_backToCancel.json @@ -0,0 +1 @@ +{"v":"5.9.6","fr":60,"ip":0,"op":60,"w":99,"h":99,"nm":"Back to Cancel","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Artboard Copy 3 Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[49.5,49.5,0],"ix":2,"l":2},"a":{"a":0,"k":[49.5,49.5,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.2,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-18.672,-16.5],[-2.172,0],[-18.672,16.5]],"c":false}]},{"i":{"x":0.8,"y":1},"o":{"x":0.8,"y":0},"t":15,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-8.25,-16.5],[8.25,0],[-8.25,16.5]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.2,"y":0},"t":30,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-8.25,-16.5],[8.25,0],[-8.25,16.5]],"c":false}]},{"t":45,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-18.672,-16.5],[-2.172,0],[-18.672,16.5]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.2],"y":[0]},"t":0,"s":[50]},{"i":{"x":[0.8],"y":[1]},"o":{"x":[0.8],"y":[0]},"t":15,"s":[0]},{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.2],"y":[0]},"t":30,"s":[0]},{"t":45,"s":[50]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.2],"y":[0]},"t":0,"s":[50]},{"i":{"x":[0.8],"y":[1]},"o":{"x":[0.8],"y":[0]},"t":15,"s":[100]},{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.2],"y":[0]},"t":30,"s":[100]},{"t":45,"s":[50]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":6,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[41.25,49.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.2,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-1.5,-17.25],[-18.75,0],[-1.5,17.25]],"c":false}]},{"i":{"x":0.8,"y":1},"o":{"x":0.8,"y":0},"t":15,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[8.25,-16.5],[-8.25,0],[8.25,16.5]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.2,"y":0},"t":30,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[8.25,-16.5],[-8.25,0],[8.25,16.5]],"c":false}]},{"t":45,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-1.5,-17.25],[-18.75,0],[-1.5,17.25]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":6,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[57.75,49.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-25.955,0],[0,25.955],[25.955,0],[0,-25.955]],"o":[[25.955,0],[0,-25.955],[-25.955,0],[0,25.955]],"v":[[0,46.995],[46.995,0],[0,-46.995],[-46.995,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5.01,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[49.515,49.515],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"ct":1,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 29d98105ae..a1ab39e263 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -971,8 +971,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G transitionCompletion() }, presentStickers: { [weak self] completion in if let strongSelf = self { - let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, view, rect in - completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) + let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { result in + if let (fileReference, view, rect) = result { + completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) + } return true }) strongSelf.present(controller, in: .window(.root)) @@ -3764,8 +3766,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let mediaReference = mediaReference, let peer = message.peers[message.id.peerId] { legacyMediaEditor(context: strongSelf.context, peer: peer, threadTitle: strongSelf.threadInfo?.title, media: mediaReference, initialCaption: NSAttributedString(string: message.text), snapshots: [], transitionCompletion: nil, presentStickers: { [weak self] completion in if let strongSelf = self { - let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, view, rect in - completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) + let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { result in + if let (fileReference, view, rect) = result { + completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) + } return true }) strongSelf.present(controller, in: .window(.root)) @@ -11878,8 +11882,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }, presentStickers: { [weak self] completion in if let strongSelf = self { - let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, view, rect in - completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) + let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { result in + if let (fileReference, view, rect) = result { + completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) + } return true }) strongSelf.present(controller, in: .window(.root)) @@ -12519,8 +12525,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }, presentStickers: { [weak self] completion in if let strongSelf = self { - let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, view, rect in - completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) + let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { result in + if let (fileReference, view, rect) = result { + completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) + } return true }) strongSelf.present(controller, in: .window(.root)) @@ -12617,8 +12625,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }, presentStickers: { [weak self] completion in if let strongSelf = self { - let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, view, rect in - completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) + let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { result in + if let (fileReference, view, rect) = result { + completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) + } return true }) strongSelf.present(controller, in: .window(.root)) @@ -12829,8 +12839,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } controller.presentStickers = { [weak self] completion in if let strongSelf = self { - let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, view, rect in - completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) + let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { result in + if let (fileReference, view, rect) = result { + completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) + } return true }) strongSelf.present(controller, in: .window(.root)) @@ -12926,8 +12938,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G })) controller.presentStickers = { [weak self] completion in if let strongSelf = self { - let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, view, rect in - completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) + let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { result in + if let (fileReference, view, rect) = result { + completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) + } return true }) strongSelf.present(controller, in: .window(.root)) @@ -12973,8 +12987,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }, presentStickers: { [weak self] completion in if let strongSelf = self { - let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, view, rect in - completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) + let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { result in + if let (fileReference, view, rect) = result { + completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) + } return true }) strongSelf.present(controller, in: .window(.root)) @@ -13030,8 +13046,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G })) controller.presentStickers = { [weak self] completion in if let strongSelf = self { - let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, view, rect in - completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) + let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { result in + if let (fileReference, view, rect) = result { + completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) + } return true }) strongSelf.present(controller, in: .window(.root)) @@ -13937,8 +13955,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } |> deliverOnMainQueue).start(next: { [weak self] settings in if let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { - strongSelf.chatDisplayNode.dismissInput() - + strongSelf.chatDisplayNode.dismissInput() let controller = mediaPasteboardScreen( context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, diff --git a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift index efdbdffdc1..62de96da12 100644 --- a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift @@ -83,10 +83,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { return hasPremium } - static func inputData(context: AccountContext, interfaceInteraction: ChatPanelInterfaceInteraction, controllerInteraction: ChatControllerInteraction?, chatPeerId: PeerId?, areCustomEmojiEnabled: Bool) -> Signal { - let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) - let isPremiumDisabled = premiumConfiguration.isPremiumDisabled - + static func inputData(context: AccountContext, interfaceInteraction: ChatPanelInterfaceInteraction, controllerInteraction: ChatControllerInteraction?, chatPeerId: PeerId?, areCustomEmojiEnabled: Bool) -> Signal { let animationCache = context.animationCache let animationRenderer = context.animationRenderer @@ -94,458 +91,10 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { let stickerNamespaces: [ItemCollectionId.Namespace] = [Namespaces.ItemCollection.CloudStickerPacks] let stickerOrderedItemListCollectionIds: [Int32] = [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudAllPremiumStickers] - - struct PeerSpecificPackData: Equatable { - var info: StickerPackCollectionInfo - var items: [StickerPackItem] - var peer: EnginePeer - - static func ==(lhs: PeerSpecificPackData, rhs: PeerSpecificPackData) -> Bool { - if lhs.info.id != rhs.info.id { - return false - } - if lhs.items != rhs.items { - return false - } - if lhs.peer != rhs.peer { - return false - } - return true - } - } - - let peerSpecificPack: Signal - if let chatPeerId = chatPeerId { - peerSpecificPack = combineLatest( - context.engine.peers.peerSpecificStickerPack(peerId: chatPeerId), - context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: chatPeerId)) - ) - |> map { packData, peer -> PeerSpecificPackData? in - guard let peer = peer else { - return nil - } - - guard let (info, items) = packData.packInfo else { - return nil - } - - return PeerSpecificPackData(info: info, items: items.compactMap { $0 as? StickerPackItem }, peer: peer) - } - |> distinctUntilChanged - } else { - peerSpecificPack = .single(nil) - } - let strings = context.sharedContext.currentPresentationData.with({ $0 }).strings - let stickerItems: Signal = combineLatest( - context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: stickerOrderedItemListCollectionIds, namespaces: stickerNamespaces, aroundIndex: nil, count: 10000000), - ChatEntityKeyboardInputNode.hasPremium(context: context, chatPeerId: chatPeerId, premiumIfSavedMessages: false), - context.account.viewTracker.featuredStickerPacks(), - context.engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: Namespaces.CachedItemCollection.featuredStickersConfiguration, id: ValueBoxKey(length: 0))), - ApplicationSpecificNotice.dismissedTrendingStickerPacks(accountManager: context.sharedContext.accountManager), - peerSpecificPack - ) - |> map { view, hasPremium, featuredStickerPacks, featuredStickersConfiguration, dismissedTrendingStickerPacks, peerSpecificPack -> EmojiPagerContentComponent in - struct ItemGroup { - var supergroupId: AnyHashable - var id: AnyHashable - var title: String - var subtitle: String? - var actionButtonTitle: String? - var isPremiumLocked: Bool - var isFeatured: Bool - var displayPremiumBadges: Bool - var headerItem: EntityKeyboardAnimationData? - var items: [EmojiPagerContentComponent.Item] - } - var itemGroups: [ItemGroup] = [] - var itemGroupIndexById: [AnyHashable: Int] = [:] - - var savedStickers: OrderedItemListView? - var recentStickers: OrderedItemListView? - var cloudPremiumStickers: OrderedItemListView? - for orderedView in view.orderedItemListsViews { - if orderedView.collectionId == Namespaces.OrderedItemList.CloudRecentStickers { - recentStickers = orderedView - } else if orderedView.collectionId == Namespaces.OrderedItemList.CloudSavedStickers { - savedStickers = orderedView - } else if orderedView.collectionId == Namespaces.OrderedItemList.CloudAllPremiumStickers { - cloudPremiumStickers = orderedView - } - } - - var installedCollectionIds = Set() - for (id, _, _) in view.collectionInfos { - installedCollectionIds.insert(id) - } - - let dismissedTrendingStickerPacksSet = Set(dismissedTrendingStickerPacks ?? []) - let featuredStickerPacksSet = Set(featuredStickerPacks.map(\.info.id.id)) - - if dismissedTrendingStickerPacksSet != featuredStickerPacksSet { - let featuredStickersConfiguration = featuredStickersConfiguration?.get(FeaturedStickersConfiguration.self) - for featuredStickerPack in featuredStickerPacks { - if installedCollectionIds.contains(featuredStickerPack.info.id) { - continue - } - - guard let item = featuredStickerPack.topItems.first else { - continue - } - - let animationData: EntityKeyboardAnimationData - - if let thumbnail = featuredStickerPack.info.thumbnail { - let type: EntityKeyboardAnimationData.ItemType - if item.file.isAnimatedSticker { - type = .lottie - } else if item.file.isVideoEmoji || item.file.isVideoSticker { - type = .video - } else { - type = .still - } - - animationData = EntityKeyboardAnimationData( - id: .stickerPackThumbnail(featuredStickerPack.info.id), - type: type, - resource: .stickerPackThumbnail(stickerPack: .id(id: featuredStickerPack.info.id.id, accessHash: featuredStickerPack.info.accessHash), resource: thumbnail.resource), - dimensions: thumbnail.dimensions.cgSize, - immediateThumbnailData: featuredStickerPack.info.immediateThumbnailData, - isReaction: false - ) - } else { - animationData = EntityKeyboardAnimationData(file: item.file) - } - - let resultItem = EmojiPagerContentComponent.Item( - animationData: animationData, - content: .animation(animationData), - itemFile: item.file, - subgroupId: nil, - icon: .none, - accentTint: false - ) - - let supergroupId = "featuredTop" - let groupId: AnyHashable = supergroupId - let isPremiumLocked: Bool = item.file.isPremiumSticker && !hasPremium - if isPremiumLocked && isPremiumDisabled { - continue - } - if let groupIndex = itemGroupIndexById[groupId] { - itemGroups[groupIndex].items.append(resultItem) - } else { - itemGroupIndexById[groupId] = itemGroups.count - - let trendingIsPremium = featuredStickersConfiguration?.isPremium ?? false - let title = trendingIsPremium ? strings.Stickers_TrendingPremiumStickers : strings.StickerPacksSettings_FeaturedPacks - - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: title, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, headerItem: nil, items: [resultItem])) - } - } - } - - if let savedStickers = savedStickers { - for item in savedStickers.items { - guard let item = item.contents.get(SavedStickerItem.self) else { - continue - } - if isPremiumDisabled && item.file.isPremiumSticker { - continue - } - - let animationData = EntityKeyboardAnimationData(file: item.file) - let resultItem = EmojiPagerContentComponent.Item( - animationData: animationData, - content: .animation(animationData), - itemFile: item.file, - subgroupId: nil, - icon: .none, - accentTint: false - ) - - let groupId = "saved" - if let groupIndex = itemGroupIndexById[groupId] { - itemGroups[groupIndex].items.append(resultItem) - } else { - itemGroupIndexById[groupId] = itemGroups.count - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: strings.EmojiInput_SectionTitleFavoriteStickers, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, headerItem: nil, items: [resultItem])) - } - } - } - - if let recentStickers = recentStickers { - for item in recentStickers.items { - guard let item = item.contents.get(RecentMediaItem.self) else { - continue - } - if isPremiumDisabled && item.media.isPremiumSticker { - continue - } - - let animationData = EntityKeyboardAnimationData(file: item.media) - let resultItem = EmojiPagerContentComponent.Item( - animationData: animationData, - content: .animation(animationData), - itemFile: item.media, - subgroupId: nil, - icon: .none, - accentTint: false - ) - - let groupId = "recent" - if let groupIndex = itemGroupIndexById[groupId] { - itemGroups[groupIndex].items.append(resultItem) - } else { - itemGroupIndexById[groupId] = itemGroups.count - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: strings.Stickers_FrequentlyUsed, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, headerItem: nil, items: [resultItem])) - } - } - } - - var premiumStickers: [StickerPackItem] = [] - if hasPremium { - for entry in view.entries { - guard let item = entry.item as? StickerPackItem else { - continue - } - - if item.file.isPremiumSticker { - premiumStickers.append(item) - } - } - - if let cloudPremiumStickers = cloudPremiumStickers, !cloudPremiumStickers.items.isEmpty { - premiumStickers.append(contentsOf: cloudPremiumStickers.items.compactMap { item -> StickerPackItem? in guard let item = item.contents.get(RecentMediaItem.self) else { - return nil - } - return StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: 0), file: item.media, indexKeys: []) - }) - } - } - - if !premiumStickers.isEmpty { - var processedIds = Set() - for item in premiumStickers { - if isPremiumDisabled && item.file.isPremiumSticker { - continue - } - if processedIds.contains(item.file.fileId) { - continue - } - processedIds.insert(item.file.fileId) - - let animationData = EntityKeyboardAnimationData(file: item.file) - let resultItem = EmojiPagerContentComponent.Item( - animationData: animationData, - content: .animation(animationData), - itemFile: item.file, - subgroupId: nil, - icon: .none, - accentTint: false - ) - - let groupId = "premium" - if let groupIndex = itemGroupIndexById[groupId] { - itemGroups[groupIndex].items.append(resultItem) - } else { - itemGroupIndexById[groupId] = itemGroups.count - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: strings.EmojiInput_SectionTitlePremiumStickers, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, headerItem: nil, items: [resultItem])) - } - } - } - - var avatarPeer: EnginePeer? - if let peerSpecificPack = peerSpecificPack { - avatarPeer = peerSpecificPack.peer - - var processedIds = Set() - for item in peerSpecificPack.items { - if isPremiumDisabled && item.file.isPremiumSticker { - continue - } - if processedIds.contains(item.file.fileId) { - continue - } - processedIds.insert(item.file.fileId) - - let animationData = EntityKeyboardAnimationData(file: item.file) - let resultItem = EmojiPagerContentComponent.Item( - animationData: animationData, - content: .animation(animationData), - itemFile: item.file, - subgroupId: nil, - icon: .none, - accentTint: false - ) - - let groupId = "peerSpecific" - if let groupIndex = itemGroupIndexById[groupId] { - itemGroups[groupIndex].items.append(resultItem) - } else { - itemGroupIndexById[groupId] = itemGroups.count - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: peerSpecificPack.peer.compactDisplayTitle, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, headerItem: nil, items: [resultItem])) - } - } - } - - for entry in view.entries { - guard let item = entry.item as? StickerPackItem else { - continue - } - let animationData = EntityKeyboardAnimationData(file: item.file) - let resultItem = EmojiPagerContentComponent.Item( - animationData: animationData, - content: .animation(animationData), - itemFile: item.file, - subgroupId: nil, - icon: .none, - accentTint: false - ) - let groupId = entry.index.collectionId - if let groupIndex = itemGroupIndexById[groupId] { - itemGroups[groupIndex].items.append(resultItem) - } else { - itemGroupIndexById[groupId] = itemGroups.count - - var title = "" - var headerItem: EntityKeyboardAnimationData? - inner: for (id, info, _) in view.collectionInfos { - if id == groupId, let info = info as? StickerPackCollectionInfo { - title = info.title - - if let thumbnail = info.thumbnail { - let type: EntityKeyboardAnimationData.ItemType - if item.file.isAnimatedSticker { - type = .lottie - } else if item.file.isVideoEmoji || item.file.isVideoSticker { - type = .video - } else { - type = .still - } - - headerItem = EntityKeyboardAnimationData( - id: .stickerPackThumbnail(info.id), - type: type, - resource: .stickerPackThumbnail(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), resource: thumbnail.resource), - dimensions: thumbnail.dimensions.cgSize, - immediateThumbnailData: info.immediateThumbnailData, - isReaction: false - ) - } - - break inner - } - } - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: title, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: true, headerItem: headerItem, items: [resultItem])) - } - } - - for featuredStickerPack in featuredStickerPacks { - if installedCollectionIds.contains(featuredStickerPack.info.id) { - continue - } - - for item in featuredStickerPack.topItems { - let animationData = EntityKeyboardAnimationData(file: item.file) - let resultItem = EmojiPagerContentComponent.Item( - animationData: animationData, - content: .animation(animationData), - itemFile: item.file, - subgroupId: nil, - icon: .none, - accentTint: false - ) - - let supergroupId = featuredStickerPack.info.id - let groupId: AnyHashable = supergroupId - let isPremiumLocked: Bool = item.file.isPremiumSticker && !hasPremium - if isPremiumLocked && isPremiumDisabled { - continue - } - if let groupIndex = itemGroupIndexById[groupId] { - itemGroups[groupIndex].items.append(resultItem) - } else { - itemGroupIndexById[groupId] = itemGroups.count - - let subtitle: String = strings.StickerPack_StickerCount(Int32(featuredStickerPack.info.count)) - var headerItem: EntityKeyboardAnimationData? - - if let thumbnailFileId = featuredStickerPack.info.thumbnailFileId, let file = featuredStickerPack.topItems.first(where: { $0.file.fileId.id == thumbnailFileId }) { - headerItem = EntityKeyboardAnimationData(file: file.file) - } else if let thumbnail = featuredStickerPack.info.thumbnail { - let info = featuredStickerPack.info - let type: EntityKeyboardAnimationData.ItemType - if item.file.isAnimatedSticker { - type = .lottie - } else if item.file.isVideoEmoji || item.file.isVideoSticker { - type = .video - } else { - type = .still - } - - headerItem = EntityKeyboardAnimationData( - id: .stickerPackThumbnail(info.id), - type: type, - resource: .stickerPackThumbnail(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), resource: thumbnail.resource), - dimensions: thumbnail.dimensions.cgSize, - immediateThumbnailData: info.immediateThumbnailData, - isReaction: false - ) - } - - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: featuredStickerPack.info.title, subtitle: subtitle, actionButtonTitle: strings.Stickers_Install, isPremiumLocked: isPremiumLocked, isFeatured: true, displayPremiumBadges: false, headerItem: headerItem, items: [resultItem])) - } - } - } - - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - return EmojiPagerContentComponent( - id: "stickers", - context: context, - avatarPeer: avatarPeer, - animationCache: animationCache, - animationRenderer: animationRenderer, - inputInteractionHolder: EmojiPagerContentComponent.InputInteractionHolder(), - itemGroups: itemGroups.map { group -> EmojiPagerContentComponent.ItemGroup in - var hasClear = false - var isEmbedded = false - if group.id == AnyHashable("recent") { - hasClear = true - } else if group.id == AnyHashable("featuredTop") { - hasClear = true - isEmbedded = true - } - - return EmojiPagerContentComponent.ItemGroup( - supergroupId: group.supergroupId, - groupId: group.id, - title: group.title, - subtitle: group.subtitle, - actionButtonTitle: group.actionButtonTitle, - isFeatured: group.isFeatured, - isPremiumLocked: group.isPremiumLocked, - isEmbedded: isEmbedded, - hasClear: hasClear, - collapsedLineCount: nil, - displayPremiumBadges: group.displayPremiumBadges, - headerItem: group.headerItem, - items: group.items - ) - }, - itemLayoutType: .detailed, - itemContentUniqueId: nil, - warpContentsOnEdges: false, - displaySearchWithPlaceholder: presentationData.strings.StickersSearch_SearchStickersPlaceholder, - searchInitiallyHidden: false, - searchIsPlaceholderOnly: true, - emptySearchResults: nil, - enableLongPress: false, - selectedItems: Set() - ) - } + let stickerItems = EmojiPagerContentComponent.stickerInputData(context: context, animationCache: animationCache, animationRenderer: animationRenderer, stickerNamespaces: stickerNamespaces, stickerOrderedItemListCollectionIds: stickerOrderedItemListCollectionIds, chatPeerId: chatPeerId) let reactions: Signal<[String], NoError> = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.App()) |> map { appConfiguration -> [String] in diff --git a/submodules/TelegramUI/Sources/DrawingStickersScreen.swift b/submodules/TelegramUI/Sources/DrawingStickersScreen.swift index 72ecc4dc8c..dc40c81abe 100644 --- a/submodules/TelegramUI/Sources/DrawingStickersScreen.swift +++ b/submodules/TelegramUI/Sources/DrawingStickersScreen.swift @@ -40,7 +40,7 @@ private struct DrawingPaneArrangement { private final class DrawingStickersScreenNode: ViewControllerTracingNode { private let context: AccountContext private var presentationData: PresentationData - fileprivate var selectSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? + fileprivate var selectSticker: (((FileMediaReference, UIView, CGRect)?) -> Bool)? private var searchItemContext = StickerPaneSearchGlobalItemContext() private let themeAndStringsPromise: Promise<(PresentationTheme, PresentationStrings)> @@ -98,7 +98,7 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode { fileprivate var dismiss: (() -> Void)? - init(context: AccountContext, selectSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?) { + init(context: AccountContext, selectSticker: (((FileMediaReference, UIView, CGRect)?) -> Bool)?) { self.context = context let presentationData = context.sharedContext.currentPresentationData.with { $0 } self.presentationData = presentationData @@ -106,14 +106,14 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode { self.themeAndStringsPromise = Promise((self.presentationData.theme, self.presentationData.strings)) - var selectStickerImpl: ((FileMediaReference, UIView, CGRect) -> Bool)? +// var selectStickerImpl: (((FileMediaReference, UIView, CGRect)?) -> Bool)? self.controllerInteraction = ChatControllerInteraction(openMessage: { _, _ in return false }, openPeer: { _, _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _, _ in }, openMessageReactionContextMenu: { _, _, _, _ in }, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in }, navigateToThreadMessage: { _, _, _ in - }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { fileReference, _, _, _, _, node, rect, _, _ in return selectStickerImpl?(fileReference, node, rect) ?? false }, sendEmoji: { _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in + }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { fileReference, _, _, _, _, node, rect, _, _ in return false }, sendEmoji: { _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in }, presentController: { _, _ in }, presentControllerInCurrent: { _, _ in }, navigationController: { @@ -743,9 +743,9 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode { self?.updatePaneDidScroll(pane: pane, state: state, transition: transition) } - selectStickerImpl = { [weak self] fileReference, node, rect in - return self?.selectSticker?(fileReference, node, rect) ?? false - } +// selectStickerImpl = { [weak self] fileReferenceNodeAndRect in +// return self?.selectSticker?(fileReferenceNodeAndRect) ?? false +// } self.segmentedControlNode.selectedIndexChanged = { [weak self] index in if let strongSelf = self { @@ -767,6 +767,7 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode { } @objc private func cancelPressed() { + let _ = self.selectSticker?(nil) self.animateOut() } @@ -1311,7 +1312,7 @@ final class DrawingStickersScreen: ViewController, TGPhotoPaintStickersScreen { public var screenWillDisappear: (() -> Void)? private let context: AccountContext - var selectSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? + var selectSticker: (((FileMediaReference, UIView, CGRect)?) -> Bool)? private var controllerNode: DrawingStickersScreenNode { return self.displayNode as! DrawingStickersScreenNode @@ -1327,7 +1328,7 @@ final class DrawingStickersScreen: ViewController, TGPhotoPaintStickersScreen { return self._ready } - public init(context: AccountContext, selectSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? = nil) { + public init(context: AccountContext, selectSticker: (((FileMediaReference, UIView, CGRect)?) -> Bool)? = nil) { self.context = context self.selectSticker = selectSticker @@ -1367,10 +1368,10 @@ final class DrawingStickersScreen: ViewController, TGPhotoPaintStickersScreen { override public func loadDisplayNode() { self.displayNode = DrawingStickersScreenNode( context: self.context, - selectSticker: { [weak self] file, sourceView, sourceRect in + selectSticker: { [weak self] fileSourceViewSourceRect in if let strongSelf = self, let selectSticker = strongSelf.selectSticker { (strongSelf.displayNode as! DrawingStickersScreenNode).animateOut() - return selectSticker(file, sourceView, sourceRect) + return selectSticker(fileSourceViewSourceRect) } else { return false } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 21f64f24ab..563885fd6b 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -3811,8 +3811,10 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate transitionCompletion() }, presentStickers: { [weak self] completion in if let strongSelf = self { - let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, view, rect in - completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) + let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { result in + if let (fileReference, view, rect) = result { + completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) + } return true }) strongSelf.controller?.present(controller, in: .window(.root)) @@ -6910,10 +6912,12 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate let paintStickersContext = LegacyPaintStickersContext(context: strongSelf.context) paintStickersContext.presentStickersController = { completion in - let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, view, rect in - let coder = PostboxEncoder() - coder.encodeRootObject(fileReference.media) - completion?(coder.makeData(), fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) + let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { result in + if let (fileReference, view, rect) = result { + let coder = PostboxEncoder() + coder.encodeRootObject(fileReference.media) + completion?(coder.makeData(), fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) + } return true }) strongSelf.controller?.present(controller, in: .window(.root)) diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index b7739277b0..89bec87cd2 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1505,7 +1505,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { public func makeStickerPackScreen(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, mainStickerPack: StickerPackReference, stickerPacks: [StickerPackReference], loadedStickerPacks: [LoadedStickerPack], parentNavigationController: NavigationController?, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?) -> ViewController { return StickerPackScreen(context: context, updatedPresentationData: updatedPresentationData, mainStickerPack: mainStickerPack, stickerPacks: stickerPacks, loadedStickerPacks: loadedStickerPacks, parentNavigationController: parentNavigationController, sendSticker: sendSticker) } - + public func makeProxySettingsController(sharedContext: SharedAccountContext, account: UnauthorizedAccount) -> ViewController { return proxySettingsController(accountManager: sharedContext.accountManager, postbox: account.postbox, network: account.network, mode: .modal, presentationData: sharedContext.currentPresentationData.with { $0 }, updatedPresentationData: sharedContext.presentationData) } diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index cfe86921ea..72330d4912 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -15,7 +15,7 @@ import AppBundle import DatePickerNode import DebugSettingsUI import TabBarUI -import PremiumUI +import DrawingUI public final class TelegramRootController: NavigationController { private let context: AccountContext