import Foundation import UIKit import Display import ComponentFlow import LegacyComponents import TelegramCore import Postbox import SwiftSignalKit import TelegramPresentationData import AccountContext import AppBundle import PresentationDataUtils import LegacyComponents import ComponentDisplayAdapters import LottieAnimationComponent import ViewControllerComponent import ContextUI import ChatEntityKeyboardInputNode enum DrawingToolState: Equatable { enum Key: CaseIterable { case pen case marker case neon case pencil case lasso case eraser } struct BrushState: Equatable { enum Mode: Equatable { case round case arrow } let color: DrawingColor let size: CGFloat let mode: Mode func withUpdatedColor(_ color: DrawingColor) -> BrushState { return BrushState(color: color, size: self.size, mode: self.mode) } func withUpdatedSize(_ size: CGFloat) -> BrushState { return BrushState(color: self.color, size: size, mode: self.mode) } func withUpdatedMode(_ mode: Mode) -> BrushState { return BrushState(color: self.color, size: self.size, mode: mode) } } struct EraserState: Equatable { enum Mode: Equatable { case bitmap case vector case blur } let size: CGFloat let mode: Mode func withUpdatedSize(_ size: CGFloat) -> EraserState { return EraserState(size: size, mode: self.mode) } func withUpdatedMode(_ mode: Mode) -> EraserState { return EraserState(size: self.size, mode: mode) } } case pen(BrushState) case marker(BrushState) case neon(BrushState) case pencil(BrushState) case lasso case eraser(EraserState) func withUpdatedColor(_ color: DrawingColor) -> DrawingToolState { switch self { case let .pen(state): return .pen(state.withUpdatedColor(color)) case let .marker(state): return .marker(state.withUpdatedColor(color)) case let .neon(state): return .neon(state.withUpdatedColor(color)) case let .pencil(state): return .pencil(state.withUpdatedColor(color)) case .lasso, .eraser: return self } } func withUpdatedSize(_ size: CGFloat) -> DrawingToolState { switch self { case let .pen(state): return .pen(state.withUpdatedSize(size)) case let .marker(state): return .marker(state.withUpdatedSize(size)) case let .neon(state): return .neon(state.withUpdatedSize(size)) case let .pencil(state): return .pencil(state.withUpdatedSize(size)) case .lasso: return self case let .eraser(state): return .eraser(state.withUpdatedSize(size)) } } func withUpdatedBrushMode(_ mode: BrushState.Mode) -> DrawingToolState { switch self { case let .pen(state): return .pen(state.withUpdatedMode(mode)) case let .marker(state): return .marker(state.withUpdatedMode(mode)) case let .neon(state): return .neon(state.withUpdatedMode(mode)) case let .pencil(state): return .pencil(state.withUpdatedMode(mode)) case .lasso, .eraser: return self } } func withUpdatedEraserMode(_ mode: EraserState.Mode) -> DrawingToolState { switch self { case .pen: return self case .marker: return self case .neon: return self case .pencil: return self case .lasso: return self case let .eraser(state): return .eraser(state.withUpdatedMode(mode)) } } var color: DrawingColor? { switch self { case let .pen(state), let .marker(state), let .neon(state), let .pencil(state): return state.color default: return nil } } var size: CGFloat? { switch self { case let .pen(state), let .marker(state), let .neon(state), let .pencil(state): return state.size case let .eraser(state): return state.size default: return nil } } var brushMode: DrawingToolState.BrushState.Mode? { switch self { case let .pen(state), let .marker(state), let .neon(state), let .pencil(state): return state.mode default: return nil } } var eraserMode: DrawingToolState.EraserState.Mode? { switch self { case let .eraser(state): return state.mode default: return nil } } var key: DrawingToolState.Key { switch self { case .pen: return .pen case .marker: return .marker case .neon: return .neon case .pencil: return .pencil case .lasso: return .lasso case .eraser: return .eraser } } } struct DrawingState: Equatable { let selectedTool: DrawingToolState.Key let tools: [DrawingToolState] var currentToolState: DrawingToolState { return self.toolState(for: self.selectedTool) } func toolState(for key: DrawingToolState.Key) -> DrawingToolState { for tool in self.tools { if tool.key == key { return tool } } return .lasso } func withUpdatedSelectedTool(_ selectedTool: DrawingToolState.Key) -> DrawingState { return DrawingState( selectedTool: selectedTool, tools: self.tools ) } func withUpdatedColor(_ color: DrawingColor) -> DrawingState { var tools = self.tools if let index = tools.firstIndex(where: { $0.key == self.selectedTool }) { let updated = tools[index].withUpdatedColor(color) tools.remove(at: index) tools.insert(updated, at: index) } return DrawingState( selectedTool: self.selectedTool, tools: tools ) } func withUpdatedSize(_ size: CGFloat) -> DrawingState { var tools = self.tools if let index = tools.firstIndex(where: { $0.key == self.selectedTool }) { let updated = tools[index].withUpdatedSize(size) tools.remove(at: index) tools.insert(updated, at: index) } return DrawingState( selectedTool: self.selectedTool, tools: tools ) } func withUpdatedBrushMode(_ mode: DrawingToolState.BrushState.Mode) -> DrawingState { var tools = self.tools if let index = tools.firstIndex(where: { $0.key == self.selectedTool }) { let updated = tools[index].withUpdatedBrushMode(mode) tools.remove(at: index) tools.insert(updated, at: index) } return DrawingState( selectedTool: self.selectedTool, tools: tools ) } func withUpdatedEraserMode(_ mode: DrawingToolState.EraserState.Mode) -> DrawingState { var tools = self.tools if let index = tools.firstIndex(where: { $0.key == self.selectedTool }) { let updated = tools[index].withUpdatedEraserMode(mode) tools.remove(at: index) tools.insert(updated, at: index) } return DrawingState( selectedTool: self.selectedTool, tools: tools ) } static var initial: DrawingState { return DrawingState( selectedTool: .pen, tools: [ .pen(DrawingToolState.BrushState(color: DrawingColor(rgb: 0xe22400), size: 0.2, mode: .round)), .marker(DrawingToolState.BrushState(color: DrawingColor(rgb: 0xfee21b), size: 0.75, mode: .round)), .neon(DrawingToolState.BrushState(color: DrawingColor(rgb: 0x34ffab), size: 0.4, mode: .round)), .pencil(DrawingToolState.BrushState(color: DrawingColor(rgb: 0x2570f0), size: 0.3, mode: .round)), .lasso, .eraser(DrawingToolState.EraserState(size: 0.5, mode: .bitmap)) ] ) } } private final class ReferenceContentSource: ContextReferenceContentSource { private let sourceView: UIView init(sourceView: UIView) { self.sourceView = sourceView } func transitionInfo() -> ContextControllerReferenceViewInfo? { return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds, customPosition: CGPoint(x: 7.0, y: 3.0)) } } enum DrawingScreenTransition { case animateIn case animateOut } private let undoButtonTag = GenericComponentViewTag() private let redoButtonTag = GenericComponentViewTag() private let clearAllButtonTag = GenericComponentViewTag() private let colorButtonTag = GenericComponentViewTag() private let addButtonTag = GenericComponentViewTag() private let toolsTag = GenericComponentViewTag() private let modeTag = GenericComponentViewTag() private let doneButtonTag = GenericComponentViewTag() private final class DrawingScreenComponent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext let isAvatar: Bool let present: (ViewController) -> Void let updateState: ActionSlot let updateColor: ActionSlot let performAction: ActionSlot let updateToolState: ActionSlot let updateSelectedEntity: ActionSlot let insertEntity: ActionSlot let deselectEntity: ActionSlot let updatePlayback: ActionSlot let previewBrushSize: ActionSlot let apply: ActionSlot let dismiss: ActionSlot let presentColorPicker: (DrawingColor) -> Void let presentFastColorPicker: (UIView) -> Void let updateFastColorPickerPan: (CGPoint) -> Void let dismissFastColorPicker: () -> Void init( context: AccountContext, isAvatar: Bool, present: @escaping (ViewController) -> Void, updateState: ActionSlot, updateColor: ActionSlot, performAction: ActionSlot, updateToolState: ActionSlot, updateSelectedEntity: ActionSlot, insertEntity: ActionSlot, deselectEntity: ActionSlot, updatePlayback: ActionSlot, previewBrushSize: ActionSlot, apply: ActionSlot, dismiss: ActionSlot, presentColorPicker: @escaping (DrawingColor) -> Void, presentFastColorPicker: @escaping (UIView) -> Void, updateFastColorPickerPan: @escaping (CGPoint) -> Void, dismissFastColorPicker: @escaping () -> Void ) { self.context = context self.isAvatar = isAvatar self.present = present self.updateState = updateState self.updateColor = updateColor self.performAction = performAction self.updateToolState = updateToolState self.updateSelectedEntity = updateSelectedEntity self.insertEntity = insertEntity self.deselectEntity = deselectEntity self.updatePlayback = updatePlayback self.previewBrushSize = previewBrushSize self.apply = apply self.dismiss = dismiss self.presentColorPicker = presentColorPicker self.presentFastColorPicker = presentFastColorPicker self.updateFastColorPickerPan = updateFastColorPickerPan self.dismissFastColorPicker = dismissFastColorPicker } static func ==(lhs: DrawingScreenComponent, rhs: DrawingScreenComponent) -> Bool { if lhs.context !== rhs.context { return false } if lhs.isAvatar != rhs.isAvatar { return false } return true } final class State: ComponentState { enum ImageKey: Hashable { case undo case redo case done case add case round case arrow case remove case blur case fill case stroke case flip case zoomOut } private var cachedImages: [ImageKey: UIImage] = [:] func image(_ key: ImageKey) -> UIImage { if let image = self.cachedImages[key] { return image } else { var image: UIImage switch key { case .undo: image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Undo"), color: .white)! case .redo: image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Redo"), color: .white)! case .done: image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Done"), color: .white)! case .add: image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Add"), color: .white)! case .round: image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/BrushRound"), color: .white)! case .arrow: image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/BrushArrow"), color: .white)! case .remove: image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/BrushRemove"), color: .white)! case .blur: image = UIImage(bundleImageName: "Media Editor/BrushBlur")! case .fill: image = UIImage(bundleImageName: "Media Editor/Fill")! case .stroke: image = UIImage(bundleImageName: "Media Editor/Stroke")! case .flip: image = UIImage(bundleImageName: "Media Editor/Flip")! case .zoomOut: image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/ZoomOut"), color: .white)! } cachedImages[key] = image return image } } enum Mode { case drawing case sticker case text } private let context: AccountContext private let updateToolState: ActionSlot private let insertEntity: ActionSlot private let deselectEntity: ActionSlot private let updatePlayback: ActionSlot private let present: (ViewController) -> Void var currentMode: Mode var drawingState: DrawingState var drawingViewState: DrawingView.NavigationState var toolIsFocused = false var currentColor: DrawingColor var selectedEntity: DrawingEntity? var lastFontSize: CGFloat = 0.5 init(context: AccountContext, updateToolState: ActionSlot, insertEntity: ActionSlot, deselectEntity: ActionSlot, updatePlayback: ActionSlot, present: @escaping (ViewController) -> Void) { self.context = context self.updateToolState = updateToolState self.insertEntity = insertEntity self.deselectEntity = deselectEntity self.updatePlayback = updatePlayback self.present = present self.currentMode = .drawing self.drawingState = .initial self.drawingViewState = DrawingView.NavigationState(canUndo: false, canRedo: false, canClear: false, canZoomOut: false) self.currentColor = self.drawingState.tools.first?.color ?? DrawingColor(rgb: 0xffffff) self.updateToolState.invoke(self.drawingState.currentToolState) } private var currentToolState: DrawingToolState { return self.drawingState.toolState(for: self.drawingState.selectedTool) } func updateColor(_ color: DrawingColor, animated: Bool = false) { self.currentColor = color if let selectedEntity = self.selectedEntity { selectedEntity.color = color selectedEntity.currentEntityView?.update() } else { self.drawingState = self.drawingState.withUpdatedColor(color) self.updateToolState.invoke(self.drawingState.currentToolState) } self.updated(transition: animated ? .easeInOut(duration: 0.2) : .immediate) } func updateSelectedTool(_ tool: DrawingToolState.Key) { self.drawingState = self.drawingState.withUpdatedSelectedTool(tool) self.currentColor = self.drawingState.currentToolState.color ?? self.currentColor self.updateToolState.invoke(self.drawingState.currentToolState) self.updated(transition: .easeInOut(duration: 0.2)) } func updateBrushSize(_ size: CGFloat) { if let selectedEntity = self.selectedEntity { if let textEntity = selectedEntity as? DrawingTextEntity { textEntity.fontSize = size } else { selectedEntity.lineWidth = size } selectedEntity.currentEntityView?.update() } else { self.drawingState = self.drawingState.withUpdatedSize(size) self.updateToolState.invoke(self.drawingState.currentToolState) } self.updated(transition: .immediate) } func updateBrushMode(_ mode: DrawingToolState.BrushState.Mode) { self.drawingState = self.drawingState.withUpdatedBrushMode(mode) self.updateToolState.invoke(self.drawingState.currentToolState) self.updated(transition: .easeInOut(duration: 0.2)) } func updateEraserMode(_ mode: DrawingToolState.EraserState.Mode) { self.drawingState = self.drawingState.withUpdatedEraserMode(mode) self.updateToolState.invoke(self.drawingState.currentToolState) self.updated(transition: .easeInOut(duration: 0.2)) } func updateToolIsFocused(_ isFocused: Bool) { self.toolIsFocused = isFocused self.updated(transition: .easeInOut(duration: 0.2)) } func updateDrawingState(_ state: DrawingView.NavigationState) { self.drawingViewState = state self.updated(transition: .easeInOut(duration: 0.2)) } func updateSelectedEntity(_ entity: DrawingEntity?) { self.selectedEntity = entity if let entity = entity { if !entity.color.isClear { self.currentColor = entity.color } if entity is DrawingStickerEntity { self.currentMode = .sticker } else if entity is DrawingTextEntity { self.currentMode = .text } else { self.currentMode = .drawing } } else { self.currentMode = .drawing self.currentColor = self.drawingState.currentToolState.color ?? self.currentColor } self.updated(transition: .easeInOut(duration: 0.2)) } func presentShapePicker(_ sourceView: UIView) { let items: [ContextMenuItem] = [ .action( ContextMenuActionItem( text: "Rectangle", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/ShapeRectangle"), color: theme.contextMenu.primaryColor)}, action: { [weak self] f in f.dismissWithResult(.default) if let strongSelf = self { strongSelf.insertEntity.invoke(DrawingSimpleShapeEntity(shapeType: .rectangle, drawType: .stroke, color: strongSelf.currentColor, lineWidth: 0.15)) } } ) ), .action( ContextMenuActionItem( text: "Ellipse", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/ShapeEllipse"), color: theme.contextMenu.primaryColor)}, action: { [weak self] f in f.dismissWithResult(.default) if let strongSelf = self { strongSelf.insertEntity.invoke(DrawingSimpleShapeEntity(shapeType: .ellipse, drawType: .stroke, color: strongSelf.currentColor, lineWidth: 0.15)) } } ) ), .action( ContextMenuActionItem( text: "Bubble", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/ShapeBubble"), color: theme.contextMenu.primaryColor)}, action: { [weak self] f in f.dismissWithResult(.default) if let strongSelf = self { strongSelf.insertEntity.invoke(DrawingBubbleEntity(drawType: .stroke, color: strongSelf.currentColor, lineWidth: 0.15)) } } ) ), .action( ContextMenuActionItem( text: "Star", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/ShapeStar"), color: theme.contextMenu.primaryColor)}, action: { [weak self] f in f.dismissWithResult(.default) if let strongSelf = self { strongSelf.insertEntity.invoke(DrawingSimpleShapeEntity(shapeType: .star, drawType: .stroke, color: strongSelf.currentColor, lineWidth: 0.15)) } } ) ), .action( ContextMenuActionItem( text: "Arrow", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/ShapeArrow"), color: theme.contextMenu.primaryColor)}, action: { [weak self] f in f.dismissWithResult(.default) if let strongSelf = self { strongSelf.insertEntity.invoke(DrawingVectorEntity(type: .oneSidedArrow, color: strongSelf.currentColor, lineWidth: 0.5)) } } ) ) ] let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme) let contextController = ContextController(account: self.context.account, presentationData: presentationData, source: .reference(ReferenceContentSource(sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items)))) self.present(contextController) } func presentBrushModePicker(_ sourceView: UIView) { let items: [ContextMenuItem] = [ .action( ContextMenuActionItem( text: "Round", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/BrushRound"), color: theme.contextMenu.primaryColor)}, action: { [weak self] f in f.dismissWithResult(.default) if let strongSelf = self { strongSelf.updateBrushMode(.round) } } ) ), .action( ContextMenuActionItem( text: "Arrow", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/BrushArrow"), color: theme.contextMenu.primaryColor)}, action: { [weak self] f in f.dismissWithResult(.default) if let strongSelf = self { strongSelf.updateBrushMode(.arrow) } } ) ) ] let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme) let contextController = ContextController(account: self.context.account, presentationData: presentationData, source: .reference(ReferenceContentSource(sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items)))) self.present(contextController) } func presentEraserModePicker(_ sourceView: UIView) { let items: [ContextMenuItem] = [ .action( ContextMenuActionItem( text: "Eraser", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/BrushRound"), color: theme.contextMenu.primaryColor)}, action: { [weak self] f in f.dismissWithResult(.default) self?.updateEraserMode(.bitmap) } ) ), .action( ContextMenuActionItem( text: "Object Eraser", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/BrushRemove"), color: theme.contextMenu.primaryColor)}, action: { [weak self] f in f.dismissWithResult(.default) self?.updateEraserMode(.vector) } ) ), .action( ContextMenuActionItem( text: "Background Blur", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/BrushBlur"), color: theme.contextMenu.primaryColor)}, action: { [weak self] f in f.dismissWithResult(.default) self?.updateEraserMode(.blur) } ) ) ] let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme) let contextController = ContextController(account: self.context.account, presentationData: presentationData, source: .reference(ReferenceContentSource(sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items)))) self.present(contextController) } func updateCurrentMode(_ mode: Mode) { self.currentMode = mode if let selectedEntity = self.selectedEntity { if selectedEntity is DrawingStickerEntity || selectedEntity is DrawingTextEntity { self.deselectEntity.invoke(Void()) } } self.updated(transition: .easeInOut(duration: 0.2)) } func addTextEntity() { let textEntity = DrawingTextEntity(text: NSAttributedString(), style: .regular, font: .sanFrancisco, alignment: .center, fontSize: 1.0, color: DrawingColor(color: .white)) self.insertEntity.invoke(textEntity) } func presentStickerPicker() { self.currentMode = .sticker self.updatePlayback.invoke(false) let controller = StickerPickerScreen(context: self.context) controller.completion = { [weak self] file in self?.updatePlayback.invoke(true) if let file = file { let stickerEntity = DrawingStickerEntity(file: file) self?.insertEntity.invoke(stickerEntity) } else { self?.updateCurrentMode(.drawing) } } self.present(controller) self.updated(transition: .easeInOut(duration: 0.2)) } } func makeState() -> State { return State(context: self.context, updateToolState: self.updateToolState, insertEntity: self.insertEntity, deselectEntity: self.deselectEntity, updatePlayback: self.updatePlayback, present: self.present) } static var body: Body { let undoButton = Child(Button.self) let redoButton = Child(Button.self) let clearAllButton = Child(Button.self) let zoomOutButton = Child(Button.self) let tools = Child(ToolsComponent.self) let modeAndSize = Child(ModeAndSizeComponent.self) let colorButton = Child(ColorSwatchComponent.self) let textSettings = Child(TextSettingsComponent.self) let swatch1Button = Child(ColorSwatchComponent.self) let swatch2Button = Child(ColorSwatchComponent.self) let swatch3Button = Child(ColorSwatchComponent.self) let swatch4Button = Child(ColorSwatchComponent.self) let swatch5Button = Child(ColorSwatchComponent.self) let swatch6Button = Child(ColorSwatchComponent.self) let swatch7Button = Child(ColorSwatchComponent.self) let addButton = Child(Button.self) let flipButton = Child(Button.self) let fillButton = Child(Button.self) let fillButtonTag = GenericComponentViewTag() let stickerFlipButton = Child(Button.self) let backButton = Child(Button.self) let doneButton = Child(Button.self) let brushModeButton = Child(Button.self) let brushModeButtonTag = GenericComponentViewTag() let textSize = Child(TextSizeSliderComponent.self) let textCancelButton = Child(Button.self) let textDoneButton = Child(Button.self) let presetColors: [DrawingColor] = [ DrawingColor(rgb: 0xffffff), DrawingColor(rgb: 0x000000), DrawingColor(rgb: 0x106bff), DrawingColor(rgb: 0x2ecb46), DrawingColor(rgb: 0xfd8d0e), DrawingColor(rgb: 0xfc1a4d), DrawingColor(rgb: 0xaf39ee) ] return { context in let environment = context.environment[ViewControllerComponentContainer.Environment.self].value let component = context.component let state = context.state let controller = environment.controller let previewBrushSize = component.previewBrushSize let performAction = component.performAction component.updateState.connect { [weak state] updatedState in state?.updateDrawingState(updatedState) } component.updateColor.connect { [weak state] color in state?.updateColor(color) } component.updateSelectedEntity.connect { [weak state] entity in state?.updateSelectedEntity(entity) } let apply = component.apply let dismiss = component.dismiss let presentColorPicker = component.presentColorPicker let presentFastColorPicker = component.presentFastColorPicker let updateFastColorPickerPan = component.updateFastColorPickerPan let dismissFastColorPicker = component.dismissFastColorPicker if let textEntity = state.selectedEntity as? DrawingTextEntity { let textSettings = textSettings.update( component: TextSettingsComponent( color: nil, style: DrawingTextStyle(style: textEntity.style), alignment: DrawingTextAlignment(alignment: textEntity.alignment), font: DrawingTextFont(font: textEntity.font), isEmojiKeyboard: false, toggleStyle: { [weak state, weak textEntity] in guard let textEntity = textEntity else { return } var nextStyle: DrawingTextEntity.Style switch textEntity.style { case .regular: nextStyle = .filled case .filled: nextStyle = .semi case .semi: nextStyle = .stroke case .stroke: nextStyle = .regular } textEntity.style = nextStyle if let entityView = textEntity.currentEntityView { entityView.update() } state?.updated(transition: .easeInOut(duration: 0.2)) }, toggleAlignment: { [weak state, weak textEntity] in guard let textEntity = textEntity else { return } var nextAlignment: DrawingTextEntity.Alignment switch textEntity.alignment { case .left: nextAlignment = .center case .center: nextAlignment = .right case .right: nextAlignment = .left } textEntity.alignment = nextAlignment if let entityView = textEntity.currentEntityView { entityView.update() } state?.updated(transition: .easeInOut(duration: 0.2)) }, updateFont: { [weak state, weak textEntity] font in guard let textEntity = textEntity else { return } textEntity.font = font.font if let entityView = textEntity.currentEntityView { entityView.update() } state?.updated(transition: .easeInOut(duration: 0.2)) }, toggleKeyboard: nil ), availableSize: CGSize(width: context.availableSize.width - 84.0, height: 44.0), transition: context.transition ) context.add(textSettings .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - environment.safeInsets.bottom - textSettings.size.height / 2.0 - 51.0)) .appear(.default(scale: false, alpha: true)) .disappear(.default(scale: false, alpha: true)) ) } else if state.currentMode == .sticker { } else if state.selectedEntity != nil { let rightButtonPosition = context.availableSize.width - environment.safeInsets.right - 44.0 / 2.0 - 3.0 var offsetX: CGFloat = environment.safeInsets.left + 44.0 / 2.0 + 3.0 let delta: CGFloat = (rightButtonPosition - offsetX) / 7.0 offsetX += delta var delay: Double = 0.0 let swatch1Button = swatch1Button.update( component: ColorSwatchComponent( type: .pallete(state.currentColor == presetColors[0]), color: presetColors[0], action: { [weak state] in state?.updateColor(presetColors[0], animated: true) } ), availableSize: CGSize(width: 33.0, height: 33.0), transition: context.transition ) context.add(swatch1Button .position(CGPoint(x: offsetX, y: context.availableSize.height - environment.safeInsets.bottom - swatch1Button.size.height / 2.0 - 57.0)) .appear(Transition.Appear { _, view, transition in transition.animateScale(view: view, from: 0.1, to: 1.0) transition.animateAlpha(view: view, from: 0.0, to: 1.0) }) .disappear(Transition.Disappear { view, transition, completion in transition.setScale(view: view, scale: 0.1) transition.setAlpha(view: view, alpha: 0.0, completion: { _ in completion() }) }) ) offsetX += delta let swatch2Button = swatch2Button.update( component: ColorSwatchComponent( type: .pallete(state.currentColor == presetColors[1]), color: presetColors[1], action: { [weak state] in state?.updateColor(presetColors[1], animated: true) } ), availableSize: CGSize(width: 33.0, height: 33.0), transition: context.transition ) context.add(swatch2Button .position(CGPoint(x: offsetX, y: context.availableSize.height - environment.safeInsets.bottom - swatch2Button.size.height / 2.0 - 57.0)) .appear(Transition.Appear { _, view, transition in transition.animateScale(view: view, from: 0.1, to: 1.0, delay: 0.025) transition.animateAlpha(view: view, from: 0.0, to: 1.0, delay: 0.025) }) .disappear(Transition.Disappear { view, transition, completion in transition.setScale(view: view, scale: 0.1) transition.setAlpha(view: view, alpha: 0.0, completion: { _ in completion() }) }) ) offsetX += delta let swatch3Button = swatch3Button.update( component: ColorSwatchComponent( type: .pallete(state.currentColor == presetColors[2]), color: presetColors[2], action: { [weak state] in state?.updateColor(presetColors[2], animated: true) } ), availableSize: CGSize(width: 33.0, height: 33.0), transition: context.transition ) context.add(swatch3Button .position(CGPoint(x: offsetX, y: context.availableSize.height - environment.safeInsets.bottom - swatch3Button.size.height / 2.0 - 57.0)) .appear(Transition.Appear { _, view, transition in transition.animateScale(view: view, from: 0.1, to: 1.0, delay: 0.05) transition.animateAlpha(view: view, from: 0.0, to: 1.0, delay: 0.05) }) .disappear(Transition.Disappear { view, transition, completion in transition.setScale(view: view, scale: 0.1) transition.setAlpha(view: view, alpha: 0.0, completion: { _ in completion() }) }) ) offsetX += delta let swatch4Button = swatch4Button.update( component: ColorSwatchComponent( type: .pallete(state.currentColor == presetColors[3]), color: presetColors[3], action: { [weak state] in state?.updateColor(presetColors[3], animated: true) } ), availableSize: CGSize(width: 33.0, height: 33.0), transition: context.transition ) context.add(swatch4Button .position(CGPoint(x: offsetX, y: context.availableSize.height - environment.safeInsets.bottom - swatch4Button.size.height / 2.0 - 57.0)) .appear(Transition.Appear { _, view, transition in transition.animateScale(view: view, from: 0.1, to: 1.0, delay: 0.075) transition.animateAlpha(view: view, from: 0.0, to: 1.0, delay: 0.075) }) .disappear(Transition.Disappear { view, transition, completion in transition.setScale(view: view, scale: 0.1) transition.setAlpha(view: view, alpha: 0.0, completion: { _ in completion() }) }) ) offsetX += delta let swatch5Button = swatch5Button.update( component: ColorSwatchComponent( type: .pallete(state.currentColor == presetColors[4]), color: presetColors[4], action: { [weak state] in state?.updateColor(presetColors[4], animated: true) } ), availableSize: CGSize(width: 33.0, height: 33.0), transition: context.transition ) context.add(swatch5Button .position(CGPoint(x: offsetX, y: context.availableSize.height - environment.safeInsets.bottom - swatch5Button.size.height / 2.0 - 57.0)) .appear(Transition.Appear { _, view, transition in transition.animateScale(view: view, from: 0.1, to: 1.0, delay: 0.1) transition.animateAlpha(view: view, from: 0.0, to: 1.0, delay: 0.1) }) .disappear(Transition.Disappear { view, transition, completion in transition.setScale(view: view, scale: 0.1) transition.setAlpha(view: view, alpha: 0.0, completion: { _ in completion() }) }) ) offsetX += delta delay += 0.025 let swatch6Button = swatch6Button.update( component: ColorSwatchComponent( type: .pallete(state.currentColor == presetColors[5]), color: presetColors[5], action: { [weak state] in state?.updateColor(presetColors[5], animated: true) } ), availableSize: CGSize(width: 33.0, height: 33.0), transition: context.transition ) context.add(swatch6Button .position(CGPoint(x: offsetX, y: context.availableSize.height - environment.safeInsets.bottom - swatch6Button.size.height / 2.0 - 57.0)) .appear(Transition.Appear { _, view, transition in transition.animateScale(view: view, from: 0.1, to: 1.0, delay: 0.125) transition.animateAlpha(view: view, from: 0.0, to: 1.0, delay: 0.125) }) .disappear(Transition.Disappear { view, transition, completion in transition.setScale(view: view, scale: 0.1) transition.setAlpha(view: view, alpha: 0.0, completion: { _ in completion() }) }) ) offsetX += delta let swatch7Button = swatch7Button.update( component: ColorSwatchComponent( type: .pallete(state.currentColor == presetColors[6]), color: presetColors[6], action: { [weak state] in state?.updateColor(presetColors[6], animated: true) } ), availableSize: CGSize(width: 33.0, height: 33.0), transition: context.transition ) context.add(swatch7Button .position(CGPoint(x: offsetX, y: context.availableSize.height - environment.safeInsets.bottom - swatch7Button.size.height / 2.0 - 57.0)) .appear(Transition.Appear { _, view, transition in transition.animateScale(view: view, from: 0.1, to: 1.0, delay: 0.15) transition.animateAlpha(view: view, from: 0.0, to: 1.0, delay: 0.15) }) .disappear(Transition.Disappear { view, transition, completion in transition.setScale(view: view, scale: 0.1) transition.setAlpha(view: view, alpha: 0.0, completion: { _ in completion() }) }) ) } else { let tools = tools.update( component: ToolsComponent( state: state.drawingState, isFocused: state.toolIsFocused, tag: toolsTag, toolPressed: { [weak state] tool in if let state = state { if state.drawingState.selectedTool == tool, tool != .lasso { state.updateToolIsFocused(!state.toolIsFocused) } else { state.updateSelectedTool(tool) } } }, toolResized: { [weak state] _, size in state?.updateBrushSize(size) if state?.selectedEntity == nil { previewBrushSize.invoke(size) } }, sizeReleased: { previewBrushSize.invoke(nil) } ), availableSize: CGSize(width: context.availableSize.width - environment.safeInsets.left - environment.safeInsets.right, height: 120.0), transition: context.transition ) context.add(tools .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - environment.safeInsets.bottom - tools.size.height / 2.0 - 41.0)) .appear(Transition.Appear({ _, view, transition in if let view = view as? ToolsComponent.View, !transition.animation.isImmediate { view.animateIn(completion: {}) } })) .disappear(Transition.Disappear({ view, transition, completion in if let view = view as? ToolsComponent.View, !transition.animation.isImmediate { view.animateOut(completion: completion) } else { completion() } })) ) } var editingText = false var fontSize: CGFloat? if let textEntity = state.selectedEntity as? DrawingTextEntity, let entityView = textEntity.currentEntityView as? DrawingTextEntityView, entityView.isEditing { editingText = true fontSize = textEntity.fontSize state.lastFontSize = textEntity.fontSize } else { if state.drawingViewState.canZoomOut { let zoomOutButton = zoomOutButton.update( component: Button( content: AnyComponent( ZoomOutButtonContent( title: "Zoom Out", image: state.image(.zoomOut) ) ), action: { performAction.invoke(.zoomOut) } ).minSize(CGSize(width: 44.0, height: 44.0)), availableSize: CGSize(width: 120.0, height: 33.0), transition: .immediate ) context.add(zoomOutButton .position(CGPoint(x: context.availableSize.width / 2.0, y: environment.safeInsets.top + 32.0 - UIScreenPixel)) .appear(.default(scale: true, alpha: true)) .disappear(.default(scale: true, alpha: true)) ) } } let topInset = environment.safeInsets.top + 31.0 let textSize = textSize.update( component: TextSizeSliderComponent( value: fontSize ?? state.lastFontSize, updated: { [weak state] size in state?.updateBrushSize(size) } ), availableSize: CGSize(width: 30.0, height: 240.0), transition: context.transition ) context.add(textSize .position(CGPoint(x: editingText ? textSize.size.width / 2.0 : textSize.size.width / 2.0 - 33.0, y: topInset + (context.availableSize.height - topInset - environment.inputHeight) / 2.0)) ) let undoButton = undoButton.update( component: Button( content: AnyComponent( Image(image: state.image(.undo)) ), isEnabled: state.drawingViewState.canUndo, action: { performAction.invoke(.undo) } ).minSize(CGSize(width: 44.0, height: 44.0)).tagged(undoButtonTag), availableSize: CGSize(width: 24.0, height: 24.0), transition: context.transition ) context.add(undoButton .position(CGPoint(x: environment.safeInsets.left + undoButton.size.width / 2.0 + 2.0, y: environment.safeInsets.top + 31.0)) .scale(editingText ? 0.01 : 1.0) .opacity(editingText ? 0.0 : 1.0) ) if state.drawingViewState.canRedo && !editingText { let redoButton = redoButton.update( component: Button( content: AnyComponent( Image(image: state.image(.redo)) ), action: { performAction.invoke(.redo) } ).minSize(CGSize(width: 44.0, height: 44.0)).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: environment.safeInsets.top + 31.0)) .appear(.default(scale: true, alpha: true)) .disappear(.default(scale: true, alpha: true)) ) } let clearAllButton = clearAllButton.update( component: Button( content: AnyComponent( Text(text: "Clear All", font: Font.regular(17.0), color: .white) ), isEnabled: state.drawingViewState.canClear, action: { performAction.invoke(.clear) } ).tagged(clearAllButtonTag), availableSize: CGSize(width: 100.0, height: 30.0), transition: context.transition ) context.add(clearAllButton .position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - clearAllButton.size.width / 2.0 - 13.0, y: environment.safeInsets.top + 31.0)) .scale(editingText ? 0.01 : 1.0) .opacity(editingText ? 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, let entityView = entity.currentEntityView as? DrawingTextEntityView { entityView.endEditing(reset: true) } } ), availableSize: CGSize(width: 100.0, height: 30.0), transition: context.transition ) context.add(textCancelButton .position(CGPoint(x: environment.safeInsets.left + textCancelButton.size.width / 2.0 + 13.0, y: environment.safeInsets.top + 31.0)) .scale(editingText ? 1.0 : 0.01) .opacity(editingText ? 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, let entityView = entity.currentEntityView as? DrawingTextEntityView { entityView.endEditing() } } ), availableSize: CGSize(width: 100.0, height: 30.0), transition: context.transition ) context.add(textDoneButton .position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - textDoneButton.size.width / 2.0 - 13.0, y: environment.safeInsets.top + 31.0)) .scale(editingText ? 1.0 : 0.01) .opacity(editingText ? 1.0 : 0.0) ) var isEditingSize = false if state.toolIsFocused { isEditingSize = true } else if let entity = state.selectedEntity { if entity is DrawingSimpleShapeEntity || entity is DrawingVectorEntity || entity is DrawingBubbleEntity { isEditingSize = true } } if !state.toolIsFocused { var color: DrawingColor? if let entity = state.selectedEntity, !(entity is DrawingTextEntity) && presetColors.contains(entity.color) { color = nil } else { color = state.currentColor } if let _ = state.selectedEntity as? DrawingStickerEntity { let stickerFlipButton = stickerFlipButton.update( component: Button( content: AnyComponent( Image(image: state.image(.flip)) ), action: { [weak state] in guard let state = state else { return } if let entity = state.selectedEntity as? DrawingStickerEntity { entity.mirrored = !entity.mirrored entity.currentEntityView?.update(animated: true) } state.updated(transition: .easeInOut(duration: 0.2)) } ).minSize(CGSize(width: 44.0, height: 44.0)), availableSize: CGSize(width: 33.0, height: 33.0), transition: .immediate ) context.add(stickerFlipButton .position(CGPoint(x: environment.safeInsets.left + stickerFlipButton.size.width / 2.0 + 3.0, y: context.availableSize.height - environment.safeInsets.bottom - stickerFlipButton.size.height / 2.0 - 51.0)) .appear(.default(scale: true)) .disappear(.default(scale: true)) ) } else { if [.lasso, .eraser].contains(state.drawingState.selectedTool) { } else { let colorButton = colorButton.update( component: ColorSwatchComponent( type: .main, color: color, tag: colorButtonTag, action: { [weak state] in if let state = state { presentColorPicker(state.currentColor) } }, holdAction: { if let controller = controller() as? DrawingScreen, let buttonView = controller.node.componentHost.findTaggedView(tag: colorButtonTag) { presentFastColorPicker(buttonView) } }, pan: { point in updateFastColorPickerPan(point) }, release: { dismissFastColorPicker() } ), availableSize: CGSize(width: 44.0, height: 44.0), transition: context.transition ) context.add(colorButton .position(CGPoint(x: environment.safeInsets.left + colorButton.size.width / 2.0 + 3.0, y: context.availableSize.height - environment.safeInsets.bottom - colorButton.size.height / 2.0 - 51.0)) .appear(.default(scale: true)) .disappear(.default(scale: true)) ) } } } var isModeControlEnabled = true var modeRightInset: CGFloat = 57.0 if isEditingSize { if state.toolIsFocused { let title: String let image: UIImage? var isEraser = false if let mode = state.drawingState.toolState(for: state.drawingState.selectedTool).brushMode { switch mode { case .round: title = "Round" image = state.image(.round) case .arrow: title = "Arrow" image = state.image(.arrow) } } else if let mode = state.drawingState.toolState(for: state.drawingState.selectedTool).eraserMode { isEraser = true switch mode { case .bitmap: title = "Eraser" image = state.image(.round) case .vector: title = "Object" image = state.image(.remove) case .blur: title = "Blur" image = state.image(.blur) } } else { title = "" image = nil } if [.pen, .eraser].contains(state.drawingState.selectedTool) { let brushModeButton = brushModeButton.update( component: Button( content: AnyComponent( BrushButtonContent( title: title, image: image ?? UIImage() ) ), action: { [weak state] in guard let controller = controller() as? DrawingScreen else { return } if let buttonView = controller.node.componentHost.findTaggedView(tag: brushModeButtonTag) as? Button.View { if isEraser { state?.presentEraserModePicker(buttonView) } else { state?.presentBrushModePicker(buttonView) } } } ).minSize(CGSize(width: 44.0, height: 44.0)).tagged(brushModeButtonTag), availableSize: CGSize(width: 75.0, height: 33.0), transition: .immediate ) context.add(brushModeButton .position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - brushModeButton.size.width / 2.0 - 5.0, y: context.availableSize.height - environment.safeInsets.bottom - brushModeButton.size.height / 2.0 - 2.0 - UIScreenPixel)) .appear(.default(alpha: true)) .disappear(.default(alpha: true)) ) modeRightInset += 35.0 } else { modeRightInset = 16.0 } } else { var isFilled = false if let entity = state.selectedEntity as? DrawingSimpleShapeEntity, case .fill = entity.drawType { isFilled = true isModeControlEnabled = false } else if let entity = state.selectedEntity as? DrawingBubbleEntity, case .fill = entity.drawType { isFilled = true isModeControlEnabled = false } if let _ = state.selectedEntity as? DrawingBubbleEntity { let flipButton = flipButton.update( component: Button( content: AnyComponent( Image(image: state.image(.flip)) ), action: { [weak state] in guard let state = state else { return } if let entity = state.selectedEntity as? DrawingBubbleEntity { var updatedTailPosition = entity.tailPosition updatedTailPosition.x = 1.0 - updatedTailPosition.x entity.tailPosition = updatedTailPosition entity.currentEntityView?.update() } state.updated(transition: .easeInOut(duration: 0.2)) } ).minSize(CGSize(width: 44.0, height: 44.0)), availableSize: CGSize(width: 33.0, height: 33.0), transition: .immediate ) context.add(flipButton .position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - flipButton.size.width / 2.0 - 3.0 - flipButton.size.width, y: context.availableSize.height - environment.safeInsets.bottom - flipButton.size.height / 2.0 - 2.0 - UIScreenPixel)) .appear(.default(scale: true)) .disappear(.default(scale: true)) ) modeRightInset += 35.0 } let fillButton = fillButton.update( component: Button( content: AnyComponent( Image(image: state.image(isFilled ? .fill : .stroke)) ), action: { [weak state] in guard let state = state else { return } if let entity = state.selectedEntity as? DrawingSimpleShapeEntity { if case .fill = entity.drawType { entity.drawType = .stroke } else { entity.drawType = .fill } entity.currentEntityView?.update() } else if let entity = state.selectedEntity as? DrawingBubbleEntity { if case .fill = entity.drawType { entity.drawType = .stroke } else { entity.drawType = .fill } entity.currentEntityView?.update() } else if let entity = state.selectedEntity as? DrawingVectorEntity { if case .oneSidedArrow = entity.type { entity.type = .twoSidedArrow } else if case .twoSidedArrow = entity.type { entity.type = .line } else { entity.type = .oneSidedArrow } entity.currentEntityView?.update() } state.updated(transition: .easeInOut(duration: 0.2)) } ).minSize(CGSize(width: 44.0, height: 44.0)).tagged(fillButtonTag), availableSize: CGSize(width: 33.0, height: 33.0), transition: .immediate ) context.add(fillButton .position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - fillButton.size.width / 2.0 - 3.0, y: context.availableSize.height - environment.safeInsets.bottom - fillButton.size.height / 2.0 - 2.0 - UIScreenPixel)) .appear(.default(scale: true)) .disappear(.default(scale: true)) ) } } else { let addButton = addButton.update( component: Button( content: AnyComponent(ZStack([ AnyComponentWithIdentity( id: "background", component: AnyComponent( BlurredRectangle( color: UIColor(rgb: 0x888888, alpha: 0.3), radius: 16.5 ) ) ), AnyComponentWithIdentity( id: "icon", component: AnyComponent( Image(image: state.image(.add)) ) ), ])), action: { [weak state] in guard let controller = controller() as? DrawingScreen, let state = state else { return } switch state.currentMode { case .drawing: if let buttonView = controller.node.componentHost.findTaggedView(tag: addButtonTag) as? Button.View { state.presentShapePicker(buttonView) } case .sticker: state.presentStickerPicker() case .text: state.addTextEntity() } } ).minSize(CGSize(width: 44.0, height: 44.0)).tagged(addButtonTag), availableSize: CGSize(width: 33.0, height: 33.0), transition: .immediate ) context.add(addButton .position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - addButton.size.width / 2.0 - 3.0, y: context.availableSize.height - environment.safeInsets.bottom - addButton.size.height / 2.0 - 51.0)) .appear(.default(scale: true)) .disappear(.default(scale: true)) ) let doneButton = doneButton.update( component: Button( content: AnyComponent( Image(image: state.image(.done)) ), action: { apply.invoke(Void()) } ).minSize(CGSize(width: 44.0, height: 44.0)).tagged(doneButtonTag), availableSize: CGSize(width: 33.0, height: 33.0), transition: .immediate ) context.add(doneButton .position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - doneButton.size.width / 2.0 - 3.0, y: context.availableSize.height - environment.safeInsets.bottom - doneButton.size.height / 2.0 - 2.0 - UIScreenPixel)) .appear(Transition.Appear { _, view, transition in transition.animateScale(view: view, from: 0.1, to: 1.0) transition.animateAlpha(view: view, from: 0.0, to: 1.0) transition.animatePosition(view: view, from: CGPoint(x: 12.0, y: 0.0), to: CGPoint(), additive: true) }) .disappear(Transition.Disappear { view, transition, completion in transition.setScale(view: view, scale: 0.1) transition.setAlpha(view: view, alpha: 0.0, completion: { _ in completion() }) transition.animatePosition(view: view, from: CGPoint(), to: CGPoint(x: 12.0, y: 0.0), additive: true) }) ) } let selectedIndex: Int switch state.currentMode { case .drawing: selectedIndex = 0 case .sticker: selectedIndex = 1 case .text: selectedIndex = 2 } var selectedSize: CGFloat = 0.0 if let entity = state.selectedEntity { selectedSize = entity.lineWidth } else { selectedSize = state.drawingState.toolState(for: state.drawingState.selectedTool).size ?? 0.0 } let modeAndSize = modeAndSize.update( component: ModeAndSizeComponent( values: ["Draw", "Sticker", "Text"], sizeValue: selectedSize, isEditing: isEditingSize, isEnabled: isModeControlEnabled, rightInset: modeRightInset - 57.0, tag: modeTag, selectedIndex: selectedIndex, selectionChanged: { [weak state] index in guard let state = state else { return } switch index { case 1: state.presentStickerPicker() case 2: state.addTextEntity() default: state.updateCurrentMode(.drawing) } }, sizeUpdated: { [weak state] size in if let state = state { state.updateBrushSize(size) if state.selectedEntity == nil { previewBrushSize.invoke(size) } } }, sizeReleased: { previewBrushSize.invoke(nil) } ), availableSize: CGSize(width: context.availableSize.width - 57.0 - modeRightInset, height: context.availableSize.height), transition: context.transition ) context.add(modeAndSize .position(CGPoint(x: context.availableSize.width / 2.0 - (modeRightInset - 57.0) / 2.0, y: context.availableSize.height - environment.safeInsets.bottom - modeAndSize.size.height / 2.0 - 9.0)) .opacity(isModeControlEnabled ? 1.0 : 0.4) ) var animatingOut = false if let appearanceTransition = context.transition.userData(DrawingScreenTransition.self), case .animateOut = appearanceTransition { animatingOut = true } let deselectEntity = component.deselectEntity let backButton = backButton.update( component: Button( content: AnyComponent( LottieAnimationComponent( animation: LottieAnimationComponent.AnimationItem( name: "media_backToCancel", mode: .animating(loop: false), range: isEditingSize || animatingOut || 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 { if state.toolIsFocused { state.updateToolIsFocused(false) } else if let selectedEntity = state.selectedEntity, !(selectedEntity is DrawingStickerEntity || selectedEntity is DrawingTextEntity) { deselectEntity.invoke(Void()) } else { dismiss.invoke(Void()) } } } ).minSize(CGSize(width: 44.0, height: 44.0)), availableSize: CGSize(width: 33.0, height: 33.0), transition: .immediate ) context.add(backButton .position(CGPoint(x: environment.safeInsets.left + backButton.size.width / 2.0 + 3.0, y: context.availableSize.height - environment.safeInsets.bottom - backButton.size.height / 2.0 - 2.0 - UIScreenPixel)) ) return context.availableSize } } } public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController { fileprivate final class Node: ViewControllerTracingNode { private weak var controller: DrawingScreen? private let context: AccountContext private let updateState: ActionSlot private let updateColor: ActionSlot private let performAction: ActionSlot private let updateToolState: ActionSlot private let updateSelectedEntity: ActionSlot private let insertEntity: ActionSlot private let deselectEntity: ActionSlot private let updatePlayback: ActionSlot private let previewBrushSize: ActionSlot private let apply: ActionSlot private let dismiss: ActionSlot fileprivate let componentHost: ComponentView private let textEditAccessoryView: UIInputView private let textEditAccessoryHost: ComponentView private var presentationData: PresentationData private let hapticFeedback = HapticFeedback() private var validLayout: ContainerViewLayout? private var _drawingView: DrawingView? var drawingView: DrawingView { if self._drawingView == nil, let controller = self.controller { self._drawingView = DrawingView(size: controller.size) self._drawingView?.shouldBegin = { [weak self] _ in if let strongSelf = self { if strongSelf._entitiesView?.hasSelection == true { strongSelf._entitiesView?.selectEntity(nil) return false } return true } else { return false } } self._drawingView?.stateUpdated = { [weak self] state in if let strongSelf = self { strongSelf.updateState.invoke(state) } } self._drawingView?.requestMenu = { [weak self] elements, rect in if let strongSelf = self, let drawingView = strongSelf._drawingView { let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } var actions: [ContextMenuAction] = [] actions.append(ContextMenuAction(content: .text(title: presentationData.strings.Paint_Delete, accessibilityLabel: presentationData.strings.Paint_Delete), action: { [weak self] in if let strongSelf = self { strongSelf._drawingView?.removeElements(elements) } })) actions.append(ContextMenuAction(content: .text(title: presentationData.strings.Paint_Duplicate, accessibilityLabel: presentationData.strings.Paint_Duplicate), action: { [weak self] in if let strongSelf = self { strongSelf._drawingView?.removeElements(elements) } })) let strokeFrame = drawingView.lassoView.convert(rect, to: strongSelf.view).offsetBy(dx: 0.0, dy: -6.0) let controller = ContextMenuController(actions: actions) strongSelf.currentMenuController = controller strongSelf.controller?.present( controller, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in if let strongSelf = self { return (strongSelf, strokeFrame, strongSelf, strongSelf.bounds) } else { return nil } }) ) } } self.performAction.connect { [weak self] action in if let strongSelf = self { if action == .clear { let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)) actionSheet.setItemGroups([ ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: strongSelf.presentationData.strings.Paint_ClearConfirm, color: .destructive, action: { [weak actionSheet, weak self] in actionSheet?.dismissAnimated() self?._drawingView?.performAction(action) }) ]), 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._drawingView?.performAction(action) } } } self.updateToolState.connect { [weak self] state in if let strongSelf = self { strongSelf._drawingView?.updateToolState(state) } } self.previewBrushSize.connect { [weak self] size in if let strongSelf = self { strongSelf._drawingView?.setBrushSizePreview(size) } } } return self._drawingView! } private weak var currentMenuController: ContextMenuController? private var _entitiesView: DrawingEntitiesView? var entitiesView: DrawingEntitiesView { if self._entitiesView == nil, let controller = self.controller { self._entitiesView = DrawingEntitiesView(context: self.context, size: controller.size) self._drawingView?.entitiesView = self._entitiesView let entitiesLayer = self.entitiesView.layer self._drawingView?.getFullImage = { [weak self, weak entitiesLayer] withDrawing in if let strongSelf = self, let controller = strongSelf.controller, let currentImage = controller.getCurrentImage() { if withDrawing { let image = generateImage(controller.size, contextGenerator: { size, context in let bounds = CGRect(origin: .zero, size: size) if let cgImage = currentImage.cgImage { context.draw(cgImage, in: bounds) } if let cgImage = strongSelf.drawingView.drawingImage?.cgImage { context.draw(cgImage, in: bounds) } context.translateBy(x: size.width / 2.0, y: size.height / 2.0) context.scaleBy(x: 1.0, y: -1.0) context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) entitiesLayer?.render(in: context) }, opaque: true, scale: 1.0) return image } else { return currentImage } } else { return nil } } self._entitiesView?.selectionContainerView = self.selectionContainerView self._entitiesView?.selectionChanged = { [weak self] entity in if let strongSelf = self { strongSelf.updateSelectedEntity.invoke(entity) } } self._entitiesView?.requestedMenuForEntityView = { [weak self] entityView, isTopmost in guard let strongSelf = self else { return } if strongSelf.currentMenuController != nil { if let entityView = entityView as? DrawingTextEntityView { entityView.beginEditing(accessoryView: strongSelf.textEditAccessoryView) } return } var actions: [ContextMenuAction] = [] actions.append(ContextMenuAction(content: .text(title: strongSelf.presentationData.strings.Paint_Delete, accessibilityLabel: strongSelf.presentationData.strings.Paint_Delete), action: { [weak self, weak entityView] in if let strongSelf = self, let entityView = entityView { strongSelf.entitiesView.remove(uuid: entityView.entity.uuid) } })) if let entityView = entityView as? DrawingTextEntityView { actions.append(ContextMenuAction(content: .text(title: strongSelf.presentationData.strings.Paint_Edit, accessibilityLabel: strongSelf.presentationData.strings.Paint_Edit), action: { [weak self, weak entityView] in if let strongSelf = self, let entityView = entityView { entityView.beginEditing(accessoryView: strongSelf.textEditAccessoryView) strongSelf.entitiesView.selectEntity(entityView.entity) } })) } if !isTopmost { actions.append(ContextMenuAction(content: .text(title: "Move Forward", accessibilityLabel: "Move Forward"), action: { [weak self, weak entityView] in if let strongSelf = self, let entityView = entityView { strongSelf.entitiesView.bringToFront(uuid: entityView.entity.uuid) } })) } actions.append(ContextMenuAction(content: .text(title: strongSelf.presentationData.strings.Paint_Duplicate, accessibilityLabel: strongSelf.presentationData.strings.Paint_Duplicate), action: { [weak self, weak entityView] in if let strongSelf = self, let entityView = entityView { let newEntity = strongSelf.entitiesView.duplicate(entityView.entity) strongSelf.entitiesView.selectEntity(newEntity) } })) let entityFrame = entityView.convert(entityView.selectionBounds, to: strongSelf.view).offsetBy(dx: 0.0, dy: -6.0) let controller = ContextMenuController(actions: actions) strongSelf.currentMenuController = controller strongSelf.controller?.present( controller, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in if let strongSelf = self { return (strongSelf, entityFrame, strongSelf, strongSelf.bounds) } else { return nil } }) ) } self.insertEntity.connect { [weak self] entity in if let strongSelf = self, let entitiesView = strongSelf._entitiesView { entitiesView.prepareNewEntity(entity) entitiesView.add(entity) entitiesView.selectEntity(entity) if let entityView = entitiesView.getView(for: entity.uuid) as? DrawingTextEntityView { entityView.beginEditing(accessoryView: strongSelf.textEditAccessoryView) } } } self.deselectEntity.connect { [weak self] in if let strongSelf = self, let entitiesView = strongSelf._entitiesView { entitiesView.selectEntity(nil) } } self.updatePlayback.connect { [weak self] play in if let strongSelf = self, let entitiesView = strongSelf._entitiesView { if play { entitiesView.play() } else { entitiesView.pause() } } } } return self._entitiesView! } private var _selectionContainerView: DrawingSelectionContainerView? var selectionContainerView: DrawingSelectionContainerView { if self._selectionContainerView == nil { self._selectionContainerView = DrawingSelectionContainerView(frame: .zero) } return self._selectionContainerView! } private var _contentWrapperView: PortalSourceView? var contentWrapperView: PortalSourceView { if self._contentWrapperView == nil { self._contentWrapperView = PortalSourceView() } return self._contentWrapperView! } init(controller: DrawingScreen, context: AccountContext) { self.controller = controller self.context = context self.updateState = ActionSlot() self.updateColor = ActionSlot() self.performAction = ActionSlot() self.updateToolState = ActionSlot() self.updateSelectedEntity = ActionSlot() self.insertEntity = ActionSlot() self.deselectEntity = ActionSlot() self.updatePlayback = ActionSlot() self.previewBrushSize = ActionSlot() self.apply = ActionSlot() self.dismiss = ActionSlot() self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 } self.componentHost = ComponentView() self.textEditAccessoryView = UIInputView(frame: CGRect(origin: .zero, size: CGSize(width: 100.0, height: 44.0)), inputViewStyle: .keyboard) self.textEditAccessoryHost = ComponentView() super.init() self.apply.connect { [weak self] _ in if let strongSelf = self { strongSelf.controller?.requestApply() } } self.dismiss.connect { [weak self] _ in if let strongSelf = self { if !strongSelf.drawingView.isEmpty { 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 } func presentEyedropper(dismissed: @escaping () -> Void) { guard let controller = self.controller else { return } self.entitiesView.pause() guard let currentImage = controller.getCurrentImage() else { return } let sourceImage = generateImage(controller.drawingView.imageSize, contextGenerator: { size, context in let bounds = CGRect(origin: .zero, size: size) if let cgImage = currentImage.cgImage { context.draw(cgImage, in: bounds) } if let cgImage = controller.drawingView.drawingImage?.cgImage { context.draw(cgImage, in: bounds) } context.translateBy(x: size.width / 2.0, y: size.height / 2.0) context.scaleBy(x: 1.0, y: -1.0) context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) controller.entitiesView.layer.render(in: context) }, opaque: true, scale: 1.0) guard let sourceImage = sourceImage else { return } let eyedropperView = EyedropperView(containerSize: controller.contentWrapperView.frame.size, drawingView: controller.drawingView, sourceImage: sourceImage) eyedropperView.completed = { [weak self, weak controller] color in if let strongSelf = self, let controller = controller { strongSelf.updateColor.invoke(color) controller.entitiesView.play() dismissed() } } eyedropperView.frame = controller.contentWrapperView.convert(controller.contentWrapperView.bounds, to: controller.view) controller.view.addSubview(eyedropperView) } func presentColorPicker(initialColor: DrawingColor, dismissed: @escaping () -> Void = {}) { guard let controller = self.controller else { return } self.hapticFeedback.impact(.medium) let colorController = ColorPickerScreen(context: self.context, initialColor: initialColor, updated: { [weak self] color in self?.updateColor.invoke(color) }, openEyedropper: { [weak self] in self?.presentEyedropper(dismissed: dismissed) }, dismissed: { dismissed() }) controller.present(colorController, in: .window(.root)) } private var fastColorPickerView: ColorSpectrumPickerView? func presentFastColorPicker(sourceView: UIView) { guard self.fastColorPickerView == nil, let superview = sourceView.superview else { return } self.hapticFeedback.impact(.medium) let size = CGSize(width: min(350.0, superview.frame.width - 8.0 - 24.0), height: 296.0) let fastColorPickerView = ColorSpectrumPickerView(frame: CGRect(origin: CGPoint(x: sourceView.frame.minX + 5.0, y: sourceView.frame.maxY - size.height - 6.0), size: size)) fastColorPickerView.selected = { [weak self] color in self?.updateColor.invoke(color) } let _ = fastColorPickerView.updateLayout(size: size, selectedColor: nil) sourceView.superview?.addSubview(fastColorPickerView) fastColorPickerView.animateIn() self.fastColorPickerView = fastColorPickerView } func updateFastColorPickerPan(_ point: CGPoint) { guard let fastColorPickerView = self.fastColorPickerView else { return } fastColorPickerView.handlePan(point: point) } func dismissFastColorPicker() { guard let fastColorPickerView = self.fastColorPickerView else { return } self.fastColorPickerView = nil fastColorPickerView.animateOut(completion: { [weak fastColorPickerView] in fastColorPickerView?.removeFromSuperview() }) } func animateIn() { if let buttonView = self.componentHost.findTaggedView(tag: undoButtonTag) { buttonView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) buttonView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3) } if let buttonView = self.componentHost.findTaggedView(tag: clearAllButtonTag) { buttonView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) buttonView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3) } if let buttonView = self.componentHost.findTaggedView(tag: addButtonTag) { buttonView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) buttonView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3) } } func animateOut(completion: @escaping () -> Void) { if let layout = self.validLayout { self.containerLayoutUpdated(layout: layout, animateOut: true, transition: .easeInOut(duration: 0.2)) } if let buttonView = self.componentHost.findTaggedView(tag: undoButtonTag) { buttonView.alpha = 0.0 buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) buttonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3) } if let buttonView = self.componentHost.findTaggedView(tag: 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 view = self.componentHost.findTaggedView(tag: toolsTag) as? ToolsComponent.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 containerLayoutUpdated(layout: ContainerViewLayout, animateOut: Bool = false, transition: Transition) { guard let controller = self.controller else { return } let isFirstTime = self.validLayout == nil self.validLayout = layout let environment = ViewControllerComponentContainer.Environment( statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: 0.0, safeInsets: UIEdgeInsets(top: layout.intrinsicInsets.top + layout.safeInsets.top, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom + layout.safeInsets.bottom, right: layout.safeInsets.right), inputHeight: layout.inputHeight ?? 0.0, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, isVisible: true, theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, controller: { [weak self] in return self?.controller } ) var transition = transition if isFirstTime { transition = transition.withUserData(DrawingScreenTransition.animateIn) } else if animateOut { transition = transition.withUserData(DrawingScreenTransition.animateOut) } let componentSize = self.componentHost.update( transition: transition, component: AnyComponent( DrawingScreenComponent( context: self.context, isAvatar: controller.isAvatar, present: { [weak self] c in self?.controller?.present(c, in: .window(.root)) }, updateState: self.updateState, updateColor: self.updateColor, performAction: self.performAction, updateToolState: self.updateToolState, updateSelectedEntity: self.updateSelectedEntity, insertEntity: self.insertEntity, deselectEntity: self.deselectEntity, updatePlayback: self.updatePlayback, previewBrushSize: self.previewBrushSize, apply: self.apply, dismiss: self.dismiss, presentColorPicker: { [weak self] initialColor in self?.presentColorPicker(initialColor: initialColor) }, presentFastColorPicker: { [weak self] sourceView in self?.presentFastColorPicker(sourceView: sourceView) }, updateFastColorPickerPan: { [weak self] point in self?.updateFastColorPickerPan(point) }, dismissFastColorPicker: { [weak self] in self?.dismissFastColorPicker() } ) ), environment: { environment }, forceUpdate: animateOut, containerSize: layout.size ) if let componentView = self.componentHost.view { if componentView.superview == nil { self.view.insertSubview(componentView, at: 0) componentView.clipsToBounds = true } let componentFrame = CGRect(origin: .zero, size: componentSize) transition.setFrame(view: componentView, frame: CGRect(origin: componentFrame.origin, size: CGSize(width: componentFrame.width, height: componentFrame.height))) if isFirstTime { self.animateIn() } } if let entityView = self.entitiesView.selectedEntityView as? DrawingTextEntityView, let textEntity = entityView.entity as? DrawingTextEntity { var isFirstTime = true if let componentView = self.textEditAccessoryHost.view, componentView.superview != nil { isFirstTime = false } 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), alignment: DrawingTextAlignment(alignment: textEntity.alignment), font: DrawingTextFont(font: textEntity.font), isEmojiKeyboard: entityView.textView.inputView != nil, presentColorPicker: { [weak self] in guard let strongSelf = self, let entityView = strongSelf.entitiesView.selectedEntityView as? DrawingTextEntityView, let textEntity = entityView.entity as? DrawingTextEntity else { return } entityView.suspendEditing() self?.presentColorPicker(initialColor: textEntity.color, dismissed: { entityView.resumeEditing() }) }, presentFastColorPicker: { [weak self] buttonTag in if let buttonView = self?.textEditAccessoryHost.findTaggedView(tag: buttonTag) { self?.presentFastColorPicker(sourceView: buttonView) } }, updateFastColorPickerPan: { [weak self] point in self?.updateFastColorPickerPan(point) }, dismissFastColorPicker: { [weak self] in self?.dismissFastColorPicker() }, toggleStyle: { [weak self] in guard let strongSelf = self, let entityView = strongSelf.entitiesView.selectedEntityView as? DrawingTextEntityView, let textEntity = entityView.entity as? DrawingTextEntity else { return } var nextStyle: DrawingTextEntity.Style switch textEntity.style { case .regular: nextStyle = .filled case .filled: nextStyle = .semi case .semi: nextStyle = .stroke case .stroke: nextStyle = .regular } textEntity.style = nextStyle entityView.update() if let layout = strongSelf.validLayout { strongSelf.containerLayoutUpdated(layout: layout, transition: .immediate) } }, toggleAlignment: { [weak self] in guard let strongSelf = self, let entityView = strongSelf.entitiesView.selectedEntityView as? DrawingTextEntityView, let textEntity = entityView.entity as? DrawingTextEntity else { return } var nextAlignment: DrawingTextEntity.Alignment switch textEntity.alignment { case .left: nextAlignment = .center case .center: nextAlignment = .right case .right: nextAlignment = .left } textEntity.alignment = nextAlignment entityView.update() if let layout = strongSelf.validLayout { strongSelf.containerLayoutUpdated(layout: layout, transition: .immediate) } }, updateFont: { [weak self] font in guard let strongSelf = self, let entityView = strongSelf.entitiesView.selectedEntityView as? DrawingTextEntityView, let textEntity = entityView.entity as? DrawingTextEntity else { return } textEntity.font = font.font entityView.update() if let layout = strongSelf.validLayout { strongSelf.containerLayoutUpdated(layout: layout, transition: .immediate) } }, toggleKeyboard: { [weak self] in guard let strongSelf = self else { return } 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) } } } } 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) 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() } // inputView?.presentController = { [weak self] c in // guard let strongSelf = self else { // return // } // strongSelf.presentController(c) // } textView.inputView = inputView } else { textView.inputView = nil } if textView.isFirstResponder { textView.reloadInputViews() } else { textView.becomeFirstResponder() } if let layout = self.validLayout { self.containerLayoutUpdated(layout: layout, animateOut: false, transition: .immediate) } } } fileprivate var node: Node { return self.displayNode as! Node } private let context: AccountContext private let size: CGSize private let originalSize: CGSize private let isAvatar: Bool public var requestDismiss: (() -> Void)! public var requestApply: (() -> Void)! public var getCurrentImage: (() -> UIImage?)! public init(context: AccountContext, size: CGSize, originalSize: CGSize, isAvatar: Bool) { self.context = context self.size = size self.originalSize = originalSize self.isAvatar = isAvatar super.init(navigationBarPresentationData: nil) self.statusBar.statusBarStyle = .Hide } public var drawingView: DrawingView { return self.node.drawingView } public var entitiesView: DrawingEntitiesView { return self.node.entitiesView } public var selectionContainerView: DrawingSelectionContainerView { return self.node.selectionContainerView } public var contentWrapperView: PortalSourceView { return self.node.contentWrapperView } required public init(coder: NSCoder) { preconditionFailure() } deinit { print() } override public func loadDisplayNode() { self.displayNode = Node(controller: self, context: self.context) super.displayNodeDidLoad() } public func generateResultData() -> TGPaintingData! { if self.drawingView.isEmpty && self.entitiesView.entities.isEmpty { return nil } let paintingImage = generateImage(self.drawingView.imageSize, contextGenerator: { size, context in let bounds = CGRect(origin: .zero, size: size) context.clear(bounds) if let cgImage = self.drawingView.drawingImage?.cgImage { context.draw(cgImage, in: bounds) } }, opaque: false, scale: 1.0) var hasAnimatedEntities = false for entity in self.entitiesView.entities { if entity.isAnimated { hasAnimatedEntities = true break } // if let entity = entity as? DrawingStickerEntity { // let coder = PostboxEncoder() // coder.encodeRootObject(entity.file) // // let baseSize = max(10.0, min(entity.referenceDrawingSize.width, entity.referenceDrawingSize.height) * 0.38) // if let stickerEntity = TGPhotoPaintStickerEntity(document: coder.makeData(), baseSize: CGSize(width: baseSize, height: baseSize), animated: entity.isAnimated) { // stickerEntity.position = entity.position // stickerEntity.scale = entity.scale // stickerEntity.angle = entity.rotation // legacyEntities.append(stickerEntity) // } // } else if let entity = entity as? DrawingTextEntity, let view = self.entitiesView.getView(for: entity.uuid) as? DrawingTextEntityView { // let textEntity = TGPhotoPaintStaticEntity() // textEntity.position = entity.position // textEntity.angle = entity.rotation // textEntity.renderImage = view.getRenderImage() // legacyEntities.append(textEntity) // } else if let _ = entity as? DrawingSimpleShapeEntity { // // } else if let _ = entity as? DrawingBubbleEntity { // // } else if let _ = entity as? DrawingVectorEntity { // // } } let finalImage = generateImage(self.drawingView.imageSize, contextGenerator: { size, context in let bounds = CGRect(origin: .zero, size: size) context.clear(bounds) if let cgImage = paintingImage?.cgImage { context.draw(cgImage, in: bounds) } context.translateBy(x: size.width / 2.0, y: size.height / 2.0) context.scaleBy(x: 1.0, y: -1.0) context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) //hide animated self.entitiesView.layer.render(in: context) }, opaque: false, scale: 1.0) var image = paintingImage var stillImage: UIImage? if hasAnimatedEntities { stillImage = finalImage } else { image = finalImage } return TGPaintingData(drawing: nil, entitiesData: self.entitiesView.entitiesData, image: image, stillImage: stillImage, hasAnimation: hasAnimatedEntities) // return TGPaintingData(painting: nil, image: image, stillImage: stillImage, entities: legacyEntities, undoManager: nil) } public func resultImage() -> UIImage! { let image = generateImage(self.drawingView.imageSize, contextGenerator: { size, context in let bounds = CGRect(origin: .zero, size: size) context.clear(bounds) if let cgImage = self.drawingView.drawingImage?.cgImage { context.draw(cgImage, in: bounds) } context.translateBy(x: size.width / 2.0, y: size.height / 2.0) context.scaleBy(x: 1.0, y: -1.0) context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) self.entitiesView.layer.render(in: context) }, opaque: false, scale: 1.0) return image } public func animateOut(_ completion: (() -> Void)!) { self.selectionContainerView.alpha = 0.0 self.node.animateOut(completion: completion) } override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) (self.displayNode as! Node).containerLayoutUpdated(layout: layout, transition: Transition(transition)) } public func adapterContainerLayoutUpdatedSize(_ size: CGSize, intrinsicInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, statusBarHeight: CGFloat, inputHeight: CGFloat, animated: Bool) { let layout = ContainerViewLayout( size: size, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact), deviceMetrics: DeviceMetrics(screenSize: size, scale: UIScreen.main.scale, statusBarHeight: statusBarHeight, onScreenNavigationHeight: nil), intrinsicInsets: intrinsicInsets, safeInsets: safeInsets, additionalInsets: .zero, statusBarHeight: statusBarHeight, inputHeight: inputHeight, inputHeightIsInteractivellyChanging: false, inVoiceOver: false ) self.containerLayoutUpdated(layout, transition: animated ? .animated(duration: 0.3, curve: .easeInOut) : .immediate) } }