Swiftgram/submodules/DrawingUI/Sources/DrawingScreen.swift
Ilya Laktyushin e5762bd9c8 Various fixes
2025-05-18 04:35:56 +04:00

3696 lines
170 KiB
Swift

import Foundation
import UIKit
import CoreServices
import AsyncDisplayKit
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 BlurredBackgroundComponent
import MultilineTextComponent
import ContextUI
import ChatEntityKeyboardInputNode
import EntityKeyboard
import TelegramUIPreferences
import FastBlur
import MediaEditor
import StickerPickerScreen
import ImageObjectSeparation
public struct DrawingResultData {
public let data: Data?
public let drawingImage: UIImage?
public let entities: [CodableDrawingEntity]
}
public enum DrawingToolState: Equatable, Codable {
private enum CodingKeys: String, CodingKey {
case type
case brushState
case eraserState
}
enum Key: Int32, RawRepresentable, CaseIterable, Codable {
case pen = 0
case arrow = 1
case marker = 2
case neon = 3
case blur = 4
case eraser = 5
}
public struct BrushState: Equatable, Codable {
private enum CodingKeys: String, CodingKey {
case color
case size
}
public let color: DrawingColor
public let size: CGFloat
public init(color: DrawingColor, size: CGFloat) {
self.color = color
self.size = size
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.color = try container.decode(DrawingColor.self, forKey: .color)
self.size = try container.decode(CGFloat.self, forKey: .size)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.color, forKey: .color)
try container.encode(self.size, forKey: .size)
}
func withUpdatedColor(_ color: DrawingColor) -> BrushState {
return BrushState(color: color, size: self.size)
}
func withUpdatedSize(_ size: CGFloat) -> BrushState {
return BrushState(color: self.color, size: size)
}
}
public struct EraserState: Equatable, Codable {
private enum CodingKeys: String, CodingKey {
case size
}
public let size: CGFloat
public init(size: CGFloat) {
self.size = size
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.size = try container.decode(CGFloat.self, forKey: .size)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.size, forKey: .size)
}
func withUpdatedSize(_ size: CGFloat) -> EraserState {
return EraserState(size: size)
}
}
case pen(BrushState)
case arrow(BrushState)
case marker(BrushState)
case neon(BrushState)
case blur(EraserState)
case eraser(EraserState)
public func withUpdatedColor(_ color: DrawingColor) -> DrawingToolState {
switch self {
case let .pen(state):
return .pen(state.withUpdatedColor(color))
case let .arrow(state):
return .arrow(state.withUpdatedColor(color))
case let .marker(state):
return .marker(state.withUpdatedColor(color))
case let .neon(state):
return .neon(state.withUpdatedColor(color))
case .blur, .eraser:
return self
}
}
public func withUpdatedSize(_ size: CGFloat) -> DrawingToolState {
switch self {
case let .pen(state):
return .pen(state.withUpdatedSize(size))
case let .arrow(state):
return .arrow(state.withUpdatedSize(size))
case let .marker(state):
return .marker(state.withUpdatedSize(size))
case let .neon(state):
return .neon(state.withUpdatedSize(size))
case let .blur(state):
return .blur(state.withUpdatedSize(size))
case let .eraser(state):
return .eraser(state.withUpdatedSize(size))
}
}
public var color: DrawingColor? {
switch self {
case let .pen(state), let .arrow(state), let .marker(state), let .neon(state):
return state.color
default:
return nil
}
}
public var size: CGFloat? {
switch self {
case let .pen(state), let .arrow(state), let .marker(state), let .neon(state):
return state.size
case let .blur(state), let .eraser(state):
return state.size
}
}
var key: DrawingToolState.Key {
switch self {
case .pen:
return .pen
case .arrow:
return .arrow
case .marker:
return .marker
case .neon:
return .neon
case .blur:
return .blur
case .eraser:
return .eraser
}
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let typeValue = try container.decode(Int32.self, forKey: .type)
if let type = DrawingToolState.Key(rawValue: typeValue) {
switch type {
case .pen:
self = .pen(try container.decode(BrushState.self, forKey: .brushState))
case .arrow:
self = .arrow(try container.decode(BrushState.self, forKey: .brushState))
case .marker:
self = .marker(try container.decode(BrushState.self, forKey: .brushState))
case .neon:
self = .neon(try container.decode(BrushState.self, forKey: .brushState))
case .blur:
self = .blur(try container.decode(EraserState.self, forKey: .eraserState))
case .eraser:
self = .eraser(try container.decode(EraserState.self, forKey: .eraserState))
}
} else {
self = .pen(BrushState(color: DrawingColor(rgb: 0x000000), size: 0.5))
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case let .pen(state):
try container.encode(DrawingToolState.Key.pen.rawValue, forKey: .type)
try container.encode(state, forKey: .brushState)
case let .arrow(state):
try container.encode(DrawingToolState.Key.arrow.rawValue, forKey: .type)
try container.encode(state, forKey: .brushState)
case let .marker(state):
try container.encode(DrawingToolState.Key.marker.rawValue, forKey: .type)
try container.encode(state, forKey: .brushState)
case let .neon(state):
try container.encode(DrawingToolState.Key.neon.rawValue, forKey: .type)
try container.encode(state, forKey: .brushState)
case let .blur(state):
try container.encode(DrawingToolState.Key.blur.rawValue, forKey: .type)
try container.encode(state, forKey: .eraserState)
case let .eraser(state):
try container.encode(DrawingToolState.Key.eraser.rawValue, forKey: .type)
try container.encode(state, forKey: .eraserState)
}
}
}
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 .eraser(DrawingToolState.EraserState(size: 0.5))
}
func withUpdatedSelectedTool(_ selectedTool: DrawingToolState.Key) -> DrawingState {
return DrawingState(
selectedTool: selectedTool,
tools: self.tools
)
}
func withUpdatedTools(_ tools: [DrawingToolState]) -> DrawingState {
return DrawingState(
selectedTool: self.selectedTool,
tools: 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
)
}
static var initial: DrawingState {
return DrawingState(
selectedTool: .pen,
tools: [
.pen(DrawingToolState.BrushState(color: DrawingColor(rgb: 0xff453a), size: 0.23)),
.arrow(DrawingToolState.BrushState(color: DrawingColor(rgb: 0xff8a00), size: 0.23)),
.marker(DrawingToolState.BrushState(color: DrawingColor(rgb: 0xffd60a), size: 0.75)),
.neon(DrawingToolState.BrushState(color: DrawingColor(rgb: 0x34c759), size: 0.4)),
.blur(DrawingToolState.EraserState(size: 0.5)),
.eraser(DrawingToolState.EraserState(size: 0.5))
]
)
}
func forVideo() -> DrawingState {
return DrawingState(
selectedTool: self.selectedTool,
tools: self.tools.filter { tool in
if case .blur = tool {
return false
} else {
return true
}
}
)
}
}
final class DrawingSettings: Codable, Equatable {
let tools: [DrawingToolState]
let colors: [DrawingColor]
init(tools: [DrawingToolState], colors: [DrawingColor]) {
self.tools = tools
self.colors = colors
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: StringCodingKey.self)
if let data = try container.decodeIfPresent(Data.self, forKey: "tools"), let tools = try? JSONDecoder().decode([DrawingToolState].self, from: data) {
self.tools = tools
} else {
self.tools = DrawingState.initial.tools
}
if let data = try container.decodeIfPresent(Data.self, forKey: "colors"), let colors = try? JSONDecoder().decode([DrawingColor].self, from: data) {
self.colors = colors
} else {
self.colors = []
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: StringCodingKey.self)
if let data = try? JSONEncoder().encode(self.tools) {
try container.encode(data, forKey: "tools")
}
if let data = try? JSONEncoder().encode(self.colors) {
try container.encode(data, forKey: "colors")
}
}
static func ==(lhs: DrawingSettings, rhs: DrawingSettings) -> Bool {
return lhs.tools == rhs.tools && lhs.colors == rhs.colors
}
}
private final class ReferenceContentSource: ContextReferenceContentSource {
private let sourceView: UIView
private let contentArea: CGRect
private let customPosition: CGPoint
init(sourceView: UIView, contentArea: CGRect, customPosition: CGPoint) {
self.sourceView = sourceView
self.contentArea = contentArea
self.customPosition = customPosition
}
func transitionInfo() -> ContextControllerReferenceViewInfo? {
return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: self.contentArea, customPosition: self.customPosition, actionsPosition: .top)
}
}
private final class BlurredGradientComponent: Component {
enum Position {
case top
case bottom
}
let position: Position
let tag: AnyObject?
public init(
position: Position,
tag: AnyObject?
) {
self.position = position
self.tag = tag
}
public static func ==(lhs: BlurredGradientComponent, rhs: BlurredGradientComponent) -> Bool {
if lhs.position != rhs.position {
return false
}
return true
}
public final class View: BlurredBackgroundView, ComponentTaggedView {
private var component: BlurredGradientComponent?
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
}
private var gradientMask = UIImageView()
private var gradientForeground = SimpleGradientLayer()
public func update(component: BlurredGradientComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
self.component = component
self.isUserInteractionEnabled = false
self.updateColor(color: UIColor(rgb: 0x000000, alpha: component.position == .top ? 0.15 : 0.25), transition: transition.containedViewLayoutTransition)
if self.mask == nil {
self.mask = self.gradientMask
self.gradientMask.image = generateGradientImage(
size: CGSize(width: 1.0, height: availableSize.height),
colors: [UIColor(rgb: 0xffffff, alpha: 1.0), UIColor(rgb: 0xffffff, alpha: 1.0), UIColor(rgb: 0xffffff, alpha: 0.0)],
locations: component.position == .top ? [0.0, 0.8, 1.0] : [1.0, 0.5, 0.0],
direction: .vertical
)
self.gradientForeground.colors = [UIColor(rgb: 0x000000, alpha: 0.35).cgColor, UIColor(rgb: 0x000000, alpha: 0.0).cgColor]
self.gradientForeground.startPoint = CGPoint(x: 0.5, y: component.position == .top ? 0.0 : 1.0)
self.gradientForeground.endPoint = CGPoint(x: 0.5, y: component.position == .top ? 1.0 : 0.0)
self.layer.addSublayer(self.gradientForeground)
}
transition.setFrame(view: self.gradientMask, frame: CGRect(origin: .zero, size: availableSize))
transition.setFrame(layer: self.gradientForeground, frame: CGRect(origin: .zero, size: availableSize))
self.update(size: availableSize, transition: transition.containedViewLayoutTransition)
return availableSize
}
}
public func makeView() -> View {
return View(color: nil, enableBlur: true)
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
enum DrawingScreenTransition {
case animateIn
case animateOut
}
private let topGradientTag = GenericComponentViewTag()
private let bottomGradientTag = GenericComponentViewTag()
private let undoButtonTag = GenericComponentViewTag()
private let redoButtonTag = GenericComponentViewTag()
private let clearAllButtonTag = GenericComponentViewTag()
private let colorButtonTag = GenericComponentViewTag()
private let addButtonTag = GenericComponentViewTag()
private let toolsTag = GenericComponentViewTag()
private let modeTag = GenericComponentViewTag()
private let flipButtonTag = GenericComponentViewTag()
private let fillButtonTag = GenericComponentViewTag()
private let zoomOutButtonTag = GenericComponentViewTag()
private let textSettingsTag = GenericComponentViewTag()
private let sizeSliderTag = GenericComponentViewTag()
private let fontTag = GenericComponentViewTag()
private let color1Tag = GenericComponentViewTag()
private let color2Tag = GenericComponentViewTag()
private let color3Tag = GenericComponentViewTag()
private let color4Tag = GenericComponentViewTag()
private let color5Tag = GenericComponentViewTag()
private let color6Tag = GenericComponentViewTag()
private let color7Tag = GenericComponentViewTag()
private let color8Tag = GenericComponentViewTag()
private let colorTags = [color1Tag, color2Tag, color3Tag, color4Tag, color5Tag, color6Tag, color7Tag, color8Tag]
private let cancelButtonTag = GenericComponentViewTag()
private let doneButtonTag = GenericComponentViewTag()
private final class DrawingScreenComponent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let sourceHint: DrawingScreen.SourceHint?
let existingStickerPickerInputData: Promise<StickerPickerInput>?
let isVideo: Bool
let isAvatar: Bool
let isInteractingWithEntities: Bool
let present: (ViewController) -> Void
let updateState: ActionSlot<DrawingView.NavigationState>
let updateColor: ActionSlot<DrawingColor>
let performAction: ActionSlot<DrawingView.Action>
let updateToolState: ActionSlot<DrawingToolState>
let updateSelectedEntity: ActionSlot<DrawingEntity?>
let insertEntity: ActionSlot<DrawingEntity>
let deselectEntity: ActionSlot<Void>
let updateEntitiesPlayback: ActionSlot<Bool>
let previewBrushSize: ActionSlot<CGFloat?>
let dismissEyedropper: ActionSlot<Void>
let requestPresentColorPicker: ActionSlot<Void>
let toggleWithEraser: ActionSlot<Void>
let toggleWithPreviousTool: ActionSlot<Void>
let insertSticker: ActionSlot<Void>
let insertText: ActionSlot<Void>
let updateEntityView: ActionSlot<(UUID, Bool)>
let endEditingTextEntityView: ActionSlot<(UUID, Bool)>
let entityViewForEntity: (DrawingEntity) -> DrawingEntityView?
let presentGallery: (() -> Void)?
let apply: ActionSlot<Void>
let dismiss: ActionSlot<Void>
let presentColorPicker: (DrawingColor) -> Void
let presentFastColorPicker: (UIView) -> Void
let updateFastColorPickerPan: (CGPoint) -> Void
let dismissFastColorPicker: () -> Void
let presentFontPicker: (UIView) -> Void
init(
context: AccountContext,
sourceHint: DrawingScreen.SourceHint?,
existingStickerPickerInputData: Promise<StickerPickerInput>?,
isVideo: Bool,
isAvatar: Bool,
isInteractingWithEntities: Bool,
present: @escaping (ViewController) -> Void,
updateState: ActionSlot<DrawingView.NavigationState>,
updateColor: ActionSlot<DrawingColor>,
performAction: ActionSlot<DrawingView.Action>,
updateToolState: ActionSlot<DrawingToolState>,
updateSelectedEntity: ActionSlot<DrawingEntity?>,
insertEntity: ActionSlot<DrawingEntity>,
deselectEntity: ActionSlot<Void>,
updateEntitiesPlayback: ActionSlot<Bool>,
previewBrushSize: ActionSlot<CGFloat?>,
dismissEyedropper: ActionSlot<Void>,
requestPresentColorPicker: ActionSlot<Void>,
toggleWithEraser: ActionSlot<Void>,
toggleWithPreviousTool: ActionSlot<Void>,
insertSticker: ActionSlot<Void>,
insertText: ActionSlot<Void>,
updateEntityView: ActionSlot<(UUID, Bool)>,
endEditingTextEntityView: ActionSlot<(UUID, Bool)>,
entityViewForEntity: @escaping (DrawingEntity) -> DrawingEntityView?,
presentGallery: (() -> Void)?,
apply: ActionSlot<Void>,
dismiss: ActionSlot<Void>,
presentColorPicker: @escaping (DrawingColor) -> Void,
presentFastColorPicker: @escaping (UIView) -> Void,
updateFastColorPickerPan: @escaping (CGPoint) -> Void,
dismissFastColorPicker: @escaping () -> Void,
presentFontPicker: @escaping (UIView) -> Void
) {
self.context = context
self.sourceHint = sourceHint
self.existingStickerPickerInputData = existingStickerPickerInputData
self.isVideo = isVideo
self.isAvatar = isAvatar
self.isInteractingWithEntities = isInteractingWithEntities
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.updateEntitiesPlayback = updateEntitiesPlayback
self.previewBrushSize = previewBrushSize
self.dismissEyedropper = dismissEyedropper
self.requestPresentColorPicker = requestPresentColorPicker
self.toggleWithEraser = toggleWithEraser
self.toggleWithPreviousTool = toggleWithPreviousTool
self.insertSticker = insertSticker
self.insertText = insertText
self.updateEntityView = updateEntityView
self.endEditingTextEntityView = endEditingTextEntityView
self.entityViewForEntity = entityViewForEntity
self.presentGallery = presentGallery
self.apply = apply
self.dismiss = dismiss
self.presentColorPicker = presentColorPicker
self.presentFastColorPicker = presentFastColorPicker
self.updateFastColorPickerPan = updateFastColorPickerPan
self.dismissFastColorPicker = dismissFastColorPicker
self.presentFontPicker = presentFontPicker
}
static func ==(lhs: DrawingScreenComponent, rhs: DrawingScreenComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.isVideo != rhs.isVideo {
return false
}
if lhs.isAvatar != rhs.isAvatar {
return false
}
if lhs.isInteractingWithEntities != rhs.isInteractingWithEntities {
return false
}
return true
}
final class State: ComponentState {
enum ImageKey: Hashable {
case undo
case redo
case done
case add
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 .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<DrawingToolState>
private let insertEntity: ActionSlot<DrawingEntity>
private let deselectEntity: ActionSlot<Void>
private let updateEntitiesPlayback: ActionSlot<Bool>
private let dismissEyedropper: ActionSlot<Void>
private let toggleWithEraser: ActionSlot<Void>
private let toggleWithPreviousTool: ActionSlot<Void>
private let insertSticker: ActionSlot<Void>
private let insertText: ActionSlot<Void>
fileprivate var presentGallery: (() -> Void)?
private let updateEntityView: ActionSlot<(UUID, Bool)>
private let endEditingTextEntityView: ActionSlot<(UUID, Bool)>
private let entityViewForEntity: (DrawingEntity) -> DrawingEntityView?
private let present: (ViewController) -> Void
var currentMode: Mode
var drawingState: DrawingState
var drawingViewState: DrawingView.NavigationState
var currentColor: DrawingColor
var selectedEntity: DrawingEntity?
var lastSize: CGFloat = 0.5
private let stickerPickerInputData: Promise<StickerPickerInput>
init(
context: AccountContext,
existingStickerPickerInputData: Promise<StickerPickerInput>?,
updateToolState: ActionSlot<DrawingToolState>,
insertEntity: ActionSlot<DrawingEntity>,
deselectEntity: ActionSlot<Void>,
updateEntitiesPlayback: ActionSlot<Bool>,
dismissEyedropper: ActionSlot<Void>,
toggleWithEraser: ActionSlot<Void>,
toggleWithPreviousTool: ActionSlot<Void>,
insertSticker: ActionSlot<Void>,
insertText: ActionSlot<Void>,
presentGallery: (() -> Void)?,
updateEntityView: ActionSlot<(UUID, Bool)>,
endEditingTextEntityView: ActionSlot<(UUID, Bool)>,
entityViewForEntity: @escaping (DrawingEntity) -> DrawingEntityView?,
present: @escaping (ViewController) -> Void)
{
self.context = context
self.updateToolState = updateToolState
self.insertEntity = insertEntity
self.deselectEntity = deselectEntity
self.updateEntitiesPlayback = updateEntitiesPlayback
self.dismissEyedropper = dismissEyedropper
self.toggleWithEraser = toggleWithEraser
self.toggleWithPreviousTool = toggleWithPreviousTool
self.insertSticker = insertSticker
self.insertText = insertText
self.presentGallery = presentGallery
self.updateEntityView = updateEntityView
self.endEditingTextEntityView = endEditingTextEntityView
self.entityViewForEntity = entityViewForEntity
self.present = present
self.currentMode = .drawing
self.drawingState = .initial
self.drawingViewState = DrawingView.NavigationState(canUndo: false, canRedo: false, canClear: false, canZoomOut: false, isDrawing: false)
self.currentColor = self.drawingState.tools.first?.color ?? DrawingColor(rgb: 0xffffff)
self.updateToolState.invoke(self.drawingState.currentToolState)
if let existingStickerPickerInputData {
self.stickerPickerInputData = existingStickerPickerInputData
} else {
self.stickerPickerInputData = Promise<StickerPickerInput>()
let stickerPickerInputData = self.stickerPickerInputData
Queue.concurrentDefaultQueue().after(0.5, {
let emojiItems = EmojiPagerContentComponent.emojiInputData(
context: context,
animationCache: context.animationCache,
animationRenderer: context.animationRenderer,
isStandalone: false,
subject: .emoji,
hasTrending: false,
topReactionItems: [],
areUnicodeEmojiEnabled: true,
areCustomEmojiEnabled: true,
chatPeerId: context.account.peerId,
hasSearch: true,
forceHasPremium: true
)
let stickerItems = 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,
hasSearch: true,
hasTrending: true,
forceHasPremium: true
)
let signal = combineLatest(queue: .mainQueue(),
emojiItems,
stickerItems
) |> map { emoji, stickers -> StickerPickerInput in
return StickerPickerInputData(emoji: emoji, stickers: stickers, gifs: nil)
}
stickerPickerInputData.set(signal)
})
}
super.init()
self.loadToolState()
self.toggleWithEraser.connect { [weak self] _ in
if let self {
if self.drawingState.selectedTool == .eraser {
self.updateSelectedTool(self.nextToEraserTool)
} else {
self.updateSelectedTool(.eraser)
}
}
}
self.toggleWithPreviousTool.connect { [weak self] _ in
if let self {
self.updateSelectedTool(self.previousTool)
}
}
self.insertText.connect { [weak self] _ in
if let self {
self.addTextEntity()
}
}
self.insertSticker.connect { [weak self] _ in
if let self {
self.presentStickerPicker()
}
}
}
func loadToolState() {
let _ = (self.context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.drawingSettings])
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] sharedData in
guard let strongSelf = self else {
return
}
if let drawingSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.drawingSettings]?.get(DrawingSettings.self) {
strongSelf.drawingState = strongSelf.drawingState.withUpdatedTools(drawingSettings.tools)
strongSelf.currentColor = strongSelf.drawingState.currentToolState.color ?? strongSelf.currentColor
strongSelf.updated(transition: .immediate)
strongSelf.updateToolState.invoke(strongSelf.drawingState.currentToolState)
}
})
}
func saveToolState() {
let tools = self.drawingState.tools
let _ = (self.context.sharedContext.accountManager.transaction { transaction -> Void in
transaction.updateSharedData(ApplicationSpecificSharedDataKeys.drawingSettings, { _ in
return PreferencesEntry(DrawingSettings(tools: tools, colors: []))
})
}).start()
}
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, let selectedEntityView = self.entityViewForEntity(selectedEntity) {
if let textEntity = selectedEntity as? DrawingTextEntity, let textEntityView = selectedEntityView as? DrawingTextEntityView {
textEntity.setColor(color, range: textEntityView.selectedRange)
textEntityView.update(animated: false, keepSelectedRange: true)
} else {
selectedEntity.color = color
selectedEntityView.update(animated: false)
}
} else {
self.drawingState = self.drawingState.withUpdatedColor(color)
self.updateToolState.invoke(self.drawingState.currentToolState)
}
self.updated(transition: animated ? .easeInOut(duration: 0.2) : .immediate)
}
var previousTool: DrawingToolState.Key = .eraser
var nextToEraserTool: DrawingToolState.Key = .pen
func updateSelectedTool(_ tool: DrawingToolState.Key, update: Bool = true) {
if self.selectedEntity != nil {
self.skipSelectedEntityUpdate = true
self.updateCurrentMode(.drawing, update: false)
self.skipSelectedEntityUpdate = false
}
if tool != self.drawingState.selectedTool {
if self.drawingState.selectedTool == .eraser {
self.nextToEraserTool = tool
} else if tool == .eraser {
self.nextToEraserTool = self.drawingState.selectedTool
}
self.previousTool = self.drawingState.selectedTool
}
self.drawingState = self.drawingState.withUpdatedSelectedTool(tool)
self.currentColor = self.drawingState.currentToolState.color ?? self.currentColor
self.updateToolState.invoke(self.drawingState.currentToolState)
if update {
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
}
self.updateEntityView.invoke((selectedEntity.uuid, false))
} else {
self.drawingState = self.drawingState.withUpdatedSize(size)
self.updateToolState.invoke(self.drawingState.currentToolState)
}
self.updated(transition: .immediate)
}
func updateDrawingState(_ state: DrawingView.NavigationState) {
self.drawingViewState = state
self.updated(transition: .easeInOut(duration: 0.2))
}
var skipSelectedEntityUpdate = false
func updateSelectedEntity(_ entity: DrawingEntity?) {
self.dismissEyedropper.invoke(Void())
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
}
if !self.skipSelectedEntityUpdate {
self.updated(transition: .easeInOut(duration: 0.2))
}
}
func presentShapePicker(_ sourceView: UIView) {
let strings = self.context.sharedContext.currentPresentationData.with { $0 }.strings
let items: [ContextMenuItem] = [
.action(
ContextMenuActionItem(
text: strings.Paint_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: strings.Paint_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: strings.Paint_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: strings.Paint_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: strings.Paint_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.3))
}
}
)
)
]
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme)
let contextController = ContextController(presentationData: presentationData, source: .reference(ReferenceContentSource(sourceView: sourceView, contentArea: UIScreen.main.bounds, customPosition: CGPoint(x: 7.0, y: 3.0))), items: .single(ContextController.Items(content: .list(items))))
self.present(contextController)
}
func updateCurrentMode(_ mode: Mode, update: Bool = true) {
self.currentMode = mode
if let selectedEntity = self.selectedEntity {
if selectedEntity is DrawingStickerEntity || selectedEntity is DrawingTextEntity {
self.deselectEntity.invoke(Void())
}
}
if update {
self.updated(transition: .easeInOut(duration: 0.2))
}
}
func addTextEntity() {
let textEntity = DrawingTextEntity(text: NSAttributedString(), style: .filled, animation: .none, font: .sanFrancisco, alignment: .center, fontSize: 1.0, color: DrawingColor(color: .white))
self.insertEntity.invoke(textEntity)
}
func presentStickerPicker() {
self.currentMode = .sticker
self.updateEntitiesPlayback.invoke(false)
let controller = StickerPickerScreen(context: self.context, inputData: self.stickerPickerInputData.get(), forceDark: true, hasInteractiveStickers: false)
if let presentGallery = self.presentGallery {
controller.presentGallery = presentGallery
}
controller.completion = { [weak self] content in
self?.updateEntitiesPlayback.invoke(true)
if let content {
let stickerEntity = DrawingStickerEntity(content: content)
self?.insertEntity.invoke(stickerEntity)
} else {
self?.updateCurrentMode(.drawing)
}
return true
}
self.present(controller)
self.updated(transition: .easeInOut(duration: 0.2))
}
}
func makeState() -> State {
return State(
context: self.context,
existingStickerPickerInputData: self.existingStickerPickerInputData,
updateToolState: self.updateToolState,
insertEntity: self.insertEntity,
deselectEntity: self.deselectEntity,
updateEntitiesPlayback: self.updateEntitiesPlayback,
dismissEyedropper: self.dismissEyedropper,
toggleWithEraser: self.toggleWithEraser,
toggleWithPreviousTool: self.toggleWithPreviousTool,
insertSticker: self.insertSticker,
insertText: self.insertText,
presentGallery: self.presentGallery,
updateEntityView: self.updateEntityView,
endEditingTextEntityView: self.endEditingTextEntityView,
entityViewForEntity: self.entityViewForEntity,
present: self.present
)
}
static var body: Body {
let topGradient = Child(BlurredGradientComponent.self)
let bottomGradient = Child(BlurredGradientComponent.self)
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 swatch8Button = Child(ColorSwatchComponent.self)
let addButton = Child(Button.self)
let flipButton = Child(Button.self)
let fillButton = Child(Button.self)
let backButton = Child(Button.self)
let doneButton = Child(Button.self)
let textSize = Child(TextSizeSliderComponent.self)
let textCancelButton = Child(Button.self)
let textDoneButton = Child(Button.self)
let presetColors: [DrawingColor] = [
DrawingColor(rgb: 0xff453a),
DrawingColor(rgb: 0xff8a00),
DrawingColor(rgb: 0xffd60a),
DrawingColor(rgb: 0x34c759),
DrawingColor(rgb: 0x63e6e2),
DrawingColor(rgb: 0x0a84ff),
DrawingColor(rgb: 0xbf5af2),
DrawingColor(rgb: 0xffffff)
]
return { context in
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
let component = context.component
let state = context.state
let controller = environment.controller
let strings = environment.strings
let previewBrushSize = component.previewBrushSize
let performAction = component.performAction
let dismissEyedropper = component.dismissEyedropper
let apply = component.apply
let dismiss = component.dismiss
let presentColorPicker = component.presentColorPicker
let presentFastColorPicker = component.presentFastColorPicker
let updateFastColorPickerPan = component.updateFastColorPickerPan
let dismissFastColorPicker = component.dismissFastColorPicker
let presentFontPicker = component.presentFontPicker
let updateEntityView = component.updateEntityView
let endEditingTextEntityView = component.endEditingTextEntityView
state.presentGallery = component.presentGallery
component.updateState.connect { [weak state] updatedState in
state?.updateDrawingState(updatedState)
}
component.updateColor.connect { [weak state] color in
if let state = state {
if [.eraser, .blur].contains(state.drawingState.selectedTool) || state.selectedEntity is DrawingStickerEntity {
state.updateSelectedTool(.pen, update: false)
state.updateColor(color, animated: true)
} else {
state.updateColor(color)
}
}
}
component.updateSelectedEntity.connect { [weak state] entity in
state?.updateSelectedEntity(entity)
}
component.requestPresentColorPicker.connect { [weak state] _ in
if let state = state {
presentColorPicker(state.currentColor)
}
}
var controlsAreVisible = true
if state.drawingViewState.isDrawing || component.isInteractingWithEntities {
controlsAreVisible = false
}
var controlsBottomInset: CGFloat = 0.0
let previewSize: CGSize
var previewTopInset: CGFloat = environment.statusBarHeight + 5.0
if case .regular = environment.metrics.widthClass {
let previewHeight = context.availableSize.height - previewTopInset - 75.0
previewSize = CGSize(width: floorToScreenPixels(previewHeight / 1.77778), height: previewHeight)
} else {
previewSize = CGSize(width: context.availableSize.width, height: floorToScreenPixels(context.availableSize.width * 1.77778))
if context.availableSize.height < previewSize.height + 30.0 {
previewTopInset = 0.0
controlsBottomInset = -50.0
}
}
let previewBottomInset = context.availableSize.height - previewSize.height - previewTopInset
var topInset = environment.safeInsets.top + 31.0
if component.sourceHint == .storyEditor {
topInset = previewTopInset + 31.0
}
let bottomInset: CGFloat = environment.inputHeight > 0.0 ? environment.inputHeight : 145.0
var leftEdge: CGFloat = environment.safeInsets.left
var rightEdge: CGFloat = context.availableSize.width - environment.safeInsets.right
var availableWidth = context.availableSize.width
if case .regular = environment.metrics.widthClass {
availableWidth = 430.0
leftEdge = floorToScreenPixels((context.availableSize.width - availableWidth) / 2.0)
rightEdge = floorToScreenPixels((context.availableSize.width - availableWidth) / 2.0) + availableWidth
}
if component.sourceHint != .storyEditor {
let topGradient = topGradient.update(
component: BlurredGradientComponent(
position: .top,
tag: topGradientTag
),
availableSize: CGSize(width: context.availableSize.width, height: topInset + 15.0),
transition: .immediate
)
context.add(topGradient
.position(CGPoint(x: context.availableSize.width / 2.0, y: topGradient.size.height / 2.0))
)
}
let bottomGradient = bottomGradient.update(
component: BlurredGradientComponent(
position: .bottom,
tag: bottomGradientTag
),
availableSize: CGSize(width: context.availableSize.width, height: 155.0),
transition: .immediate
)
context.add(bottomGradient
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomGradient.size.height / 2.0))
.opacity(controlsAreVisible ? 1.0 : 0.0)
)
var additionalBottomInset: CGFloat = 0.0
if component.sourceHint == .storyEditor {
additionalBottomInset = max(0.0, previewBottomInset - environment.safeInsets.bottom - 49.0)
}
if let textEntity = state.selectedEntity as? DrawingTextEntity {
let textSettings = textSettings.update(
component: TextSettingsComponent(
color: nil,
style: DrawingTextStyle(style: textEntity.style),
animation: DrawingTextAnimation(animation: textEntity.animation),
alignment: DrawingTextAlignment(alignment: textEntity.alignment),
font: DrawingTextFont(font: textEntity.font),
isEmojiKeyboard: false,
tag: textSettingsTag,
fontTag: fontTag,
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 = .regular
case .stroke:
nextStyle = .regular
case .blur:
nextStyle = .regular
}
textEntity.style = nextStyle
updateEntityView.invoke((textEntity.uuid, false))
state?.updated(transition: .easeInOut(duration: 0.2))
},
toggleAnimation: { [weak state, weak textEntity] in
guard let textEntity = textEntity else {
return
}
var nextAnimation: DrawingTextEntity.Animation
switch textEntity.animation {
case .none:
nextAnimation = .typing
case .typing:
nextAnimation = .wiggle
case .wiggle:
nextAnimation = .zoomIn
case .zoomIn:
nextAnimation = .none
}
textEntity.animation = nextAnimation
updateEntityView.invoke((textEntity.uuid, false))
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
updateEntityView.invoke((textEntity.uuid, false))
state?.updated(transition: .easeInOut(duration: 0.2))
},
presentFontPicker: {
if let controller = controller() as? DrawingScreen, let buttonView = controller.node.componentHost.findTaggedView(tag: fontTag) {
presentFontPicker(buttonView)
}
},
toggleKeyboard: nil
),
availableSize: CGSize(width: availableWidth - 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 - 89.0 - additionalBottomInset))
.appear(ComponentTransition.Appear({ _, view, transition in
if let view = view as? TextSettingsComponent.View, !transition.animation.isImmediate {
view.animateIn()
}
}))
.disappear(ComponentTransition.Disappear({ view, transition, completion in
if let view = view as? TextSettingsComponent.View, !transition.animation.isImmediate {
view.animateOut(completion: completion)
} else {
completion()
}
}))
.opacity(controlsAreVisible ? 1.0 : 0.0)
)
}
let rightButtonPosition = rightEdge - 24.0
var offsetX: CGFloat = leftEdge + 24.0
let delta: CGFloat = (rightButtonPosition - offsetX) / 7.0
let applySwatchColor: (DrawingColor) -> Void = { [weak state] color in
dismissEyedropper.invoke(Void())
if let state = state {
if [.eraser, .blur].contains(state.drawingState.selectedTool) || state.selectedEntity is DrawingStickerEntity {
state.updateSelectedTool(.pen, update: false)
}
state.updateColor(color, animated: true)
}
}
var currentColor: DrawingColor? = state.currentColor
if [.eraser, .blur].contains(state.drawingState.selectedTool) || state.selectedEntity is DrawingStickerEntity {
currentColor = nil
}
var delay: Double = 0.0
let swatch1Button = swatch1Button.update(
component: ColorSwatchComponent(
type: .pallete(currentColor == presetColors[0]),
color: presetColors[0],
tag: color1Tag,
action: {
applySwatchColor(presetColors[0])
}
),
availableSize: CGSize(width: 24.0, height: 24.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 - additionalBottomInset))
.appear(ComponentTransition.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(ComponentTransition.Disappear { view, transition, completion in
transition.setScale(view: view, scale: 0.1)
transition.setAlpha(view: view, alpha: 0.0, completion: { _ in
completion()
})
})
.opacity(controlsAreVisible ? 1.0 : 0.0)
)
offsetX += delta
let swatch2Button = swatch2Button.update(
component: ColorSwatchComponent(
type: .pallete(currentColor == presetColors[1]),
color: presetColors[1],
tag: color2Tag,
action: {
applySwatchColor(presetColors[1])
}
),
availableSize: CGSize(width: 24.0, height: 24.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 - additionalBottomInset))
.appear(ComponentTransition.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(ComponentTransition.Disappear { view, transition, completion in
transition.setScale(view: view, scale: 0.1)
transition.setAlpha(view: view, alpha: 0.0, completion: { _ in
completion()
})
})
.opacity(controlsAreVisible ? 1.0 : 0.0)
)
offsetX += delta
let swatch3Button = swatch3Button.update(
component: ColorSwatchComponent(
type: .pallete(currentColor == presetColors[2]),
color: presetColors[2],
tag: color3Tag,
action: {
applySwatchColor(presetColors[2])
}
),
availableSize: CGSize(width: 24.0, height: 24.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 - additionalBottomInset))
.appear(ComponentTransition.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(ComponentTransition.Disappear { view, transition, completion in
transition.setScale(view: view, scale: 0.1)
transition.setAlpha(view: view, alpha: 0.0, completion: { _ in
completion()
})
})
.opacity(controlsAreVisible ? 1.0 : 0.0)
)
offsetX += delta
let swatch4Button = swatch4Button.update(
component: ColorSwatchComponent(
type: .pallete(currentColor == presetColors[3]),
color: presetColors[3],
tag: color4Tag,
action: {
applySwatchColor(presetColors[3])
}
),
availableSize: CGSize(width: 24.0, height: 24.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 - additionalBottomInset))
.appear(ComponentTransition.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(ComponentTransition.Disappear { view, transition, completion in
transition.setScale(view: view, scale: 0.1)
transition.setAlpha(view: view, alpha: 0.0, completion: { _ in
completion()
})
})
.opacity(controlsAreVisible ? 1.0 : 0.0)
)
offsetX += delta
let swatch5Button = swatch5Button.update(
component: ColorSwatchComponent(
type: .pallete(currentColor == presetColors[4]),
color: presetColors[4],
tag: color5Tag,
action: {
applySwatchColor(presetColors[4])
}
),
availableSize: CGSize(width: 24.0, height: 24.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 - additionalBottomInset))
.appear(ComponentTransition.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(ComponentTransition.Disappear { view, transition, completion in
transition.setScale(view: view, scale: 0.1)
transition.setAlpha(view: view, alpha: 0.0, completion: { _ in
completion()
})
})
.opacity(controlsAreVisible ? 1.0 : 0.0)
)
offsetX += delta
delay += 0.025
let swatch6Button = swatch6Button.update(
component: ColorSwatchComponent(
type: .pallete(currentColor == presetColors[5]),
color: presetColors[5],
tag: color6Tag,
action: {
applySwatchColor(presetColors[5])
}
),
availableSize: CGSize(width: 24.0, height: 24.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 - additionalBottomInset))
.appear(ComponentTransition.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(ComponentTransition.Disappear { view, transition, completion in
transition.setScale(view: view, scale: 0.1)
transition.setAlpha(view: view, alpha: 0.0, completion: { _ in
completion()
})
})
.opacity(controlsAreVisible ? 1.0 : 0.0)
)
offsetX += delta
let swatch7Button = swatch7Button.update(
component: ColorSwatchComponent(
type: .pallete(currentColor == presetColors[6]),
color: presetColors[6],
tag: color7Tag,
action: {
applySwatchColor(presetColors[6])
}
),
availableSize: CGSize(width: 24.0, height: 24.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 - additionalBottomInset))
.appear(ComponentTransition.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(ComponentTransition.Disappear { view, transition, completion in
transition.setScale(view: view, scale: 0.1)
transition.setAlpha(view: view, alpha: 0.0, completion: { _ in
completion()
})
})
.opacity(controlsAreVisible ? 1.0 : 0.0)
)
offsetX += delta
let swatch8Button = swatch8Button.update(
component: ColorSwatchComponent(
type: .pallete(currentColor == presetColors[7]),
color: presetColors[7],
tag: color8Tag,
action: {
applySwatchColor(presetColors[7])
}
),
availableSize: CGSize(width: 24.0, height: 24.0),
transition: context.transition
)
context.add(swatch8Button
.position(CGPoint(x: offsetX, y: context.availableSize.height - environment.safeInsets.bottom - swatch7Button.size.height / 2.0 - 57.0 - additionalBottomInset))
.appear(ComponentTransition.Appear { _, view, transition in
transition.animateScale(view: view, from: 0.1, to: 1.0, delay: 0.175)
transition.animateAlpha(view: view, from: 0.0, to: 1.0, delay: 0.175)
})
.disappear(ComponentTransition.Disappear { view, transition, completion in
transition.setScale(view: view, scale: 0.1)
transition.setAlpha(view: view, alpha: 0.0, completion: { _ in
completion()
})
})
.opacity(controlsAreVisible ? 1.0 : 0.0)
)
if state.selectedEntity is DrawingStickerEntity || state.selectedEntity is DrawingTextEntity {
} else {
let tools = tools.update(
component: ToolsComponent(
state: component.isVideo || component.sourceHint == .storyEditor ? state.drawingState.forVideo() : state.drawingState,
isFocused: false,
tag: toolsTag,
toolPressed: { [weak state] tool in
dismissEyedropper.invoke(Void())
if let state = state {
state.updateSelectedTool(tool)
}
},
toolResized: { [weak state] _, size in
dismissEyedropper.invoke(Void())
state?.updateBrushSize(size)
if state?.selectedEntity == nil {
previewBrushSize.invoke(size)
}
},
sizeReleased: {
previewBrushSize.invoke(nil)
}
),
availableSize: CGSize(width: availableWidth - 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 - 78.0 - additionalBottomInset))
.appear(ComponentTransition.Appear({ _, view, transition in
if let view = view as? ToolsComponent.View, !transition.animation.isImmediate {
view.animateIn(completion: {})
}
}))
.disappear(ComponentTransition.Disappear({ view, transition, completion in
if let view = view as? ToolsComponent.View, !transition.animation.isImmediate {
view.animateOut(completion: completion)
} else {
completion()
}
}))
.opacity(controlsAreVisible ? 1.0 : 0.0)
)
}
var hasTopButtons = false
if let entity = state.selectedEntity {
var isFilled: Bool?
if let entity = entity as? DrawingSimpleShapeEntity {
isFilled = entity.drawType == .fill
} else if let entity = entity as? DrawingBubbleEntity {
isFilled = entity.drawType == .fill
} else if let _ = entity as? DrawingVectorEntity {
isFilled = false
}
var hasFlip = false
if state.selectedEntity is DrawingBubbleEntity || state.selectedEntity is DrawingStickerEntity {
hasFlip = true
}
hasTopButtons = isFilled != nil || hasFlip
if let isFilled = isFilled {
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
}
updateEntityView.invoke((entity.uuid, false))
} else if let entity = state.selectedEntity as? DrawingBubbleEntity {
if case .fill = entity.drawType {
entity.drawType = .stroke
} else {
entity.drawType = .fill
}
updateEntityView.invoke((entity.uuid, false))
} 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
}
updateEntityView.invoke((entity.uuid, false))
}
state.updated(transition: .easeInOut(duration: 0.2))
}
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(fillButtonTag),
availableSize: CGSize(width: 30.0, height: 30.0),
transition: .immediate
)
context.add(fillButton
.position(CGPoint(x: context.availableSize.width / 2.0 - (hasFlip ? 46.0 : 0.0), y: topInset))
.appear(.default(scale: true))
.disappear(.default(scale: true))
.opacity(!controlsAreVisible ? 0.0 : 1.0)
.shadow(component.sourceHint == .storyEditor ? Shadow(color: UIColor(rgb: 0x000000, alpha: 0.35), radius: 2.0, offset: .zero) : nil)
)
}
if hasFlip {
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
updateEntityView.invoke((entity.uuid, false))
} else if let entity = state.selectedEntity as? DrawingStickerEntity {
entity.mirrored = !entity.mirrored
updateEntityView.invoke((entity.uuid, true))
}
state.updated(transition: .easeInOut(duration: 0.2))
}
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(flipButtonTag),
availableSize: CGSize(width: 30.0, height: 30.0),
transition: .immediate
)
context.add(flipButton
.position(CGPoint(x: context.availableSize.width / 2.0 + (isFilled != nil ? 46.0 : 0.0), y: topInset))
.appear(.default(scale: true))
.disappear(.default(scale: true))
.opacity(!controlsAreVisible ? 0.0 : 1.0)
.shadow(component.sourceHint == .storyEditor ? Shadow(color: UIColor(rgb: 0x000000, alpha: 0.35), radius: 2.0, offset: .zero) : nil)
)
}
}
var sizeSliderVisible = false
var isEditingText = false
var sizeValue: CGFloat?
if let textEntity = state.selectedEntity as? DrawingTextEntity, let entityView = component.entityViewForEntity(textEntity) as? DrawingTextEntityView {
sizeSliderVisible = true
isEditingText = entityView.isEditing
sizeValue = textEntity.fontSize
} else {
if state.selectedEntity == nil || !(state.selectedEntity is DrawingStickerEntity) {
sizeSliderVisible = true
if state.selectedEntity == nil {
sizeValue = state.drawingState.currentToolState.size
} else if let entity = state.selectedEntity {
if let entity = entity as? DrawingSimpleShapeEntity {
sizeSliderVisible = entity.drawType == .stroke
} else if let entity = entity as? DrawingBubbleEntity {
sizeSliderVisible = entity.drawType == .stroke
}
sizeValue = entity.lineWidth
}
}
if state.drawingViewState.canZoomOut && !hasTopButtons {
let zoomOutButton = zoomOutButton.update(
component: Button(
content: AnyComponent(
ZoomOutButtonContent(
title: strings.Paint_ZoomOut,
image: state.image(.zoomOut)
)
),
action: {
dismissEyedropper.invoke(Void())
performAction.invoke(.zoomOut)
}
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(zoomOutButtonTag),
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))
)
}
}
if let sizeValue {
state.lastSize = sizeValue
}
if state.drawingViewState.isDrawing {
sizeSliderVisible = false
}
let textSize = textSize.update(
component: TextSizeSliderComponent(
value: sizeValue ?? state.lastSize,
tag: sizeSliderTag,
updated: { [weak state] size in
if let state = state {
dismissEyedropper.invoke(Void())
state.updateBrushSize(size)
if state.selectedEntity == nil {
previewBrushSize.invoke(size)
}
}
}, released: {
previewBrushSize.invoke(nil)
}
),
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 - bottomInset) / 2.0))
.opacity(sizeSliderVisible && controlsAreVisible ? 1.0 : 0.0)
)
let undoButton = undoButton.update(
component: Button(
content: AnyComponent(
Image(image: state.image(.undo))
),
isEnabled: state.drawingViewState.canUndo,
action: {
dismissEyedropper.invoke(Void())
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: topInset))
.scale(isEditingText ? 0.01 : 1.0)
.opacity(isEditingText || !controlsAreVisible ? 0.0 : 1.0)
.shadow(component.sourceHint == .storyEditor ? Shadow(color: UIColor(rgb: 0x000000, alpha: 0.35), radius: 2.0, offset: .zero) : nil)
)
let redoButton = redoButton.update(
component: Button(
content: AnyComponent(
Image(image: state.image(.redo))
),
action: {
dismissEyedropper.invoke(Void())
performAction.invoke(.redo)
}
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(redoButtonTag),
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: topInset))
.scale(state.drawingViewState.canRedo && !isEditingText ? 1.0 : 0.01)
.opacity(state.drawingViewState.canRedo && !isEditingText && controlsAreVisible ? 1.0 : 0.0)
.shadow(component.sourceHint == .storyEditor ? Shadow(color: UIColor(rgb: 0x000000, alpha: 0.35), radius: 2.0, offset: .zero) : nil)
)
let clearAllButton = clearAllButton.update(
component: Button(
content: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: strings.Paint_Clear, font: Font.regular(17.0), textColor: .white)),
textShadowColor: component.sourceHint == .storyEditor ? UIColor(rgb: 0x000000, alpha: 0.35) : nil,
textShadowBlur: 2.0
)
),
isEnabled: state.drawingViewState.canClear,
action: {
dismissEyedropper.invoke(Void())
performAction.invoke(.clear)
}
).tagged(clearAllButtonTag),
availableSize: CGSize(width: 180.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: topInset))
.scale(isEditingText ? 0.01 : 1.0)
.opacity(isEditingText || !controlsAreVisible ? 0.0 : 1.0)
)
let textCancelButton = textCancelButton.update(
component: Button(
content: AnyComponent(
Text(text: environment.strings.Common_Cancel, font: Font.regular(17.0), color: .white)
),
action: { [weak state] in
if let entity = state?.selectedEntity as? DrawingTextEntity {
endEditingTextEntityView.invoke((entity.uuid, 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: topInset))
.scale(isEditingText ? 1.0 : 0.01)
.opacity(isEditingText ? 1.0 : 0.0)
)
let textDoneButton = textDoneButton.update(
component: Button(
content: AnyComponent(
Text(text: environment.strings.Common_Done, font: Font.semibold(17.0), color: .white)
),
action: { [weak state] in
if let entity = state?.selectedEntity as? DrawingTextEntity {
endEditingTextEntityView.invoke((entity.uuid, false))
}
}
),
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: topInset))
.scale(isEditingText ? 1.0 : 0.01)
.opacity(isEditingText ? 1.0 : 0.0)
)
var color: DrawingColor?
if let entity = state.selectedEntity, presetColors.contains(entity.color) {
color = nil
} else if presetColors.contains(state.currentColor) {
color = nil
} else if state.selectedEntity is DrawingStickerEntity {
color = nil
} else if [.eraser, .blur].contains(state.drawingState.selectedTool) {
color = nil
} else {
color = state.currentColor
}
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: leftEdge + colorButton.size.width / 2.0 + 2.0, y: context.availableSize.height - environment.safeInsets.bottom - colorButton.size.height / 2.0 - 89.0 - additionalBottomInset))
.appear(.default(scale: true))
.disappear(.default(scale: true))
.opacity(controlsAreVisible ? 1.0 : 0.0)
)
let modeRightInset: CGFloat = 57.0
let addButton = addButton.update(
component: Button(
content: AnyComponent(ZStack([
AnyComponentWithIdentity(
id: "background",
component: AnyComponent(
BlurredBackgroundComponent(
color: UIColor(rgb: 0x888888, alpha: 0.3),
cornerRadius: 12.0
)
)
),
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:
dismissEyedropper.invoke(Void())
if let buttonView = controller.node.componentHost.findTaggedView(tag: addButtonTag) as? Button.View {
state.presentShapePicker(buttonView)
}
case .sticker:
dismissEyedropper.invoke(Void())
state.presentStickerPicker()
case .text:
dismissEyedropper.invoke(Void())
state.addTextEntity()
}
}
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(addButtonTag),
availableSize: CGSize(width: 24.0, height: 24.0),
transition: .immediate
)
context.add(addButton
.position(CGPoint(x: rightEdge - addButton.size.width / 2.0 - 2.0, y: context.availableSize.height - environment.safeInsets.bottom - addButton.size.height / 2.0 - 89.0 - additionalBottomInset))
.appear(.default(scale: true))
.disappear(.default(scale: true))
.cornerRadius(12.0)
.opacity(controlsAreVisible ? 1.0 : 0.0)
)
let doneButton = doneButton.update(
component: Button(
content: AnyComponent(
Image(image: state.image(.done))
),
action: { [weak state] in
dismissEyedropper.invoke(Void())
state?.saveToolState()
apply.invoke(Void())
}
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(doneButtonTag),
availableSize: CGSize(width: 33.0, height: 33.0),
transition: .immediate
)
var doneButtonPosition = 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)
if component.sourceHint == .storyEditor {
doneButtonPosition.x = doneButtonPosition.x - 2.0
if case .regular = environment.metrics.widthClass {
doneButtonPosition.x -= 20.0
}
doneButtonPosition.y = floorToScreenPixels(context.availableSize.height - previewBottomInset + 3.0 + doneButton.size.height / 2.0) + controlsBottomInset
}
context.add(doneButton
.position(doneButtonPosition)
.appear(ComponentTransition.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(ComponentTransition.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)
})
.opacity(controlsAreVisible ? 1.0 : 0.0)
)
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: [ strings.Paint_Draw, strings.Paint_Sticker, strings.Paint_Text],
sizeValue: selectedSize,
isEditing: false,
isEnabled: true,
rightInset: modeRightInset - 57.0,
tag: modeTag,
selectedIndex: selectedIndex,
selectionChanged: { [weak state] index in
dismissEyedropper.invoke(Void())
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 {
dismissEyedropper.invoke(Void())
state.updateBrushSize(size)
if state.selectedEntity == nil {
previewBrushSize.invoke(size)
}
}
},
sizeReleased: {
previewBrushSize.invoke(nil)
}
),
availableSize: CGSize(width: availableWidth - 57.0 - modeRightInset, height: context.availableSize.height),
transition: context.transition
)
var modeAndSizePosition = 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)
if component.sourceHint == .storyEditor {
modeAndSizePosition.y = floorToScreenPixels(context.availableSize.height - previewBottomInset + 8.0 + modeAndSize.size.height / 2.0) + controlsBottomInset
}
context.add(modeAndSize
.position(modeAndSizePosition)
.opacity(controlsAreVisible ? 1.0 : 0.0)
)
var animatingOut = false
if let appearanceTransition = context.transition.userData(DrawingScreenTransition.self), case .animateOut = appearanceTransition {
animatingOut = true
}
if animatingOut && component.sourceHint == .storyEditor {
} else {
let backButton = backButton.update(
component: Button(
content: AnyComponent(
LottieAnimationComponent(
animation: LottieAnimationComponent.AnimationItem(
name: "media_backToCancel",
mode: .animating(loop: false),
range: animatingOut || component.isAvatar ? (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 {
dismissEyedropper.invoke(Void())
state.saveToolState()
dismiss.invoke(Void())
}
}
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(cancelButtonTag),
availableSize: CGSize(width: 33.0, height: 33.0),
transition: .immediate
)
var backButtonPosition = 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)
if component.sourceHint == .storyEditor {
backButtonPosition.x = backButtonPosition.x + 2.0
if case .regular = environment.metrics.widthClass {
backButtonPosition.x += 20.0
}
backButtonPosition.y = floorToScreenPixels(context.availableSize.height - previewBottomInset + 3.0 + backButton.size.height / 2.0) + controlsBottomInset
}
context.add(backButton
.position(backButtonPosition)
.opacity(controlsAreVisible ? 1.0 : 0.0)
)
}
return context.availableSize
}
}
}
public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, UIDropInteractionDelegate {
fileprivate final class Node: ViewControllerTracingNode {
private weak var controller: DrawingScreen?
private let context: AccountContext
private var interaction: DrawingToolsInteraction?
private let updateState: ActionSlot<DrawingView.NavigationState>
private let updateColor: ActionSlot<DrawingColor>
private let performAction: ActionSlot<DrawingView.Action>
private let updateToolState: ActionSlot<DrawingToolState>
private let updateSelectedEntity: ActionSlot<DrawingEntity?>
fileprivate let insertEntity: ActionSlot<DrawingEntity>
private let deselectEntity: ActionSlot<Void>
private let updateEntitiesPlayback: ActionSlot<Bool>
private let previewBrushSize: ActionSlot<CGFloat?>
private let dismissEyedropper: ActionSlot<Void>
private let requestPresentColorPicker: ActionSlot<Void>
private let toggleWithEraser: ActionSlot<Void>
private let toggleWithPreviousTool: ActionSlot<Void>
fileprivate let insertSticker: ActionSlot<Void>
fileprivate let insertText: ActionSlot<Void>
private let updateEntityView: ActionSlot<(UUID, Bool)>
private let endEditingTextEntityView: ActionSlot<(UUID, Bool)>
private var isInteractingWithEntities = false
private let apply: ActionSlot<Void>
private let dismiss: ActionSlot<Void>
fileprivate let componentHost: ComponentView<ViewControllerComponentContainer.Environment>
private var presentationData: PresentationData
private let hapticFeedback = HapticFeedback()
private var validLayout: (ContainerViewLayout, UIInterfaceOrientation?)?
var _drawingView: DrawingView?
var drawingView: DrawingView {
if self._drawingView == nil, let controller = self.controller {
if let externalDrawingView = controller.externalDrawingView {
self._drawingView = externalDrawingView
} else {
self._drawingView = DrawingView(size: controller.size)
}
self._drawingView?.animationsEnabled = self.context.sharedContext.energyUsageSettings.fullTranslucency
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?.requestedColorPicker = { [weak self] in
if let self, let interaction = self.interaction {
if let _ = interaction.colorPickerScreen {
interaction.dismissColorPicker()
} else {
self.requestPresentColorPicker.invoke(Void())
}
}
}
self._drawingView?.requestedEraserToggle = { [weak self] in
if let self {
self.toggleWithEraser.invoke(Void())
}
}
self._drawingView?.requestedToolsToggle = { [weak self] in
if let self {
self.toggleWithPreviousTool.invoke(Void())
}
}
self.performAction.connect { [weak self] action in
if let self {
if case .clear = action {
let actionSheet = ActionSheetController(presentationData: self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme))
actionSheet.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: self.presentationData.strings.Paint_ClearConfirm, color: .destructive, action: { [weak actionSheet, weak self] in
actionSheet?.dismissAnimated()
self?._drawingView?.performAction(action)
})
]),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])
])
self.controller?.present(actionSheet, in: .window(.root))
} else {
self._drawingView?.performAction(action)
}
}
}
self.updateToolState.connect { [weak self] state in
if let self {
self._drawingView?.updateToolState(state)
}
}
self.previewBrushSize.connect { [weak self] size in
if let self {
self._drawingView?.setBrushSizePreview(size)
}
}
self.dismissEyedropper.connect { [weak self] in
if let self {
self.interaction?.dismissCurrentEyedropper()
}
}
}
return self._drawingView!
}
var _entitiesView: DrawingEntitiesView?
var entitiesView: DrawingEntitiesView {
if self._entitiesView == nil, let controller = self.controller {
if let externalEntitiesView = controller.externalEntitiesView {
self._entitiesView = externalEntitiesView
} else {
self._entitiesView = DrawingEntitiesView(context: self.context, size: controller.size)
}
self._drawingView?.entitiesView = self._entitiesView
self._entitiesView?.drawingView = self._drawingView
self._entitiesView?.entityAdded = { [weak self] entity in
self?._drawingView?.onEntityAdded(entity)
}
self._entitiesView?.entityRemoved = { [weak self] entity in
self?._drawingView?.onEntityRemoved(entity)
}
self._drawingView?.getFullImage = { [weak self] in
if let strongSelf = self, let controller = strongSelf.controller, let currentImage = controller.getCurrentImage() {
let size = controller.size.fitted(CGSize(width: 256.0, height: 256.0))
if let imageContext = DrawingContext(size: size, scale: 1.0, opaque: true, clear: false) {
imageContext.withFlippedContext { c in
let bounds = CGRect(origin: .zero, size: size)
if let cgImage = currentImage.cgImage {
c.draw(cgImage, in: bounds)
}
if let cgImage = strongSelf.drawingView.drawingImage?.cgImage {
c.draw(cgImage, in: bounds)
}
telegramFastBlurMore(Int32(imageContext.size.width * imageContext.scale), Int32(imageContext.size.height * imageContext.scale), Int32(imageContext.bytesPerRow), imageContext.bytes)
telegramFastBlurMore(Int32(imageContext.size.width * imageContext.scale), Int32(imageContext.size.height * imageContext.scale), Int32(imageContext.bytesPerRow), imageContext.bytes)
}
return imageContext.generateImage()
} else {
return nil
}
} else {
return nil
}
}
self._entitiesView?.selectionContainerView = self.selectionContainerView
self._entitiesView?.selectionChanged = { [weak self] entity in
if let strongSelf = self {
strongSelf.updateSelectedEntity.invoke(entity)
}
}
self.insertEntity.connect { [weak self] entity in
if let self, let interaction = self.interaction {
interaction.insertEntity(entity)
}
}
self.deselectEntity.connect { [weak self] in
if let strongSelf = self, let entitiesView = strongSelf._entitiesView {
entitiesView.selectEntity(nil)
}
}
self.updateEntitiesPlayback.connect { [weak self] play in
if let strongSelf = self, let entitiesView = strongSelf._entitiesView {
if play {
entitiesView.play()
} else {
entitiesView.pause()
}
}
}
self.updateEntityView.connect { [weak self] uuid, animated in
if let strongSelf = self, let entitiesView = strongSelf._entitiesView {
entitiesView.getView(for: uuid)?.update(animated: animated)
}
}
self.endEditingTextEntityView.connect { [weak self] uuid, reset in
if let strongSelf = self, let entitiesView = strongSelf._entitiesView {
if let textEntityView = entitiesView.getView(for: uuid) as? DrawingTextEntityView {
textEntityView.endEditing(reset: reset)
}
}
}
}
return self._entitiesView!
}
private var _selectionContainerView: DrawingSelectionContainerView?
var selectionContainerView: DrawingSelectionContainerView {
if self._selectionContainerView == nil, let controller = self.controller {
if let externalSelectionContainerView = controller.externalSelectionContainerView {
self._selectionContainerView = externalSelectionContainerView
} else {
self._selectionContainerView = DrawingSelectionContainerView(frame: .zero)
}
}
return self._selectionContainerView!
}
private var _contentWrapperView: UIView?
var contentWrapperView: UIView {
if self._contentWrapperView == nil {
self._contentWrapperView = UIView()
}
return self._contentWrapperView!
}
init(controller: DrawingScreen) {
self.controller = controller
self.context = controller.context
self.updateState = ActionSlot<DrawingView.NavigationState>()
self.updateColor = ActionSlot<DrawingColor>()
self.performAction = ActionSlot<DrawingView.Action>()
self.updateToolState = ActionSlot<DrawingToolState>()
self.updateSelectedEntity = ActionSlot<DrawingEntity?>()
self.insertEntity = ActionSlot<DrawingEntity>()
self.deselectEntity = ActionSlot<Void>()
self.updateEntitiesPlayback = ActionSlot<Bool>()
self.previewBrushSize = ActionSlot<CGFloat?>()
self.dismissEyedropper = ActionSlot<Void>()
self.requestPresentColorPicker = ActionSlot<Void>()
self.toggleWithEraser = ActionSlot<Void>()
self.toggleWithPreviousTool = ActionSlot<Void>()
self.insertSticker = ActionSlot<Void>()
self.insertText = ActionSlot<Void>()
self.updateEntityView = ActionSlot<(UUID, Bool)>()
self.endEditingTextEntityView = ActionSlot<(UUID, Bool)>()
self.apply = ActionSlot<Void>()
self.dismiss = ActionSlot<Void>()
self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
self.componentHost = ComponentView<ViewControllerComponentContainer.Environment>()
super.init()
self.apply.connect { [weak self] _ in
if let strongSelf = self {
strongSelf.controller?.requestApply()
}
}
self.dismiss.connect { [weak self] _ in
if let strongSelf = self {
if strongSelf.drawingView.canUndo || strongSelf.entitiesView.hasChanges {
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme))
actionSheet.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.PhotoEditor_DiscardChanges, color: .accent, action: { [weak actionSheet, weak self] in
actionSheet?.dismissAnimated()
self?.controller?.requestDismiss()
})
]),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])
])
strongSelf.controller?.present(actionSheet, in: .window(.root))
} else {
strongSelf.controller?.requestDismiss()
}
}
}
}
override func didLoad() {
super.didLoad()
self.view.disablesInteractiveKeyboardGestureRecognizer = true
self.view.disablesInteractiveTransitionGestureRecognizer = true
guard let controller = self.controller else {
return
}
self.interaction = DrawingToolsInteraction(
context: self.context,
drawingView: self.drawingView,
entitiesView: self.entitiesView,
contentWrapperView: self.contentWrapperView,
selectionContainerView: self.selectionContainerView,
isVideo: controller.isVideo,
autoselectEntityOnPan: false,
updateSelectedEntity: { [weak self] entity in
if let self {
self.updateSelectedEntity.invoke(entity)
}
},
updateVideoPlayback: { [weak controller] isPlaying in
if let controller {
controller.updateVideoPlayback(isPlaying)
}
},
updateColor: { [weak self] color in
if let self {
self.updateColor.invoke(color)
}
},
onInteractionUpdated: { [weak self] isInteracting in
if let self {
self.isInteractingWithEntities = isInteracting
self.requestUpdate(transition: .easeInOut(duration: 0.2))
}
},
onTextEditingEnded: { _ in },
editEntity: { _ in },
shouldDeleteEntity: { _ in
return true
},
getCurrentImage: { [weak controller] in
return controller?.getCurrentImage()
},
getControllerNode: { [weak self] in
return self
},
present: { [weak self] c, i, a in
if let self {
self.controller?.present(c, in: i, with: a)
}
},
addSubview: { [weak self] view in
if let self {
self.view.addSubview(view)
}
}
)
}
func animateIn() {
self.entitiesView.selectEntity(nil)
if let view = self.componentHost.findTaggedView(tag: topGradientTag) {
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
}
if let view = self.componentHost.findTaggedView(tag: bottomGradientTag) {
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
}
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)
}
var delay: Double = 0.0
for tag in colorTags {
if let view = self.componentHost.findTaggedView(tag: tag) {
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, delay: delay)
view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3, delay: delay)
delay += 0.02
}
}
if let view = self.componentHost.findTaggedView(tag: sizeSliderTag) {
view.layer.animatePosition(from: CGPoint(x: -33.0, y: 0.0), to: CGPoint(), duration: 0.3, additive: true)
}
}
func animateOut(completion: @escaping () -> Void) {
if let (layout, orientation) = self.validLayout {
self.containerLayoutUpdated(layout: layout, orientation: orientation, animateOut: true, transition: .easeInOut(duration: 0.2))
}
if let view = self.componentHost.findTaggedView(tag: topGradientTag) {
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
}
if let view = self.componentHost.findTaggedView(tag: bottomGradientTag) {
if self.controller?.sourceHint == .storyEditor {
view.isHidden = true
}
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
}
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: redoButtonTag), buttonView.alpha > 0.0 {
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 buttonView = self.componentHost.findTaggedView(tag: flipButtonTag) {
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: fillButtonTag) {
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: zoomOutButtonTag) {
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: sizeSliderTag) {
view.layer.animatePosition(from: CGPoint(), to: CGPoint(x: -33.0, y: 0.0), duration: 0.3, removeOnCompletion: false, additive: true)
}
for tag in colorTags {
if let view = self.componentHost.findTaggedView(tag: tag) {
view.alpha = 0.0
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
view.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: {
completion()
})
} else if let view = self.componentHost.findTaggedView(tag: textSettingsTag) as? TextSettingsComponent.View {
view.animateOut(completion: {
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 requestUpdate(transition: ComponentTransition = .immediate) {
if let (layout, orientation) = self.validLayout {
self.containerLayoutUpdated(layout: layout, orientation: orientation, transition: transition)
}
}
func containerLayoutUpdated(layout: ContainerViewLayout, orientation: UIInterfaceOrientation?, forceUpdate: Bool = false, animateOut: Bool = false, transition: ComponentTransition) {
guard let controller = self.controller else {
return
}
let isFirstTime = self.validLayout == nil
self.validLayout = (layout, orientation)
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
),
additionalInsets: layout.additionalInsets,
inputHeight: layout.inputHeight ?? 0.0,
metrics: layout.metrics,
deviceMetrics: layout.deviceMetrics,
orientation: orientation,
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,
sourceHint: controller.sourceHint,
existingStickerPickerInputData: controller.existingStickerPickerInputData,
isVideo: controller.isVideo,
isAvatar: controller.isAvatar,
isInteractingWithEntities: self.isInteractingWithEntities,
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,
updateEntitiesPlayback: self.updateEntitiesPlayback,
previewBrushSize: self.previewBrushSize,
dismissEyedropper: self.dismissEyedropper,
requestPresentColorPicker: self.requestPresentColorPicker,
toggleWithEraser: self.toggleWithEraser,
toggleWithPreviousTool: self.toggleWithPreviousTool,
insertSticker: self.insertSticker,
insertText: self.insertText,
updateEntityView: self.updateEntityView,
endEditingTextEntityView: self.endEditingTextEntityView,
entityViewForEntity: { [weak self] entity in
if let self, let entityView = self.entitiesView.getView(for: entity.uuid) {
return entityView
} else {
return nil
}
},
presentGallery: self.controller?.presentGallery,
apply: self.apply,
dismiss: self.dismiss,
presentColorPicker: { [weak self] initialColor in
self?.interaction?.presentColorPicker(initialColor: initialColor)
},
presentFastColorPicker: { [weak self] sourceView in
self?.interaction?.presentFastColorPicker(sourceView: sourceView)
},
updateFastColorPickerPan: { [weak self] point in
self?.interaction?.updateFastColorPickerPan(point)
},
dismissFastColorPicker: { [weak self] in
self?.interaction?.dismissFastColorPicker()
},
presentFontPicker: { [weak self] sourceView in
self?.interaction?.presentFontPicker(sourceView: sourceView)
}
)
),
environment: {
environment
},
forceUpdate: 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()
}
}
self.interaction?.containerLayoutUpdated(layout: layout, transition: transition)
}
}
fileprivate var node: Node {
return self.displayNode as! Node
}
public enum SourceHint {
case storyEditor
}
private let context: AccountContext
private let sourceHint: SourceHint?
private let size: CGSize
private let originalSize: CGSize
private let isVideo: Bool
private let isAvatar: Bool
private let externalDrawingView: DrawingView?
private let externalEntitiesView: DrawingEntitiesView?
private let externalSelectionContainerView: DrawingSelectionContainerView?
private let existingStickerPickerInputData: Promise<StickerPickerInput>?
public var requestDismiss: () -> Void = {}
public var requestApply: () -> Void = {}
public var getCurrentImage: () -> UIImage? = { return nil }
public var updateVideoPlayback: (Bool) -> Void = { _ in }
public var presentGallery: (() -> Void)?
public init(context: AccountContext, sourceHint: SourceHint? = nil, size: CGSize, originalSize: CGSize, isVideo: Bool, isAvatar: Bool, drawingView: DrawingView?, entitiesView: (UIView & TGPhotoDrawingEntitiesView)?, selectionContainerView: DrawingSelectionContainerView?, existingStickerPickerInputData: Promise<StickerPickerInput>? = nil) {
self.context = context
self.sourceHint = sourceHint
self.size = size
self.originalSize = originalSize
self.isVideo = isVideo
self.isAvatar = isAvatar
self.existingStickerPickerInputData = existingStickerPickerInputData
if let drawingView {
self.externalDrawingView = drawingView
} else {
self.externalDrawingView = nil
}
if let entitiesView = entitiesView as? DrawingEntitiesView {
self.externalEntitiesView = entitiesView
} else {
self.externalEntitiesView = nil
}
if let selectionContainerView = selectionContainerView {
self.externalSelectionContainerView = selectionContainerView
} else {
self.externalSelectionContainerView = nil
}
super.init(navigationBarPresentationData: nil)
self.statusBar.statusBarStyle = .Hide
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
}
required public init(coder: NSCoder) {
preconditionFailure()
}
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: UIView {
return self.node.contentWrapperView
}
override public func loadDisplayNode() {
self.displayNode = Node(controller: self)
super.displayNodeDidLoad()
let dropInteraction = UIDropInteraction(delegate: self)
self.drawingView.addInteraction(dropInteraction)
}
public func generateDrawingResultData() -> DrawingResultData? {
if self.drawingView.isEmpty && self.entitiesView.entities.isEmpty {
return nil
}
let drawingImage = self.drawingView.drawingImage
let _ = self.entitiesView.entitiesData
let codableEntities = self.entitiesView.entities.filter { !($0 is DrawingMediaEntity) }.compactMap({ CodableDrawingEntity(entity: $0) })
return DrawingResultData(data: self.drawingView.drawingData, drawingImage: drawingImage, entities: codableEntities)
}
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
for entity in self.entitiesView.entities {
if entity.isAnimated {
hasAnimatedEntities = true
break
}
}
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)
if #available(iOS 16.0, *) {
let path = NSTemporaryDirectory() + "img.jpg"
try? finalImage?.jpegData(compressionQuality: 0.9)?.write(to: URL(filePath: path))
}
var image = paintingImage
var stillImage: UIImage?
if hasAnimatedEntities {
stillImage = finalImage
} else {
image = finalImage
}
let drawingData = self.drawingView.drawingData
let entitiesData = self.entitiesView.entitiesData
var stickers: [Any] = []
for entity in self.entitiesView.entities {
if let sticker = entity as? DrawingStickerEntity, case let .file(file, _) = sticker.content {
let coder = PostboxEncoder()
coder.encodeRootObject(file.media)
stickers.append(coder.makeData())
} else if let text = entity as? DrawingTextEntity, let subEntities = text.renderSubEntities {
for sticker in subEntities {
if let sticker = sticker as? DrawingStickerEntity, case let .file(file, _) = sticker.content {
let coder = PostboxEncoder()
coder.encodeRootObject(file.media)
stickers.append(coder.makeData())
}
}
}
}
return TGPaintingData(drawing: drawingData, entitiesData: entitiesData, image: image, stillImage: stillImage, hasAnimation: hasAnimatedEntities, stickers: stickers)
}
public func animateOut(_ completion: @escaping (() -> Void)) {
self.entitiesView.selectEntity(nil)
self.node.animateOut(completion: {
completion()
})
}
private var orientation: UIInterfaceOrientation?
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
(self.displayNode as! Node).containerLayoutUpdated(layout: layout, orientation: self.orientation, transition: ComponentTransition(transition))
}
public func adapterContainerLayoutUpdatedSize(_ size: CGSize, intrinsicInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, statusBarHeight: CGFloat, inputHeight: CGFloat, orientation: UIInterfaceOrientation, isRegular: Bool, animated: Bool) {
var intrinsicInsets = intrinsicInsets
if intrinsicInsets.top.isZero {
intrinsicInsets.top = statusBarHeight
}
let layout = ContainerViewLayout(
size: size,
metrics: LayoutMetrics(widthClass: isRegular ? .regular : .compact, heightClass: isRegular ? .regular : .compact, orientation: nil),
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.orientation = orientation
self.containerLayoutUpdated(layout, transition: animated ? .animated(duration: 0.3, curve: .easeInOut) : .immediate)
}
@available(iOSApplicationExtension 11.0, iOS 11.0, *)
public func dropInteraction(_ interaction: UIDropInteraction, canHandle session: UIDropSession) -> Bool {
return session.hasItemsConforming(toTypeIdentifiers: [kUTTypeImage as String])
}
@available(iOSApplicationExtension 11.0, iOS 11.0, *)
public func dropInteraction(_ interaction: UIDropInteraction, sessionDidUpdate session: UIDropSession) -> UIDropProposal {
let operation: UIDropOperation
operation = .copy
return UIDropProposal(operation: operation)
}
@available(iOSApplicationExtension 11.0, iOS 11.0, *)
public func dropInteraction(_ interaction: UIDropInteraction, performDrop session: UIDropSession) {
session.loadObjects(ofClass: UIImage.self) { [weak self] imageItems in
guard let strongSelf = self else {
return
}
let images = imageItems as! [UIImage]
if images.count == 1, let image = images.first, max(image.size.width, image.size.height) > 1.0 {
let entity = DrawingStickerEntity(content: .image(image, .sticker))
strongSelf.node.insertEntity.invoke(entity)
}
}
}
@available(iOSApplicationExtension 11.0, iOS 11.0, *)
public func dropInteraction(_ interaction: UIDropInteraction, sessionDidExit session: UIDropSession) {
}
@available(iOSApplicationExtension 11.0, iOS 11.0, *)
public func dropInteraction(_ interaction: UIDropInteraction, sessionDidEnd session: UIDropSession) {
}
}
public final class DrawingToolsInteraction {
private let context: AccountContext
private let drawingView: DrawingView
private let entitiesView: DrawingEntitiesView
private weak var contentWrapperView: UIView?
private let selectionContainerView: DrawingSelectionContainerView
private let isVideo: Bool
private let autoSelectEntityOnPan: Bool
private let updateSelectedEntity: (DrawingEntity?) -> Void
private let updateVideoPlayback: (Bool) -> Void
private let updateColor: (DrawingColor) -> Void
private let onInteractionUpdated: (Bool) -> Void
private let onTextEditingEnded: (Bool) -> Void
private let editEntity: (DrawingEntity) -> Void
private let shouldDeleteEntity: (DrawingEntity) -> Bool
public let getCurrentImage: () -> UIImage?
private let getControllerNode: () -> ASDisplayNode?
private let present: (ViewController, PresentationContextType, Any?) -> Void
private let addSubview: (UIView) -> Void
private let textEditAccessoryView: UIInputView
private let textEditAccessoryHost: ComponentView<Empty>
private var currentEyedropperView: EyedropperView?
private weak var currentMenuController: ContextMenuController?
private let hapticFeedback = HapticFeedback()
private var isActive = false
private var validLayout: ContainerViewLayout?
public init(
context: AccountContext,
drawingView: DrawingView,
entitiesView: DrawingEntitiesView,
contentWrapperView: UIView,
selectionContainerView: DrawingSelectionContainerView,
isVideo: Bool,
autoselectEntityOnPan: Bool,
updateSelectedEntity: @escaping (DrawingEntity?) -> Void,
updateVideoPlayback: @escaping (Bool) -> Void,
updateColor: @escaping (DrawingColor) -> Void,
onInteractionUpdated: @escaping (Bool) -> Void,
onTextEditingEnded: @escaping (Bool) -> Void,
editEntity: @escaping (DrawingEntity) -> Void,
shouldDeleteEntity: @escaping (DrawingEntity) -> Bool,
getCurrentImage: @escaping () -> UIImage?,
getControllerNode: @escaping () -> ASDisplayNode?,
present: @escaping (ViewController, PresentationContextType, Any?) -> Void,
addSubview: @escaping (UIView) -> Void
) {
self.context = context
self.drawingView = drawingView
self.entitiesView = entitiesView
self.contentWrapperView = contentWrapperView
self.selectionContainerView = selectionContainerView
self.isVideo = isVideo
self.autoSelectEntityOnPan = autoselectEntityOnPan
self.updateSelectedEntity = updateSelectedEntity
self.updateVideoPlayback = updateVideoPlayback
self.updateColor = updateColor
self.onInteractionUpdated = onInteractionUpdated
self.onTextEditingEnded = onTextEditingEnded
self.editEntity = editEntity
self.shouldDeleteEntity = shouldDeleteEntity
self.getCurrentImage = getCurrentImage
self.getControllerNode = getControllerNode
self.present = present
self.addSubview = addSubview
self.textEditAccessoryView = UIInputView(frame: CGRect(origin: .zero, size: CGSize(width: 100.0, height: 44.0)), inputViewStyle: .keyboard)
self.textEditAccessoryHost = ComponentView<Empty>()
self.activate()
}
public func reset() {
self.drawingView.stateUpdated = { _ in }
}
public func activate() {
self.isActive = true
self.entitiesView.autoSelectEntities = self.autoSelectEntityOnPan
self.entitiesView.selectionContainerView = self.selectionContainerView
self.entitiesView.selectionChanged = { [weak self] entity in
if let self {
self.updateSelectedEntity(entity)
}
}
self.entitiesView.onInteractionUpdated = { [weak self] isInteracting in
if let self {
self.onInteractionUpdated(isInteracting)
}
}
self.entitiesView.onTextEditingEnded = { [weak self] reset in
if let self {
self.onTextEditingEnded(reset)
}
}
self.entitiesView.requestedMenuForEntityView = { [weak self] entityView, isTopmost in
guard let self, let node = self.getControllerNode() else {
return
}
if self.currentMenuController != nil {
if let entityView = entityView as? DrawingTextEntityView {
entityView.beginEditing(accessoryView: self.textEditAccessoryView)
}
return
}
var isVideo = false
var isAdditional = false
var isMessage = false
var isLink = false
var isWeather = false
if let entity = entityView.entity as? DrawingStickerEntity {
if case let .dualVideoReference(isAdditionalValue) = entity.content {
isVideo = true
isAdditional = isAdditionalValue
} else if case .message = entity.content {
isMessage = true
} else if case .gift = entity.content {
isMessage = true
}
} else if entityView.entity is DrawingLinkEntity {
isLink = true
} else if entityView.entity is DrawingWeatherEntity {
isWeather = true
}
guard (!isVideo || isAdditional) && (!isMessage || !isTopmost) else {
return
}
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme)
var actions: [ContextMenuAction] = []
if !isMessage {
actions.append(ContextMenuAction(content: .text(title: presentationData.strings.Paint_Delete, accessibilityLabel: presentationData.strings.Paint_Delete), action: { [weak self, weak entityView] in
if let self, let entityView {
if self.shouldDeleteEntity(entityView.entity) {
self.entitiesView.remove(uuid: entityView.entity.uuid, animated: true)
}
}
}))
}
if entityView is DrawingLocationEntityView || entityView is DrawingLinkEntityView {
actions.append(ContextMenuAction(content: .text(title: presentationData.strings.Paint_Edit, accessibilityLabel: presentationData.strings.Paint_Edit), action: { [weak self, weak entityView] in
if let self, let entityView {
self.editEntity(entityView.entity)
self.entitiesView.selectEntity(entityView.entity)
}
}))
} else if let entityView = entityView as? DrawingTextEntityView {
actions.append(ContextMenuAction(content: .text(title: presentationData.strings.Paint_Edit, accessibilityLabel: presentationData.strings.Paint_Edit), action: { [weak self, weak entityView] in
if let self, let entityView {
entityView.beginEditing(accessoryView: self.textEditAccessoryView)
self.entitiesView.selectEntity(entityView.entity)
}
}))
} else if (entityView is DrawingStickerEntityView || entityView is DrawingBubbleEntityView) && !isVideo && !isMessage {
actions.append(ContextMenuAction(content: .text(title: presentationData.strings.Paint_Flip, accessibilityLabel: presentationData.strings.Paint_Flip), action: { [weak self] in
if let self {
self.flipSelectedEntity()
}
}))
}
if !isTopmost && !isVideo {
actions.append(ContextMenuAction(content: .text(title: presentationData.strings.Paint_MoveForward, accessibilityLabel: presentationData.strings.Paint_MoveForward), action: { [weak self, weak entityView] in
if let self, let entityView {
self.entitiesView.bringToFront(uuid: entityView.entity.uuid)
}
}))
}
if !isVideo && !isMessage && !isLink && !isWeather {
if let stickerEntity = entityView.entity as? DrawingStickerEntity, case let .file(_, type) = stickerEntity.content, case .reaction = type {
} else {
actions.append(ContextMenuAction(content: .text(title: presentationData.strings.Paint_Duplicate, accessibilityLabel: presentationData.strings.Paint_Duplicate), action: { [weak self, weak entityView] in
if let self, let entityView {
self.duplicateEntity(entityView.entity)
}
}))
}
}
if #available(iOS 17.0, *), let stickerEntity = entityView.entity as? DrawingStickerEntity, stickerEntity.canCutOut {
actions.append(ContextMenuAction(content: .text(title: presentationData.strings.Paint_CutOut, accessibilityLabel: presentationData.strings.Paint_CutOut), action: { [weak self, weak entityView] in
if let self, let entityView, let entity = entityView.entity as? DrawingStickerEntity, case let .image(image, _) = entity.content {
let _ = (cutoutStickerImage(from: image)
|> deliverOnMainQueue).start(next: { [weak entity] result in
if let result, let entity {
let newEntity = DrawingStickerEntity(content: .image(result, .sticker))
newEntity.referenceDrawingSize = entity.referenceDrawingSize
newEntity.scale = entity.scale
newEntity.position = entity.position
newEntity.rotation = entity.rotation
newEntity.mirrored = entity.mirrored
let newEntityView = self.entitiesView.add(newEntity)
entityView.selectionView?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
if let newEntityView = newEntityView as? DrawingStickerEntityView {
newEntityView.playCutoutAnimation()
}
self.entitiesView.selectEntity(newEntity, animate: false)
newEntityView.selectionView?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
if let entityView = entityView as? DrawingStickerEntityView {
entityView.playDissolveAnimation()
self.entitiesView.remove(uuid: entity.uuid, animated: false)
}
}
})
}
}))
}
let entityFrame = entityView.convert(entityView.selectionBounds, to: node.view).offsetBy(dx: 0.0, dy: -6.0)
let controller = makeContextMenuController(actions: actions)
let bounds = node.bounds.insetBy(dx: 0.0, dy: 160.0)
self.present(
controller,
.window(.root),
ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak node] in
if let node {
return (node, entityFrame, node, bounds)
} else {
return nil
}
})
)
self.currentMenuController = controller
}
}
public func deactivate() {
self.isActive = false
}
public func insertEntity(_ entity: DrawingEntity, scale: CGFloat? = nil, position: CGPoint? = nil, select: Bool = true) {
self.entitiesView.prepareNewEntity(entity, scale: scale, position: position)
self.entitiesView.add(entity)
if select {
self.entitiesView.selectEntity(entity, animate: !(entity is DrawingTextEntity))
}
if let entityView = self.entitiesView.getView(for: entity.uuid) {
if let textEntityView = entityView as? DrawingTextEntityView {
textEntityView.beginEditing(accessoryView: self.textEditAccessoryView)
textEntityView.replaceWithImage = { [weak self] image, isSticker in
if let self {
self.insertEntity(DrawingStickerEntity(content: .image(image, isSticker ? .sticker : .rectangle)), scale: 2.5)
}
}
textEntityView.replaceWithAnimatedImage = { [weak self] data, thumbnailImage in
if let self {
self.insertEntity(DrawingStickerEntity(content: .animatedImage(data, thumbnailImage)), scale: 2.5)
}
}
} else {
if self.isVideo {
entityView.seek(to: 0.0)
entityView.play()
}
entityView.animateInsertion()
}
}
}
public func duplicateEntity(_ entity: DrawingEntity) {
let newEntity = self.entitiesView.duplicate(entity)
self.entitiesView.selectEntity(newEntity)
if let entityView = self.entitiesView.getView(for: newEntity.uuid) {
if self.isVideo {
entityView.seek(to: 0.0)
entityView.play()
}
entityView.animateInsertion()
}
}
public func endTextEditing(reset: Bool) {
if let entityView = self.entitiesView.selectedEntityView as? DrawingTextEntityView {
entityView.endEditing(reset: reset)
}
}
public func updateEntitySize(_ size: CGFloat) {
if let selectedEntityView = self.entitiesView.selectedEntityView {
if let textEntity = selectedEntityView.entity as? DrawingTextEntity {
textEntity.fontSize = size
} else {
selectedEntityView.entity.lineWidth = size
}
selectedEntityView.update()
}
}
public func flipSelectedEntity() {
if let selectedEntityView = self.entitiesView.selectedEntityView {
let selectedEntity = selectedEntityView.entity
if let entity = selectedEntity as? DrawingBubbleEntity {
var updatedTailPosition = entity.tailPosition
updatedTailPosition.x = 1.0 - updatedTailPosition.x
entity.tailPosition = updatedTailPosition
selectedEntityView.update(animated: false)
} else if let entity = selectedEntity as? DrawingStickerEntity {
entity.mirrored = !entity.mirrored
selectedEntityView.update(animated: true)
}
}
}
func presentEyedropper(retryLaterForVideo: Bool = true, dismissed: @escaping () -> Void) {
self.entitiesView.pause()
if self.isVideo && retryLaterForVideo {
self.updateVideoPlayback(false)
Queue.mainQueue().after(0.1) {
self.presentEyedropper(retryLaterForVideo: false, dismissed: dismissed)
}
return
}
let currentImage = self.getCurrentImage()
let sourceImage = generateImage(self.drawingView.imageSize, contextGenerator: { size, context in
let bounds = CGRect(origin: .zero, size: size)
if let cgImage = currentImage?.cgImage {
context.draw(cgImage, in: bounds)
}
if self.drawingView.superview !== self.entitiesView {
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: true, scale: 1.0)
guard let sourceImage, var contentWrapperView = self.contentWrapperView, let controllerView = self.getControllerNode()?.view else {
return
}
if contentWrapperView.frame.width.isZero {
contentWrapperView = self.entitiesView.superview!
}
let eyedropperView = EyedropperView(containerSize: contentWrapperView.frame.size, drawingView: self.drawingView, sourceImage: sourceImage)
eyedropperView.completed = { [weak self] color in
if let self {
self.updateColor(color)
self.entitiesView.play()
self.updateVideoPlayback(true)
dismissed()
}
}
eyedropperView.dismissed = { [weak self] in
if let self {
self.entitiesView.play()
self.updateVideoPlayback(true)
}
}
eyedropperView.frame = contentWrapperView.convert(contentWrapperView.bounds, to: controllerView)
self.addSubview(eyedropperView)
self.currentEyedropperView = eyedropperView
}
func dismissCurrentEyedropper() {
if let currentEyedropperView = self.currentEyedropperView {
self.currentEyedropperView = nil
currentEyedropperView.dismiss()
}
}
weak var colorPickerScreen: ColorPickerScreen?
func presentColorPicker(initialColor: DrawingColor, dismissed: @escaping () -> Void = {}) {
self.dismissCurrentEyedropper()
self.dismissFontPicker()
self.hapticFeedback.impact(.medium)
var didDismiss = false
let colorController = ColorPickerScreen(context: self.context, initialColor: initialColor, updated: { [weak self] color in
if let self {
self.updateColor(color)
}
}, openEyedropper: { [weak self] in
if let self {
self.presentEyedropper(dismissed: dismissed)
}
}, dismissed: {
if !didDismiss {
didDismiss = true
dismissed()
}
})
self.present(colorController, .window(.root), nil)
self.colorPickerScreen = colorController
}
func dismissColorPicker() {
if let colorPickerScreen = self.colorPickerScreen {
self.colorPickerScreen = nil
colorPickerScreen.dismiss()
}
}
private var fastColorPickerView: ColorSpectrumPickerView?
func presentFastColorPicker(sourceView: UIView) {
self.dismissCurrentEyedropper()
self.dismissFontPicker()
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
if let self {
self.updateColor(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()
})
}
private weak var currentFontPicker: ContextController?
func presentFontPicker(sourceView: UIView) {
guard !self.dismissFontPicker(), let validLayout = self.validLayout else {
return
}
if let entityView = self.entitiesView.selectedEntityView as? DrawingTextEntityView {
entityView.textChanged = { [weak self] in
self?.dismissFontPicker()
}
}
let fonts: [DrawingTextFont] = [
.sanFrancisco,
.other("AmericanTypewriter", "Typewriter"),
.other("AvenirNext-DemiBoldItalic", "Avenir Next"),
.other("CourierNewPS-BoldMT", "Courier New"),
.other("Noteworthy-Bold", "Noteworthy"),
.other("Georgia-Bold", "Georgia"),
.other("Papyrus", "Papyrus"),
.other("SnellRoundhand-Bold", "Snell Roundhand")
]
var items: [ContextMenuItem] = []
for font in fonts {
items.append(.action(ContextMenuActionItem(text: font.title, textFont: .custom(font: font.uiFont(size: 17.0), height: 42.0, verticalOffset: font.title == "Noteworthy" ? -6.0 : nil), icon: { _ in return nil }, animationName: nil, action: { [weak self] f in
f.dismissWithResult(.default)
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: .easeInOut(duration: 0.2))
}
})))
}
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme)
let contextController = ContextController(presentationData: presentationData, source: .reference(ReferenceContentSource(sourceView: sourceView, contentArea: CGRect(origin: .zero, size: CGSize(width: validLayout.size.width, height: validLayout.size.height - (validLayout.inputHeight ?? 0.0))), customPosition: CGPoint(x: 0.0, y: 1.0))), items: .single(ContextController.Items(content: .list(items))))
self.present(contextController, .window(.root), nil)
self.currentFontPicker = contextController
contextController.view.disablesInteractiveKeyboardGestureRecognizer = true
}
@discardableResult
func dismissFontPicker() -> Bool {
if let currentFontPicker = self.currentFontPicker {
self.currentFontPicker = nil
currentFontPicker.dismiss()
return true
}
return false
}
private func toggleInputMode() {
guard let entityView = self.entitiesView.selectedEntityView as? DrawingTextEntityView else {
return
}
let textView = entityView.textView
var shouldHaveInputView = false
if textView.isFirstResponder {
if textView.inputView == nil {
shouldHaveInputView = true
}
} else {
shouldHaveInputView = true
}
if shouldHaveInputView {
let inputView = EntityInputView(
context: self.context,
isDark: true,
areCustomEmojiEnabled: true,
hideBackground: true,
forceHasPremium: true
)
inputView.insertText = { [weak entityView] text in
entityView?.insertText(text)
}
inputView.deleteBackwards = { [weak textView] in
textView?.deleteBackward()
}
inputView.switchToKeyboard = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.toggleInputMode()
}
textView.inputView = inputView
} else {
textView.inputView = nil
}
if textView.isFirstResponder {
textView.reloadInputViews()
} else {
textView.becomeFirstResponder()
}
if let layout = self.validLayout {
self.containerLayoutUpdated(layout: layout, transition: .immediate)
}
}
public func containerLayoutUpdated(layout: ContainerViewLayout, transition: ComponentTransition) {
self.validLayout = layout
guard self.isActive else {
return
}
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
}
UIView.performWithoutAnimation {
let accessorySize = self.textEditAccessoryHost.update(
transition: isFirstTime ? .immediate : .easeInOut(duration: 0.2),
component: AnyComponent(
TextSettingsComponent(
color: textEntity.color,
style: DrawingTextStyle(style: textEntity.style),
animation: DrawingTextAnimation(animation: textEntity.animation),
alignment: DrawingTextAlignment(alignment: textEntity.alignment),
font: DrawingTextFont(font: textEntity.font),
isEmojiKeyboard: entityView.textView.inputView != nil,
tag: nil,
fontTag: fontTag,
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(in: entityView.selectedRange), 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
self?.dismissFontPicker()
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 = .regular
case .stroke:
nextStyle = .regular
case .blur:
nextStyle = .regular
}
textEntity.style = nextStyle
entityView.update()
if let layout = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(layout: layout, transition: .immediate)
}
},
toggleAnimation: { [weak self] in
self?.dismissFontPicker()
guard let strongSelf = self, let entityView = strongSelf.entitiesView.selectedEntityView as? DrawingTextEntityView, let textEntity = entityView.entity as? DrawingTextEntity else {
return
}
var nextAnimation: DrawingTextEntity.Animation
switch textEntity.animation {
case .none:
nextAnimation = .typing
case .typing:
nextAnimation = .wiggle
case .wiggle:
nextAnimation = .zoomIn
case .zoomIn:
nextAnimation = .none
}
textEntity.animation = nextAnimation
entityView.update()
if let layout = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(layout: layout, transition: .immediate)
}
},
toggleAlignment: { [weak self] in
self?.dismissFontPicker()
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)
}
},
presentFontPicker: { [weak self] in
if let buttonView = self?.textEditAccessoryHost.findTaggedView(tag: fontTag) {
self?.presentFontPicker(sourceView: buttonView)
}
},
toggleKeyboard: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.dismissFontPicker()
strongSelf.toggleInputMode()
}
)
),
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)
}
self.textEditAccessoryView.frame = CGRect(origin: .zero, size: accessorySize)
componentView.frame = CGRect(origin: .zero, size: accessorySize)
}
}
}
}
}