diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 7550088fd0..97a82aa15a 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -8518,6 +8518,7 @@ Sorry for the inconvenience."; "Privacy.ProfilePhoto.CustomOverrideInfo" = "You can add users or entire groups which will not see your profile photo."; "Privacy.ProfilePhoto.CustomOverrideAddInfo" = "Add users or entire groups which will still see your profile photo."; +"Privacy.ProfilePhoto.CustomOverrideBothInfo" = "You can add users or entire groups as exceptions that will override the settings above."; "WebApp.AddToAttachmentAllowMessages" = "Allow **%@** to send me messages"; diff --git a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift index 03dac59ab2..ce8367c994 100644 --- a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift @@ -127,6 +127,12 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin self.iconDisposable?.dispose() } + override func didLoad() { + super.didLoad() + + self.view.isExclusiveTouch = true + } + @objc private func pressed() { guard let controller = self.getController() else { return diff --git a/submodules/DrawingUI/BUILD b/submodules/DrawingUI/BUILD index d0b9ea1261..6f8d77266d 100644 --- a/submodules/DrawingUI/BUILD +++ b/submodules/DrawingUI/BUILD @@ -91,6 +91,7 @@ swift_library( "//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode:ChatEntityKeyboardInputNode", "//submodules/FeaturedStickersScreen:FeaturedStickersScreen", "//submodules/TelegramNotices:TelegramNotices", + "//submodules/FastBlur:FastBlur", ], visibility = [ "//visibility:public", diff --git a/submodules/DrawingUI/Resources/pencil.png b/submodules/DrawingUI/Resources/pencil.png deleted file mode 100644 index 17c0a20afa..0000000000 Binary files a/submodules/DrawingUI/Resources/pencil.png and /dev/null differ diff --git a/submodules/DrawingUI/Sources/ColorPickerScreen.swift b/submodules/DrawingUI/Sources/ColorPickerScreen.swift index a8ddcdc0e8..0314c8ac07 100644 --- a/submodules/DrawingUI/Sources/ColorPickerScreen.swift +++ b/submodules/DrawingUI/Sources/ColorPickerScreen.swift @@ -277,7 +277,7 @@ private class ColorSliderComponent: Component { self.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))) if !self.transparencyLayer.isHidden { - self.transparencyLayer.contents = generateCheckeredImage(size: sliderSize, whiteColor: UIColor(rgb: 0xffffff, alpha: 0.1), blackColor: .clear, length: 12.0)?.cgImage + self.transparencyLayer.contents = generateCheckeredImage(size: sliderSize, whiteColor: UIColor(rgb: 0xffffff, alpha: 1.0), blackColor: .clear, length: 12.0)?.cgImage } self.knob.contents = generateKnobImage()?.cgImage @@ -437,6 +437,10 @@ private class ColorFieldComponent: Component { } } + func textFieldDidBeginEditing(_ textField: UITextField) { + textField.selectAll(nil) + } + func textFieldShouldReturn(_ textField: UITextField) -> Bool { return false } @@ -1247,13 +1251,14 @@ private final class ColorSlidersComponent: CombinedComponent { type: .number, value: "\(Int(component.color.red * 255.0))", updated: { value in - if let intValue = Int(value) { - updateColor(currentColor.withUpdatedRed(CGFloat(intValue) / 255.0)) - } + let intValue = Int(value) ?? 0 + updateColor(currentColor.withUpdatedRed(CGFloat(intValue) / 255.0)) }, shouldUpdate: { value in if let intValue = Int(value), intValue >= 0 && intValue <= 255 { return true + } else if value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + return true } else { return false } @@ -1313,13 +1318,14 @@ private final class ColorSlidersComponent: CombinedComponent { type: .number, value: "\(Int(component.color.green * 255.0))", updated: { value in - if let intValue = Int(value) { - updateColor(currentColor.withUpdatedGreen(CGFloat(intValue) / 255.0)) - } + let intValue = Int(value) ?? 0 + updateColor(currentColor.withUpdatedGreen(CGFloat(intValue) / 255.0)) }, shouldUpdate: { value in if let intValue = Int(value), intValue >= 0 && intValue <= 255 { return true + } else if value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + return true } else { return false } @@ -1379,13 +1385,14 @@ private final class ColorSlidersComponent: CombinedComponent { type: .number, value: "\(Int(component.color.blue * 255.0))", updated: { value in - if let intValue = Int(value) { - updateColor(currentColor.withUpdatedBlue(CGFloat(intValue) / 255.0)) - } + let intValue = Int(value) ?? 0 + updateColor(currentColor.withUpdatedBlue(CGFloat(intValue) / 255.0)) }, shouldUpdate: { value in if let intValue = Int(value), intValue >= 0 && intValue <= 255 { return true + } else if value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + return true } else { return false } @@ -2198,8 +2205,19 @@ private final class ColorPickerContent: CombinedComponent { type: .number, value: "\(Int(state.selectedColor.alpha * 100.0))", suffix: "%", - updated: { _ in }, - shouldUpdate: { _ in return true } + updated: { value in + let intValue = Int(value) ?? 0 + state.updateAlpha(CGFloat(intValue) / 100.0) + }, + shouldUpdate: { value in + if let intValue = Int(value), intValue >= 0 && intValue <= 100 { + return true + } else if value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + return true + } else { + return false + } + } ), availableSize: CGSize(width: 77.0, height: 36.0), transition: .immediate diff --git a/submodules/DrawingUI/Sources/DrawingBubbleEntity.swift b/submodules/DrawingUI/Sources/DrawingBubbleEntity.swift index d221c5649b..0e71863519 100644 --- a/submodules/DrawingUI/Sources/DrawingBubbleEntity.swift +++ b/submodules/DrawingUI/Sources/DrawingBubbleEntity.swift @@ -39,6 +39,8 @@ public final class DrawingBubbleEntity: DrawingEntity, Codable { return self.position } + public var scale: CGFloat = 1.0 + public var renderImage: UIImage? init(drawType: DrawType, color: DrawingColor, lineWidth: CGFloat) { @@ -174,6 +176,11 @@ final class DrawingBubbleEntityView: DrawingEntityView { return max(10.0, max(self.bubbleEntity.referenceDrawingSize.width, self.bubbleEntity.referenceDrawingSize.height) * 0.1) } + fileprivate var minimumSize: CGSize { + let minSize = min(self.bubbleEntity.referenceDrawingSize.width, self.bubbleEntity.referenceDrawingSize.height) + return CGSize(width: minSize * 0.1, height: minSize * 0.1) + } + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { let lineWidth = self.maxLineWidth * 0.5 let expandedBounds = self.bounds.insetBy(dx: -lineWidth, dy: -lineWidth) @@ -285,7 +292,7 @@ final class DrawingBubbleEntititySelectionView: DrawingEntitySelectionView, UIGe self.snapTool.onSnapYUpdated = { [weak self] snapped in if let strongSelf = self, let entityView = strongSelf.entityView { - entityView.onSnapToXAxis(snapped) + entityView.onSnapToYAxis(snapped) } } } @@ -312,7 +319,7 @@ final class DrawingBubbleEntititySelectionView: DrawingEntitySelectionView, UIGe private var currentHandle: CALayer? @objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { - guard let entityView = self.entityView, let entity = entityView.entity as? DrawingBubbleEntity else { + guard let entityView = self.entityView as? DrawingBubbleEntityView, let entity = entityView.entity as? DrawingBubbleEntity else { return } let location = gestureRecognizer.location(in: self) @@ -338,37 +345,39 @@ final class DrawingBubbleEntititySelectionView: DrawingEntitySelectionView, UIGe var updatedPosition = entity.position var updatedTailPosition = entity.tailPosition + let minimumSize = entityView.minimumSize + if self.currentHandle === self.leftHandle { - updatedSize.width -= delta.x + updatedSize.width = max(minimumSize.width, updatedSize.width - delta.x) updatedPosition.x -= delta.x * -0.5 } else if self.currentHandle === self.rightHandle { - updatedSize.width += delta.x + updatedSize.width = max(minimumSize.width, updatedSize.width + delta.x) updatedPosition.x += delta.x * 0.5 } else if self.currentHandle === self.topHandle { - updatedSize.height -= delta.y + updatedSize.height = max(minimumSize.height, updatedSize.height - delta.y) updatedPosition.y += delta.y * 0.5 } else if self.currentHandle === self.bottomHandle { - updatedSize.height += delta.y + updatedSize.height = max(minimumSize.height, updatedSize.height + delta.y) updatedPosition.y += delta.y * 0.5 } else if self.currentHandle === self.topLeftHandle { - updatedSize.width -= delta.x + updatedSize.width = max(minimumSize.width, updatedSize.width - delta.x) updatedPosition.x -= delta.x * -0.5 - updatedSize.height -= delta.y + updatedSize.height = max(minimumSize.height, updatedSize.height - delta.y) updatedPosition.y += delta.y * 0.5 } else if self.currentHandle === self.topRightHandle { - updatedSize.width += delta.x + updatedSize.width = max(minimumSize.width, updatedSize.width + delta.x) updatedPosition.x += delta.x * 0.5 - updatedSize.height -= delta.y + updatedSize.height = max(minimumSize.height, updatedSize.height - delta.y) updatedPosition.y += delta.y * 0.5 } else if self.currentHandle === self.bottomLeftHandle { - updatedSize.width -= delta.x + updatedSize.width = max(minimumSize.width, updatedSize.width - delta.x) updatedPosition.x -= delta.x * -0.5 - updatedSize.height += delta.y + updatedSize.height = max(minimumSize.height, updatedSize.height + delta.y) updatedPosition.y += delta.y * 0.5 } else if self.currentHandle === self.bottomRightHandle { - updatedSize.width += delta.x + updatedSize.width = max(minimumSize.width, updatedSize.width + delta.x) updatedPosition.x += delta.x * 0.5 - updatedSize.height += delta.y + updatedSize.height = max(minimumSize.height, updatedSize.height + delta.y) updatedPosition.y += delta.y * 0.5 } else if self.currentHandle === self.tailHandle { updatedTailPosition = CGPoint(x: max(0.0, min(1.0, updatedTailPosition.x + delta.x / updatedSize.width)), y: max(0.0, min(updatedSize.height, updatedTailPosition.y + delta.y))) @@ -382,7 +391,7 @@ final class DrawingBubbleEntititySelectionView: DrawingEntitySelectionView, UIGe entity.size = updatedSize entity.position = updatedPosition entity.tailPosition = updatedTailPosition - entityView.update() + entityView.update(animated: false) gestureRecognizer.setTranslation(.zero, in: entityView) case .ended: diff --git a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift index 2d57b09f46..4705d1016f 100644 --- a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift +++ b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift @@ -4,6 +4,7 @@ import Display import LegacyComponents import AccountContext + public protocol DrawingEntity: AnyObject { var uuid: UUID { get } var isAnimated: Bool { get } @@ -11,7 +12,9 @@ public protocol DrawingEntity: AnyObject { var lineWidth: CGFloat { get set } var color: DrawingColor { get set } - + + var scale: CGFloat { get set } + func duplicate() -> DrawingEntity var currentEntityView: DrawingEntityView? { get } @@ -123,6 +126,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { private let context: AccountContext private let size: CGSize + weak var drawingView: DrawingView? weak var selectionContainerView: DrawingSelectionContainerView? private var tapGestureRecognizer: UITapGestureRecognizer! @@ -167,6 +171,10 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { fatalError("init(coder:) has not been implemented") } + deinit { + print() + } + public override func layoutSubviews() { super.layoutSubviews() @@ -188,9 +196,12 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { return entities } + private var initialEntitiesData: Data? public func setup(withEntitiesData entitiesData: Data!) { self.clear() + self.initialEntitiesData = entitiesData + if let entitiesData = entitiesData, let codableEntities = try? JSONDecoder().decode([CodableDrawingEntity].self, from: entitiesData) { let entities = codableEntities.map { $0.entity } for entity in entities { @@ -215,6 +226,15 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { } } + var hasChanges: Bool { + if let initialEntitiesData = self.initialEntitiesData { + let entitiesData = self.entitiesData + return entitiesData != initialEntitiesData + } else { + return !self.entities.isEmpty + } + } + private func startPosition(relativeTo entity: DrawingEntity?) -> CGPoint { let offsetLength = round(self.size.width * 0.1) let offset = CGPoint(x: offsetLength, y: offsetLength) @@ -244,14 +264,15 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { } private func newEntitySize() -> CGSize { - let width = round(self.size.width * 0.5) - + let zoomScale = 1.0 / (self.drawingView?.zoomScale ?? 1.0) + let width = round(self.size.width * 0.5) * zoomScale return CGSize(width: width, height: width) } func prepareNewEntity(_ entity: DrawingEntity, setup: Bool = true, relativeTo: DrawingEntity? = nil) { let center = self.startPosition(relativeTo: relativeTo) let rotation = self.getEntityInitialRotation() + let zoomScale = 1.0 / (self.drawingView?.zoomScale ?? 1.0) if let shape = entity as? DrawingSimpleShapeEntity { shape.position = center @@ -280,7 +301,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { sticker.rotation = rotation if setup { sticker.referenceDrawingSize = self.size - sticker.scale = 1.0 + sticker.scale = zoomScale } } else if let bubble = entity as? DrawingBubbleEntity { bubble.position = center @@ -298,6 +319,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { text.referenceDrawingSize = self.size text.width = floor(self.size.width * 0.9) text.fontSize = 0.3 + text.scale = zoomScale } } } @@ -368,7 +390,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { view?.removeFromSuperview() }) if !(view.entity is DrawingVectorEntity) { - view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false) + view.layer.animateScale(from: view.entity.scale, to: 0.1, duration: 0.2, removeOnCompletion: false) } if let selectionView = view.selectionView { selectionView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak selectionView] _ in diff --git a/submodules/DrawingUI/Sources/DrawingGesture.swift b/submodules/DrawingUI/Sources/DrawingGesture.swift index affa06b605..8ad353cc98 100644 --- a/submodules/DrawingUI/Sources/DrawingGesture.swift +++ b/submodules/DrawingUI/Sources/DrawingGesture.swift @@ -255,7 +255,22 @@ class DrawingGestureRecognizer: UIGestureRecognizer, UIGestureRecognizerDelegate } class DrawingGesturePipeline { + struct Point { + let location: CGPoint + let velocity: CGFloat + let timestamp: Double + + var x: CGFloat { + return self.location.x + } + + var y: CGFloat { + return self.location.y + } + } + enum Mode { + case direct case location case smoothCurve case polyline @@ -269,6 +284,7 @@ class DrawingGesturePipeline { } enum DrawingResult { + case point(DrawingGesturePipeline.Point) case location(Polyline.Point) case smoothCurve(BezierPath) case polyline(Polyline) @@ -303,6 +319,7 @@ class DrawingGesturePipeline { view.addGestureRecognizer(gestureRecognizer) } + var previousPoint: Point? @objc private func handleGesture(_ gestureRecognizer: DrawingGestureRecognizer) { let state: DrawingGestureState switch gestureRecognizer.state { @@ -322,6 +339,29 @@ class DrawingGesturePipeline { state = .cancelled } + if case .direct = self.mode, let touch = self.pendingTouches.first { + if state == .began { + self.previousPoint = nil + } + + var velocity: Double = 0.0 + if let previousPoint = self.previousPoint { + let distance = touch.location.distance(to: previousPoint.location) + let elapsed = max(0.0, touch.timestamp - previousPoint.timestamp) + velocity = elapsed > 0.0 ? distance / elapsed : 0.0 + } else { + velocity = 0.0 + } + + let point = Point(location: touch.location, velocity: velocity, timestamp: touch.timestamp) + self.previousPoint = point + + self.onDrawing(state, .point(point)) + + self.pendingTouches.removeAll() + return + } + let touchDeltas = self.processTouchEvents(self.pendingTouches) let polylineDeltas = self.processTouchPaths(inputDeltas: touchDeltas) let simplifiedPolylineDeltas = self.simplifyPolylines(inputDeltas: polylineDeltas) @@ -339,6 +379,8 @@ class DrawingGesturePipeline { if let polyline = self.simplifiedPolylines.last { self.onDrawing(state, .polyline(polyline)) } + case .direct: + break } self.pendingTouches.removeAll() diff --git a/submodules/DrawingUI/Sources/DrawingMetalView.swift b/submodules/DrawingUI/Sources/DrawingMetalView.swift index 22ba6b4893..688c2226f1 100644 --- a/submodules/DrawingUI/Sources/DrawingMetalView.swift +++ b/submodules/DrawingUI/Sources/DrawingMetalView.swift @@ -22,7 +22,7 @@ final class DrawingMetalView: MTKView { init?(size: CGSize) { var size = size if Int(size.width) % 16 != 0 { - size.width = round(size.width / 16.0) * 16.0 + size.width = ceil(size.width / 16.0) * 16.0 } let mainBundle = Bundle(for: DrawingView.self) @@ -46,11 +46,14 @@ final class DrawingMetalView: MTKView { self.commandQueue = commandQueue self.size = size - + super.init(frame: CGRect(origin: .zero, size: size), device: device) + self.autoResizeDrawable = false self.isOpaque = false + self.contentScaleFactor = 1.0 self.drawableSize = self.size + //self.presentsWithTransaction = true self.setup() } @@ -87,7 +90,7 @@ final class DrawingMetalView: MTKView { private func setup() { self.drawable = Drawable(size: self.size, pixelFormat: self.colorPixelFormat, device: device) - + let size = self.size let w = size.width, h = size.height let vertices = [ @@ -97,32 +100,46 @@ final class DrawingMetalView: MTKView { Vertex(position: CGPoint(x: w , y: h), texCoord: CGPoint(x: 1, y: 1)), ] self.render_target_vertex = self.device?.makeBuffer(bytes: vertices, length: MemoryLayout.stride * vertices.count, options: .cpuCacheModeWriteCombined) - + let matrix = Matrix.identity matrix.scaling(x: 2.0 / Float(size.width), y: -2.0 / Float(size.height), z: 1) matrix.translation(x: -1, y: 1, z: 0) self.render_target_uniform = self.device?.makeBuffer(bytes: matrix.m, length: MemoryLayout.size * 16, options: []) - + let vertexFunction = self.library.makeFunction(name: "vertex_render_target") let fragmentFunction = self.library.makeFunction(name: "fragment_render_target") let pipelineDescription = MTLRenderPipelineDescriptor() pipelineDescription.vertexFunction = vertexFunction pipelineDescription.fragmentFunction = fragmentFunction pipelineDescription.colorAttachments[0].pixelFormat = colorPixelFormat - + do { self.pipelineState = try self.device?.makeRenderPipelineState(descriptor: pipelineDescription) } catch { fatalError(error.localizedDescription) } - + if let url = getAppBundle().url(forResource: "marker", withExtension: "png"), let data = try? Data(contentsOf: url) { self.markerBrush = Brush(texture: self.makeTexture(with: data), target: self, rotation: .fixed(-0.55)) } + + self.drawable?.clear() } - var clearOnce = false + + override var frame: CGRect { + get { + return super.frame + } set { + super.frame = newValue + self.drawableSize = self.size + } + } + override func draw(_ rect: CGRect) { + guard !self.isHidden || (self.drawable?.isClearing ?? false) else { + return + } super.draw(rect) guard let drawable = self.drawable, let texture = drawable.texture?.texture else { @@ -135,38 +152,35 @@ final class DrawingMetalView: MTKView { attachment?.texture = self.currentDrawable?.texture attachment?.loadAction = .clear attachment?.storeAction = .store - + guard let _ = attachment?.texture else { return } - + let commandBuffer = self.commandQueue.makeCommandBuffer() let commandEncoder = commandBuffer?.makeRenderCommandEncoder(descriptor: renderPassDescriptor) - + commandEncoder?.setRenderPipelineState(self.pipelineState) - + commandEncoder?.setVertexBuffer(self.render_target_vertex, offset: 0, index: 0) commandEncoder?.setVertexBuffer(self.render_target_uniform, offset: 0, index: 1) commandEncoder?.setFragmentTexture(texture, index: 0) commandEncoder?.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4) - + commandEncoder?.endEncoding() if let drawable = self.currentDrawable { commandBuffer?.present(drawable) } commandBuffer?.commit() } - + func clear() { guard let drawable = self.drawable else { return } - self.clearOnce = true drawable.updateBuffer(with: self.size) drawable.clear() - - drawable.commit(wait: true) } enum BrushType { @@ -215,7 +229,9 @@ private class Drawable { self.updateBuffer(with: size) } + var isClearing = false func clear() { + self.isClearing = true self.texture?.clear() self.commit(wait: true) } @@ -246,6 +262,10 @@ private class Drawable { self.commandBuffer?.waitUntilCompleted() } self.commandBuffer = nil + + if self.isClearing && wait { + self.isClearing = false + } } internal func makeTexture() -> Texture? { @@ -645,7 +665,7 @@ final class Texture { textureDescriptor.height = height textureDescriptor.usage = [.renderTarget, .shaderRead] textureDescriptor.storageMode = .shared - + guard let texture = device.makeTexture(descriptor: textureDescriptor) else { return nil } @@ -706,7 +726,7 @@ final class Texture { bitsPerComponent: 8, bitsPerPixel: 8 * 4, bytesPerRow: self.bytesPerRow, - space: DeviceGraphicsContextSettings.shared.colorSpace, + space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: DeviceGraphicsContextSettings.shared.transparentBitmapInfo, provider: dataProvider, decode: nil, diff --git a/submodules/DrawingUI/Sources/DrawingScreen.swift b/submodules/DrawingUI/Sources/DrawingScreen.swift index d736acdff4..2ff7d85923 100644 --- a/submodules/DrawingUI/Sources/DrawingScreen.swift +++ b/submodules/DrawingUI/Sources/DrawingScreen.swift @@ -18,6 +18,7 @@ import ContextUI import ChatEntityKeyboardInputNode import EntityKeyboard import TelegramUIPreferences +import FastBlur enum DrawingToolState: Equatable, Codable { private enum CodingKeys: String, CodingKey { @@ -31,8 +32,8 @@ enum DrawingToolState: Equatable, Codable { case arrow = 1 case marker = 2 case neon = 3 - case eraser = 4 - case blur = 5 + case blur = 4 + case eraser = 5 } struct BrushState: Equatable, Codable { @@ -100,8 +101,8 @@ enum DrawingToolState: Equatable, Codable { case arrow(BrushState) case marker(BrushState) case neon(BrushState) - case eraser(EraserState) case blur(EraserState) + case eraser(EraserState) func withUpdatedColor(_ color: DrawingColor) -> DrawingToolState { switch self { @@ -113,7 +114,7 @@ enum DrawingToolState: Equatable, Codable { return .marker(state.withUpdatedColor(color)) case let .neon(state): return .neon(state.withUpdatedColor(color)) - case .eraser, .blur: + case .blur, .eraser: return self } } @@ -128,10 +129,10 @@ enum DrawingToolState: Equatable, Codable { return .marker(state.withUpdatedSize(size)) case let .neon(state): return .neon(state.withUpdatedSize(size)) - case let .eraser(state): - return .eraser(state.withUpdatedSize(size)) case let .blur(state): return .blur(state.withUpdatedSize(size)) + case let .eraser(state): + return .eraser(state.withUpdatedSize(size)) } } @@ -148,7 +149,7 @@ enum DrawingToolState: Equatable, Codable { switch self { case let .pen(state), let .arrow(state), let .marker(state), let .neon(state): return state.size - case let .eraser(state), let .blur(state): + case let .blur(state), let .eraser(state): return state.size } } @@ -163,10 +164,10 @@ enum DrawingToolState: Equatable, Codable { return .marker case .neon: return .neon - case .eraser: - return .eraser case .blur: return .blur + case .eraser: + return .eraser } } @@ -183,10 +184,10 @@ enum DrawingToolState: Equatable, Codable { self = .marker(try container.decode(BrushState.self, forKey: .brushState)) case .neon: self = .neon(try container.decode(BrushState.self, forKey: .brushState)) - case .eraser: - self = .eraser(try container.decode(EraserState.self, forKey: .eraserState)) 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)) @@ -208,12 +209,12 @@ enum DrawingToolState: Equatable, Codable { case let .neon(state): try container.encode(DrawingToolState.Key.neon.rawValue, forKey: .type) try container.encode(state, forKey: .brushState) - case let .eraser(state): - try container.encode(DrawingToolState.Key.eraser.rawValue, forKey: .type) - try container.encode(state, forKey: .eraserState) 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) } } } @@ -285,11 +286,24 @@ struct DrawingState: Equatable { .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)), - .eraser(DrawingToolState.EraserState(size: 0.5)), - .blur(DrawingToolState.EraserState(size: 0.5)) + .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 { @@ -365,6 +379,7 @@ private final class DrawingScreenComponent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext + let isVideo: Bool let isAvatar: Bool let present: (ViewController) -> Void let updateState: ActionSlot @@ -374,8 +389,9 @@ private final class DrawingScreenComponent: CombinedComponent { let updateSelectedEntity: ActionSlot let insertEntity: ActionSlot let deselectEntity: ActionSlot - let updatePlayback: ActionSlot + let updateEntitiesPlayback: ActionSlot let previewBrushSize: ActionSlot + let dismissEyedropper: ActionSlot let apply: ActionSlot let dismiss: ActionSlot @@ -386,6 +402,7 @@ private final class DrawingScreenComponent: CombinedComponent { init( context: AccountContext, + isVideo: Bool, isAvatar: Bool, present: @escaping (ViewController) -> Void, updateState: ActionSlot, @@ -395,8 +412,9 @@ private final class DrawingScreenComponent: CombinedComponent { updateSelectedEntity: ActionSlot, insertEntity: ActionSlot, deselectEntity: ActionSlot, - updatePlayback: ActionSlot, + updateEntitiesPlayback: ActionSlot, previewBrushSize: ActionSlot, + dismissEyedropper: ActionSlot, apply: ActionSlot, dismiss: ActionSlot, presentColorPicker: @escaping (DrawingColor) -> Void, @@ -405,6 +423,7 @@ private final class DrawingScreenComponent: CombinedComponent { dismissFastColorPicker: @escaping () -> Void ) { self.context = context + self.isVideo = isVideo self.isAvatar = isAvatar self.present = present self.updateState = updateState @@ -414,8 +433,9 @@ private final class DrawingScreenComponent: CombinedComponent { self.updateSelectedEntity = updateSelectedEntity self.insertEntity = insertEntity self.deselectEntity = deselectEntity - self.updatePlayback = updatePlayback + self.updateEntitiesPlayback = updateEntitiesPlayback self.previewBrushSize = previewBrushSize + self.dismissEyedropper = dismissEyedropper self.apply = apply self.dismiss = dismiss self.presentColorPicker = presentColorPicker @@ -484,7 +504,8 @@ private final class DrawingScreenComponent: CombinedComponent { private let updateToolState: ActionSlot private let insertEntity: ActionSlot private let deselectEntity: ActionSlot - private let updatePlayback: ActionSlot + private let updateEntitiesPlayback: ActionSlot + private let dismissEyedropper: ActionSlot private let present: (ViewController) -> Void var currentMode: Mode @@ -497,12 +518,13 @@ private final class DrawingScreenComponent: CombinedComponent { private let stickerPickerInputData = Promise() - init(context: AccountContext, updateToolState: ActionSlot, insertEntity: ActionSlot, deselectEntity: ActionSlot, updatePlayback: ActionSlot, present: @escaping (ViewController) -> Void) { + init(context: AccountContext, updateToolState: ActionSlot, insertEntity: ActionSlot, deselectEntity: ActionSlot, updateEntitiesPlayback: ActionSlot, dismissEyedropper: ActionSlot, present: @escaping (ViewController) -> Void) { self.context = context self.updateToolState = updateToolState self.insertEntity = insertEntity self.deselectEntity = deselectEntity - self.updatePlayback = updatePlayback + self.updateEntitiesPlayback = updateEntitiesPlayback + self.dismissEyedropper = dismissEyedropper self.present = present self.currentMode = .drawing @@ -647,6 +669,8 @@ private final class DrawingScreenComponent: CombinedComponent { var skipSelectedEntityUpdate = false func updateSelectedEntity(_ entity: DrawingEntity?) { + self.dismissEyedropper.invoke(Void()) + self.selectedEntity = entity if let entity = entity { if !entity.color.isClear { @@ -758,10 +782,10 @@ private final class DrawingScreenComponent: CombinedComponent { func presentStickerPicker() { self.currentMode = .sticker - self.updatePlayback.invoke(false) + self.updateEntitiesPlayback.invoke(false) let controller = StickerPickerScreen(context: self.context, inputData: self.stickerPickerInputData.get()) controller.completion = { [weak self] file in - self?.updatePlayback.invoke(true) + self?.updateEntitiesPlayback.invoke(true) if let file = file { let stickerEntity = DrawingStickerEntity(file: file) @@ -776,7 +800,7 @@ private final class DrawingScreenComponent: CombinedComponent { } func makeState() -> State { - return State(context: self.context, updateToolState: self.updateToolState, insertEntity: self.insertEntity, deselectEntity: self.deselectEntity, updatePlayback: self.updatePlayback, present: self.present) + return State(context: self.context, updateToolState: self.updateToolState, insertEntity: self.insertEntity, deselectEntity: self.deselectEntity, updateEntitiesPlayback: self.updateEntitiesPlayback, dismissEyedropper: self.dismissEyedropper, present: self.present) } static var body: Body { @@ -836,6 +860,8 @@ private final class DrawingScreenComponent: CombinedComponent { let previewBrushSize = component.previewBrushSize let performAction = component.performAction + let dismissEyedropper = component.dismissEyedropper + component.updateState.connect { [weak state] updatedState in state?.updateDrawingState(updatedState) } @@ -952,6 +978,7 @@ private final class DrawingScreenComponent: CombinedComponent { 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) @@ -1186,15 +1213,17 @@ private final class DrawingScreenComponent: CombinedComponent { } else { let tools = tools.update( component: ToolsComponent( - state: state.drawingState, + state: component.isVideo ? 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) @@ -1353,6 +1382,7 @@ private final class DrawingScreenComponent: CombinedComponent { ) ), action: { + dismissEyedropper.invoke(Void()) performAction.invoke(.zoomOut) } ).minSize(CGSize(width: 44.0, height: 44.0)).tagged(zoomOutButtonTag), @@ -1379,6 +1409,7 @@ private final class DrawingScreenComponent: CombinedComponent { tag: sizeSliderTag, updated: { [weak state] size in if let state = state { + dismissEyedropper.invoke(Void()) state.updateBrushSize(size) if state.selectedEntity == nil { previewBrushSize.invoke(size) @@ -1402,6 +1433,7 @@ private final class DrawingScreenComponent: CombinedComponent { ), isEnabled: state.drawingViewState.canUndo, action: { + dismissEyedropper.invoke(Void()) performAction.invoke(.undo) } ).minSize(CGSize(width: 44.0, height: 44.0)).tagged(undoButtonTag), @@ -1421,6 +1453,7 @@ private final class DrawingScreenComponent: CombinedComponent { Image(image: state.image(.redo)) ), action: { + dismissEyedropper.invoke(Void()) performAction.invoke(.redo) } ).minSize(CGSize(width: 44.0, height: 44.0)).tagged(redoButtonTag), @@ -1440,6 +1473,7 @@ private final class DrawingScreenComponent: CombinedComponent { ), isEnabled: state.drawingViewState.canClear, action: { + dismissEyedropper.invoke(Void()) performAction.invoke(.clear) } ).tagged(clearAllButtonTag), @@ -1536,7 +1570,6 @@ private final class DrawingScreenComponent: CombinedComponent { .disappear(.default(scale: true)) ) - let modeRightInset: CGFloat = 57.0 let addButton = addButton.update( component: Button( @@ -1563,12 +1596,15 @@ private final class DrawingScreenComponent: CombinedComponent { } 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() } } @@ -1588,6 +1624,7 @@ private final class DrawingScreenComponent: CombinedComponent { Image(image: state.image(.done)) ), action: { [weak state] in + dismissEyedropper.invoke(Void()) state?.saveToolState() apply.invoke(Void()) } @@ -1639,6 +1676,7 @@ private final class DrawingScreenComponent: CombinedComponent { tag: modeTag, selectedIndex: selectedIndex, selectionChanged: { [weak state] index in + dismissEyedropper.invoke(Void()) guard let state = state else { return } @@ -1653,6 +1691,7 @@ private final class DrawingScreenComponent: CombinedComponent { }, sizeUpdated: { [weak state] size in if let state = state { + dismissEyedropper.invoke(Void()) state.updateBrushSize(size) if state.selectedEntity == nil { previewBrushSize.invoke(size) @@ -1690,6 +1729,7 @@ private final class DrawingScreenComponent: CombinedComponent { ), action: { [weak state] in if let state = state { + dismissEyedropper.invoke(Void()) state.saveToolState() dismiss.invoke(Void()) } @@ -1718,8 +1758,9 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController { private let updateSelectedEntity: ActionSlot private let insertEntity: ActionSlot private let deselectEntity: ActionSlot - private let updatePlayback: ActionSlot + private let updateEntitiesPlayback: ActionSlot private let previewBrushSize: ActionSlot + private let dismissEyedropper: ActionSlot private let apply: ActionSlot private let dismiss: ActionSlot @@ -1752,36 +1793,6 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController { 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 { @@ -1816,6 +1827,11 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController { strongSelf._drawingView?.setBrushSizePreview(size) } } + self.dismissEyedropper.connect { [weak self] in + if let strongSelf = self { + strongSelf.dismissCurrentEyedropper() + } + } } return self._drawingView! } @@ -1826,33 +1842,48 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController { if self._entitiesView == nil, let controller = self.controller { 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) } - let entitiesLayer = self.entitiesView.layer - self._drawingView?.getFullImage = { [weak self, weak entitiesLayer] withDrawing in + self._drawingView?.getFullImage = { [weak self] 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 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 { - context.draw(cgImage, in: bounds) + c.draw(cgImage, in: bounds) } if let cgImage = strongSelf.drawingView.drawingImage?.cgImage { - context.draw(cgImage, in: bounds) + c.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 + 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 currentImage + return nil } + +// 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) +// } +// telegramFastBlurMore(Int32(imageContext.size.width * imageContext.scale), Int32(imageContext.size.height * imageContext.scale), Int32(imageContext.bytesPerRow), imageContext.bytes) +// +// +// +// }, opaque: true, scale: 1.0) +// return image } else { return nil } @@ -1908,7 +1939,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController { in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in if let strongSelf = self { - return (strongSelf, entityFrame, strongSelf, strongSelf.bounds) + return (strongSelf, entityFrame, strongSelf, strongSelf.bounds.insetBy(dx: 0.0, dy: 160.0)) } else { return nil } @@ -1926,7 +1957,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController { textEntityView.beginEditing(accessoryView: strongSelf.textEditAccessoryView) } else { entityView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - entityView.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) + entityView.layer.animateScale(from: 0.1, to: entity.scale, duration: 0.2) if let selectionView = entityView.selectionView { selectionView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.2) @@ -1940,7 +1971,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController { entitiesView.selectEntity(nil) } } - self.updatePlayback.connect { [weak self] play in + self.updateEntitiesPlayback.connect { [weak self] play in if let strongSelf = self, let entitiesView = strongSelf._entitiesView { if play { entitiesView.play() @@ -1979,8 +2010,9 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController { self.updateSelectedEntity = ActionSlot() self.insertEntity = ActionSlot() self.deselectEntity = ActionSlot() - self.updatePlayback = ActionSlot() + self.updateEntitiesPlayback = ActionSlot() self.previewBrushSize = ActionSlot() + self.dismissEyedropper = ActionSlot() self.apply = ActionSlot() self.dismiss = ActionSlot() @@ -2000,7 +2032,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController { } self.dismiss.connect { [weak self] _ in if let strongSelf = self { - if !strongSelf.drawingView.isEmpty { + if strongSelf.drawingView.canUndo || strongSelf.entitiesView.hasChanges { let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)) actionSheet.setItemGroups([ ActionSheetItemGroup(items: [ @@ -2031,13 +2063,30 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController { self.view.disablesInteractiveTransitionGestureRecognizer = true } - func presentEyedropper(dismissed: @escaping () -> Void) { + private var currentEyedropperView: EyedropperView? + func presentEyedropper(retryLaterForVideo: Bool = true, dismissed: @escaping () -> Void) { guard let controller = self.controller else { return } self.entitiesView.pause() + if controller.isVideo && retryLaterForVideo { + controller.updateVideoPlayback(false) + Queue.mainQueue().after(0.1) { + self.presentEyedropper(retryLaterForVideo: false, dismissed: dismissed) + } + return + } + guard let currentImage = controller.getCurrentImage() else { +// if controller.isVideo && retryIfFailed { +// Queue.mainQueue().after(0.01) { +// self.presentEyedropper(retryIfFailed: false, dismissed: dismissed) +// } +// } else { + self.entitiesView.play() + controller.updateVideoPlayback(true) +// } return } @@ -2063,14 +2112,29 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController { if let strongSelf = self, let controller = controller { strongSelf.updateColor.invoke(color) controller.entitiesView.play() + controller.updateVideoPlayback(true) dismissed() } } + eyedropperView.dismissed = { + controller.entitiesView.play() + controller.updateVideoPlayback(true) + } eyedropperView.frame = controller.contentWrapperView.convert(controller.contentWrapperView.bounds, to: controller.view) controller.view.addSubview(eyedropperView) + self.currentEyedropperView = eyedropperView + } + + func dismissCurrentEyedropper() { + if let currentEyedropperView = self.currentEyedropperView { + self.currentEyedropperView = nil + currentEyedropperView.dismiss() + } } func presentColorPicker(initialColor: DrawingColor, dismissed: @escaping () -> Void = {}) { + self.dismissCurrentEyedropper() + guard let controller = self.controller else { return } @@ -2087,6 +2151,8 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController { private var fastColorPickerView: ColorSpectrumPickerView? func presentFastColorPicker(sourceView: UIView) { + self.dismissCurrentEyedropper() + guard self.fastColorPickerView == nil, let superview = sourceView.superview else { return } @@ -2275,6 +2341,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController { component: AnyComponent( DrawingScreenComponent( context: self.context, + isVideo: controller.isVideo, isAvatar: controller.isAvatar, present: { [weak self] c in self?.controller?.present(c, in: .window(.root)) @@ -2286,8 +2353,9 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController { updateSelectedEntity: self.updateSelectedEntity, insertEntity: self.insertEntity, deselectEntity: self.deselectEntity, - updatePlayback: self.updatePlayback, + updateEntitiesPlayback: self.updateEntitiesPlayback, previewBrushSize: self.previewBrushSize, + dismissEyedropper: self.dismissEyedropper, apply: self.apply, dismiss: self.dismiss, presentColorPicker: { [weak self] initialColor in @@ -2496,16 +2564,19 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController { private let context: AccountContext private let size: CGSize private let originalSize: CGSize + private let isVideo: Bool private let isAvatar: Bool public var requestDismiss: (() -> Void)! public var requestApply: (() -> Void)! public var getCurrentImage: (() -> UIImage?)! + public var updateVideoPlayback: ((Bool) -> Void)! - public init(context: AccountContext, size: CGSize, originalSize: CGSize, isAvatar: Bool) { + public init(context: AccountContext, size: CGSize, originalSize: CGSize, isVideo: Bool, isAvatar: Bool ) { self.context = context self.size = size self.originalSize = originalSize + self.isVideo = isVideo self.isAvatar = isAvatar super.init(navigationBarPresentationData: nil) @@ -2585,7 +2656,6 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController { } let drawingData = self.drawingView.drawingData - let entitiesData = self.entitiesView.entitiesData var stickers: [Any] = [] diff --git a/submodules/DrawingUI/Sources/DrawingSimpleShapeEntity.swift b/submodules/DrawingUI/Sources/DrawingSimpleShapeEntity.swift index bc381e977e..83a0d4f52b 100644 --- a/submodules/DrawingUI/Sources/DrawingSimpleShapeEntity.swift +++ b/submodules/DrawingUI/Sources/DrawingSimpleShapeEntity.swift @@ -45,6 +45,8 @@ public final class DrawingSimpleShapeEntity: DrawingEntity, Codable { return self.position } + public var scale: CGFloat = 1.0 + public var renderImage: UIImage? init(shapeType: ShapeType, drawType: DrawType, color: DrawingColor, lineWidth: CGFloat) { @@ -305,7 +307,7 @@ final class DrawingSimpleShapeEntititySelectionView: DrawingEntitySelectionView, self.snapTool.onSnapYUpdated = { [weak self] snapped in if let strongSelf = self, let entityView = strongSelf.entityView { - entityView.onSnapToXAxis(snapped) + entityView.onSnapToYAxis(snapped) } } } @@ -407,36 +409,37 @@ final class DrawingSimpleShapeEntititySelectionView: DrawingEntitySelectionView, if isAspectLocked { delta = CGPoint(x: delta.x, y: delta.x) } - updatedSize.width -= delta.x + + updatedSize.width = max(minimumSize.width, updatedSize.width - delta.x) updatedPosition.x -= delta.x * -0.5 - updatedSize.height -= delta.y + updatedSize.height = max(minimumSize.height, updatedSize.height - delta.y) updatedPosition.y += delta.y * 0.5 } else if self.currentHandle === self.topRightHandle { var delta = delta if isAspectLocked { delta = CGPoint(x: delta.x, y: -delta.x) } - updatedSize.width += delta.x + updatedSize.width = max(minimumSize.width, updatedSize.width + delta.x) updatedPosition.x += delta.x * 0.5 - updatedSize.height -= delta.y + updatedSize.height = max(minimumSize.height, updatedSize.height - delta.y) updatedPosition.y += delta.y * 0.5 } else if self.currentHandle === self.bottomLeftHandle { var delta = delta if isAspectLocked { delta = CGPoint(x: delta.x, y: -delta.x) } - updatedSize.width -= delta.x + updatedSize.width = max(minimumSize.width, updatedSize.width - delta.x) updatedPosition.x -= delta.x * -0.5 - updatedSize.height += delta.y + updatedSize.height = max(minimumSize.height, updatedSize.height + delta.y) updatedPosition.y += delta.y * 0.5 } else if self.currentHandle === self.bottomRightHandle { var delta = delta if isAspectLocked { delta = CGPoint(x: delta.x, y: delta.x) } - updatedSize.width += delta.x + updatedSize.width = max(minimumSize.width, updatedSize.width + delta.x) updatedPosition.x += delta.x * 0.5 - updatedSize.height += delta.y + updatedSize.height = max(minimumSize.height, updatedSize.height + delta.y) updatedPosition.y += delta.y * 0.5 } else if self.currentHandle === self.layer { updatedPosition.x += delta.x diff --git a/submodules/DrawingUI/Sources/DrawingStickerEntity.swift b/submodules/DrawingUI/Sources/DrawingStickerEntity.swift index c41087397c..234b1b6938 100644 --- a/submodules/DrawingUI/Sources/DrawingStickerEntity.swift +++ b/submodules/DrawingUI/Sources/DrawingStickerEntity.swift @@ -254,6 +254,7 @@ final class DrawingStickerEntityView: DrawingEntityView { self.applyVisibility() } } + self.update(animated: false) } } } @@ -537,15 +538,20 @@ class DrawingEntitySnapTool { private var xState: (skipped: CGFloat, waitForLeave: Bool)? private var yState: (skipped: CGFloat, waitForLeave: Bool)? + private var rotationState: (skipped: CGFloat, waitForLeave: Bool)? + var onSnapXUpdated: (Bool) -> Void = { _ in } var onSnapYUpdated: (Bool) -> Void = { _ in } + var onSnapRotationUpdated: (CGFloat?) -> Void = { _ in } func reset() { self.xState = nil self.yState = nil + self.rotationState = nil self.onSnapXUpdated(false) self.onSnapYUpdated(false) + self.onSnapRotationUpdated(nil) } func maybeSkipFromStart(entityView: DrawingEntityView, position: CGPoint) { @@ -565,7 +571,7 @@ class DrawingEntitySnapTool { } } } - + func update(entityView: DrawingEntityView, velocity: CGPoint, delta: CGPoint, updatedPosition: CGPoint) -> CGPoint { var updatedPosition = updatedPosition @@ -640,4 +646,17 @@ class DrawingEntitySnapTool { return updatedPosition } + + private let snapRotations: [CGFloat] = [0.0, .pi / 4.0, .pi / 2.0, .pi * 1.5,] + func maybeSkipFromStart(entityView: DrawingEntityView, rotation: CGFloat) { + self.rotationState = nil + + let snapDelta: CGFloat = 0.087 + for snapRotation in self.snapRotations { + if rotation > snapRotation - snapDelta && rotation < snapRotation + snapDelta { + self.rotationState = (0.0, true) + break + } + } + } } diff --git a/submodules/DrawingUI/Sources/DrawingTextEntity.swift b/submodules/DrawingUI/Sources/DrawingTextEntity.swift index c8d80d6498..be5e8ec5ef 100644 --- a/submodules/DrawingUI/Sources/DrawingTextEntity.swift +++ b/submodules/DrawingUI/Sources/DrawingTextEntity.swift @@ -350,7 +350,7 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate { self.customEmojiContainerView = customEmojiContainerView } - customEmojiContainerView.update(fontSize: self.displayFontSize * 0.8, textColor: textColor, emojiRects: customEmojiRects) + customEmojiContainerView.update(fontSize: self.displayFontSize * 0.78, textColor: textColor, emojiRects: customEmojiRects) } else if let customEmojiContainerView = self.customEmojiContainerView { customEmojiContainerView.removeFromSuperview() self.customEmojiContainerView = nil @@ -684,7 +684,7 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate { let scale = self.textEntity.scale let rotation = self.textEntity.rotation - let itemSize: CGFloat = floor(24.0 * self.displayFontSize * 0.8 / 17.0) + let itemSize: CGFloat = floor(24.0 * self.displayFontSize * 0.78 / 17.0) var entities: [DrawingStickerEntity] = [] for (emojiRect, emojiAttribute) in self.emojiRects { @@ -754,7 +754,7 @@ final class DrawingTextEntititySelectionView: DrawingEntitySelectionView, UIGest self.snapTool.onSnapYUpdated = { [weak self] snapped in if let strongSelf = self, let entityView = strongSelf.entityView { - entityView.onSnapToXAxis(snapped) + entityView.onSnapToYAxis(snapped) } } } @@ -1033,8 +1033,8 @@ private class DrawingTextLayoutManager: NSLayoutManager { context.saveGState() context.translateBy(x: origin.x, y: origin.y) - - context.setBlendMode(.normal) + + context.setBlendMode(.copy) context.setFillColor(frameColor.cgColor) context.setStrokeColor(frameColor.cgColor) diff --git a/submodules/DrawingUI/Sources/DrawingTools.swift b/submodules/DrawingUI/Sources/DrawingTools.swift index c43b98c8be..b80fc8e714 100644 --- a/submodules/DrawingUI/Sources/DrawingTools.swift +++ b/submodules/DrawingUI/Sources/DrawingTools.swift @@ -19,6 +19,10 @@ final class MarkerTool: DrawingElement, Codable { var points: [CGPoint] = [] weak var metalView: DrawingMetalView? + + var isValid: Bool { + return !self.points.isEmpty + } required init(drawingSize: CGSize, color: DrawingColor, lineWidth: CGFloat) { self.uuid = UUID() @@ -162,6 +166,10 @@ final class NeonTool: DrawingElement, Codable { var translation = CGPoint() private var currentRenderLayer: DrawingRenderLayer? + + var isValid: Bool { + return self.renderPath != nil + } required init(drawingSize: CGSize, color: DrawingColor, lineWidth: CGFloat) { self.uuid = UUID() @@ -267,17 +275,26 @@ final class NeonTool: DrawingElement, Codable { } final class FillTool: DrawingElement, Codable { + let uuid: UUID let drawingSize: CGSize let color: DrawingColor + let isBlur: Bool + var blurredImage: UIImage? var translation = CGPoint() - required init(drawingSize: CGSize, color: DrawingColor) { + var isValid: Bool { + return true + } + + required init(drawingSize: CGSize, color: DrawingColor, blur: Bool, blurredImage: UIImage?) { self.uuid = UUID() self.drawingSize = drawingSize self.color = color + self.isBlur = blur + self.blurredImage = blurredImage } private enum CodingKeys: String, CodingKey { @@ -291,6 +308,7 @@ final class FillTool: DrawingElement, Codable { self.uuid = try container.decode(UUID.self, forKey: .uuid) self.drawingSize = try container.decode(CGSize.self, forKey: .drawingSize) self.color = try container.decode(DrawingColor.self, forKey: .color) + self.isBlur = false // self.points = try container.decode([CGPoint].self, forKey: .points) } @@ -313,10 +331,16 @@ final class FillTool: DrawingElement, Codable { context.setShouldAntialias(false) context.setBlendMode(.copy) - - context.setFillColor(self.color.toCGColor()) - context.fill(CGRect(origin: .zero, size: size)) - + + if self.isBlur { + if let blurredImage = self.blurredImage?.cgImage { + context.draw(blurredImage, in: CGRect(origin: .zero, size: size)) + } + } else { + context.setFillColor(self.color.toCGColor()) + context.fill(CGRect(origin: .zero, size: size)) + } + context.setBlendMode(.normal) } @@ -382,127 +406,10 @@ final class BlurTool: DrawingElement, Codable { private var currentRenderLayer: DrawingRenderLayer? - required init(drawingSize: CGSize, lineWidth: CGFloat) { - self.uuid = UUID() - self.drawingSize = drawingSize - - let minLineWidth = max(1.0, max(drawingSize.width, drawingSize.height) * 0.003) - let maxLineWidth = max(10.0, max(drawingSize.width, drawingSize.height) * 0.09) - let lineWidth = minLineWidth + (maxLineWidth - minLineWidth) * lineWidth - - self.renderLineWidth = lineWidth + var isValid: Bool { + return self.renderPath != nil } - private enum CodingKeys: String, CodingKey { - case uuid - case drawingSize - case renderLineWidth - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.uuid = try container.decode(UUID.self, forKey: .uuid) - self.drawingSize = try container.decode(CGSize.self, forKey: .drawingSize) - self.renderLineWidth = try container.decode(CGFloat.self, forKey: .renderLineWidth) -// self.points = try container.decode([CGPoint].self, forKey: .points) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(self.uuid, forKey: .uuid) - try container.encode(self.drawingSize, forKey: .drawingSize) - try container.encode(self.renderLineWidth, forKey: .renderLineWidth) -// try container.encode(self.points, forKey: .points) - } - - func setupRenderLayer() -> DrawingRenderLayer? { - let layer = RenderLayer() - layer.setup(size: self.drawingSize, lineWidth: self.renderLineWidth, image: self.getFullImage()) - self.currentRenderLayer = layer - return layer - } - - func updatePath(_ path: DrawingGesturePipeline.DrawingResult, state: DrawingGesturePipeline.DrawingGestureState) { - guard case let .smoothCurve(bezierPath) = path else { - return - } - - self.path = bezierPath - - let renderPath = bezierPath.path.cgPath - self.renderPath = renderPath - - if let currentRenderLayer = self.currentRenderLayer as? RenderLayer { - currentRenderLayer.updatePath(renderPath) - } - } - - func draw(in context: CGContext, size: CGSize) { - context.translateBy(x: self.translation.x, y: self.translation.y) - - let renderLayer: DrawingRenderLayer? - if let currentRenderLayer = self.currentRenderLayer { - renderLayer = currentRenderLayer - } else { - renderLayer = self.setupRenderLayer() - } - renderLayer?.render(in: context) - } -} - - -final class EraserTool: DrawingElement, Codable { - class RenderLayer: SimpleLayer, DrawingRenderLayer { - var lineWidth: CGFloat = 0.0 - - let blurLayer = SimpleLayer() - let fillLayer = SimpleShapeLayer() - - func setup(size: CGSize, lineWidth: CGFloat, image: UIImage?) { - self.contentsScale = 1.0 - self.lineWidth = lineWidth - - let bounds = CGRect(origin: .zero, size: size) - self.frame = bounds - - self.blurLayer.frame = bounds - self.fillLayer.frame = bounds - - if self.blurLayer.contents == nil, let image = image { - self.blurLayer.contents = image.cgImage - self.blurLayer.contentsGravity = .resize - } - self.blurLayer.mask = self.fillLayer - - self.fillLayer.frame = bounds - self.fillLayer.contentsScale = 1.0 - self.fillLayer.strokeColor = UIColor.white.cgColor - self.fillLayer.fillColor = UIColor.clear.cgColor - self.fillLayer.lineCap = .round - self.fillLayer.lineWidth = lineWidth - - self.addSublayer(self.blurLayer) - } - - func updatePath(_ path: CGPath) { - self.fillLayer.path = path - } - } - - var getFullImage: () -> UIImage? = { return nil } - - let uuid: UUID - let drawingSize: CGSize - - var path: BezierPath? - - var renderPath: CGPath? - let renderLineWidth: CGFloat - - var translation = CGPoint() - - private var currentRenderLayer: DrawingRenderLayer? - required init(drawingSize: CGSize, lineWidth: CGFloat) { self.uuid = UUID() self.drawingSize = drawingSize @@ -575,7 +482,6 @@ enum CodableDrawingElement { case pen(PenTool) case marker(MarkerTool) case neon(NeonTool) - case eraser(EraserTool) case blur(BlurTool) case fill(FillTool) @@ -586,8 +492,6 @@ enum CodableDrawingElement { self = .marker(element) } else if let element = element as? NeonTool { self = .neon(element) - } else if let element = element as? EraserTool { - self = .eraser(element) } else if let element = element as? BlurTool { self = .blur(element) } else if let element = element as? FillTool { @@ -597,7 +501,7 @@ enum CodableDrawingElement { } } - var entity: DrawingElement { + var element: DrawingElement { switch self { case let .pen(element): return element @@ -605,8 +509,6 @@ enum CodableDrawingElement { return element case let .neon(element): return element - case let .eraser(element): - return element case let .blur(element): return element case let .fill(element): @@ -625,7 +527,6 @@ extension CodableDrawingElement: Codable { case pen case marker case neon - case eraser case blur case fill } @@ -640,8 +541,6 @@ extension CodableDrawingElement: Codable { self = .marker(try container.decode(MarkerTool.self, forKey: .element)) case .neon: self = .neon(try container.decode(NeonTool.self, forKey: .element)) - case .eraser: - self = .eraser(try container.decode(EraserTool.self, forKey: .element)) case .blur: self = .blur(try container.decode(BlurTool.self, forKey: .element)) case .fill: @@ -661,9 +560,6 @@ extension CodableDrawingElement: Codable { case let .neon(payload): try container.encode(ElementType.neon, forKey: .type) try container.encode(payload, forKey: .element) - case let .eraser(payload): - try container.encode(ElementType.eraser, forKey: .type) - try container.encode(payload, forKey: .element) case let .blur(payload): try container.encode(ElementType.blur, forKey: .type) try container.encode(payload, forKey: .element) diff --git a/submodules/DrawingUI/Sources/DrawingVectorEntity.swift b/submodules/DrawingUI/Sources/DrawingVectorEntity.swift index 7b671dc8e7..226f2b9d11 100644 --- a/submodules/DrawingUI/Sources/DrawingVectorEntity.swift +++ b/submodules/DrawingUI/Sources/DrawingVectorEntity.swift @@ -51,6 +51,8 @@ public final class DrawingVectorEntity: DrawingEntity, Codable { return self.start } + public var scale: CGFloat = 1.0 + public var renderImage: UIImage? init(type: VectorType, color: DrawingColor, lineWidth: CGFloat) { diff --git a/submodules/DrawingUI/Sources/DrawingView.swift b/submodules/DrawingUI/Sources/DrawingView.swift index 87e00a7520..5bf86f3eea 100644 --- a/submodules/DrawingUI/Sources/DrawingView.swift +++ b/submodules/DrawingUI/Sources/DrawingView.swift @@ -10,7 +10,8 @@ import ImageBlur protocol DrawingElement: AnyObject { var uuid: UUID { get } var translation: CGPoint { get set } - + var isValid: Bool { get } + func setupRenderLayer() -> DrawingRenderLayer? func updatePath(_ path: DrawingGesturePipeline.DrawingResult, state: DrawingGesturePipeline.DrawingGestureState) @@ -43,23 +44,21 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw enum Tool { case pen + case arrow case marker case neon case eraser - case lasso case blur } var tool: Tool = .pen var toolColor: DrawingColor = DrawingColor(color: .white) var toolBrushSize: CGFloat = 0.25 - var toolHasArrow: Bool = false var stateUpdated: (NavigationState) -> Void = { _ in } var shouldBegin: (CGPoint) -> Bool = { _ in return true } - var requestMenu: ([UUID], CGRect) -> Void = { _, _ in } - var getFullImage: (Bool) -> UIImage? = { _ in return nil } + var getFullImage: () -> UIImage? = { return nil } private var elements: [DrawingElement] = [] private var undoStack: [DrawingOperation] = [] @@ -69,18 +68,17 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw private(set) var drawingImage: UIImage? private let renderer: UIGraphicsImageRenderer - private var currentDrawingView: UIView + private var currentDrawingView: UIImageView private var currentDrawingLayer: DrawingRenderLayer? private var pannedSelectionView: UIView - var lassoView: DrawingLassoView private var metalView: DrawingMetalView private let brushSizePreviewLayer: SimpleShapeLayer let imageSize: CGSize - private var zoomScale: CGFloat = 1.0 + private(set) var zoomScale: CGFloat = 1.0 private var drawingGesturePipeline: DrawingGesturePipeline? private var longPressGestureRecognizer: UILongPressGestureRecognizer? @@ -109,13 +107,15 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw self.loadedTemplates.append(template) } } - + load("shape_rectangle") load("shape_circle") load("shape_star") load("shape_arrow") } + private let hapticFeedback = HapticFeedback() + init(size: CGSize) { self.imageSize = size @@ -123,7 +123,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw format.scale = 1.0 self.renderer = UIGraphicsImageRenderer(size: size, format: format) - self.currentDrawingView = UIView() + self.currentDrawingView = UIImageView() self.currentDrawingView.frame = CGRect(origin: .zero, size: size) self.currentDrawingView.contentScaleFactor = 1.0 self.currentDrawingView.backgroundColor = .clear @@ -135,9 +135,6 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw self.pannedSelectionView.backgroundColor = .clear self.pannedSelectionView.isUserInteractionEnabled = false - self.lassoView = DrawingLassoView(size: size) - self.lassoView.isHidden = true - self.metalView = DrawingMetalView(size: size)! self.metalView.isHidden = true @@ -160,11 +157,11 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw self.backgroundColor = .clear self.contentScaleFactor = 1.0 + self.isExclusiveTouch = true self.addSubview(self.currentDrawingView) self.addSubview(self.metalView) - self.lassoView.addSubview(self.pannedSelectionView) - self.addSubview(self.lassoView) + self.layer.addSublayer(self.brushSizePreviewLayer) let drawingGesturePipeline = DrawingGesturePipeline(view: self) @@ -173,7 +170,10 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw if !strongSelf.shouldBegin(point) { return false } - if !strongSelf.lassoView.isHidden && strongSelf.lassoView.point(inside: strongSelf.convert(point, to: strongSelf.lassoView), with: nil) { + if strongSelf.elements.isEmpty && !strongSelf.hasOpaqueData && strongSelf.tool == .eraser { + return false + } + if let uncommitedElement = strongSelf.uncommitedElement as? PenTool, uncommitedElement.isFinishingArrow { return false } return true @@ -200,7 +200,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw } if newElement is MarkerTool { - self?.metalView.isHidden = false + strongSelf.metalView.isHidden = false } if let renderLayer = newElement.setupRenderLayer() { @@ -208,7 +208,20 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw strongSelf.currentDrawingLayer = nil currentDrawingLayer.removeFromSuperlayer() } - strongSelf.currentDrawingView.layer.addSublayer(renderLayer) + if strongSelf.tool == .eraser { + strongSelf.currentDrawingView.removeFromSuperview() + strongSelf.currentDrawingView.backgroundColor = .white + + renderLayer.compositingFilter = "xor" + + strongSelf.currentDrawingView.layer.addSublayer(renderLayer) + strongSelf.mask = strongSelf.currentDrawingView + } else if strongSelf.tool == .blur { + strongSelf.currentDrawingView.layer.mask = renderLayer + strongSelf.currentDrawingView.image = strongSelf.preparedBlurredImage + } else { + strongSelf.currentDrawingView.layer.addSublayer(renderLayer) + } strongSelf.currentDrawingLayer = renderLayer } newElement.updatePath(path, state: state) @@ -309,45 +322,6 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw longPressGestureRecognizer.delegate = self self.addGestureRecognizer(longPressGestureRecognizer) self.longPressGestureRecognizer = longPressGestureRecognizer - - self.lassoView.requestMenu = { [weak self] elements, rect in - if let strongSelf = self { - strongSelf.requestMenu(elements, rect) - } - } - - self.lassoView.panBegan = { [weak self] elements in - if let strongSelf = self { - strongSelf.skipDrawing = Set(elements) - strongSelf.commit(reset: true) - strongSelf.updateSelectionContent() - } - } - - self.lassoView.panChanged = { [weak self] elements, offset in - if let strongSelf = self { - let offset = CGPoint(x: offset.x * -1.0, y: offset.y * -1.0) - strongSelf.lassoView.bounds = CGRect(origin: offset, size: strongSelf.lassoView.bounds.size) - } - } - - self.lassoView.panEnded = { [weak self] elements, offset in - if let strongSelf = self { - let elementsSet = Set(elements) - for element in strongSelf.elements { - if elementsSet.contains(element.uuid) { - element.translation = element.translation.offsetBy(offset) - } - } - strongSelf.skipDrawing = Set() - strongSelf.commit(reset: true, completion: { - strongSelf.pannedSelectionView.layer.contents = nil - - strongSelf.lassoView.bounds = CGRect(origin: .zero, size: strongSelf.lassoView.bounds.size) - strongSelf.lassoView.translate(offset) - }) - } - } } required init?(coder: NSCoder) { @@ -360,26 +334,33 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw } public func setup(withDrawing drawingData: Data!) { - + if let drawingData = drawingData, let image = UIImage(data: drawingData) { + self.hasOpaqueData = true + self.drawingImage = image + self.layer.contents = image.cgImage + //let codableElements = try? JSONDecoder().decode([CodableDrawingElement].self, from: drawingData) { + //self.elements = codableElements.map { $0.element } + //self.commit(reset: true) + self.updateInternalState() + } } + var hasOpaqueData = false var drawingData: Data? { - guard !self.elements.isEmpty else { + guard !self.elements.isEmpty || self.hasOpaqueData else { return nil } + return self.drawingImage?.pngData() - let codableElements = self.elements.compactMap({ CodableDrawingElement(element: $0) }) - if let data = try? JSONEncoder().encode(codableElements) { - return data - } else { - return nil - } +// let codableElements = self.elements.compactMap({ CodableDrawingElement(element: $0) }) +// if let data = try? JSONEncoder().encode(codableElements) { +// return data +// } else { +// return nil +// } } public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { - if gestureRecognizer === self.longPressGestureRecognizer, !self.lassoView.isHidden { - return false - } return true } @@ -390,10 +371,6 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw private var longPressTimer: SwiftSignalKit.Timer? private var fillCircleLayer: CALayer? @objc func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { - guard ![.eraser, .blur].contains(self.tool) else { - return - } - let location = gestureRecognizer.location(in: self) switch gestureRecognizer.state { case .began: @@ -401,33 +378,64 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw self.longPressTimer = nil if self.longPressTimer == nil { - self.longPressTimer = SwiftSignalKit.Timer(timeout: 0.25, repeat: false, completion: { [weak self] in + var toolColor = self.toolColor + var blurredImage: UIImage? + if self.tool == .marker { + toolColor = toolColor.withUpdatedAlpha(toolColor.alpha * 0.7) + } else if self.tool == .eraser { + toolColor = DrawingColor.clear + } else if self.tool == .blur { + blurredImage = self.preparedBlurredImage + } + + self.hapticFeedback.tap() + + let fillCircleLayer = SimpleShapeLayer() + self.longPressTimer = SwiftSignalKit.Timer(timeout: 0.25, repeat: false, completion: { [weak self, weak fillCircleLayer] in if let strongSelf = self { strongSelf.cancelDrawing() - let newElement = FillTool(drawingSize: strongSelf.imageSize, color: strongSelf.toolColor) - strongSelf.uncommitedElement = newElement - strongSelf.finishDrawing() + let action = { + let newElement = FillTool(drawingSize: strongSelf.imageSize, color: toolColor, blur: blurredImage != nil, blurredImage: blurredImage) + strongSelf.uncommitedElement = newElement + strongSelf.finishDrawing(synchronous: true) + } + if [.eraser, .blur].contains(strongSelf.tool) { + UIView.transition(with: strongSelf, duration: 0.2, options: .transitionCrossDissolve) { + action() + } + } else { + action() + } + + strongSelf.fillCircleLayer = nil + fillCircleLayer?.removeFromSuperlayer() + + strongSelf.hapticFeedback.impact(.medium) } }, queue: Queue.mainQueue()) self.longPressTimer?.start() - let fillCircleLayer = SimpleShapeLayer() - fillCircleLayer.bounds = CGRect(origin: .zero, size: CGSize(width: 160.0, height: 160.0)) - fillCircleLayer.position = location - fillCircleLayer.path = UIBezierPath(ovalIn: CGRect(origin: .zero, size: CGSize(width: 160.0, height: 160.0))).cgPath - fillCircleLayer.fillColor = self.toolColor.toCGColor() - self.layer.addSublayer(fillCircleLayer) - self.fillCircleLayer = fillCircleLayer - - fillCircleLayer.animateScale(from: 0.01, to: 12.0, duration: 0.35, removeOnCompletion: false, completion: { [weak self] _ in - if let strongSelf = self { - if let fillCircleLayer = strongSelf.fillCircleLayer { - strongSelf.fillCircleLayer = nil - fillCircleLayer.removeFromSuperlayer() + if [.eraser, .blur].contains(self.tool) { + + } else { + fillCircleLayer.bounds = CGRect(origin: .zero, size: CGSize(width: 160.0, height: 160.0)) + fillCircleLayer.position = location + fillCircleLayer.path = UIBezierPath(ovalIn: CGRect(origin: .zero, size: CGSize(width: 160.0, height: 160.0))).cgPath + fillCircleLayer.fillColor = toolColor.toCGColor() + + self.layer.addSublayer(fillCircleLayer) + self.fillCircleLayer = fillCircleLayer + + fillCircleLayer.animateScale(from: 0.01, to: 12.0, duration: 0.35, removeOnCompletion: false, completion: { [weak self] _ in + if let strongSelf = self { + if let fillCircleLayer = strongSelf.fillCircleLayer { + strongSelf.fillCircleLayer = nil + fillCircleLayer.removeFromSuperlayer() + } } - } - }) + }) + } } case .ended, .cancelled: self.longPressTimer?.invalidate() @@ -475,13 +483,24 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw self.layer.contents = updatedImage.cgImage if let currentDrawingLayer = self.currentDrawingLayer { + if case .eraser = self.tool { + currentDrawingLayer.removeFromSuperlayer() + self.mask = nil + self.insertSubview(self.currentDrawingView, at: 0) + self.currentDrawingView.backgroundColor = .clear + } else if case .blur = self.tool { + self.currentDrawingView.layer.mask = nil + self.currentDrawingView.image = nil + } else { + currentDrawingLayer.removeFromSuperlayer() + } self.currentDrawingLayer = nil - currentDrawingLayer.removeFromSuperlayer() } - self.metalView.clear() - self.metalView.isHidden = true - + if self.tool == .marker { + self.metalView.clear() + self.metalView.isHidden = true + } completion() } } @@ -509,13 +528,25 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw self.uncommitedElement = nil if let currentDrawingLayer = self.currentDrawingLayer { + if self.tool == .eraser { + currentDrawingLayer.removeFromSuperlayer() + self.mask = nil + self.insertSubview(self.currentDrawingView, at: 0) + self.currentDrawingView.backgroundColor = .clear + } else if self.tool == .blur { + self.currentDrawingView.mask = nil + } else { + currentDrawingLayer.removeFromSuperlayer() + } self.currentDrawingLayer = nil - currentDrawingLayer.removeFromSuperlayer() } } - fileprivate func finishDrawing() { + fileprivate func finishDrawing(synchronous: Bool = false) { let complete: (Bool) -> Void = { synchronous in + if let uncommitedElement = self.uncommitedElement, !uncommitedElement.isValid { + self.uncommitedElement = nil + } self.commit(interactive: true, synchronous: synchronous) self.redoStack.removeAll() @@ -532,7 +563,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw complete(true) }) } else { - complete(false) + complete(synchronous) } } @@ -544,6 +575,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw self.elements.removeAll() self.undoStack.removeAll() self.redoStack.removeAll() + self.hasOpaqueData = false let snapshotView = UIImageView(image: self.drawingImage) snapshotView.frame = self.bounds @@ -560,7 +592,11 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw self.updateInternalState() - self.lassoView.reset() + self.updateBlurredImage() + } + + var canUndo: Bool { + return !self.undoStack.isEmpty } private func undo() { @@ -573,16 +609,11 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw self.redoStack.append(.element(element)) self.elements.removeAll(where: { $0.uuid == element.uuid }) - let snapshotView = UIImageView(image: self.drawingImage) - snapshotView.frame = self.bounds - self.addSubview(snapshotView) - self.commit(reset: true) - - Queue.mainQueue().justDispatch { - snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in - snapshotView?.removeFromSuperview() - }) + UIView.transition(with: self, duration: 0.2, options: .transitionCrossDissolve) { + self.commit(reset: true) } + + self.updateBlurredImage() case let .addEntity(uuid): if let entityView = self.entitiesView?.getView(for: uuid) { self.entitiesView?.remove(uuid: uuid, animated: true, announce: false) @@ -592,7 +623,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw if let view = self.entitiesView?.add(entity, announce: false) { view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) if !(entity is DrawingVectorEntity) { - view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) + view.layer.animateScale(from: 0.1, to: entity.scale, duration: 0.2) } } self.redoStack.append(.addEntity(entity.uuid)) @@ -615,8 +646,12 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw self.undoStack.append(.element(element)) self.uncommitedElement = element - self.commit(reset: false) + UIView.transition(with: self, duration: 0.2, options: .transitionCrossDissolve) { + self.commit(reset: false) + } self.uncommitedElement = nil + + self.updateBlurredImage() case let .addEntity(uuid): if let entityView = self.entitiesView?.getView(for: uuid) { self.entitiesView?.remove(uuid: uuid, animated: true, announce: false) @@ -626,7 +661,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw if let view = self.entitiesView?.add(entity, announce: false) { view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) if !(entity is DrawingVectorEntity) { - view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) + view.layer.animateScale(from: 0.1, to: entity.scale, duration: 0.2) } } self.undoStack.append(.addEntity(entity.uuid)) @@ -651,71 +686,61 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw self.updateInternalState() } - private var preparredEraserImage: UIImage? + private var preparedBlurredImage: UIImage? func updateToolState(_ state: DrawingToolState) { + let previousTool = self.tool switch state { case let .pen(brushState): - self.drawingGesturePipeline?.mode = .polyline + self.drawingGesturePipeline?.mode = .direct self.tool = .pen self.toolColor = brushState.color self.toolBrushSize = brushState.size - self.toolHasArrow = false case let .arrow(brushState): - self.drawingGesturePipeline?.mode = .polyline - self.tool = .pen + self.drawingGesturePipeline?.mode = .direct + self.tool = .arrow self.toolColor = brushState.color self.toolBrushSize = brushState.size - self.toolHasArrow = true case let .marker(brushState): self.drawingGesturePipeline?.mode = .location self.tool = .marker self.toolColor = brushState.color self.toolBrushSize = brushState.size - self.toolHasArrow = false case let .neon(brushState): self.drawingGesturePipeline?.mode = .smoothCurve self.tool = .neon self.toolColor = brushState.color self.toolBrushSize = brushState.size - self.toolHasArrow = false - case let .eraser(eraserState): - self.tool = .eraser - self.drawingGesturePipeline?.mode = .smoothCurve - self.toolBrushSize = eraserState.size case let .blur(blurState): self.tool = .blur - self.drawingGesturePipeline?.mode = .smoothCurve + self.drawingGesturePipeline?.mode = .direct self.toolBrushSize = blurState.size + case let .eraser(eraserState): + self.tool = .eraser + self.drawingGesturePipeline?.mode = .direct + self.toolBrushSize = eraserState.size } - if [.eraser, .blur].contains(self.tool) { + if self.tool != previousTool { + self.updateBlurredImage() + } else { + self.preparedBlurredImage = nil + } + + } + + func updateBlurredImage() { + if case .blur = self.tool { Queue.concurrentDefaultQueue().async { - if let image = self.getFullImage(self.tool == .blur) { - if case .eraser = self.tool { - Queue.mainQueue().async { - self.preparredEraserImage = image - } - } else { -// let format = UIGraphicsImageRendererFormat() -// format.scale = 1.0 -// let size = image.size.fitted(CGSize(width: 256, height: 256)) -// let renderer = UIGraphicsImageRenderer(size: size, format: format) -// let scaledImage = renderer.image { _ in -// image.draw(in: CGRect(origin: .zero, size: size)) -// } - - let blurredImage = blurredImage(image, radius: 60.0) - Queue.mainQueue().async { - self.preparredEraserImage = blurredImage - } + if let image = self.getFullImage() { + Queue.mainQueue().async { + self.preparedBlurredImage = image } } } } else { - self.preparredEraserImage = nil + self.preparedBlurredImage = nil } - } func performAction(_ action: Action) { @@ -733,15 +758,16 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw private func updateInternalState() { self.stateUpdated(NavigationState( - canUndo: !self.elements.isEmpty || !self.undoStack.isEmpty, + canUndo: !self.undoStack.isEmpty, canRedo: !self.redoStack.isEmpty, - canClear: !self.elements.isEmpty || !(self.entitiesView?.entities.isEmpty ?? true), + canClear: !self.elements.isEmpty || self.hasOpaqueData || !(self.entitiesView?.entities.isEmpty ?? true), canZoomOut: self.zoomScale > 1.0 + .ulpOfOne, isDrawing: self.isDrawing )) } public func updateZoomScale(_ scale: CGFloat) { + self.cancelDrawing() self.zoomScale = scale self.updateInternalState() } @@ -755,7 +781,21 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw drawingSize: self.imageSize, color: self.toolColor, lineWidth: self.toolBrushSize * scale, - hasArrow: self.toolHasArrow + hasArrow: false, + isEraser: false, + isBlur: false, + blurredImage: nil + ) + element = penTool + case .arrow: + let penTool = PenTool( + drawingSize: self.imageSize, + color: self.toolColor, + lineWidth: self.toolBrushSize * scale, + hasArrow: true, + isEraser: false, + isBlur: false, + blurredImage: nil ) element = penTool case .marker: @@ -773,25 +813,27 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw lineWidth: self.toolBrushSize * scale ) case .blur: - let blurTool = BlurTool( + let penTool = PenTool( drawingSize: self.imageSize, - lineWidth: self.toolBrushSize * scale + color: self.toolColor, + lineWidth: self.toolBrushSize * scale, + hasArrow: false, + isEraser: false, + isBlur: true, + blurredImage: self.preparedBlurredImage ) - blurTool.getFullImage = { [weak self] in - return self?.preparredEraserImage - } - element = blurTool + element = penTool case .eraser: - let eraserTool = EraserTool( + let penTool = PenTool( drawingSize: self.imageSize, - lineWidth: self.toolBrushSize * scale + color: self.toolColor, + lineWidth: self.toolBrushSize * scale, + hasArrow: false, + isEraser: true, + isBlur: false, + blurredImage: nil ) - eraserTool.getFullImage = { [weak self] in - return self?.preparredEraserImage - } - element = eraserTool - default: - element = nil + element = penTool } return element } @@ -804,15 +846,16 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw func removeElements(_ elements: [UUID]) { self.elements.removeAll(where: { elements.contains($0.uuid) }) self.commit(reset: true) - - self.lassoView.reset() } func setBrushSizePreview(_ size: CGFloat?) { let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) if let size = size { - let minBrushSize = 2.0 - let maxBrushSize = 28.0 + let minLineWidth = max(1.0, max(self.frame.width, self.frame.height) * 0.002) + let maxLineWidth = max(10.0, max(self.frame.width, self.frame.height) * 0.07) + + let minBrushSize = minLineWidth + let maxBrushSize = maxLineWidth let brushSize = minBrushSize + (maxBrushSize - minBrushSize) * size self.brushSizePreviewLayer.transform = CATransform3DMakeScale(brushSize / 100.0, brushSize / 100.0, 1.0) @@ -831,10 +874,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw self.currentDrawingView.frame = self.bounds self.drawingGesturePipeline?.transform = CGAffineTransformMakeScale(1.0 / scale, 1.0 / scale) - - self.lassoView.transform = transform - self.lassoView.frame = self.bounds - + self.metalView.transform = transform self.metalView.frame = self.bounds @@ -853,171 +893,3 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw return self.uncommitedElement != nil } } - -class DrawingLassoView: UIView { - private var lassoBlackLayer: SimpleShapeLayer - private var lassoWhiteLayer: SimpleShapeLayer - - var requestMenu: ([UUID], CGRect) -> Void = { _, _ in } - - var panBegan: ([UUID]) -> Void = { _ in } - var panChanged: ([UUID], CGPoint) -> Void = { _, _ in } - var panEnded: ([UUID], CGPoint) -> Void = { _, _ in } - - private var currentScale: CGFloat = 1.0 - private var currentPoints: [CGPoint] = [] - private var selectedElements: [UUID] = [] - private var currentExpand: CGFloat = 0.0 - - init(size: CGSize) { - self.lassoBlackLayer = SimpleShapeLayer() - self.lassoBlackLayer.frame = CGRect(origin: .zero, size: size) - - self.lassoWhiteLayer = SimpleShapeLayer() - self.lassoWhiteLayer.frame = CGRect(origin: .zero, size: size) - - super.init(frame: CGRect(origin: .zero, size: size)) - - self.layer.addSublayer(self.lassoBlackLayer) - self.layer.addSublayer(self.lassoWhiteLayer) - - let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:))) - tapGestureRecognizer.numberOfTouchesRequired = 1 - self.addGestureRecognizer(tapGestureRecognizer) - - let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:))) - panGestureRecognizer.maximumNumberOfTouches = 1 - self.addGestureRecognizer(panGestureRecognizer) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func setup(scale: CGFloat) { - self.isHidden = false - - let dash: CGFloat = 5.0 / scale - - self.lassoBlackLayer.opacity = 0.5 - self.lassoBlackLayer.fillColor = UIColor.clear.cgColor - self.lassoBlackLayer.strokeColor = UIColor.black.cgColor - self.lassoBlackLayer.lineWidth = 2.0 / scale - self.lassoBlackLayer.lineJoin = .round - self.lassoBlackLayer.lineCap = .round - self.lassoBlackLayer.lineDashPattern = [dash as NSNumber, dash * 2.5 as NSNumber] - - let blackAnimation = CABasicAnimation(keyPath: "lineDashPhase") - blackAnimation.fromValue = dash * 3.5 - blackAnimation.toValue = 0 - blackAnimation.duration = 0.45 - blackAnimation.repeatCount = .infinity - self.lassoBlackLayer.add(blackAnimation, forKey: "lineDashPhase") - - self.lassoWhiteLayer.opacity = 0.5 - self.lassoWhiteLayer.fillColor = UIColor.clear.cgColor - self.lassoWhiteLayer.strokeColor = UIColor.white.cgColor - self.lassoWhiteLayer.lineWidth = 2.0 / scale - self.lassoWhiteLayer.lineJoin = .round - self.lassoWhiteLayer.lineCap = .round - self.lassoWhiteLayer.lineDashPattern = [dash as NSNumber, dash * 2.5 as NSNumber] - - let whiteAnimation = CABasicAnimation(keyPath: "lineDashPhase") - whiteAnimation.fromValue = dash * 3.5 + dash * 1.75 - whiteAnimation.toValue = dash * 1.75 - whiteAnimation.duration = 0.45 - whiteAnimation.repeatCount = .infinity - self.lassoWhiteLayer.add(whiteAnimation, forKey: "lineDashPhase") - } - - @objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { - guard let path = self.lassoBlackLayer.path else { - return - } - self.requestMenu(self.selectedElements, path.boundingBox) - } - - @objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { - let translation = gestureRecognizer.translation(in: self) - - switch gestureRecognizer.state { - case .began: - self.panBegan(self.selectedElements) - case .changed: - self.panChanged(self.selectedElements, translation) - case .ended: - self.panEnded(self.selectedElements, translation) - default: - break - } - } - - override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { - if let path = self.lassoBlackLayer.path { - return path.contains(point) - } else { - return false - } - } - - func updatePath(_ bezierPath: BezierPath) { - self.lassoBlackLayer.path = bezierPath.path.cgPath - self.lassoWhiteLayer.path = bezierPath.path.cgPath - } - - func translate(_ offset: CGPoint) { - let updatedPoints = self.currentPoints.map { $0.offsetBy(offset) } - - self.apply(scale: self.currentScale, points: updatedPoints, selectedElements: self.selectedElements, expand: self.currentExpand) - } - - func apply(scale: CGFloat, points: [CGPoint], selectedElements: [UUID], expand: CGFloat) { - self.currentScale = scale - self.currentPoints = points - self.selectedElements = selectedElements - self.currentExpand = expand - - let dash: CGFloat = 5.0 / scale - - let hullPath = concaveHullPath(points: points) - let expandedPath = expandPath(hullPath, width: expand) - self.lassoBlackLayer.path = expandedPath - self.lassoWhiteLayer.path = expandedPath - - self.lassoBlackLayer.removeAllAnimations() - self.lassoWhiteLayer.removeAllAnimations() - - let blackAnimation = CABasicAnimation(keyPath: "lineDashPhase") - blackAnimation.fromValue = 0 - blackAnimation.toValue = dash * 3.5 - blackAnimation.duration = 0.45 - blackAnimation.repeatCount = .infinity - self.lassoBlackLayer.add(blackAnimation, forKey: "lineDashPhase") - - self.lassoWhiteLayer.fillColor = UIColor.clear.cgColor - self.lassoWhiteLayer.strokeColor = UIColor.white.cgColor - self.lassoWhiteLayer.lineWidth = 2.0 / scale - self.lassoWhiteLayer.lineJoin = .round - self.lassoWhiteLayer.lineCap = .round - self.lassoWhiteLayer.lineDashPattern = [dash as NSNumber, dash * 2.5 as NSNumber] - - let whiteAnimation = CABasicAnimation(keyPath: "lineDashPhase") - whiteAnimation.fromValue = dash * 1.75 - whiteAnimation.toValue = dash * 3.5 + dash * 1.75 - whiteAnimation.duration = 0.45 - whiteAnimation.repeatCount = .infinity - self.lassoWhiteLayer.add(whiteAnimation, forKey: "lineDashPhase") - } - - func reset() { - self.bounds = CGRect(origin: .zero, size: self.bounds.size) - - self.selectedElements = [] - - self.isHidden = true - self.lassoBlackLayer.path = nil - self.lassoWhiteLayer.path = nil - self.lassoBlackLayer.removeAllAnimations() - self.lassoWhiteLayer.removeAllAnimations() - } -} diff --git a/submodules/DrawingUI/Sources/EyedropperView.swift b/submodules/DrawingUI/Sources/EyedropperView.swift index b22e95673d..90181b3d32 100644 --- a/submodules/DrawingUI/Sources/EyedropperView.swift +++ b/submodules/DrawingUI/Sources/EyedropperView.swift @@ -54,6 +54,7 @@ final class EyedropperView: UIView { private let sourceImage: (data: Data, size: CGSize, bytesPerRow: Int, info: CGBitmapInfo)? var completed: (DrawingColor) -> Void = { _ in } + var dismissed: () -> Void = { } init(containerSize: CGSize, drawingView: DrawingView, sourceImage: UIImage) { self.drawingView = drawingView @@ -162,6 +163,15 @@ final class EyedropperView: UIView { } } + func dismiss() { + self.containerView.alpha = 0.0 + self.containerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in + self?.removeFromSuperview() + }) + self.containerView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2) + self.dismissed() + } + private func getColorAt(_ point: CGPoint) -> UIColor? { guard var sourceImage = self.sourceImage, point.x >= 0 && point.x < sourceImage.size.width && point.y >= 0 && point.y < sourceImage.size.height else { return UIColor.black diff --git a/submodules/DrawingUI/Sources/PenTool.swift b/submodules/DrawingUI/Sources/PenTool.swift index 93b3e3cacd..10b323fc1b 100644 --- a/submodules/DrawingUI/Sources/PenTool.swift +++ b/submodules/DrawingUI/Sources/PenTool.swift @@ -2,49 +2,53 @@ import Foundation import UIKit import Display -final class PenTool: DrawingElement, Codable { +final class PenTool: DrawingElement, Codable { class RenderLayer: SimpleLayer, DrawingRenderLayer { private weak var element: PenTool? + private var isEraser = false + private var accumulationImage: UIImage? + private var activeLayer: ActiveLayer? + + private var start = 0 private var segmentsCount = 0 private var velocity: CGFloat? - private var previousRect: CGRect? var displayLink: ConstantDisplayLinkAnimator? - func setup(size: CGSize) { + func setup(size: CGSize, isEraser: Bool, useDisplayLink: Bool) { + self.isEraser = isEraser + self.shouldRasterize = true self.contentsScale = 1.0 + self.allowsGroupOpacity = true let bounds = CGRect(origin: .zero, size: size) self.frame = bounds - self.displayLink = ConstantDisplayLinkAnimator(update: { [weak self] in - if let strongSelf = self { - if let element = strongSelf.element, strongSelf.segmentsCount < element.segments.count, let velocity = strongSelf.velocity { - let delta = max(9, Int(velocity / 100.0)) - let start = strongSelf.segmentsCount - strongSelf.segmentsCount = min(strongSelf.segmentsCount + delta, element.segments.count) - - let rect = element.boundingRect(from: start, to: strongSelf.segmentsCount) - strongSelf.setNeedsDisplay(rect.insetBy(dx: -80.0, dy: -80.0)) - } - } - }) - self.displayLink?.frameInterval = 1 - self.displayLink?.isPaused = false - } - - - fileprivate func draw(element: PenTool, velocity: CGFloat, rect: CGRect) { - self.element = element + let activeLayer = ActiveLayer() + activeLayer.shouldRasterize = true + activeLayer.contentsScale = 1.0 + activeLayer.frame = bounds + activeLayer.parent = self + self.addSublayer(activeLayer) + self.activeLayer = activeLayer - self.previousRect = rect - if let previous = self.velocity { - self.velocity = velocity * 0.4 + previous * 0.6 - } else { - self.velocity = velocity + if useDisplayLink { + self.displayLink = ConstantDisplayLinkAnimator(update: { [weak self] in + if let strongSelf = self { + if let element = strongSelf.element, strongSelf.segmentsCount < element.segments.count, let velocity = strongSelf.velocity { + let delta = max(12, Int(velocity / 100.0)) + let start = strongSelf.segmentsCount + strongSelf.segmentsCount = min(strongSelf.segmentsCount + delta, element.segments.count) + + let rect = element.boundingRect(from: start, to: strongSelf.segmentsCount) + strongSelf.activeLayer?.setNeedsDisplay(rect.insetBy(dx: -10.0, dy: -10.0)) + } + } + }) + self.displayLink?.frameInterval = 1 + self.displayLink?.isPaused = false } - self.setNeedsDisplay(rect.insetBy(dx: -80.0, dy: -80.0)) } func animateArrowPaths(leftArrowPath: UIBezierPath, rightArrowPath: UIBezierPath, lineWidth: CGFloat, completion: @escaping () -> Void) { @@ -73,8 +77,63 @@ final class PenTool: DrawingElement, Codable { }) } - override func draw(in ctx: CGContext) { - self.element?.drawSegments(in: ctx, upTo: self.segmentsCount) + var i = 0 + fileprivate func draw(element: PenTool, velocity: CGFloat, rect: CGRect) { + self.element = element + + self.opacity = Float(element.color.alpha) + + guard !rect.isInfinite && !rect.isEmpty && !rect.isNull else { + return + } + + var rect: CGRect? = rect + + let limit = 512 + let activeCount = self.segmentsCount - self.start + if activeCount > limit { + rect = nil + let newStart = self.start + limit + let image = generateImage(self.bounds.size, contextGenerator: { size, context in + if let accumulationImage = self.accumulationImage, let cgImage = accumulationImage.cgImage { + context.draw(cgImage, in: CGRect(origin: .zero, size: size)) + } + 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) + element.drawSegments(in: context, from: self.start, upTo: newStart) + }, opaque: false, scale: 1.0) + self.accumulationImage = image + self.contents = image?.cgImage + i += 1 + print("accumulated \(i)") + + self.start = newStart + } + + self.segmentsCount = element.segments.count + + if let previous = self.velocity { + self.velocity = velocity * 0.4 + previous * 0.6 + } else { + self.velocity = velocity + } + if let rect = rect { + self.activeLayer?.setNeedsDisplay(rect.insetBy(dx: -10.0, dy: -10.0)) + } else { + self.activeLayer?.setNeedsDisplay() + } + } + + class ActiveLayer: SimpleLayer { + weak var parent: RenderLayer? + + override func draw(in ctx: CGContext) { + guard let parent = self.parent, let element = parent.element else { + return + } + element.drawSegments(in: ctx, from: parent.start, upTo: parent.segmentsCount) + } } } @@ -83,44 +142,73 @@ final class PenTool: DrawingElement, Codable { let color: DrawingColor let renderLineWidth: CGFloat let renderMinLineWidth: CGFloat + let renderColor: UIColor let hasArrow: Bool let renderArrowLength: CGFloat let renderArrowLineWidth: CGFloat - + + let isEraser: Bool + + let isBlur: Bool + + var arrowStart: CGPoint? + var arrowDirection: CGFloat? var arrowLeftPath: UIBezierPath? var arrowRightPath: UIBezierPath? var translation: CGPoint = .zero + + var blurredImage: UIImage? private weak var currentRenderLayer: DrawingRenderLayer? + + var isValid: Bool { + if self.hasArrow { + return self.arrowStart != nil && self.arrowDirection != nil + } else { + return self.segments.count > 0 + } + } - required init(drawingSize: CGSize, color: DrawingColor, lineWidth: CGFloat, hasArrow: Bool) { + required init(drawingSize: CGSize, color: DrawingColor, lineWidth: CGFloat, hasArrow: Bool, isEraser: Bool, isBlur: Bool, blurredImage: UIImage?) { self.uuid = UUID() self.drawingSize = drawingSize - self.color = color + self.color = isEraser || isBlur ? DrawingColor(rgb: 0x000000) : color self.hasArrow = hasArrow + self.isEraser = isEraser + self.isBlur = isBlur + self.blurredImage = blurredImage let minLineWidth = max(1.0, max(drawingSize.width, drawingSize.height) * 0.002) let maxLineWidth = max(10.0, max(drawingSize.width, drawingSize.height) * 0.07) let lineWidth = minLineWidth + (maxLineWidth - minLineWidth) * lineWidth + let minRenderArrowLength = max(10.0, max(drawingSize.width, drawingSize.height) * 0.02) + self.renderLineWidth = lineWidth - self.renderMinLineWidth = minLineWidth + (lineWidth - minLineWidth) * 0.3 - self.renderArrowLength = lineWidth * 3.0 - self.renderArrowLineWidth = lineWidth * 0.8 + self.renderMinLineWidth = isEraser || isBlur ? lineWidth : minLineWidth + (lineWidth - minLineWidth) * 0.3 + self.renderArrowLength = max(minRenderArrowLength, lineWidth * 3.0) + self.renderArrowLineWidth = max(minLineWidth * 1.8, lineWidth * 0.75) + + self.renderColor = color.withUpdatedAlpha(1.0).toUIColor() } private enum CodingKeys: String, CodingKey { case uuid case drawingSize case color + case isEraser case hasArrow + case isBlur case renderLineWidth case renderMinLineWidth case renderArrowLength case renderArrowLineWidth + + case arrowStart + case arrowDirection case renderSegments } @@ -130,11 +218,21 @@ final class PenTool: DrawingElement, Codable { self.uuid = try container.decode(UUID.self, forKey: .uuid) self.drawingSize = try container.decode(CGSize.self, forKey: .drawingSize) self.color = try container.decode(DrawingColor.self, forKey: .color) + self.isEraser = try container.decode(Bool.self, forKey: .isEraser) self.hasArrow = try container.decode(Bool.self, forKey: .hasArrow) + self.isBlur = try container.decode(Bool.self, forKey: .isBlur) self.renderLineWidth = try container.decode(CGFloat.self, forKey: .renderLineWidth) self.renderMinLineWidth = try container.decode(CGFloat.self, forKey: .renderMinLineWidth) self.renderArrowLength = try container.decode(CGFloat.self, forKey: .renderArrowLength) self.renderArrowLineWidth = try container.decode(CGFloat.self, forKey: .renderArrowLineWidth) + + self.arrowStart = try container.decodeIfPresent(CGPoint.self, forKey: .arrowStart) + self.arrowDirection = try container.decodeIfPresent(CGFloat.self, forKey: .arrowDirection) + + self.segments = try container.decode([Segment].self, forKey: .renderSegments) + + self.renderColor = self.color.withUpdatedAlpha(1.0).toUIColor() + self.maybeSetupArrow() } public func encode(to encoder: Encoder) throws { @@ -142,16 +240,26 @@ final class PenTool: DrawingElement, Codable { try container.encode(self.uuid, forKey: .uuid) try container.encode(self.drawingSize, forKey: .drawingSize) try container.encode(self.color, forKey: .color) + try container.encode(self.isEraser, forKey: .isEraser) try container.encode(self.hasArrow, forKey: .hasArrow) + try container.encode(self.isBlur, forKey: .isBlur) try container.encode(self.renderLineWidth, forKey: .renderLineWidth) try container.encode(self.renderMinLineWidth, forKey: .renderMinLineWidth) try container.encode(self.renderArrowLength, forKey: .renderArrowLength) try container.encode(self.renderArrowLineWidth, forKey: .renderArrowLineWidth) + + try container.encodeIfPresent(self.arrowStart, forKey: .arrowStart) + try container.encodeIfPresent(self.arrowDirection, forKey: .arrowDirection) + + try container.encode(self.segments, forKey: .renderSegments) } + var isFinishingArrow = false func finishArrow(_ completion: @escaping () -> Void) { if let arrowLeftPath, let arrowRightPath { - (self.currentRenderLayer as? RenderLayer)?.animateArrowPaths(leftArrowPath: arrowLeftPath, rightArrowPath: arrowRightPath, lineWidth: self.renderArrowLineWidth, completion: { + self.isFinishingArrow = true + (self.currentRenderLayer as? RenderLayer)?.animateArrowPaths(leftArrowPath: arrowLeftPath, rightArrowPath: arrowRightPath, lineWidth: self.renderArrowLineWidth, completion: { [weak self] in + self?.isFinishingArrow = false completion() }) } else { @@ -161,35 +269,36 @@ final class PenTool: DrawingElement, Codable { func setupRenderLayer() -> DrawingRenderLayer? { let layer = RenderLayer() - layer.setup(size: self.drawingSize) + layer.setup(size: self.drawingSize, isEraser: self.isEraser, useDisplayLink: self.color.toUIColor().rgb == 0xbf5af2) self.currentRenderLayer = layer return layer } var previousPoint: CGPoint? func updatePath(_ path: DrawingGesturePipeline.DrawingResult, state: DrawingGesturePipeline.DrawingGestureState) { - guard case let .polyline(line) = path, let point = line.points.last else { + guard case let .point(point) = path else { return } - let filterDistance: CGFloat + var filterDistance: CGFloat if point.velocity > 1200.0 { - filterDistance = 75.0 + filterDistance = 90.0 } else { - filterDistance = 35.0 + filterDistance = 20.0 } +// filterDistance = 0.0 - if let previousPoint, point.location.distance(to: previousPoint) < filterDistance, state == .changed, self.segments.count > 0 { + if let previousPoint, point.location.distance(to: previousPoint) < filterDistance, state == .changed, self.segments.count > 1 { return } self.previousPoint = point.location var velocity = point.velocity if velocity.isZero { - velocity = 600.0 + velocity = 1000.0 } - var effectiveRenderLineWidth = max(self.renderMinLineWidth, min(self.renderLineWidth + 1.0 - (velocity / 180.0), self.renderLineWidth)) + var effectiveRenderLineWidth = max(self.renderMinLineWidth, min(self.renderLineWidth + 1.0 - (velocity / 220.0), self.renderLineWidth)) if let previousRenderLineWidth = self.previousRenderLineWidth { effectiveRenderLineWidth = effectiveRenderLineWidth * 0.2 + previousRenderLineWidth * 0.8 } @@ -214,23 +323,42 @@ final class PenTool: DrawingElement, Codable { } } } - - if let point = self.smoothPoints.last?.position, let direction { - let arrowLeftPath = UIBezierPath() - arrowLeftPath.move(to: point) - arrowLeftPath.addLine(to: point.pointAt(distance: self.renderArrowLength, angle: direction - 0.45)) - - let arrowRightPath = UIBezierPath() - arrowRightPath.move(to: point) - arrowRightPath.addLine(to: point.pointAt(distance: self.renderArrowLength, angle: direction + 0.45)) - - self.arrowLeftPath = arrowLeftPath - self.arrowRightPath = arrowRightPath - } + + self.arrowStart = self.smoothPoints.last?.position + self.arrowDirection = direction + self.maybeSetupArrow() + } else if self.segments.isEmpty { + let radius = self.renderLineWidth / 2.0 + self.segments.append( + Segment( + a: CGPoint(x: point.x - radius, y: point.y), + b: CGPoint(x: point.x + radius, y: point.y), + c: CGPoint(x: point.x - radius, y: point.y + 0.1), + d: CGPoint(x: point.x + radius, y: point.y + 0.1), + radius1: radius, + radius2: radius, + rect: .zero + ) + ) } } } + func maybeSetupArrow() { + if let start = self.arrowStart, let direction = self.arrowDirection { + let arrowLeftPath = UIBezierPath() + arrowLeftPath.move(to: start) + arrowLeftPath.addLine(to: start.pointAt(distance: self.renderArrowLength, angle: direction - 0.45)) + + let arrowRightPath = UIBezierPath() + arrowRightPath.move(to: start) + arrowRightPath.addLine(to: start.pointAt(distance: self.renderArrowLength, angle: direction + 0.45)) + + self.arrowLeftPath = arrowLeftPath + self.arrowRightPath = arrowRightPath + } + } + func draw(in context: CGContext, size: CGSize) { guard !self.segments.isEmpty else { return @@ -238,14 +366,36 @@ final class PenTool: DrawingElement, Codable { context.saveGState() + if self.isEraser { + context.setBlendMode(.clear) + } else { + context.setAlpha(self.color.alpha) + context.setBlendMode(.copy) + } + context.translateBy(x: self.translation.x, y: self.translation.y) context.setShouldAntialias(true) - self.drawSegments(in: context, upTo: self.segments.count) + if self.isBlur, let blurredImage = self.blurredImage { + let maskContext = DrawingContext(size: size, scale: 0.5, clear: true) + maskContext?.withFlippedContext { maskContext in + self.drawSegments(in: maskContext, from: 0, upTo: self.segments.count) + } + if let maskImage = maskContext?.generateImage()?.cgImage, let blurredImage = blurredImage.cgImage { + context.clip(to: CGRect(origin: .zero, size: size), mask: maskImage) + 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) + context.draw(blurredImage, in: CGRect(origin: .zero, size: size)) + } + + } else { + self.drawSegments(in: context, from: 0, upTo: self.segments.count) + } if let arrowLeftPath, let arrowRightPath { - context.setStrokeColor(self.color.toCGColor()) + context.setStrokeColor(self.renderColor.cgColor) context.setLineWidth(self.renderArrowLineWidth) context.setLineCap(.round) @@ -257,9 +407,11 @@ final class PenTool: DrawingElement, Codable { } context.restoreGState() + + self.segmentPaths = [:] } - private struct Segment { + private struct Segment: Codable { let a: CGPoint let b: CGPoint let c: CGPoint @@ -267,6 +419,48 @@ final class PenTool: DrawingElement, Codable { let radius1: CGFloat let radius2: CGFloat let rect: CGRect + + init(a: CGPoint, b: CGPoint, c: CGPoint, d: CGPoint, radius1: CGFloat, radius2: CGFloat, rect: CGRect) { + self.a = a + self.b = b + self.c = c + self.d = d + self.radius1 = radius1 + self.radius2 = radius2 + self.rect = rect + } + + private enum CodingKeys: String, CodingKey { + case a + case b + case c + case d + case radius1 + case radius2 + case rect + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.a = try container.decode(CGPoint.self, forKey: .a) + self.b = try container.decode(CGPoint.self, forKey: .b) + self.c = try container.decode(CGPoint.self, forKey: .c) + self.d = try container.decode(CGPoint.self, forKey: .d) + self.radius1 = try container.decode(CGFloat.self, forKey: .radius1) + self.radius2 = try container.decode(CGFloat.self, forKey: .radius2) + self.rect = try container.decode(CGRect.self, forKey: .rect) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.a, forKey: .a) + try container.encode(self.b, forKey: .b) + try container.encode(self.c, forKey: .c) + try container.encode(self.d, forKey: .d) + try container.encode(self.radius1, forKey: .radius1) + try container.encode(self.radius2, forKey: .radius2) + try container.encode(self.rect, forKey: .rect) + } } private struct Point { @@ -291,57 +485,71 @@ final class PenTool: DrawingElement, Codable { private func append(point: Point) -> CGRect? { self.points.append(point) - guard self.points.count > 2 else { return .null } + guard self.points.count > 2 else { return nil } let index = self.points.count - 1 let point0 = self.points[index - 2] let point1 = self.points[index - 1] let point2 = self.points[index] - let newSmoothPoints = smoothPoints( + var newSmoothPoints = self.smoothPoints( fromPoint0: point0, point1: point1, point2: point2 ) - let lastOldSmoothPoint = smoothPoints.last + let previousSmoothPoint = self.smoothPoints.last self.smoothPoints.append(contentsOf: newSmoothPoints) guard self.smoothPoints.count > 1 else { return nil } - let newSegments: ([Segment], CGRect) = { - guard let lastOldSmoothPoint = lastOldSmoothPoint else { - return segments(fromSmoothPoints: newSmoothPoints) - } - return segments(fromSmoothPoints: [lastOldSmoothPoint] + newSmoothPoints) - }() - self.segments.append(contentsOf: newSegments.0) + if let previousSmoothPoint = previousSmoothPoint { + newSmoothPoints.insert(previousSmoothPoint, at: 0) + } + let (nextSegments, rect) = self.segments(fromSmoothPoints: newSmoothPoints) + self.segments.append(contentsOf: nextSegments) + + for i in self.segments.count - nextSegments.count ..< self.segments.count { + let segment = self.segments[i] + let path = self.pathForSegment(segment) + self.segmentPaths[i] = path + } - return newSegments.1 + return rect } private func smoothPoints(fromPoint0 point0: Point, point1: Point, point2: Point) -> [Point] { var smoothPoints: [Point] = [] - let midPoint1 = (point0.position + point1.position) * 0.5 - let midPoint2 = (point1.position + point2.position) * 0.5 + let midPoint1 = CGPoint( + x: (point0.position.x + point1.position.x) * 0.5, + y: (point0.position.y + point1.position.y) * 0.5 + ) + let midPoint2 = CGPoint( + x: (point1.position.x + point2.position.x) * 0.5, + y: (point1.position.y + point2.position.y) * 0.5 + ) - let segmentDistance: CGFloat = 3.0 + let midWidth1 = (point0.width + point1.width) * 0.5 + let midWidth2 = (point1.width + point2.width) * 0.5 + + let segmentDistance: CGFloat = 6.0 let distance = midPoint1.distance(to: midPoint2) let numberOfSegments = min(128, max(floor(distance / segmentDistance), 32)) let step = 1.0 / numberOfSegments for t in stride(from: 0, to: 1, by: step) { - let position = midPoint1 * pow(1 - t, 2) + point1.position * 2 * (1 - t) * t + midPoint2 * t * t - let size = pow(1 - t, 2) * ((point0.width + point1.width) * 0.5) + 2 * (1 - t) * t * point1.width + t * t * ((point1.width + point2.width) * 0.5) - let point = Point(position: position, width: size) - smoothPoints.append(point) + let pX = midPoint1.x * pow(1 - t, 2) + point1.position.x * 2.0 * (1 - t) * t + midPoint2.x * t * t + let pY = midPoint1.y * pow(1 - t, 2) + point1.position.y * 2.0 * (1 - t) * t + midPoint2.y * t * t + + let w = midWidth1 * pow(1 - t, 2) + point1.width * 2.0 * (1 - t) * t + midWidth2 * t * t + + smoothPoints.append(Point(position: CGPoint(x: pX, y: pY), width: w)) } - let finalPoint = Point(position: midPoint2, width: (point1.width + point2.width) * 0.5) - smoothPoints.append(finalPoint) + smoothPoints.append(Point(position: midPoint2, width: midWidth2)) return smoothPoints } @@ -372,7 +580,7 @@ final class PenTool: DrawingElement, Codable { return CGRect(x: minX, y: minY, width: maxX - minX, height: maxY - minY) } - private func segments(fromSmoothPoints smoothPoints: [Point]) -> ([Segment], CGRect) { + private func segments(fromSmoothPoints smoothPoints: [Point]) -> ([Segment], CGRect?) { var segments: [Segment] = [] var updateRect = CGRect.null for i in 1 ..< smoothPoints.count { @@ -380,7 +588,10 @@ final class PenTool: DrawingElement, Codable { let previousWidth = smoothPoints[i - 1].width let currentPoint = smoothPoints[i].position let currentWidth = smoothPoints[i].width - let direction = currentPoint - previousPoint + let direction = CGPoint( + x: currentPoint.x - previousPoint.x, + y: currentPoint.y - previousPoint.y + ) guard !currentPoint.isEqual(to: previousPoint, epsilon: 0.0001) else { continue @@ -389,25 +600,55 @@ final class PenTool: DrawingElement, Codable { var perpendicular = CGPoint(x: -direction.y, y: direction.x) let length = perpendicular.length if length > 0.0 { - perpendicular = perpendicular / length + perpendicular = CGPoint( + x: perpendicular.x / length, + y: perpendicular.y / length + ) } - let a = previousPoint + perpendicular * previousWidth / 2 - let b = previousPoint - perpendicular * previousWidth / 2 - let c = currentPoint + perpendicular * currentWidth / 2 - let d = currentPoint - perpendicular * currentWidth / 2 + let a = CGPoint( + x: previousPoint.x + perpendicular.x * previousWidth / 2.0, + y: previousPoint.y + perpendicular.y * previousWidth / 2.0 + ) + let b = CGPoint( + x: previousPoint.x - perpendicular.x * previousWidth / 2.0, + y: previousPoint.y - perpendicular.y * previousWidth / 2.0 + ) + let c = CGPoint( + x: currentPoint.x + perpendicular.x * currentWidth / 2.0, + y: currentPoint.y + perpendicular.y * currentWidth / 2.0 + ) + let d = CGPoint( + x: currentPoint.x - perpendicular.x * currentWidth / 2.0, + y: currentPoint.y - perpendicular.y * currentWidth / 2.0 + ) - let ab: CGPoint = { - let center = (b + a) / 2 - let radius = center - b - return .init(x: center.x - radius.y, y: center.y + radius.x) - }() - let cd: CGPoint = { - let center = (c + d) / 2 - let radius = center - c - return .init(x: center.x + radius.y, y: center.y - radius.x) - }() + let abCenter = CGPoint( + x: (a.x + b.x) / 2.0, + y: (a.y + b.y) / 2.0 + ) + let abRadius = CGPoint( + x: abCenter.x - b.x, + y: abCenter.y - b.y + ) + let ab = CGPoint( + x: abCenter.x - abRadius.y, + y: abCenter.y + abRadius.x + ) + let cdCenter = CGPoint( + x: (c.x + d.x) / 2.0, + y: (c.y + d.y) / 2.0 + ) + let cdRadius = CGPoint( + x: cdCenter.x - c.x, + y: cdCenter.y - c.y + ) + let cd = CGPoint( + x: cdCenter.x - cdRadius.y, + y: cdCenter.y + cdRadius.x + ) + let minX = min(a.x, b.x, c.x, d.x, ab.x, cd.x) let minY = min(a.y, b.y, c.y, d.y, ab.y, cd.y) let maxX = max(a.x, b.x, c.x, d.x, ab.x, cd.x) @@ -415,50 +656,72 @@ final class PenTool: DrawingElement, Codable { let segmentRect = CGRect(x: minX, y: minY, width: maxX - minX, height: maxY - minY) updateRect = updateRect.union(segmentRect) - - segments.append(Segment(a: a, b: b, c: c, d: d, radius1: previousWidth / 2.0, radius2: currentWidth / 2.0, rect: segmentRect)) + + let segment = Segment(a: a, b: b, c: c, d: d, radius1: previousWidth / 2.0, radius2: currentWidth / 2.0, rect: segmentRect) + segments.append(segment) } - return (segments, updateRect) + return (segments, !updateRect.isNull ? updateRect : nil) } - private func drawSegments(in context: CGContext, upTo: Int) { - context.setStrokeColor(self.color.toCGColor()) - context.setFillColor(self.color.toCGColor()) + private var segmentPaths: [Int: CGPath] = [:] + + private func pathForSegment(_ segment: Segment) -> CGPath { + let path = CGMutablePath() + path.move(to: segment.b) - for i in 0 ..< upTo { + let abStartAngle = atan2( + segment.b.y - segment.a.y, + segment.b.x - segment.a.x + ) + path.addArc( + center: CGPoint( + x: (segment.a.x + segment.b.x) / 2, + y: (segment.a.y + segment.b.y) / 2 + ), + radius: segment.radius1, + startAngle: abStartAngle, + endAngle: abStartAngle + .pi, + clockwise: true + ) + path.addLine(to: segment.c) + + let cdStartAngle = atan2( + segment.c.y - segment.d.y, + segment.c.x - segment.d.x + ) + path.addArc( + center: CGPoint( + x: (segment.c.x + segment.d.x) / 2, + y: (segment.c.y + segment.d.y) / 2 + ), + radius: segment.radius2, + startAngle: cdStartAngle, + endAngle: cdStartAngle + .pi, + clockwise: true + ) + path.closeSubpath() + return path + } + + private func drawSegments(in context: CGContext, from: Int, upTo: Int) { +// context.setStrokeColor(self.renderColor.cgColor) + context.setFillColor(self.renderColor.cgColor) + + for i in from ..< upTo { let segment = self.segments[i] - context.beginPath() - - context.move(to: segment.b) - let abStartAngle = atan2( - segment.b.y - segment.a.y, - segment.b.x - segment.a.x - ) - context.addArc( - center: (segment.a + segment.b)/2, - radius: segment.radius1, - startAngle: abStartAngle, - endAngle: abStartAngle + .pi, - clockwise: true - ) - context.addLine(to: segment.c) - - let cdStartAngle = atan2( - segment.c.y - segment.d.y, - segment.c.x - segment.d.x - ) - context.addArc( - center: (segment.c + segment.d) / 2, - radius: segment.radius2, - startAngle: cdStartAngle, - endAngle: cdStartAngle + .pi, - clockwise: true - ) - context.closePath() + var segmentPath: CGPath + if let current = self.segmentPaths[i] { + segmentPath = current + } else { + let path = self.pathForSegment(segment) + self.segmentPaths[i] = path + segmentPath = path + } + context.addPath(segmentPath) context.fillPath() - context.strokePath() +// context.strokePath() } } } diff --git a/submodules/DrawingUI/Sources/StickerPickerScreen.swift b/submodules/DrawingUI/Sources/StickerPickerScreen.swift index 11d1e54f5d..898b28e6d8 100644 --- a/submodules/DrawingUI/Sources/StickerPickerScreen.swift +++ b/submodules/DrawingUI/Sources/StickerPickerScreen.swift @@ -13,6 +13,8 @@ import EntityKeyboard import PagerComponent import FeaturedStickersScreen import TelegramNotices +import ChatEntityKeyboardInputNode +import ContextUI struct StickerPickerInputData: Equatable { var emoji: EmojiPagerContentComponent @@ -440,6 +442,18 @@ class StickerPickerScreen: ViewController { hideBackground: true ) + var stickerPeekBehavior: EmojiContentPeekBehaviorImpl? + if let controller = self.controller { + stickerPeekBehavior = EmojiContentPeekBehaviorImpl( + context: controller.context, + interaction: nil, + chatPeerId: nil, + present: { [weak controller] c, a in + controller?.presentInGlobalOverlay(c, with: a) + } + ) + } + content.stickers?.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction( performItemAction: { [weak self] _, item, _, _, _, _ in guard let strongSelf = self, let file = item.itemFile else { @@ -541,7 +555,7 @@ class StickerPickerScreen: ViewController { updateSearchQuery: { _, _ in }, chatPeerId: nil, - peekBehavior: nil, + peekBehavior: stickerPeekBehavior, customLayout: nil, externalBackground: nil, externalExpansionView: nil, @@ -594,6 +608,10 @@ class StickerPickerScreen: ViewController { if gestureRecognizer is UIPanGestureRecognizer && otherGestureRecognizer is UIPanGestureRecognizer { if otherGestureRecognizer is PagerPanGestureRecognizer { return false + } else if otherGestureRecognizer is UIPanGestureRecognizer, let scrollView = otherGestureRecognizer.view, scrollView.frame.width > scrollView.frame.height { + return false + } else if otherGestureRecognizer is PeekControllerGestureRecognizer { + return false } return true } diff --git a/submodules/DrawingUI/Sources/TextSettingsComponent.swift b/submodules/DrawingUI/Sources/TextSettingsComponent.swift index 7d2dcde162..a5b48f6263 100644 --- a/submodules/DrawingUI/Sources/TextSettingsComponent.swift +++ b/submodules/DrawingUI/Sources/TextSettingsComponent.swift @@ -760,6 +760,8 @@ final class TextSizeSliderComponent: Component { init() { super.init(frame: CGRect()) + self.isExclusiveTouch = true + let pressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handlePress(_:))) pressGestureRecognizer.minimumPressDuration = 0.01 pressGestureRecognizer.delegate = self diff --git a/submodules/DrawingUI/Sources/ToolsComponent.swift b/submodules/DrawingUI/Sources/ToolsComponent.swift index 035cb37a06..36cbfdeedf 100644 --- a/submodules/DrawingUI/Sources/ToolsComponent.swift +++ b/submodules/DrawingUI/Sources/ToolsComponent.swift @@ -17,6 +17,7 @@ private class ToolView: UIView, UIGestureRecognizerDelegate { var isVisible = false private var currentSize: CGFloat? + private let shadow: SimpleLayer private let tip: UIImageView private let background: SimpleLayer private let band: SimpleGradientLayer @@ -27,6 +28,8 @@ private class ToolView: UIView, UIGestureRecognizerDelegate { init(type: DrawingToolState.Key) { self.type = type + self.shadow = SimpleLayer() + self.tip = UIImageView() self.tip.isUserInteractionEnabled = false @@ -41,6 +44,7 @@ private class ToolView: UIView, UIGestureRecognizerDelegate { let backgroundImage: UIImage? let tipImage: UIImage? + let shadowImage: UIImage? var tipAbove = true var hasBand = true @@ -49,39 +53,49 @@ private class ToolView: UIView, UIGestureRecognizerDelegate { case .pen: backgroundImage = UIImage(bundleImageName: "Media Editor/ToolPen") tipImage = UIImage(bundleImageName: "Media Editor/ToolPenTip")?.withRenderingMode(.alwaysTemplate) + shadowImage = UIImage(bundleImageName: "Media Editor/ToolPenShadow") case .arrow: backgroundImage = UIImage(bundleImageName: "Media Editor/ToolArrow") tipImage = UIImage(bundleImageName: "Media Editor/ToolArrowTip")?.withRenderingMode(.alwaysTemplate) + shadowImage = UIImage(bundleImageName: "Media Editor/ToolArrowShadow") case .marker: backgroundImage = UIImage(bundleImageName: "Media Editor/ToolMarker") tipImage = UIImage(bundleImageName: "Media Editor/ToolMarkerTip")?.withRenderingMode(.alwaysTemplate) tipAbove = false + shadowImage = UIImage(bundleImageName: "Media Editor/ToolMarkerShadow") case .neon: backgroundImage = UIImage(bundleImageName: "Media Editor/ToolNeon") tipImage = UIImage(bundleImageName: "Media Editor/ToolNeonTip")?.withRenderingMode(.alwaysTemplate) tipAbove = false + shadowImage = UIImage(bundleImageName: "Media Editor/ToolNeonShadow") case .eraser: backgroundImage = UIImage(bundleImageName: "Media Editor/ToolEraser") tipImage = nil hasBand = false + shadowImage = UIImage(bundleImageName: "Media Editor/ToolEraserShadow") case .blur: backgroundImage = UIImage(bundleImageName: "Media Editor/ToolBlur") tipImage = UIImage(bundleImageName: "Media Editor/ToolBlurTip") tipAbove = false hasBand = false + shadowImage = UIImage(bundleImageName: "Media Editor/ToolBlurShadow") } self.tip.image = tipImage self.background.contents = backgroundImage?.cgImage + self.shadow.contents = shadowImage?.cgImage super.init(frame: CGRect(origin: .zero, size: toolSize)) self.tip.frame = CGRect(origin: .zero, size: toolSize) + self.shadow.frame = CGRect(origin: .zero, size: toolSize).insetBy(dx: -4.0, dy: 0.0) self.background.frame = CGRect(origin: .zero, size: toolSize) self.band.frame = CGRect(origin: CGPoint(x: 3.0, y: 64.0), size: CGSize(width: toolSize.width - 6.0, height: toolSize.width - 16.0)) self.band.anchorPoint = CGPoint(x: 0.5, y: 0.0) + self.layer.addSublayer(self.shadow) + if tipAbove { self.layer.addSublayer(self.background) self.addSubview(self.tip) @@ -107,7 +121,7 @@ private class ToolView: UIView, UIGestureRecognizerDelegate { override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { if gestureRecognizer is UIPanGestureRecognizer { - if self.isSelected && !self.isToolFocused { + if self.isSelected { return true } else { return false @@ -164,12 +178,12 @@ private class ToolView: UIView, UIGestureRecognizerDelegate { } func update(state: DrawingToolState) { + self.currentSize = state.size + if let _ = self.tip.image { let color = state.color?.toUIColor() self.tip.tintColor = color - self.currentSize = state.size - guard let color = color else { return } @@ -234,7 +248,7 @@ final class ToolsComponent: Component { } public final class View: UIView, ComponentTaggedView { - private let toolViews: [ToolView] + private var toolViews: [ToolView] = [] private let maskImageView: UIImageView private var isToolFocused: Bool? @@ -251,20 +265,12 @@ final class ToolsComponent: Component { } override init(frame: CGRect) { - var toolViews: [ToolView] = [] - for type in DrawingToolState.Key.allCases { - toolViews.append(ToolView(type: type)) - } - self.toolViews = toolViews - self.maskImageView = UIImageView() self.maskImageView.image = generateGradientImage(size: CGSize(width: 1.0, height: 120.0), colors: [UIColor.white, UIColor.white, UIColor.white.withAlphaComponent(0.0)], locations: [0.0, 0.88, 1.0], direction: .vertical) super.init(frame: frame) self.mask = self.maskImageView - - toolViews.forEach { self.addSubview($0) } } required init?(coder: NSCoder) { @@ -304,6 +310,18 @@ final class ToolsComponent: Component { func update(component: ToolsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { self.component = component + if self.toolViews.isEmpty { + var toolViews: [ToolView] = [] + for type in DrawingToolState.Key.allCases { + if component.state.tools.contains(where: { $0.key == type }) { + let toolView = ToolView(type: type) + toolViews.append(toolView) + self.addSubview(toolView) + } + } + self.toolViews = toolViews + } + let wasFocused = self.isToolFocused self.isToolFocused = component.isFocused @@ -455,64 +473,6 @@ final class ToolsComponent: Component { } } - -final class BrushButtonContent: CombinedComponent { - let title: String - let image: UIImage - - init( - title: String, - image: UIImage - ) { - self.title = title - self.image = image - } - - static func ==(lhs: BrushButtonContent, rhs: BrushButtonContent) -> Bool { - if lhs.title != rhs.title { - return false - } - if lhs.image !== rhs.image { - return false - } - return true - } - - static var body: Body { - let title = Child(Text.self) - let image = Child(Image.self) - - return { context in - let component = context.component - - let title = title.update( - component: Text( - text: component.title, - font: Font.regular(17.0), - color: .white - ), - availableSize: context.availableSize, - transition: .immediate - ) - - let image = image.update( - component: Image(image: component.image), - availableSize: CGSize(width: 24.0, height: 24.0), - transition: .immediate - ) - context.add(image - .position(CGPoint(x: context.availableSize.width - image.size.width / 2.0, y: context.availableSize.height / 2.0)) - ) - - context.add(title - .position(CGPoint(x: context.availableSize.width - image.size.width - title.size.width / 2.0, y: context.availableSize.height / 2.0)) - ) - - return context.availableSize - } - } -} - final class ZoomOutButtonContent: CombinedComponent { let title: String let image: UIImage diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/LegacyComponentsContext.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/LegacyComponentsContext.h index 06ab15f7d1..a7c7db0a09 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/LegacyComponentsContext.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/LegacyComponentsContext.h @@ -56,6 +56,7 @@ typedef enum { - (void)lockPortrait; - (void)unlockPortrait; +- (void)disableInteractiveKeyboardGesture; - (TGNavigationBarPallete *)navigationBarPallete; - (TGMenuSheetPallete *)menuSheetPallete; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h index c4b51ab07a..58749d539b 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h @@ -103,6 +103,7 @@ @property (nonatomic, copy) void(^requestDismiss)(void); @property (nonatomic, copy) void(^requestApply)(void); @property (nonatomic, copy) UIImage *(^getCurrentImage)(void); +@property (nonatomic, copy) void(^updateVideoPlayback)(bool); - (TGPaintingData *)generateResultData; - (void)animateOut:(void(^)(void))completion; @@ -142,7 +143,7 @@ - (UIView *)solidRoundedButton:(NSString *)title action:(void(^)(void))action; -- (id)drawingAdapter:(CGSize)size originalSize:(CGSize)originalSize isAvatar:(bool)isAvatar; +- (id)drawingAdapter:(CGSize)size originalSize:(CGSize)originalSize isVideo:(bool)isVideo isAvatar:(bool)isAvatar; - (UIView *)drawingEntitiesViewWithSize:(CGSize)size; diff --git a/submodules/LegacyComponents/Sources/TGMediaAssetsPickerController.m b/submodules/LegacyComponents/Sources/TGMediaAssetsPickerController.m index eb8149c18b..d57defabf2 100644 --- a/submodules/LegacyComponents/Sources/TGMediaAssetsPickerController.m +++ b/submodules/LegacyComponents/Sources/TGMediaAssetsPickerController.m @@ -495,6 +495,8 @@ } else { [(TGMediaAssetsController *)strongSelf.navigationController completeWithAvatarImage:resultImage]; } + + commit(); }; controller.didFinishEditingVideo = ^(AVAsset *asset, id adjustments, UIImage *resultImage, UIImage *thumbnailImage, bool hasChanges, void(^commit)(void)) { if (!hasChanges) diff --git a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryModel.m b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryModel.m index 4168cd47c5..53ebd775e0 100644 --- a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryModel.m +++ b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryModel.m @@ -440,6 +440,8 @@ [videoItemView setScrubbingPanelApperanceLocked:false]; [videoItemView presentScrubbingPanelAfterReload:hasChanges]; } + + commit(); }; controller.didFinishRenderingFullSizeImage = ^(UIImage *image) diff --git a/submodules/LegacyComponents/Sources/TGPhotoDrawingController.m b/submodules/LegacyComponents/Sources/TGPhotoDrawingController.m index 10f8e9b155..206e935a2e 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoDrawingController.m +++ b/submodules/LegacyComponents/Sources/TGPhotoDrawingController.m @@ -30,7 +30,7 @@ const CGFloat TGPhotoPaintTopPanelSize = 44.0f; const CGFloat TGPhotoPaintBottomPanelSize = 79.0f; const CGSize TGPhotoPaintingLightMaxSize = { 1280.0f, 1280.0f }; -const CGSize TGPhotoPaintingMaxSize = { 2560.0f, 2560.0f }; +const CGSize TGPhotoPaintingMaxSize = { 1920.0f, 1920.0f }; @interface TGPhotoDrawingController () { @@ -85,7 +85,7 @@ const CGSize TGPhotoPaintingMaxSize = { 2560.0f, 2560.0f }; _stickersContext = stickersContext; CGSize size = TGScaleToSize(photoEditor.originalSize, [TGPhotoDrawingController maximumPaintingSize]); - _drawingAdapter = [_stickersContext drawingAdapter:size originalSize:photoEditor.originalSize isAvatar:isAvatar]; + _drawingAdapter = [_stickersContext drawingAdapter:size originalSize:photoEditor.originalSize isVideo:photoEditor.forVideo isAvatar:isAvatar]; _interfaceController = (UIViewController *)_drawingAdapter.interfaceController; __weak TGPhotoDrawingController *weakSelf = self; @@ -108,6 +108,13 @@ const CGSize TGPhotoPaintingMaxSize = { 2560.0f, 2560.0f }; return [strongSelf.photoEditor currentResultImage]; }; + _interfaceController.updateVideoPlayback = ^(bool play) { + __strong TGPhotoDrawingController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf.controlVideoPlayback(play); + }; self.photoEditor = photoEditor; self.previewView = previewView; @@ -474,6 +481,7 @@ const CGSize TGPhotoPaintingMaxSize = { 2560.0f, 2560.0f }; - (void)transitionIn { [_context lockPortrait]; + [_context disableInteractiveKeyboardGesture]; // if (self.presentedForAvatarCreation) { // _drawingView.hidden = true; // } @@ -900,6 +908,7 @@ const CGSize TGPhotoPaintingMaxSize = { 2560.0f, 2560.0f }; - (CGPoint)entityCenterPoint { + //return [_scrollView convertPoint:TGPaintCenterOfRect(_scrollView.bounds) toView:_entitiesView]; return [_previewView convertPoint:TGPaintCenterOfRect(_previewView.bounds) toView:_entitiesView]; } diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift index cdd041cfd0..30b8773175 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift @@ -485,7 +485,7 @@ public final class LegacyPaintEntityRenderer: NSObject, TGPhotoPaintEntityRender public func entities(for time: CMTime, fps: Int, size: CGSize, completion: (([CIImage]?) -> Void)!) { let entities = self.entities let maxSide = max(size.width, size.height) - let paintingScale = maxSide / 2560.0 + let paintingScale = maxSide / 1920.0 self.queue.async { if entities.isEmpty { @@ -607,8 +607,8 @@ public final class LegacyPaintStickersContext: NSObject, TGPhotoPaintStickersCon let contentWrapperView: UIView! let interfaceController: TGPhotoDrawingInterfaceController! - init(context: AccountContext, size: CGSize, originalSize: CGSize, isAvatar: Bool) { - let interfaceController = DrawingScreen(context: context, size: size, originalSize: originalSize, isAvatar: isAvatar) + init(context: AccountContext, size: CGSize, originalSize: CGSize, isVideo: Bool, isAvatar: Bool) { + let interfaceController = DrawingScreen(context: context, size: size, originalSize: originalSize, isVideo: isVideo, isAvatar: isAvatar) self.interfaceController = interfaceController self.drawingView = interfaceController.drawingView self.drawingEntitiesView = interfaceController.entitiesView @@ -619,8 +619,8 @@ public final class LegacyPaintStickersContext: NSObject, TGPhotoPaintStickersCon } } - public func drawingAdapter(_ size: CGSize, originalSize: CGSize, isAvatar: Bool) -> TGPhotoDrawingAdapter! { - return LegacyDrawingAdapter(context: self.context, size: size, originalSize: originalSize, isAvatar: isAvatar) + public func drawingAdapter(_ size: CGSize, originalSize: CGSize, isVideo: Bool, isAvatar: Bool) -> TGPhotoDrawingAdapter! { + return LegacyDrawingAdapter(context: self.context, size: size, originalSize: originalSize, isVideo: isVideo, isAvatar: isAvatar) } public func solidRoundedButton(_ title: String!, action: (() -> Void)!) -> (UIView & TGPhotoSolidRoundedButtonView)! { diff --git a/submodules/LegacyUI/Sources/LegacyController.swift b/submodules/LegacyUI/Sources/LegacyController.swift index 66406534dc..d40dec056e 100644 --- a/submodules/LegacyUI/Sources/LegacyController.swift +++ b/submodules/LegacyUI/Sources/LegacyController.swift @@ -121,6 +121,13 @@ public final class LegacyControllerContext: NSObject, LegacyComponentsContext { controller.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .allButUpsideDown) } } + + public func disableInteractiveKeyboardGesture() { + if let controller = self.controller as? LegacyController { + controller.view.disablesInteractiveModalDismiss = true + controller.view.disablesInteractiveKeyboardGestureRecognizer = true + } + } public func keyCommandController() -> TGKeyCommandController! { return nil diff --git a/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift b/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift index 68fcd353ed..722045ef8b 100644 --- a/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift +++ b/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift @@ -342,9 +342,10 @@ public func fetchedAvatarGalleryEntries(engine: TelegramEngine, account: Account for photo in photos { let indexData = GalleryItemIndexData(position: index, totalCount: Int32(photos.count)) if result.isEmpty, let first = initialEntries.first { - var videoRepresentations: [VideoRepresentationWithReference] = photo.image.videoRepresentations.map({ VideoRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatarList(peer: peerReference, resource: $0.resource)) }) - if videoRepresentations.isEmpty { - videoRepresentations = first.videoRepresentations + var videoRepresentations: [VideoRepresentationWithReference] = first.videoRepresentations + let isPersonal = first.representations.first?.representation.isPersonal == true + if videoRepresentations.isEmpty, !isPersonal { + videoRepresentations = photo.image.videoRepresentations.map({ VideoRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatarList(peer: peerReference, resource: $0.resource)) }) } result.append(.image(photo.image.imageId, photo.image.reference, first.representations, videoRepresentations, peer, secondEntry != nil ? 0 : photo.date, indexData, photo.messageId, photo.image.immediateThumbnailData, nil, false)) } else { diff --git a/submodules/SegmentedControlNode/Sources/SegmentedControlNode.swift b/submodules/SegmentedControlNode/Sources/SegmentedControlNode.swift index 2cd2bbdf12..bf42171425 100644 --- a/submodules/SegmentedControlNode/Sources/SegmentedControlNode.swift +++ b/submodules/SegmentedControlNode/Sources/SegmentedControlNode.swift @@ -76,6 +76,11 @@ public struct SegmentedControlItem: Equatable { } private class SegmentedControlItemNode: HighlightTrackingButtonNode { + override func didLoad() { + super.didLoad() + + self.view.isExclusiveTouch = true + } } public final class SegmentedControlNode: ASDisplayNode, UIGestureRecognizerDelegate { diff --git a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsController.swift b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsController.swift index dc85de65c3..1e06aaf916 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsController.swift @@ -700,9 +700,12 @@ private func selectivePrivacySettingsControllerEntries(presentationData: Present } let exceptionsInfo: String if case .profilePhoto = kind { - if case .nobody = state.setting { + switch state.setting { + case .nobody: exceptionsInfo = presentationData.strings.Privacy_ProfilePhoto_CustomOverrideAddInfo - } else { + case .contacts: + exceptionsInfo = presentationData.strings.Privacy_ProfilePhoto_CustomOverrideBothInfo + case .everybody: exceptionsInfo = presentationData.strings.Privacy_ProfilePhoto_CustomOverrideInfo } } else { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerPhotoUpdater.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerPhotoUpdater.swift index b71521b026..2376b1d908 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerPhotoUpdater.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerPhotoUpdater.swift @@ -247,6 +247,14 @@ func _internal_updatePeerPhotoInternal(postbox: Postbox, network: Network, state return nil } } + } else if let customPeerPhotoMode, case .custom = customPeerPhotoMode { + transaction.updatePeerCachedData(peerIds: Set([peer.id])) { peerId, cachedPeerData in + if let cachedPeerData = cachedPeerData as? CachedUserData { + return cachedPeerData.withUpdatedPersonalPhoto(.known(image)) + } else { + return nil + } + } } } return (.complete(representations), photoResult.resource, videoResult?.resource) @@ -376,6 +384,14 @@ func _internal_updatePeerPhotoInternal(postbox: Postbox, network: Network, state return nil } } + } else if let customPeerPhotoMode, case .custom = customPeerPhotoMode { + transaction.updatePeerCachedData(peerIds: Set([peer.id])) { peerId, cachedPeerData in + if let cachedPeerData = cachedPeerData as? CachedUserData { + return cachedPeerData.withUpdatedPersonalPhoto(.known(nil)) + } else { + return nil + } + } } return .complete([]) } |> mapError { _ -> UploadPeerPhotoError in } diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift index 365d4b0aef..a8c3001c82 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift @@ -889,8 +889,11 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { if let controllerInteraction = controllerInteraction { stickerPeekBehavior = EmojiContentPeekBehaviorImpl( context: self.context, - controllerInteraction: controllerInteraction, - chatPeerId: chatPeerId + interaction: EmojiContentPeekBehaviorImpl.Interaction(sendSticker: controllerInteraction.sendSticker, presentController: controllerInteraction.presentController, presentGlobalOverlayController: controllerInteraction.presentGlobalOverlayController, navigationController: controllerInteraction.navigationController), + chatPeerId: chatPeerId, + present: { c, a in + controllerInteraction.presentGlobalOverlayController(c, a) + } ) } self.stickerInputInteraction = EmojiPagerContentComponent.InputInteraction( @@ -1974,21 +1977,37 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi } } -private final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { +public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { + public class Interaction { + public let sendSticker: (FileMediaReference, Bool, Bool, String?, Bool, UIView, CGRect, CALayer?, [ItemCollectionId]) -> Bool + public let presentController: (ViewController, Any?) -> Void + public let presentGlobalOverlayController: (ViewController, Any?) -> Void + public let navigationController: () -> NavigationController? + + public init(sendSticker: @escaping (FileMediaReference, Bool, Bool, String?, Bool, UIView, CGRect, CALayer?, [ItemCollectionId]) -> Bool, presentController: @escaping (ViewController, Any?) -> Void, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?) { + self.sendSticker = sendSticker + self.presentController = presentController + self.presentGlobalOverlayController = presentGlobalOverlayController + self.navigationController = navigationController + } + } + private let context: AccountContext - private let controllerInteraction: ChatControllerInteraction + private let interaction: Interaction? private let chatPeerId: EnginePeer.Id? + private let present: (ViewController, Any?) -> Void private var peekRecognizer: PeekControllerGestureRecognizer? private weak var peekController: PeekController? - init(context: AccountContext, controllerInteraction: ChatControllerInteraction, chatPeerId: EnginePeer.Id?) { + public init(context: AccountContext, interaction: Interaction?, chatPeerId: EnginePeer.Id?, present: @escaping (ViewController, Any?) -> Void) { self.context = context - self.controllerInteraction = controllerInteraction + self.interaction = interaction self.chatPeerId = chatPeerId + self.present = present } - func setGestureRecognizerEnabled(view: UIView, isEnabled: Bool, itemAtPoint: @escaping (CGPoint) -> (AnyHashable, EmojiPagerContentComponent.View.ItemLayer, TelegramMediaFile)?) { + public func setGestureRecognizerEnabled(view: UIView, isEnabled: Bool, itemAtPoint: @escaping (CGPoint) -> (AnyHashable, EmojiPagerContentComponent.View.ItemLayer, TelegramMediaFile)?) { if self.peekRecognizer == nil { let peekRecognizer = PeekControllerGestureRecognizer(contentAtPoint: { [weak self, weak view] point in guard let strongSelf = self else { @@ -2021,125 +2040,116 @@ private final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { return nil } var menuItems: [ContextMenuItem] = [] - + let presentationData = context.sharedContext.currentPresentationData.with { $0 } - - let sendSticker: (FileMediaReference, Bool, Bool, String?, Bool, UIView, CGRect, CALayer?) -> Void = { fileReference, silentPosting, schedule, query, clearInput, sourceView, sourceRect, sourceLayer in - guard let strongSelf = self else { - return - } - let _ = strongSelf.controllerInteraction.sendSticker(fileReference, silentPosting, schedule, query, clearInput, sourceView, sourceRect, sourceLayer, bubbleUpEmojiOrStickersets) - } - let isLocked = file.isPremiumSticker && !hasPremium - if let chatPeerId = strongSelf.chatPeerId, !isLocked { - if chatPeerId != strongSelf.context.account.peerId && chatPeerId.namespace != Namespaces.Peer.SecretChat { - menuItems.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_SendMessage_SendSilently, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/SilentIcon"), color: theme.actionSheet.primaryTextColor) + if let interaction = strongSelf.interaction { + let sendSticker: (FileMediaReference, Bool, Bool, String?, Bool, UIView, CGRect, CALayer?) -> Void = { fileReference, silentPosting, schedule, query, clearInput, sourceView, sourceRect, sourceLayer in + let _ = interaction.sendSticker(fileReference, silentPosting, schedule, query, clearInput, sourceView, sourceRect, sourceLayer, bubbleUpEmojiOrStickersets) + } + + if let chatPeerId = strongSelf.chatPeerId, !isLocked { + if chatPeerId != strongSelf.context.account.peerId && chatPeerId.namespace != Namespaces.Peer.SecretChat { + menuItems.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_SendMessage_SendSilently, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/SilentIcon"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + if let strongSelf = self, let peekController = strongSelf.peekController { + if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { + sendSticker(.standalone(media: file), true, false, nil, false, animationNode.view, animationNode.bounds, nil) + } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode { + sendSticker(.standalone(media: file), true, false, nil, false, imageNode.view, imageNode.bounds, nil) + } + } + f(.default) + }))) + } + + menuItems.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_SendMessage_ScheduleMessage, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/ScheduleIcon"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in if let strongSelf = self, let peekController = strongSelf.peekController { if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { - sendSticker(.standalone(media: file), true, false, nil, false, animationNode.view, animationNode.bounds, nil) + let _ = sendSticker(.standalone(media: file), false, true, nil, false, animationNode.view, animationNode.bounds, nil) } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode { - sendSticker(.standalone(media: file), true, false, nil, false, imageNode.view, imageNode.bounds, nil) + let _ = sendSticker(.standalone(media: file), false, true, nil, false, imageNode.view, imageNode.bounds, nil) } } f(.default) }))) } - - menuItems.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_SendMessage_ScheduleMessage, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/ScheduleIcon"), color: theme.actionSheet.primaryTextColor) - }, action: { _, f in - if let strongSelf = self, let peekController = strongSelf.peekController { - if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { - let _ = sendSticker(.standalone(media: file), false, true, nil, false, animationNode.view, animationNode.bounds, nil) - } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode { - let _ = sendSticker(.standalone(media: file), false, true, nil, false, imageNode.view, imageNode.bounds, nil) - } - } - f(.default) - }))) - } - - menuItems.append( - .action(ContextMenuActionItem(text: isStarred ? presentationData.strings.Stickers_RemoveFromFavorites : presentationData.strings.Stickers_AddToFavorites, icon: { theme in generateTintedImage(image: isStarred ? UIImage(bundleImageName: "Chat/Context Menu/Unfave") : UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.contextMenu.primaryColor) }, action: { _, f in - f(.default) - - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let _ = (context.engine.stickers.toggleStickerSaved(file: file, saved: !isStarred) - |> deliverOnMainQueue).start(next: { result in + + menuItems.append( + .action(ContextMenuActionItem(text: isStarred ? presentationData.strings.Stickers_RemoveFromFavorites : presentationData.strings.Stickers_AddToFavorites, icon: { theme in generateTintedImage(image: isStarred ? UIImage(bundleImageName: "Chat/Context Menu/Unfave") : UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.contextMenu.primaryColor) }, action: { _, f in + f(.default) + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let _ = (context.engine.stickers.toggleStickerSaved(file: file, saved: !isStarred) + |> deliverOnMainQueue).start(next: { result in + switch result { + case .generic: + interaction.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, title: nil, text: !isStarred ? presentationData.strings.Conversation_StickerAddedToFavorites : presentationData.strings.Conversation_StickerRemovedFromFavorites, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), nil) + case let .limitExceeded(limit, premiumLimit): + let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) + let text: String + if limit == premiumLimit || premiumConfiguration.isPremiumDisabled { + text = presentationData.strings.Premium_MaxFavedStickersFinalText + } else { + text = presentationData.strings.Premium_MaxFavedStickersText("\(premiumLimit)").string + } + interaction.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, title: presentationData.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil, customAction: nil), elevatedLayout: false, action: { action in + if case .info = action { + let controller = PremiumIntroScreen(context: context, source: .savedStickers) + interaction.navigationController()?.pushViewController(controller) + return true + } + return false + }), nil) + } + }) + })) + ) + menuItems.append( + .action(ContextMenuActionItem(text: presentationData.strings.StickerPack_ViewPack, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Sticker"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + f(.default) + guard let strongSelf = self else { return } - switch result { - case .generic: - strongSelf.controllerInteraction.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, title: nil, text: !isStarred ? presentationData.strings.Conversation_StickerAddedToFavorites : presentationData.strings.Conversation_StickerRemovedFromFavorites, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), nil) - case let .limitExceeded(limit, premiumLimit): - let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) - let text: String - if limit == premiumLimit || premiumConfiguration.isPremiumDisabled { - text = presentationData.strings.Premium_MaxFavedStickersFinalText - } else { - text = presentationData.strings.Premium_MaxFavedStickersText("\(premiumLimit)").string - } - strongSelf.controllerInteraction.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, title: presentationData.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil, customAction: nil), elevatedLayout: false, action: { action in - guard let strongSelf = self else { - return false - } - if case .info = action { - let controller = PremiumIntroScreen(context: context, source: .savedStickers) - strongSelf.controllerInteraction.navigationController()?.pushViewController(controller) - return true - } - return false - }), nil) - } - }) - })) - ) - menuItems.append( - .action(ContextMenuActionItem(text: presentationData.strings.StickerPack_ViewPack, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Sticker"), color: theme.actionSheet.primaryTextColor) - }, action: { _, f in - f(.default) - - guard let strongSelf = self else { - return - } - loop: for attribute in file.attributes { - switch attribute { - case let .CustomEmoji(_, _, _, packReference), let .Sticker(_, packReference, _): - if let packReference = packReference { - let controller = strongSelf.context.sharedContext.makeStickerPackScreen(context: context, updatedPresentationData: nil, mainStickerPack: packReference, stickerPacks: [packReference], loadedStickerPacks: [], parentNavigationController: strongSelf.controllerInteraction.navigationController(), sendSticker: { file, sourceView, sourceRect in - sendSticker(file, false, false, nil, false, sourceView, sourceRect, nil) - return true - }) - - strongSelf.controllerInteraction.navigationController()?.view.window?.endEditing(true) - strongSelf.controllerInteraction.presentController(controller, nil) + switch attribute { + case let .CustomEmoji(_, _, _, packReference), let .Sticker(_, packReference, _): + if let packReference = packReference { + let controller = strongSelf.context.sharedContext.makeStickerPackScreen(context: context, updatedPresentationData: nil, mainStickerPack: packReference, stickerPacks: [packReference], loadedStickerPacks: [], parentNavigationController: interaction.navigationController(), sendSticker: { file, sourceView, sourceRect in + sendSticker(file, false, false, nil, false, sourceView, sourceRect, nil) + return true + }) + + interaction.navigationController()?.view.window?.endEditing(true) + interaction.presentController(controller, nil) + } + break loop + default: + break } - break loop - default: - break } - } - })) - ) + })) + ) + } guard let view = view else { return nil } return (view, itemLayer.convert(itemLayer.bounds, to: view.layer), StickerPreviewPeekContent(account: context.account, theme: presentationData.theme, strings: presentationData.strings, item: .pack(file), isLocked: isLocked && !isStarred, menu: menuItems, openPremiumIntro: { - guard let strongSelf = self else { + guard let strongSelf = self, let interaction = strongSelf.interaction else { return } let controller = PremiumIntroScreen(context: context, source: .stickers) - strongSelf.controllerInteraction.navigationController()?.pushViewController(controller) + interaction.navigationController()?.pushViewController(controller) })) } }, present: { [weak self] content, sourceView, sourceRect in @@ -2157,7 +2167,7 @@ private final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { self?.simulateUpdateLayout(isVisible: !visible) }*/ strongSelf.peekController = controller - strongSelf.controllerInteraction.presentGlobalOverlayController(controller, nil) + strongSelf.present(controller, nil) return controller }, updateContent: { [weak self] content in guard let strongSelf = self else { diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolArrowShadow.imageset/Arrow.png b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolArrowShadow.imageset/Arrow.png new file mode 100644 index 0000000000..995d2735df Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolArrowShadow.imageset/Arrow.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolArrowShadow.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolArrowShadow.imageset/Contents.json new file mode 100644 index 0000000000..39362cd425 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolArrowShadow.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Arrow.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolBlurShadow.imageset/Blur.png b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolBlurShadow.imageset/Blur.png new file mode 100644 index 0000000000..952055729b Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolBlurShadow.imageset/Blur.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolBlurShadow.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolBlurShadow.imageset/Contents.json new file mode 100644 index 0000000000..cf5d650c80 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolBlurShadow.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Blur.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolEraserShadow.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolEraserShadow.imageset/Contents.json new file mode 100644 index 0000000000..c699ad2b98 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolEraserShadow.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Eraser.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolEraserShadow.imageset/Eraser.png b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolEraserShadow.imageset/Eraser.png new file mode 100644 index 0000000000..30346ce6aa Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolEraserShadow.imageset/Eraser.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolMarkerShadow.imageset/Brush.png b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolMarkerShadow.imageset/Brush.png new file mode 100644 index 0000000000..18edfc1e2d Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolMarkerShadow.imageset/Brush.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolMarkerShadow.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolMarkerShadow.imageset/Contents.json new file mode 100644 index 0000000000..345d08c426 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolMarkerShadow.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Brush.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolNeonShadow.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolNeonShadow.imageset/Contents.json new file mode 100644 index 0000000000..60554c019c --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolNeonShadow.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Neon.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolNeonShadow.imageset/Neon.png b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolNeonShadow.imageset/Neon.png new file mode 100644 index 0000000000..55b44fd177 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolNeonShadow.imageset/Neon.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolPenShadow.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolPenShadow.imageset/Contents.json new file mode 100644 index 0000000000..aa99f8a6e5 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolPenShadow.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Pen.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolPenShadow.imageset/Pen.png b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolPenShadow.imageset/Pen.png new file mode 100644 index 0000000000..62291a163d Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolPenShadow.imageset/Pen.png differ diff --git a/submodules/TelegramUI/Sources/ChatAvatarNavigationNode.swift b/submodules/TelegramUI/Sources/ChatAvatarNavigationNode.swift index 397c22fca5..9d70c36fd2 100644 --- a/submodules/TelegramUI/Sources/ChatAvatarNavigationNode.swift +++ b/submodules/TelegramUI/Sources/ChatAvatarNavigationNode.swift @@ -122,9 +122,25 @@ final class ChatAvatarNavigationNode: ASDisplayNode { guard let strongSelf = self else { return } - let cachedPeerData = peerView.cachedData - if let cachedPeerData = cachedPeerData as? CachedUserData, case let .known(maybePhoto) = cachedPeerData.photo { - if let photo = maybePhoto, let video = smallestVideoRepresentation(photo.videoRepresentations), let peerReference = PeerReference(peer._asPeer()) { + let cachedPeerData = peerView.cachedData as? CachedUserData + var personalPhoto: TelegramMediaImage? + var profilePhoto: TelegramMediaImage? + var isKnown = false + + if let cachedPeerData = cachedPeerData { + if case let .known(maybePersonalPhoto) = cachedPeerData.personalPhoto { + personalPhoto = maybePersonalPhoto + isKnown = true + } + if case let .known(maybePhoto) = cachedPeerData.photo { + profilePhoto = maybePhoto + isKnown = true + } + } + + if isKnown { + let photo = personalPhoto ?? profilePhoto + if let photo = photo, let video = smallestVideoRepresentation(photo.videoRepresentations), let peerReference = PeerReference(peer._asPeer()) { let videoId = photo.id?.id ?? peer.id.id._internalGetInt64Value() let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: photo.representations, videoThumbnails: [], immediateThumbnailData: photo.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.dimensions, flags: [])])) let videoContent = NativeVideoContent(id: .profileVideo(videoId, "header"), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: false) @@ -157,7 +173,7 @@ final class ChatAvatarNavigationNode: ASDisplayNode { strongSelf.hierarchyTrackingLayer?.removeFromSuperlayer() strongSelf.hierarchyTrackingLayer = nil } - + strongSelf.updateVideoVisibility() } else { let _ = context.engine.peers.fetchAndUpdateCachedPeerData(peerId: peer.id).start() diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 3a5121b750..8d1b1f8eeb 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -1404,7 +1404,17 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL removeText = user.photo.first?.hasVideo == true ? presentationData.strings.UserInfo_RemoveCustomVideo : presentationData.strings.UserInfo_RemoveCustomPhoto } - items[.peerDataSettings]!.append(PeerInfoScreenActionItem(id: ItemReset, text: removeText, color: .accent, icon: nil, iconSignal: peerAvatarCompleteImage(account: context.account, peer: EnginePeer(user), forceProvidedRepresentation: true, representation: representation, size: CGSize(width: 28.0, height: 28.0)), action: { + let imageSignal: Signal + if let representation, let signal = peerAvatarImage(account: context.account, peerReference: PeerReference(user), authorOfMessage: nil, representation: representation, displayDimensions: CGSize(width: 28.0, height: 28.0)) { + imageSignal = signal + |> map { data -> UIImage? in + return data?.0 + } + } else { + imageSignal = peerAvatarCompleteImage(account: context.account, peer: EnginePeer(user), forceProvidedRepresentation: true, representation: representation, size: CGSize(width: 28.0, height: 28.0)) + } + + items[.peerDataSettings]!.append(PeerInfoScreenActionItem(id: ItemReset, text: removeText, color: .accent, icon: nil, iconSignal: imageSignal, action: { interaction.resetCustomPhoto() })) } @@ -6872,6 +6882,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate (strongSelf.controller?.parentController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .image(image: image, title: nil, text: strongSelf.presentationData.strings.Privacy_ProfilePhoto_PublicPhotoSuccess, round: true, undo: false), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) case .custom: strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, text: strongSelf.presentationData.strings.UserInfo_SetCustomPhoto_SuccessPhotoText(peer.compactDisplayTitle).string, action: nil, duration: 5), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) + + let _ = (strongSelf.context.peerChannelMemberCategoriesContextsManager.profilePhotos(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, peerId: strongSelf.peerId, fetch: peerInfoProfilePhotos(context: strongSelf.context, peerId: strongSelf.peerId)) |> ignoreValues).start() case .suggest: if let navigationController = (strongSelf.controller?.navigationController as? NavigationController) { strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), keepStack: .default, completion: { _ in @@ -7076,6 +7088,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate (strongSelf.controller?.parentController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .image(image: image, title: nil, text: strongSelf.presentationData.strings.Privacy_ProfilePhoto_PublicVideoSuccess, round: true, undo: false), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) case .custom: strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, text: strongSelf.presentationData.strings.UserInfo_SetCustomPhoto_SuccessVideoText(peer.compactDisplayTitle).string, action: nil, duration: 5), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) + + let _ = (strongSelf.context.peerChannelMemberCategoriesContextsManager.profilePhotos(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, peerId: strongSelf.peerId, fetch: peerInfoProfilePhotos(context: strongSelf.context, peerId: strongSelf.peerId)) |> ignoreValues).start() case .suggest: if let navigationController = (strongSelf.controller?.navigationController as? NavigationController) { strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), keepStack: .default, completion: { _ in