mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-10-09 03:20:48 +00:00
Merge commit '36918ecaf9e88be39eb2ba2cb4ad8bedf7581024'
This commit is contained in:
commit
a3297656ce
@ -8449,7 +8449,11 @@ Sorry for the inconvenience.";
|
||||
|
||||
"UserInfo.SuggestPhoto" = "Suggest Photo for %@";
|
||||
"UserInfo.SetCustomPhoto" = "Set Photo for %@";
|
||||
"UserInfo.ChangeCustomPhoto" = "Change Photo for %@";
|
||||
"UserInfo.ResetCustomPhoto" = "Reset to Original Photo";
|
||||
"UserInfo.ResetCustomVideo" = "Reset to Original Video";
|
||||
"UserInfo.RemoveCustomPhoto" = "Remove Photo";
|
||||
"UserInfo.RemoveCustomVideo" = "Remove Video";
|
||||
"UserInfo.CustomPhotoInfo" = "You can replace %@’s photo with another photo that only you will see.";
|
||||
|
||||
"UserInfo.SuggestPhotoTitle" = "Do you want to suggest a profile picture for %@?";
|
||||
@ -8538,3 +8542,20 @@ Sorry for the inconvenience.";
|
||||
|
||||
"Attachment.EnableSpoiler" = "Hide With Spoiler";
|
||||
"Attachment.DisableSpoiler" = "Disable Spoiler";
|
||||
|
||||
"ProfilePhoto.PublicPhoto" = "public photo";
|
||||
"ProfilePhoto.PublicVideo" = "public video";
|
||||
|
||||
"Paint.Draw" = "Draw";
|
||||
"Paint.Sticker" = "Sticker";
|
||||
"Paint.Text" = "Text";
|
||||
"Paint.ZoomOut" = "Zoom Out";
|
||||
|
||||
"Paint.Rectangle" = "Rectangle";
|
||||
"Paint.Ellipse" = "Ellipse";
|
||||
"Paint.Bubble" = "Bubble";
|
||||
"Paint.Star" = "Star";
|
||||
"Paint.Arrow" = "Arrow";
|
||||
|
||||
"Paint.MoveForward" = "Move Forward";
|
||||
|
||||
|
@ -27,6 +27,7 @@ open class ViewControllerComponentContainer: ViewController {
|
||||
public let inputHeight: CGFloat
|
||||
public let metrics: LayoutMetrics
|
||||
public let deviceMetrics: DeviceMetrics
|
||||
public let orientation: UIInterfaceOrientation?
|
||||
public let isVisible: Bool
|
||||
public let theme: PresentationTheme
|
||||
public let strings: PresentationStrings
|
||||
@ -40,6 +41,7 @@ open class ViewControllerComponentContainer: ViewController {
|
||||
inputHeight: CGFloat,
|
||||
metrics: LayoutMetrics,
|
||||
deviceMetrics: DeviceMetrics,
|
||||
orientation: UIInterfaceOrientation? = nil,
|
||||
isVisible: Bool,
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
@ -52,6 +54,7 @@ open class ViewControllerComponentContainer: ViewController {
|
||||
self.inputHeight = inputHeight
|
||||
self.metrics = metrics
|
||||
self.deviceMetrics = deviceMetrics
|
||||
self.orientation = orientation
|
||||
self.isVisible = isVisible
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
@ -82,6 +85,9 @@ open class ViewControllerComponentContainer: ViewController {
|
||||
if lhs.deviceMetrics != rhs.deviceMetrics {
|
||||
return false
|
||||
}
|
||||
if lhs.orientation != rhs.orientation {
|
||||
return false
|
||||
}
|
||||
if lhs.isVisible != rhs.isVisible {
|
||||
return false
|
||||
}
|
||||
|
@ -276,6 +276,18 @@ final class DrawingBubbleEntititySelectionView: DrawingEntitySelectionView, UIGe
|
||||
panGestureRecognizer.delegate = self
|
||||
self.addGestureRecognizer(panGestureRecognizer)
|
||||
self.panGestureRecognizer = panGestureRecognizer
|
||||
|
||||
self.snapTool.onSnapXUpdated = { [weak self] snapped in
|
||||
if let strongSelf = self, let entityView = strongSelf.entityView {
|
||||
entityView.onSnapToXAxis(snapped)
|
||||
}
|
||||
}
|
||||
|
||||
self.snapTool.onSnapYUpdated = { [weak self] snapped in
|
||||
if let strongSelf = self, let entityView = strongSelf.entityView {
|
||||
entityView.onSnapToXAxis(snapped)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
@ -296,6 +308,8 @@ final class DrawingBubbleEntititySelectionView: DrawingEntitySelectionView, UIGe
|
||||
return true
|
||||
}
|
||||
|
||||
private let snapTool = DrawingEntitySnapTool()
|
||||
|
||||
private var currentHandle: CALayer?
|
||||
@objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
|
||||
guard let entityView = self.entityView, let entity = entityView.entity as? DrawingBubbleEntity else {
|
||||
@ -305,6 +319,8 @@ final class DrawingBubbleEntititySelectionView: DrawingEntitySelectionView, UIGe
|
||||
|
||||
switch gestureRecognizer.state {
|
||||
case .began:
|
||||
self.snapTool.maybeSkipFromStart(entityView: entityView, position: entity.position)
|
||||
|
||||
if let sublayers = self.layer.sublayers {
|
||||
for layer in sublayers {
|
||||
if layer.frame.contains(location) {
|
||||
@ -316,6 +332,7 @@ final class DrawingBubbleEntititySelectionView: DrawingEntitySelectionView, UIGe
|
||||
self.currentHandle = self.layer
|
||||
case .changed:
|
||||
let delta = gestureRecognizer.translation(in: entityView.superview)
|
||||
let velocity = gestureRecognizer.velocity(in: entityView.superview)
|
||||
|
||||
var updatedSize = entity.size
|
||||
var updatedPosition = entity.position
|
||||
@ -358,6 +375,8 @@ final class DrawingBubbleEntititySelectionView: DrawingEntitySelectionView, UIGe
|
||||
} else if self.currentHandle === self.layer {
|
||||
updatedPosition.x += delta.x
|
||||
updatedPosition.y += delta.y
|
||||
|
||||
updatedPosition = self.snapTool.update(entityView: entityView, velocity: velocity, delta: delta, updatedPosition: updatedPosition)
|
||||
}
|
||||
|
||||
entity.size = updatedSize
|
||||
@ -367,7 +386,9 @@ final class DrawingBubbleEntititySelectionView: DrawingEntitySelectionView, UIGe
|
||||
|
||||
gestureRecognizer.setTranslation(.zero, in: entityView)
|
||||
case .ended:
|
||||
break
|
||||
self.snapTool.reset()
|
||||
case .cancelled:
|
||||
self.snapTool.reset()
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import LegacyComponents
|
||||
import AccountContext
|
||||
|
||||
@ -10,7 +11,7 @@ public protocol DrawingEntity: AnyObject {
|
||||
|
||||
var lineWidth: CGFloat { get set }
|
||||
var color: DrawingColor { get set }
|
||||
|
||||
|
||||
func duplicate() -> DrawingEntity
|
||||
|
||||
var currentEntityView: DrawingEntityView? { get }
|
||||
@ -127,25 +128,58 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
||||
private var tapGestureRecognizer: UITapGestureRecognizer!
|
||||
private(set) var selectedEntityView: DrawingEntityView?
|
||||
|
||||
public var getEntityCenterPosition: () -> CGPoint = { return .zero }
|
||||
public var getEntityInitialRotation: () -> CGFloat = { return 0.0 }
|
||||
public var hasSelectionChanged: (Bool) -> Void = { _ in }
|
||||
var selectionChanged: (DrawingEntity?) -> Void = { _ in }
|
||||
var requestedMenuForEntityView: (DrawingEntityView, Bool) -> Void = { _, _ in }
|
||||
|
||||
var entityAdded: (DrawingEntity) -> Void = { _ in }
|
||||
var entityRemoved: (DrawingEntity) -> Void = { _ in }
|
||||
|
||||
private let xAxisView = UIView()
|
||||
private let yAxisView = UIView()
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
|
||||
public init(context: AccountContext, size: CGSize) {
|
||||
self.context = context
|
||||
self.size = size
|
||||
|
||||
|
||||
super.init(frame: CGRect(origin: .zero, size: size))
|
||||
|
||||
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))
|
||||
self.addGestureRecognizer(tapGestureRecognizer)
|
||||
self.tapGestureRecognizer = tapGestureRecognizer
|
||||
|
||||
self.xAxisView.alpha = 0.0
|
||||
self.xAxisView.backgroundColor = UIColor(rgb: 0x5fc1f0)
|
||||
self.xAxisView.isUserInteractionEnabled = false
|
||||
|
||||
self.yAxisView.alpha = 0.0
|
||||
self.yAxisView.backgroundColor = UIColor(rgb: 0x5fc1f0)
|
||||
self.yAxisView.isUserInteractionEnabled = false
|
||||
|
||||
self.addSubview(self.xAxisView)
|
||||
self.addSubview(self.yAxisView)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
let point = self.getEntityCenterPosition()
|
||||
self.xAxisView.bounds = CGRect(origin: .zero, size: CGSize(width: 10.0, height: 3000.0))
|
||||
self.xAxisView.center = point
|
||||
self.xAxisView.transform = CGAffineTransform(rotationAngle: self.getEntityInitialRotation())
|
||||
|
||||
self.yAxisView.bounds = CGRect(origin: .zero, size: CGSize(width: 3000.0, height: 10.0))
|
||||
self.yAxisView.center = point
|
||||
self.yAxisView.transform = CGAffineTransform(rotationAngle: self.getEntityInitialRotation())
|
||||
}
|
||||
|
||||
var entities: [DrawingEntity] {
|
||||
var entities: [DrawingEntity] = []
|
||||
for case let view as DrawingEntityView in self.subviews {
|
||||
@ -160,13 +194,16 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
||||
if let entitiesData = entitiesData, let codableEntities = try? JSONDecoder().decode([CodableDrawingEntity].self, from: entitiesData) {
|
||||
let entities = codableEntities.map { $0.entity }
|
||||
for entity in entities {
|
||||
self.add(entity)
|
||||
self.add(entity, announce: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var entitiesData: Data? {
|
||||
let entities = self.entities
|
||||
guard !entities.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
for entity in entities {
|
||||
entity.prepareForRender()
|
||||
}
|
||||
@ -185,7 +222,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
||||
return entity.center.offsetBy(dx: offset.x, dy: offset.y)
|
||||
} else {
|
||||
let minimalDistance: CGFloat = round(offsetLength * 0.5)
|
||||
var position = CGPoint(x: self.size.width / 2.0, y: self.size.height / 2.0) // place good here
|
||||
var position = self.getEntityCenterPosition()
|
||||
|
||||
while true {
|
||||
var occupied = false
|
||||
@ -214,8 +251,11 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
||||
|
||||
func prepareNewEntity(_ entity: DrawingEntity, setup: Bool = true, relativeTo: DrawingEntity? = nil) {
|
||||
let center = self.startPosition(relativeTo: relativeTo)
|
||||
let rotation = self.getEntityInitialRotation()
|
||||
|
||||
if let shape = entity as? DrawingSimpleShapeEntity {
|
||||
shape.position = center
|
||||
shape.rotation = rotation
|
||||
|
||||
if setup {
|
||||
let size = self.newEntitySize()
|
||||
@ -237,12 +277,14 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
||||
}
|
||||
} else if let sticker = entity as? DrawingStickerEntity {
|
||||
sticker.position = center
|
||||
sticker.rotation = rotation
|
||||
if setup {
|
||||
sticker.referenceDrawingSize = self.size
|
||||
sticker.scale = 1.0
|
||||
}
|
||||
} else if let bubble = entity as? DrawingBubbleEntity {
|
||||
bubble.position = center
|
||||
bubble.rotation = rotation
|
||||
if setup {
|
||||
let size = self.newEntitySize()
|
||||
bubble.referenceDrawingSize = self.size
|
||||
@ -251,6 +293,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
||||
}
|
||||
} else if let text = entity as? DrawingTextEntity {
|
||||
text.position = center
|
||||
text.rotation = rotation
|
||||
if setup {
|
||||
text.referenceDrawingSize = self.size
|
||||
text.width = floor(self.size.width * 0.9)
|
||||
@ -260,11 +303,45 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func add(_ entity: DrawingEntity) -> DrawingEntityView {
|
||||
func add(_ entity: DrawingEntity, announce: Bool = true) -> DrawingEntityView {
|
||||
let view = entity.makeView(context: self.context)
|
||||
view.containerView = self
|
||||
|
||||
view.onSnapToXAxis = { [weak self] snappedToX in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
|
||||
if snappedToX {
|
||||
if strongSelf.xAxisView.alpha < 1.0 {
|
||||
strongSelf.hapticFeedback.impact(.light)
|
||||
}
|
||||
transition.updateAlpha(layer: strongSelf.xAxisView.layer, alpha: 1.0)
|
||||
} else {
|
||||
transition.updateAlpha(layer: strongSelf.xAxisView.layer, alpha: 0.0)
|
||||
}
|
||||
}
|
||||
view.onSnapToYAxis = { [weak self] snappedToY in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
|
||||
if snappedToY {
|
||||
if strongSelf.yAxisView.alpha < 1.0 {
|
||||
strongSelf.hapticFeedback.impact(.light)
|
||||
}
|
||||
transition.updateAlpha(layer: strongSelf.yAxisView.layer, alpha: 1.0)
|
||||
} else {
|
||||
transition.updateAlpha(layer: strongSelf.yAxisView.layer, alpha: 0.0)
|
||||
}
|
||||
}
|
||||
|
||||
view.update()
|
||||
self.addSubview(view)
|
||||
|
||||
if announce {
|
||||
self.entityAdded(entity)
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
@ -279,15 +356,35 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
||||
return newEntity
|
||||
}
|
||||
|
||||
func remove(uuid: UUID) {
|
||||
func remove(uuid: UUID, animated: Bool = false, announce: Bool = true) {
|
||||
if let view = self.getView(for: uuid) {
|
||||
if self.selectedEntityView === view {
|
||||
self.selectedEntityView?.removeFromSuperview()
|
||||
self.selectedEntityView = nil
|
||||
self.selectionChanged(nil)
|
||||
self.hasSelectionChanged(false)
|
||||
}
|
||||
view.removeFromSuperview()
|
||||
if animated {
|
||||
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak view] _ in
|
||||
view?.removeFromSuperview()
|
||||
})
|
||||
if !(view.entity is DrawingVectorEntity) {
|
||||
view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false)
|
||||
}
|
||||
if let selectionView = view.selectionView {
|
||||
selectionView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak selectionView] _ in
|
||||
selectionView?.removeFromSuperview()
|
||||
})
|
||||
if !(view.entity is DrawingVectorEntity) {
|
||||
selectionView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
view.removeFromSuperview()
|
||||
}
|
||||
|
||||
if announce {
|
||||
self.entityRemoved(view.entity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -475,6 +572,9 @@ public class DrawingEntityView: UIView {
|
||||
weak var selectionView: DrawingEntitySelectionView?
|
||||
weak var containerView: DrawingEntitiesView?
|
||||
|
||||
var onSnapToXAxis: (Bool) -> Void = { _ in }
|
||||
var onSnapToYAxis: (Bool) -> Void = { _ in }
|
||||
|
||||
init(context: AccountContext, entity: DrawingEntity) {
|
||||
self.context = context
|
||||
self.entity = entity
|
||||
|
@ -17,9 +17,7 @@ final class DrawingMetalView: MTKView {
|
||||
private var render_target_vertex: MTLBuffer!
|
||||
private var render_target_uniform: MTLBuffer!
|
||||
|
||||
private var penBrush: Brush?
|
||||
private var markerBrush: Brush?
|
||||
private var pencilBrush: Brush?
|
||||
|
||||
init?(size: CGSize) {
|
||||
var size = size
|
||||
@ -75,12 +73,6 @@ final class DrawingMetalView: MTKView {
|
||||
}
|
||||
|
||||
func drawInContext(_ cgContext: CGContext) {
|
||||
// guard let texture = self.drawable?.texture, let ciImage = CIImage(mtlTexture: texture, options: [.colorSpace: CGColorSpaceCreateDeviceRGB()])?.oriented(forExifOrientation: 1) else {
|
||||
// return
|
||||
// }
|
||||
// let context = CIContext(cgContext: cgContext)
|
||||
// let rect = CGRect(origin: .zero, size: ciImage.extent.size)
|
||||
// context.draw(ciImage, in: rect, from: rect)
|
||||
guard let texture = self.drawable?.texture, let image = texture.createCGImage() else {
|
||||
return
|
||||
}
|
||||
@ -123,16 +115,10 @@ final class DrawingMetalView: MTKView {
|
||||
} catch {
|
||||
fatalError(error.localizedDescription)
|
||||
}
|
||||
|
||||
self.penBrush = Brush(texture: nil, target: self, rotation: .ahead)
|
||||
|
||||
|
||||
if let url = getAppBundle().url(forResource: "marker", withExtension: "png"), let data = try? Data(contentsOf: url) {
|
||||
self.markerBrush = Brush(texture: self.makeTexture(with: data), target: self, rotation: .fixed(-0.55))
|
||||
}
|
||||
|
||||
if let url = getAppBundle().url(forResource: "pencil", withExtension: "png"), let data = try? Data(contentsOf: url) {
|
||||
self.pencilBrush = Brush(texture: self.makeTexture(with: data), target: self, rotation: .random)
|
||||
}
|
||||
}
|
||||
|
||||
var clearOnce = false
|
||||
@ -184,30 +170,20 @@ final class DrawingMetalView: MTKView {
|
||||
}
|
||||
|
||||
enum BrushType {
|
||||
case pen
|
||||
case marker
|
||||
case pencil
|
||||
}
|
||||
|
||||
func updated(_ point: Polyline.Point, state: DrawingGesturePipeline.DrawingGestureState, brush: BrushType, color: DrawingColor, size: CGFloat) {
|
||||
switch brush {
|
||||
case .pen:
|
||||
self.penBrush?.updated(point, color: color, state: state, size: size)
|
||||
case .marker:
|
||||
self.markerBrush?.updated(point, color: color, state: state, size: size)
|
||||
case .pencil:
|
||||
self.pencilBrush?.updated(point, color: color, state: state, size: size)
|
||||
}
|
||||
}
|
||||
|
||||
func setup(_ points: [CGPoint], brush: BrushType, color: DrawingColor, size: CGFloat) {
|
||||
switch brush {
|
||||
case .pen:
|
||||
self.penBrush?.setup(points, color: color, size: size)
|
||||
case .marker:
|
||||
self.markerBrush?.setup(points, color: color, size: size)
|
||||
case .pencil:
|
||||
self.pencilBrush?.setup(points, color: color, size: size)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -353,7 +329,6 @@ private class Brush {
|
||||
target.prepareForDraw()
|
||||
|
||||
let commandEncoder = target.makeCommandEncoder()
|
||||
|
||||
commandEncoder?.setRenderPipelineState(self.pipelineState)
|
||||
|
||||
if let vertex_buffer = stroke.preparedBuffer(rotation: self.rotation) {
|
||||
|
@ -17,21 +17,50 @@ import ViewControllerComponent
|
||||
import ContextUI
|
||||
import ChatEntityKeyboardInputNode
|
||||
import EntityKeyboard
|
||||
import TelegramUIPreferences
|
||||
|
||||
enum DrawingToolState: Equatable {
|
||||
enum Key: CaseIterable {
|
||||
case pen
|
||||
case arrow
|
||||
case marker
|
||||
case neon
|
||||
case eraser
|
||||
case blur
|
||||
enum DrawingToolState: Equatable, Codable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case type
|
||||
case brushState
|
||||
case eraserState
|
||||
}
|
||||
|
||||
struct BrushState: Equatable {
|
||||
enum Key: Int32, RawRepresentable, CaseIterable, Codable {
|
||||
case pen = 0
|
||||
case arrow = 1
|
||||
case marker = 2
|
||||
case neon = 3
|
||||
case eraser = 4
|
||||
case blur = 5
|
||||
}
|
||||
|
||||
struct BrushState: Equatable, Codable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case color
|
||||
case size
|
||||
}
|
||||
|
||||
let color: DrawingColor
|
||||
let size: CGFloat
|
||||
|
||||
init(color: DrawingColor, size: CGFloat) {
|
||||
self.color = color
|
||||
self.size = size
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self.color = try container.decode(DrawingColor.self, forKey: .color)
|
||||
self.size = try container.decode(CGFloat.self, forKey: .size)
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(self.color, forKey: .color)
|
||||
try container.encode(self.size, forKey: .size)
|
||||
}
|
||||
|
||||
func withUpdatedColor(_ color: DrawingColor) -> BrushState {
|
||||
return BrushState(color: color, size: self.size)
|
||||
}
|
||||
@ -41,9 +70,27 @@ enum DrawingToolState: Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
struct EraserState: Equatable {
|
||||
struct EraserState: Equatable, Codable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case size
|
||||
}
|
||||
|
||||
let size: CGFloat
|
||||
|
||||
init(size: CGFloat) {
|
||||
self.size = size
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self.size = try container.decode(CGFloat.self, forKey: .size)
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(self.size, forKey: .size)
|
||||
}
|
||||
|
||||
func withUpdatedSize(_ size: CGFloat) -> EraserState {
|
||||
return EraserState(size: size)
|
||||
}
|
||||
@ -122,6 +169,53 @@ enum DrawingToolState: Equatable {
|
||||
return .blur
|
||||
}
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
let typeValue = try container.decode(Int32.self, forKey: .type)
|
||||
if let type = DrawingToolState.Key(rawValue: typeValue) {
|
||||
switch type {
|
||||
case .pen:
|
||||
self = .pen(try container.decode(BrushState.self, forKey: .brushState))
|
||||
case .arrow:
|
||||
self = .arrow(try container.decode(BrushState.self, forKey: .brushState))
|
||||
case .marker:
|
||||
self = .marker(try container.decode(BrushState.self, forKey: .brushState))
|
||||
case .neon:
|
||||
self = .neon(try container.decode(BrushState.self, forKey: .brushState))
|
||||
case .eraser:
|
||||
self = .eraser(try container.decode(EraserState.self, forKey: .eraserState))
|
||||
case .blur:
|
||||
self = .blur(try container.decode(EraserState.self, forKey: .eraserState))
|
||||
}
|
||||
} else {
|
||||
self = .pen(BrushState(color: DrawingColor(rgb: 0x000000), size: 0.5))
|
||||
}
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
switch self {
|
||||
case let .pen(state):
|
||||
try container.encode(DrawingToolState.Key.pen.rawValue, forKey: .type)
|
||||
try container.encode(state, forKey: .brushState)
|
||||
case let .arrow(state):
|
||||
try container.encode(DrawingToolState.Key.arrow.rawValue, forKey: .type)
|
||||
try container.encode(state, forKey: .brushState)
|
||||
case let .marker(state):
|
||||
try container.encode(DrawingToolState.Key.marker.rawValue, forKey: .type)
|
||||
try container.encode(state, forKey: .brushState)
|
||||
case let .neon(state):
|
||||
try container.encode(DrawingToolState.Key.neon.rawValue, forKey: .type)
|
||||
try container.encode(state, forKey: .brushState)
|
||||
case let .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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DrawingState: Equatable {
|
||||
@ -148,6 +242,13 @@ struct DrawingState: Equatable {
|
||||
)
|
||||
}
|
||||
|
||||
func withUpdatedTools(_ tools: [DrawingToolState]) -> DrawingState {
|
||||
return DrawingState(
|
||||
selectedTool: self.selectedTool,
|
||||
tools: tools
|
||||
)
|
||||
}
|
||||
|
||||
func withUpdatedColor(_ color: DrawingColor) -> DrawingState {
|
||||
var tools = self.tools
|
||||
if let index = tools.firstIndex(where: { $0.key == self.selectedTool }) {
|
||||
@ -191,6 +292,36 @@ struct DrawingState: Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
final class DrawingSettings: Codable, Equatable {
|
||||
let tools: [DrawingToolState]
|
||||
|
||||
init(tools: [DrawingToolState]) {
|
||||
self.tools = tools
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: StringCodingKey.self)
|
||||
|
||||
if let data = try container.decodeIfPresent(Data.self, forKey: "tools"), let tools = try? JSONDecoder().decode([DrawingToolState].self, from: data) {
|
||||
self.tools = tools
|
||||
} else {
|
||||
self.tools = DrawingState.initial.tools
|
||||
}
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: StringCodingKey.self)
|
||||
|
||||
if let data = try? JSONEncoder().encode(self.tools) {
|
||||
try container.encode(data, forKey: "tools")
|
||||
}
|
||||
}
|
||||
|
||||
static func ==(lhs: DrawingSettings, rhs: DrawingSettings) -> Bool {
|
||||
return lhs.tools == rhs.tools
|
||||
}
|
||||
}
|
||||
|
||||
private final class ReferenceContentSource: ContextReferenceContentSource {
|
||||
private let sourceView: UIView
|
||||
|
||||
@ -217,6 +348,7 @@ private let toolsTag = GenericComponentViewTag()
|
||||
private let modeTag = GenericComponentViewTag()
|
||||
private let flipButtonTag = GenericComponentViewTag()
|
||||
private let fillButtonTag = GenericComponentViewTag()
|
||||
private let zoomOutButtonTag = GenericComponentViewTag()
|
||||
private let textSettingsTag = GenericComponentViewTag()
|
||||
private let sizeSliderTag = GenericComponentViewTag()
|
||||
private let color1Tag = GenericComponentViewTag()
|
||||
@ -379,7 +511,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
self.currentColor = self.drawingState.tools.first?.color ?? DrawingColor(rgb: 0xffffff)
|
||||
|
||||
self.updateToolState.invoke(self.drawingState.currentToolState)
|
||||
|
||||
|
||||
let stickerPickerInputData = self.stickerPickerInputData
|
||||
Queue.concurrentDefaultQueue().after(0.5, {
|
||||
let emojiItems = EmojiPagerContentComponent.emojiInputData(
|
||||
@ -432,6 +564,35 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
|
||||
stickerPickerInputData.set(signal)
|
||||
})
|
||||
|
||||
super.init()
|
||||
|
||||
self.loadToolState()
|
||||
}
|
||||
|
||||
func loadToolState() {
|
||||
let _ = (self.context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.drawingSettings])
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] sharedData in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if let drawingSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.drawingSettings]?.get(DrawingSettings.self) {
|
||||
strongSelf.drawingState = strongSelf.drawingState.withUpdatedTools(drawingSettings.tools)
|
||||
strongSelf.currentColor = strongSelf.drawingState.currentToolState.color ?? strongSelf.currentColor
|
||||
strongSelf.updated(transition: .immediate)
|
||||
strongSelf.updateToolState.invoke(strongSelf.drawingState.currentToolState)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func saveToolState() {
|
||||
let tools = self.drawingState.tools
|
||||
let _ = (self.context.sharedContext.accountManager.transaction { transaction -> Void in
|
||||
transaction.updateSharedData(ApplicationSpecificSharedDataKeys.drawingSettings, { _ in
|
||||
return PreferencesEntry(DrawingSettings(tools: tools))
|
||||
})
|
||||
}).start()
|
||||
}
|
||||
|
||||
private var currentToolState: DrawingToolState {
|
||||
@ -508,10 +669,12 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
}
|
||||
|
||||
func presentShapePicker(_ sourceView: UIView) {
|
||||
let strings = self.context.sharedContext.currentPresentationData.with { $0 }.strings
|
||||
|
||||
let items: [ContextMenuItem] = [
|
||||
.action(
|
||||
ContextMenuActionItem(
|
||||
text: "Rectangle",
|
||||
text: strings.Paint_Rectangle,
|
||||
icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/ShapeRectangle"), color: theme.contextMenu.primaryColor)},
|
||||
action: { [weak self] f in
|
||||
f.dismissWithResult(.default)
|
||||
@ -523,7 +686,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
),
|
||||
.action(
|
||||
ContextMenuActionItem(
|
||||
text: "Ellipse",
|
||||
text: strings.Paint_Ellipse,
|
||||
icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/ShapeEllipse"), color: theme.contextMenu.primaryColor)},
|
||||
action: { [weak self] f in
|
||||
f.dismissWithResult(.default)
|
||||
@ -535,7 +698,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
),
|
||||
.action(
|
||||
ContextMenuActionItem(
|
||||
text: "Bubble",
|
||||
text: strings.Paint_Bubble,
|
||||
icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/ShapeBubble"), color: theme.contextMenu.primaryColor)},
|
||||
action: { [weak self] f in
|
||||
f.dismissWithResult(.default)
|
||||
@ -547,7 +710,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
),
|
||||
.action(
|
||||
ContextMenuActionItem(
|
||||
text: "Star",
|
||||
text: strings.Paint_Star,
|
||||
icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/ShapeStar"), color: theme.contextMenu.primaryColor)},
|
||||
action: { [weak self] f in
|
||||
f.dismissWithResult(.default)
|
||||
@ -559,7 +722,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
),
|
||||
.action(
|
||||
ContextMenuActionItem(
|
||||
text: "Arrow",
|
||||
text: strings.Paint_Arrow,
|
||||
icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/ShapeArrow"), color: theme.contextMenu.primaryColor)},
|
||||
action: { [weak self] f in
|
||||
f.dismissWithResult(.default)
|
||||
@ -669,13 +832,23 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
let state = context.state
|
||||
let controller = environment.controller
|
||||
|
||||
let strings = environment.strings
|
||||
|
||||
let previewBrushSize = component.previewBrushSize
|
||||
let performAction = component.performAction
|
||||
component.updateState.connect { [weak state] updatedState in
|
||||
state?.updateDrawingState(updatedState)
|
||||
}
|
||||
component.updateColor.connect { [weak state] color in
|
||||
state?.updateColor(color)
|
||||
if let state = state {
|
||||
if [.eraser, .blur].contains(state.drawingState.selectedTool) || state.selectedEntity is DrawingStickerEntity {
|
||||
state.updateSelectedTool(.pen, update: false)
|
||||
state.updateColor(color, animated: true)
|
||||
} else {
|
||||
state.updateColor(color)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
component.updateSelectedEntity.connect { [weak state] entity in
|
||||
state?.updateSelectedEntity(entity)
|
||||
@ -1175,14 +1348,14 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
component: Button(
|
||||
content: AnyComponent(
|
||||
ZoomOutButtonContent(
|
||||
title: "Zoom Out",
|
||||
title: strings.Paint_ZoomOut,
|
||||
image: state.image(.zoomOut)
|
||||
)
|
||||
),
|
||||
action: {
|
||||
performAction.invoke(.zoomOut)
|
||||
}
|
||||
).minSize(CGSize(width: 44.0, height: 44.0)),
|
||||
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(zoomOutButtonTag),
|
||||
availableSize: CGSize(width: 120.0, height: 33.0),
|
||||
transition: .immediate
|
||||
)
|
||||
@ -1241,30 +1414,29 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
.opacity(isEditingText ? 0.0 : 1.0)
|
||||
)
|
||||
|
||||
if state.drawingViewState.canRedo && !isEditingText {
|
||||
let redoButton = redoButton.update(
|
||||
component: Button(
|
||||
content: AnyComponent(
|
||||
Image(image: state.image(.redo))
|
||||
),
|
||||
action: {
|
||||
performAction.invoke(.redo)
|
||||
}
|
||||
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(redoButtonTag),
|
||||
availableSize: CGSize(width: 24.0, height: 24.0),
|
||||
transition: context.transition
|
||||
)
|
||||
context.add(redoButton
|
||||
.position(CGPoint(x: environment.safeInsets.left + undoButton.size.width + 2.0 + redoButton.size.width / 2.0, y: topInset))
|
||||
.appear(.default(scale: true, alpha: true))
|
||||
.disappear(.default(scale: true, alpha: true))
|
||||
)
|
||||
}
|
||||
|
||||
let redoButton = redoButton.update(
|
||||
component: Button(
|
||||
content: AnyComponent(
|
||||
Image(image: state.image(.redo))
|
||||
),
|
||||
action: {
|
||||
performAction.invoke(.redo)
|
||||
}
|
||||
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(redoButtonTag),
|
||||
availableSize: CGSize(width: 24.0, height: 24.0),
|
||||
transition: context.transition
|
||||
)
|
||||
context.add(redoButton
|
||||
.position(CGPoint(x: environment.safeInsets.left + undoButton.size.width + 2.0 + redoButton.size.width / 2.0, y: topInset))
|
||||
.scale(state.drawingViewState.canRedo && !isEditingText ? 1.0 : 0.01)
|
||||
.opacity(state.drawingViewState.canRedo && !isEditingText ? 1.0 : 0.0)
|
||||
)
|
||||
|
||||
let clearAllButton = clearAllButton.update(
|
||||
component: Button(
|
||||
content: AnyComponent(
|
||||
Text(text: "Clear All", font: Font.regular(17.0), color: .white)
|
||||
Text(text: strings.Paint_Clear, font: Font.regular(17.0), color: .white)
|
||||
),
|
||||
isEnabled: state.drawingViewState.canClear,
|
||||
action: {
|
||||
@ -1327,6 +1499,8 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
color = nil
|
||||
} else if state.selectedEntity is DrawingStickerEntity {
|
||||
color = nil
|
||||
} else if [.eraser, .blur].contains(state.drawingState.selectedTool) {
|
||||
color = nil
|
||||
} else {
|
||||
color = state.currentColor
|
||||
}
|
||||
@ -1413,7 +1587,8 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
content: AnyComponent(
|
||||
Image(image: state.image(.done))
|
||||
),
|
||||
action: {
|
||||
action: { [weak state] in
|
||||
state?.saveToolState()
|
||||
apply.invoke(Void())
|
||||
}
|
||||
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(doneButtonTag),
|
||||
@ -1456,7 +1631,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
|
||||
let modeAndSize = modeAndSize.update(
|
||||
component: ModeAndSizeComponent(
|
||||
values: ["Draw", "Sticker", "Text"],
|
||||
values: [ strings.Paint_Draw, strings.Paint_Sticker, strings.Paint_Text],
|
||||
sizeValue: selectedSize,
|
||||
isEditing: false,
|
||||
isEnabled: true,
|
||||
@ -1500,7 +1675,6 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
animatingOut = true
|
||||
}
|
||||
|
||||
let deselectEntity = component.deselectEntity
|
||||
let backButton = backButton.update(
|
||||
component: Button(
|
||||
content: AnyComponent(
|
||||
@ -1516,11 +1690,8 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
),
|
||||
action: { [weak state] in
|
||||
if let state = state {
|
||||
if let selectedEntity = state.selectedEntity, !(selectedEntity is DrawingStickerEntity || selectedEntity is DrawingTextEntity) {
|
||||
deselectEntity.invoke(Void())
|
||||
} else {
|
||||
dismiss.invoke(Void())
|
||||
}
|
||||
state.saveToolState()
|
||||
dismiss.invoke(Void())
|
||||
}
|
||||
}
|
||||
).minSize(CGSize(width: 44.0, height: 44.0)),
|
||||
@ -1559,7 +1730,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
||||
|
||||
private var presentationData: PresentationData
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
private var validLayout: ContainerViewLayout?
|
||||
private var validLayout: (ContainerViewLayout, UIInterfaceOrientation?)?
|
||||
|
||||
private var _drawingView: DrawingView?
|
||||
var drawingView: DrawingView {
|
||||
@ -1655,6 +1826,12 @@ 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?.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
|
||||
if let strongSelf = self, let controller = strongSelf.controller, let currentImage = controller.getCurrentImage() {
|
||||
@ -1699,7 +1876,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
||||
var actions: [ContextMenuAction] = []
|
||||
actions.append(ContextMenuAction(content: .text(title: strongSelf.presentationData.strings.Paint_Delete, accessibilityLabel: strongSelf.presentationData.strings.Paint_Delete), action: { [weak self, weak entityView] in
|
||||
if let strongSelf = self, let entityView = entityView {
|
||||
strongSelf.entitiesView.remove(uuid: entityView.entity.uuid)
|
||||
strongSelf.entitiesView.remove(uuid: entityView.entity.uuid, animated: true)
|
||||
}
|
||||
}))
|
||||
if let entityView = entityView as? DrawingTextEntityView {
|
||||
@ -1711,7 +1888,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
||||
}))
|
||||
}
|
||||
if !isTopmost {
|
||||
actions.append(ContextMenuAction(content: .text(title: "Move Forward", accessibilityLabel: "Move Forward"), action: { [weak self, weak entityView] in
|
||||
actions.append(ContextMenuAction(content: .text(title: strongSelf.presentationData.strings.Paint_MoveForward, accessibilityLabel: strongSelf.presentationData.strings.Paint_MoveForward), action: { [weak self, weak entityView] in
|
||||
if let strongSelf = self, let entityView = entityView {
|
||||
strongSelf.entitiesView.bringToFront(uuid: entityView.entity.uuid)
|
||||
}
|
||||
@ -1975,8 +2152,8 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
||||
}
|
||||
|
||||
func animateOut(completion: @escaping () -> Void) {
|
||||
if let layout = self.validLayout {
|
||||
self.containerLayoutUpdated(layout: layout, animateOut: true, transition: .easeInOut(duration: 0.2))
|
||||
if let (layout, orientation) = self.validLayout {
|
||||
self.containerLayoutUpdated(layout: layout, orientation: orientation, animateOut: true, transition: .easeInOut(duration: 0.2))
|
||||
}
|
||||
|
||||
if let buttonView = self.componentHost.findTaggedView(tag: undoButtonTag) {
|
||||
@ -2012,6 +2189,11 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
||||
buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
|
||||
buttonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3)
|
||||
}
|
||||
if let buttonView = self.componentHost.findTaggedView(tag: zoomOutButtonTag) {
|
||||
buttonView.alpha = 0.0
|
||||
buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
|
||||
buttonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3)
|
||||
}
|
||||
if let view = self.componentHost.findTaggedView(tag: sizeSliderTag) {
|
||||
view.layer.animatePosition(from: CGPoint(), to: CGPoint(x: -33.0, y: 0.0), duration: 0.3, removeOnCompletion: false, additive: true)
|
||||
}
|
||||
@ -2052,12 +2234,12 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
||||
return result
|
||||
}
|
||||
|
||||
func containerLayoutUpdated(layout: ContainerViewLayout, animateOut: Bool = false, transition: Transition) {
|
||||
func containerLayoutUpdated(layout: ContainerViewLayout, orientation: UIInterfaceOrientation?, animateOut: Bool = false, transition: Transition) {
|
||||
guard let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
let isFirstTime = self.validLayout == nil
|
||||
self.validLayout = layout
|
||||
self.validLayout = (layout, orientation)
|
||||
|
||||
let environment = ViewControllerComponentContainer.Environment(
|
||||
statusBarHeight: layout.statusBarHeight ?? 0.0,
|
||||
@ -2071,6 +2253,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
||||
inputHeight: layout.inputHeight ?? 0.0,
|
||||
metrics: layout.metrics,
|
||||
deviceMetrics: layout.deviceMetrics,
|
||||
orientation: orientation,
|
||||
isVisible: true,
|
||||
theme: self.presentationData.theme,
|
||||
strings: self.presentationData.strings,
|
||||
@ -2195,8 +2378,8 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
||||
textEntity.style = nextStyle
|
||||
entityView.update()
|
||||
|
||||
if let layout = strongSelf.validLayout {
|
||||
strongSelf.containerLayoutUpdated(layout: layout, transition: .immediate)
|
||||
if let (layout, orientation) = strongSelf.validLayout {
|
||||
strongSelf.containerLayoutUpdated(layout: layout, orientation: orientation, transition: .immediate)
|
||||
}
|
||||
},
|
||||
toggleAlignment: { [weak self] in
|
||||
@ -2215,8 +2398,8 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
||||
textEntity.alignment = nextAlignment
|
||||
entityView.update()
|
||||
|
||||
if let layout = strongSelf.validLayout {
|
||||
strongSelf.containerLayoutUpdated(layout: layout, transition: .immediate)
|
||||
if let (layout, orientation) = strongSelf.validLayout {
|
||||
strongSelf.containerLayoutUpdated(layout: layout, orientation: orientation, transition: .immediate)
|
||||
}
|
||||
},
|
||||
updateFont: { [weak self] font in
|
||||
@ -2226,8 +2409,8 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
||||
textEntity.font = font.font
|
||||
entityView.update()
|
||||
|
||||
if let layout = strongSelf.validLayout {
|
||||
strongSelf.containerLayoutUpdated(layout: layout, transition: .immediate)
|
||||
if let (layout, orientation) = strongSelf.validLayout {
|
||||
strongSelf.containerLayoutUpdated(layout: layout, orientation: orientation, transition: .immediate)
|
||||
}
|
||||
},
|
||||
toggleKeyboard: { [weak self] in
|
||||
@ -2300,8 +2483,8 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
||||
textView.becomeFirstResponder()
|
||||
}
|
||||
|
||||
if let layout = self.validLayout {
|
||||
self.containerLayoutUpdated(layout: layout, animateOut: false, transition: .immediate)
|
||||
if let (layout, orientation) = self.validLayout {
|
||||
self.containerLayoutUpdated(layout: layout, orientation: orientation, animateOut: false, transition: .immediate)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2328,6 +2511,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
||||
super.init(navigationBarPresentationData: nil)
|
||||
|
||||
self.statusBar.statusBarStyle = .Hide
|
||||
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
|
||||
}
|
||||
|
||||
public var drawingView: DrawingView {
|
||||
@ -2350,10 +2534,6 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
deinit {
|
||||
print()
|
||||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
self.displayNode = Node(controller: self, context: self.context)
|
||||
|
||||
@ -2404,7 +2584,26 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
||||
image = finalImage
|
||||
}
|
||||
|
||||
return TGPaintingData(drawing: nil, entitiesData: self.entitiesView.entitiesData, image: image, stillImage: stillImage, hasAnimation: hasAnimatedEntities)
|
||||
let drawingData = self.drawingView.drawingData
|
||||
|
||||
let entitiesData = self.entitiesView.entitiesData
|
||||
|
||||
var stickers: [Any] = []
|
||||
for entity in self.entitiesView.entities {
|
||||
if let sticker = entity as? DrawingStickerEntity {
|
||||
let coder = PostboxEncoder()
|
||||
coder.encodeRootObject(sticker.file)
|
||||
stickers.append(coder.makeData())
|
||||
} else if let text = entity as? DrawingTextEntity, let subEntities = text.renderSubEntities {
|
||||
for sticker in subEntities {
|
||||
let coder = PostboxEncoder()
|
||||
coder.encodeRootObject(sticker.file)
|
||||
stickers.append(coder.makeData())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return TGPaintingData(drawing: drawingData, entitiesData: entitiesData, image: image, stillImage: stillImage, hasAnimation: hasAnimatedEntities, stickers: stickers)
|
||||
}
|
||||
|
||||
public func resultImage() -> UIImage! {
|
||||
@ -2428,13 +2627,14 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
||||
self.node.animateOut(completion: completion)
|
||||
}
|
||||
|
||||
private var orientation: UIInterfaceOrientation?
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
(self.displayNode as! Node).containerLayoutUpdated(layout: layout, transition: Transition(transition))
|
||||
(self.displayNode as! Node).containerLayoutUpdated(layout: layout, orientation: orientation, transition: Transition(transition))
|
||||
}
|
||||
|
||||
public func adapterContainerLayoutUpdatedSize(_ size: CGSize, intrinsicInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, statusBarHeight: CGFloat, inputHeight: CGFloat, animated: Bool) {
|
||||
public func adapterContainerLayoutUpdatedSize(_ size: CGSize, intrinsicInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, statusBarHeight: CGFloat, inputHeight: CGFloat, orientation: UIInterfaceOrientation, animated: Bool) {
|
||||
let layout = ContainerViewLayout(
|
||||
size: size,
|
||||
metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact),
|
||||
@ -2447,6 +2647,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
||||
inputHeightIsInteractivellyChanging: false,
|
||||
inVoiceOver: false
|
||||
)
|
||||
self.orientation = orientation
|
||||
self.containerLayoutUpdated(layout, transition: animated ? .animated(duration: 0.3, curve: .easeInOut) : .immediate)
|
||||
}
|
||||
}
|
||||
|
@ -185,6 +185,11 @@ final class DrawingSimpleShapeEntityView: DrawingEntityView {
|
||||
return max(10.0, max(self.shapeEntity.referenceDrawingSize.width, self.shapeEntity.referenceDrawingSize.height) * 0.05)
|
||||
}
|
||||
|
||||
fileprivate var minimumSize: CGSize {
|
||||
let minSize = min(self.shapeEntity.referenceDrawingSize.width, self.shapeEntity.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)
|
||||
@ -291,6 +296,18 @@ final class DrawingSimpleShapeEntititySelectionView: DrawingEntitySelectionView,
|
||||
panGestureRecognizer.delegate = self
|
||||
self.addGestureRecognizer(panGestureRecognizer)
|
||||
self.panGestureRecognizer = panGestureRecognizer
|
||||
|
||||
self.snapTool.onSnapXUpdated = { [weak self] snapped in
|
||||
if let strongSelf = self, let entityView = strongSelf.entityView {
|
||||
entityView.onSnapToXAxis(snapped)
|
||||
}
|
||||
}
|
||||
|
||||
self.snapTool.onSnapYUpdated = { [weak self] snapped in
|
||||
if let strongSelf = self, let entityView = strongSelf.entityView {
|
||||
entityView.onSnapToXAxis(snapped)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
@ -311,9 +328,11 @@ final class DrawingSimpleShapeEntititySelectionView: DrawingEntitySelectionView,
|
||||
return true
|
||||
}
|
||||
|
||||
private let snapTool = DrawingEntitySnapTool()
|
||||
|
||||
private var currentHandle: CALayer?
|
||||
@objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
|
||||
guard let entityView = self.entityView, let entity = entityView.entity as? DrawingSimpleShapeEntity else {
|
||||
guard let entityView = self.entityView as? DrawingSimpleShapeEntityView, let entity = entityView.entity as? DrawingSimpleShapeEntity else {
|
||||
return
|
||||
}
|
||||
let isAspectLocked = [.star].contains(entity.shapeType)
|
||||
@ -321,6 +340,8 @@ final class DrawingSimpleShapeEntititySelectionView: DrawingEntitySelectionView,
|
||||
|
||||
switch gestureRecognizer.state {
|
||||
case .began:
|
||||
self.snapTool.maybeSkipFromStart(entityView: entityView, position: entity.position)
|
||||
|
||||
if let sublayers = self.layer.sublayers {
|
||||
for layer in sublayers {
|
||||
if layer.frame.contains(location) {
|
||||
@ -332,50 +353,54 @@ final class DrawingSimpleShapeEntititySelectionView: DrawingEntitySelectionView,
|
||||
self.currentHandle = self.layer
|
||||
case .changed:
|
||||
let delta = gestureRecognizer.translation(in: entityView.superview)
|
||||
let velocity = gestureRecognizer.velocity(in: entityView.superview)
|
||||
|
||||
var updatedSize = entity.size
|
||||
var updatedPosition = entity.position
|
||||
|
||||
let minimumSize = entityView.minimumSize
|
||||
|
||||
if self.currentHandle === self.leftHandle {
|
||||
let deltaX = delta.x * cos(entity.rotation)
|
||||
let deltaY = delta.x * sin(entity.rotation)
|
||||
|
||||
updatedSize.width -= deltaX
|
||||
|
||||
updatedSize.width = max(minimumSize.width, updatedSize.width - deltaX)
|
||||
updatedPosition.x -= deltaX * -0.5
|
||||
updatedPosition.y -= deltaY * -0.5
|
||||
|
||||
if isAspectLocked {
|
||||
updatedSize.height -= delta.x
|
||||
updatedSize.height = updatedSize.width
|
||||
}
|
||||
} else if self.currentHandle === self.rightHandle {
|
||||
let deltaX = delta.x * cos(entity.rotation)
|
||||
let deltaY = delta.x * sin(entity.rotation)
|
||||
|
||||
updatedSize.width += deltaX
|
||||
updatedSize.width = max(minimumSize.width, updatedSize.width + deltaX)
|
||||
print(updatedSize.width)
|
||||
updatedPosition.x += deltaX * 0.5
|
||||
updatedPosition.y += deltaY * 0.5
|
||||
if isAspectLocked {
|
||||
updatedSize.height += delta.x
|
||||
updatedSize.height = updatedSize.width
|
||||
}
|
||||
} else if self.currentHandle === self.topHandle {
|
||||
let deltaX = delta.y * sin(entity.rotation)
|
||||
let deltaY = delta.y * cos(entity.rotation)
|
||||
|
||||
updatedSize.height -= deltaY
|
||||
updatedSize.height = max(minimumSize.height, updatedSize.height - deltaY)
|
||||
updatedPosition.x += deltaX * 0.5
|
||||
updatedPosition.y += deltaY * 0.5
|
||||
if isAspectLocked {
|
||||
updatedSize.width -= delta.y
|
||||
updatedSize.width = updatedSize.height
|
||||
}
|
||||
} else if self.currentHandle === self.bottomHandle {
|
||||
let deltaX = delta.y * sin(entity.rotation)
|
||||
let deltaY = delta.y * cos(entity.rotation)
|
||||
|
||||
updatedSize.height += deltaY
|
||||
updatedSize.height = max(minimumSize.height, updatedSize.height + deltaY)
|
||||
updatedPosition.x += deltaX * 0.5
|
||||
updatedPosition.y += deltaY * 0.5
|
||||
if isAspectLocked {
|
||||
updatedSize.width += delta.y
|
||||
updatedSize.width = updatedSize.height
|
||||
}
|
||||
} else if self.currentHandle === self.topLeftHandle {
|
||||
var delta = delta
|
||||
@ -416,15 +441,19 @@ final class DrawingSimpleShapeEntititySelectionView: DrawingEntitySelectionView,
|
||||
} else if self.currentHandle === self.layer {
|
||||
updatedPosition.x += delta.x
|
||||
updatedPosition.y += delta.y
|
||||
|
||||
updatedPosition = self.snapTool.update(entityView: entityView, velocity: velocity, delta: delta, updatedPosition: updatedPosition)
|
||||
}
|
||||
|
||||
entity.size = updatedSize
|
||||
entity.position = updatedPosition
|
||||
entityView.update()
|
||||
entityView.update(animated: false)
|
||||
|
||||
gestureRecognizer.setTranslation(.zero, in: entityView)
|
||||
case .ended:
|
||||
break
|
||||
self.snapTool.reset()
|
||||
case .cancelled:
|
||||
self.snapTool.reset()
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
@ -126,7 +126,7 @@ final class DrawingStickerEntityView: DrawingEntityView {
|
||||
super.init(context: context, entity: entity)
|
||||
|
||||
self.addSubview(self.imageNode.view)
|
||||
|
||||
|
||||
self.setup()
|
||||
}
|
||||
|
||||
@ -340,6 +340,10 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView, UIG
|
||||
self.border.lineCap = .round
|
||||
self.border.fillColor = UIColor.clear.cgColor
|
||||
self.border.strokeColor = UIColor(rgb: 0xffffff, alpha: 0.5).cgColor
|
||||
self.border.shadowColor = UIColor.black.cgColor
|
||||
self.border.shadowRadius = 1.0
|
||||
self.border.shadowOpacity = 0.5
|
||||
self.border.shadowOffset = CGSize()
|
||||
self.layer.addSublayer(self.border)
|
||||
|
||||
for handle in handles {
|
||||
@ -356,6 +360,18 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView, UIG
|
||||
panGestureRecognizer.delegate = self
|
||||
self.addGestureRecognizer(panGestureRecognizer)
|
||||
self.panGestureRecognizer = panGestureRecognizer
|
||||
|
||||
self.snapTool.onSnapXUpdated = { [weak self] snapped in
|
||||
if let strongSelf = self, let entityView = strongSelf.entityView {
|
||||
entityView.onSnapToXAxis(snapped)
|
||||
}
|
||||
}
|
||||
|
||||
self.snapTool.onSnapYUpdated = { [weak self] snapped in
|
||||
if let strongSelf = self, let entityView = strongSelf.entityView {
|
||||
entityView.onSnapToYAxis(snapped)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
@ -376,6 +392,8 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView, UIG
|
||||
return true
|
||||
}
|
||||
|
||||
private let snapTool = DrawingEntitySnapTool()
|
||||
|
||||
private var currentHandle: CALayer?
|
||||
@objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
|
||||
guard let entityView = self.entityView, let entity = entityView.entity as? DrawingStickerEntity else {
|
||||
@ -383,8 +401,11 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView, UIG
|
||||
}
|
||||
let location = gestureRecognizer.location(in: self)
|
||||
|
||||
|
||||
switch gestureRecognizer.state {
|
||||
case .began:
|
||||
self.snapTool.maybeSkipFromStart(entityView: entityView, position: entity.position)
|
||||
|
||||
if let sublayers = self.layer.sublayers {
|
||||
for layer in sublayers {
|
||||
if layer.frame.contains(location) {
|
||||
@ -397,7 +418,8 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView, UIG
|
||||
case .changed:
|
||||
let delta = gestureRecognizer.translation(in: entityView.superview)
|
||||
let parentLocation = gestureRecognizer.location(in: self.superview)
|
||||
|
||||
let velocity = gestureRecognizer.velocity(in: entityView.superview)
|
||||
|
||||
var updatedPosition = entity.position
|
||||
var updatedScale = entity.scale
|
||||
var updatedRotation = entity.rotation
|
||||
@ -419,6 +441,8 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView, UIG
|
||||
} else if self.currentHandle === self.layer {
|
||||
updatedPosition.x += delta.x
|
||||
updatedPosition.y += delta.y
|
||||
|
||||
updatedPosition = self.snapTool.update(entityView: entityView, velocity: velocity, delta: delta, updatedPosition: updatedPosition)
|
||||
}
|
||||
|
||||
entity.position = updatedPosition
|
||||
@ -428,7 +452,9 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView, UIG
|
||||
|
||||
gestureRecognizer.setTranslation(.zero, in: entityView)
|
||||
case .ended:
|
||||
break
|
||||
self.snapTool.reset()
|
||||
case .cancelled:
|
||||
self.snapTool.reset()
|
||||
default:
|
||||
break
|
||||
}
|
||||
@ -494,8 +520,124 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView, UIG
|
||||
self.leftHandle.position = CGPoint(x: inset, y: self.bounds.midY)
|
||||
self.rightHandle.position = CGPoint(x: self.bounds.maxX - inset, y: self.bounds.midY)
|
||||
|
||||
self.border.lineDashPattern = [12.0 / self.scale as NSNumber, 12.0 / self.scale as NSNumber]
|
||||
|
||||
let radius = (self.bounds.width - inset * 2.0) / 2.0
|
||||
let circumference: CGFloat = 2.0 * .pi * radius
|
||||
let count = 10
|
||||
let relativeDashLength: CGFloat = 0.25
|
||||
let dashLength = circumference / CGFloat(count)
|
||||
self.border.lineDashPattern = [dashLength * relativeDashLength, dashLength * relativeDashLength] as [NSNumber]
|
||||
|
||||
self.border.lineWidth = 2.0 / self.scale
|
||||
self.border.path = UIBezierPath(ovalIn: CGRect(origin: CGPoint(x: inset, y: inset), size: CGSize(width: self.bounds.width - inset * 2.0, height: self.bounds.height - inset * 2.0))).cgPath
|
||||
}
|
||||
}
|
||||
|
||||
class DrawingEntitySnapTool {
|
||||
private var xState: (skipped: CGFloat, waitForLeave: Bool)?
|
||||
private var yState: (skipped: CGFloat, waitForLeave: Bool)?
|
||||
|
||||
var onSnapXUpdated: (Bool) -> Void = { _ in }
|
||||
var onSnapYUpdated: (Bool) -> Void = { _ in }
|
||||
|
||||
func reset() {
|
||||
self.xState = nil
|
||||
self.yState = nil
|
||||
|
||||
self.onSnapXUpdated(false)
|
||||
self.onSnapYUpdated(false)
|
||||
}
|
||||
|
||||
func maybeSkipFromStart(entityView: DrawingEntityView, position: CGPoint) {
|
||||
self.xState = nil
|
||||
self.yState = nil
|
||||
|
||||
let snapXDelta: CGFloat = (entityView.superview?.frame.width ?? 0.0) * 0.02
|
||||
let snapYDelta: CGFloat = (entityView.superview?.frame.width ?? 0.0) * 0.02
|
||||
|
||||
if let snapLocation = (entityView.superview as? DrawingEntitiesView)?.getEntityCenterPosition() {
|
||||
if position.x > snapLocation.x - snapXDelta && position.x < snapLocation.x + snapXDelta {
|
||||
self.xState = (0.0, true)
|
||||
}
|
||||
|
||||
if position.y > snapLocation.y - snapYDelta && position.y < snapLocation.y + snapYDelta {
|
||||
self.yState = (0.0, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func update(entityView: DrawingEntityView, velocity: CGPoint, delta: CGPoint, updatedPosition: CGPoint) -> CGPoint {
|
||||
var updatedPosition = updatedPosition
|
||||
|
||||
let snapXDelta: CGFloat = (entityView.superview?.frame.width ?? 0.0) * 0.02
|
||||
let snapXVelocity: CGFloat = snapXDelta * 16.0
|
||||
let snapXSkipTranslation: CGFloat = snapXDelta * 2.0
|
||||
|
||||
if abs(velocity.x) < snapXVelocity || self.xState?.waitForLeave == true {
|
||||
if let snapLocation = (entityView.superview as? DrawingEntitiesView)?.getEntityCenterPosition() {
|
||||
if let (skipped, waitForLeave) = self.xState {
|
||||
if waitForLeave {
|
||||
if updatedPosition.x > snapLocation.x - snapXDelta * 1.5 && updatedPosition.x < snapLocation.x + snapXDelta * 1.5 {
|
||||
|
||||
} else {
|
||||
self.xState = nil
|
||||
}
|
||||
} else if abs(skipped) < snapXSkipTranslation {
|
||||
self.xState = (skipped + delta.x, false)
|
||||
updatedPosition.x = snapLocation.x
|
||||
} else {
|
||||
self.xState = (snapXSkipTranslation, true)
|
||||
self.onSnapXUpdated(false)
|
||||
}
|
||||
} else {
|
||||
if updatedPosition.x > snapLocation.x - snapXDelta && updatedPosition.x < snapLocation.x + snapXDelta {
|
||||
self.xState = (0.0, false)
|
||||
updatedPosition.x = snapLocation.x
|
||||
self.onSnapXUpdated(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if self.xState != nil {
|
||||
print()
|
||||
}
|
||||
self.xState = nil
|
||||
self.onSnapXUpdated(false)
|
||||
}
|
||||
|
||||
let snapYDelta: CGFloat = (entityView.superview?.frame.width ?? 0.0) * 0.02
|
||||
let snapYVelocity: CGFloat = snapYDelta * 16.0
|
||||
let snapYSkipTranslation: CGFloat = snapYDelta * 2.0
|
||||
|
||||
if abs(velocity.y) < snapYVelocity || self.yState?.waitForLeave == true {
|
||||
if let snapLocation = (entityView.superview as? DrawingEntitiesView)?.getEntityCenterPosition() {
|
||||
if let (skipped, waitForLeave) = self.yState {
|
||||
if waitForLeave {
|
||||
if updatedPosition.y > snapLocation.y - snapYDelta * 1.5 && updatedPosition.y < snapLocation.y + snapYDelta * 1.5 {
|
||||
|
||||
} else {
|
||||
self.yState = nil
|
||||
}
|
||||
} else if abs(skipped) < snapYSkipTranslation {
|
||||
self.yState = (skipped + delta.y, false)
|
||||
updatedPosition.y = snapLocation.y
|
||||
} else {
|
||||
self.yState = (snapYSkipTranslation, true)
|
||||
self.onSnapYUpdated(false)
|
||||
}
|
||||
} else {
|
||||
if updatedPosition.y > snapLocation.y - snapYDelta && updatedPosition.y < snapLocation.y + snapYDelta {
|
||||
self.yState = (0.0, false)
|
||||
updatedPosition.y = snapLocation.y
|
||||
self.onSnapYUpdated(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.yState = nil
|
||||
self.onSnapYUpdated(false)
|
||||
}
|
||||
|
||||
return updatedPosition
|
||||
}
|
||||
}
|
||||
|
@ -81,19 +81,7 @@ public final class DrawingTextEntity: DrawingEntity, Codable {
|
||||
case newYork
|
||||
case monospaced
|
||||
case round
|
||||
|
||||
init(font: DrawingTextEntity.Font) {
|
||||
switch font {
|
||||
case .sanFrancisco:
|
||||
self = .sanFrancisco
|
||||
case .newYork:
|
||||
self = .newYork
|
||||
case .monospaced:
|
||||
self = .monospaced
|
||||
case .round:
|
||||
self = .round
|
||||
}
|
||||
}
|
||||
case custom(String, String)
|
||||
}
|
||||
|
||||
enum Alignment: Codable {
|
||||
@ -101,17 +89,6 @@ public final class DrawingTextEntity: DrawingEntity, Codable {
|
||||
case center
|
||||
case right
|
||||
|
||||
init(font: DrawingTextEntity.Alignment) {
|
||||
switch font {
|
||||
case .left:
|
||||
self = .left
|
||||
case .center:
|
||||
self = .center
|
||||
case .right:
|
||||
self = .right
|
||||
}
|
||||
}
|
||||
|
||||
var alignment: NSTextAlignment {
|
||||
switch self {
|
||||
case .left:
|
||||
@ -567,7 +544,11 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate {
|
||||
|
||||
self.textView.drawingLayoutManager.textContainers.first?.lineFragmentPadding = floor(fontSize * 0.24)
|
||||
|
||||
let font: UIFont
|
||||
if let (font, name) = availableFonts[text.string.lowercased()] {
|
||||
self.textEntity.font = .custom(font, name)
|
||||
}
|
||||
|
||||
var font: UIFont
|
||||
switch self.textEntity.font {
|
||||
case .sanFrancisco:
|
||||
font = Font.with(size: fontSize, design: .regular, weight: .semibold)
|
||||
@ -577,7 +558,10 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate {
|
||||
font = Font.with(size: fontSize, design: .monospace, weight: .semibold)
|
||||
case .round:
|
||||
font = Font.with(size: fontSize, design: .round, weight: .semibold)
|
||||
case let .custom(fontName, _):
|
||||
font = UIFont(name: fontName, size: fontSize) ?? Font.with(size: fontSize, design: .regular, weight: .semibold)
|
||||
}
|
||||
|
||||
text.addAttribute(.font, value: font, range: range)
|
||||
self.textView.font = font
|
||||
|
||||
@ -761,6 +745,18 @@ final class DrawingTextEntititySelectionView: DrawingEntitySelectionView, UIGest
|
||||
panGestureRecognizer.delegate = self
|
||||
self.addGestureRecognizer(panGestureRecognizer)
|
||||
self.panGestureRecognizer = panGestureRecognizer
|
||||
|
||||
self.snapTool.onSnapXUpdated = { [weak self] snapped in
|
||||
if let strongSelf = self, let entityView = strongSelf.entityView {
|
||||
entityView.onSnapToXAxis(snapped)
|
||||
}
|
||||
}
|
||||
|
||||
self.snapTool.onSnapYUpdated = { [weak self] snapped in
|
||||
if let strongSelf = self, let entityView = strongSelf.entityView {
|
||||
entityView.onSnapToXAxis(snapped)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
@ -784,16 +780,19 @@ final class DrawingTextEntititySelectionView: DrawingEntitySelectionView, UIGest
|
||||
return true
|
||||
}
|
||||
|
||||
private let snapTool = DrawingEntitySnapTool()
|
||||
|
||||
private var currentHandle: CALayer?
|
||||
@objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
|
||||
guard let entityView = self.entityView, let entity = entityView.entity as? DrawingTextEntity else {
|
||||
return
|
||||
}
|
||||
|
||||
let location = gestureRecognizer.location(in: self)
|
||||
|
||||
switch gestureRecognizer.state {
|
||||
case .began:
|
||||
self.snapTool.maybeSkipFromStart(entityView: entityView, position: entity.position)
|
||||
|
||||
if let sublayers = self.layer.sublayers {
|
||||
for layer in sublayers {
|
||||
if layer.frame.contains(location) {
|
||||
@ -806,6 +805,7 @@ final class DrawingTextEntititySelectionView: DrawingEntitySelectionView, UIGest
|
||||
case .changed:
|
||||
let delta = gestureRecognizer.translation(in: entityView.superview)
|
||||
let parentLocation = gestureRecognizer.location(in: self.superview)
|
||||
let velocity = gestureRecognizer.velocity(in: entityView.superview)
|
||||
|
||||
var updatedScale = entity.scale
|
||||
var updatedPosition = entity.position
|
||||
@ -829,6 +829,8 @@ final class DrawingTextEntititySelectionView: DrawingEntitySelectionView, UIGest
|
||||
} else if self.currentHandle === self.layer {
|
||||
updatedPosition.x += delta.x
|
||||
updatedPosition.y += delta.y
|
||||
|
||||
updatedPosition = self.snapTool.update(entityView: entityView, velocity: velocity, delta: delta, updatedPosition: updatedPosition)
|
||||
}
|
||||
|
||||
entity.scale = updatedScale
|
||||
@ -838,7 +840,9 @@ final class DrawingTextEntititySelectionView: DrawingEntitySelectionView, UIGest
|
||||
|
||||
gestureRecognizer.setTranslation(.zero, in: entityView)
|
||||
case .ended:
|
||||
break
|
||||
self.snapTool.reset()
|
||||
case .cancelled:
|
||||
self.snapTool.reset()
|
||||
default:
|
||||
break
|
||||
}
|
||||
@ -903,10 +907,19 @@ final class DrawingTextEntititySelectionView: DrawingEntitySelectionView, UIGest
|
||||
|
||||
self.leftHandle.position = CGPoint(x: inset, y: self.bounds.midY)
|
||||
self.rightHandle.position = CGPoint(x: self.bounds.maxX - inset, y: self.bounds.midY)
|
||||
|
||||
let width: CGFloat = self.bounds.width - inset * 2.0
|
||||
let height: CGFloat = self.bounds.height - inset * 2.0
|
||||
let cornerRadius: CGFloat = 12.0 - self.scale
|
||||
|
||||
let perimeter: CGFloat = 2.0 * (width + height - cornerRadius * (4.0 - .pi))
|
||||
let count = 12
|
||||
let relativeDashLength: CGFloat = 0.25
|
||||
let dashLength = perimeter / CGFloat(count)
|
||||
self.border.lineDashPattern = [dashLength * relativeDashLength, dashLength * relativeDashLength] as [NSNumber]
|
||||
|
||||
self.border.lineDashPattern = [12.0 / self.scale as NSNumber, 12.0 / self.scale as NSNumber]
|
||||
self.border.lineWidth = 2.0 / self.scale
|
||||
self.border.path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: inset, y: inset), size: CGSize(width: self.bounds.width - inset * 2.0, height: self.bounds.height - inset * 2.0)), cornerRadius: 12.0 / self.scale).cgPath
|
||||
self.border.path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: inset, y: inset), size: CGSize(width: width, height: height)), cornerRadius: cornerRadius).cgPath
|
||||
}
|
||||
}
|
||||
|
||||
@ -1234,3 +1247,31 @@ class DrawingTextView: UITextView {
|
||||
self.typingAttributes = attributes
|
||||
}
|
||||
}
|
||||
|
||||
private var availableFonts: [String: (String, String)] = {
|
||||
let familyNames = UIFont.familyNames
|
||||
var result: [String: (String, String)] = [:]
|
||||
|
||||
for family in familyNames {
|
||||
let names = UIFont.fontNames(forFamilyName: family)
|
||||
|
||||
var preferredFont: String?
|
||||
for name in names {
|
||||
let originalName = name
|
||||
let name = name.lowercased()
|
||||
if (!name.contains("-") || name.contains("regular")) && preferredFont == nil {
|
||||
preferredFont = originalName
|
||||
}
|
||||
if name.contains("bold") && !name.contains("italic") {
|
||||
preferredFont = originalName
|
||||
}
|
||||
}
|
||||
|
||||
if let preferredFont {
|
||||
let shortname = family.lowercased().replacingOccurrences(of: " ", with: "", options: [])
|
||||
result[shortname] = (preferredFont, family)
|
||||
}
|
||||
}
|
||||
print(result)
|
||||
return result
|
||||
}()
|
||||
|
@ -6,53 +6,25 @@ protocol DrawingRenderLayer: CALayer {
|
||||
|
||||
}
|
||||
|
||||
final class MarkerTool: DrawingElement {
|
||||
let uuid = UUID()
|
||||
final class MarkerTool: DrawingElement, Codable {
|
||||
let uuid: UUID
|
||||
|
||||
let drawingSize: CGSize
|
||||
let color: DrawingColor
|
||||
let lineWidth: CGFloat
|
||||
let arrow: Bool
|
||||
|
||||
let renderLineWidth: CGFloat
|
||||
var renderPath = UIBezierPath()
|
||||
var renderAngle: CGFloat = 0.0
|
||||
|
||||
var translation = CGPoint()
|
||||
|
||||
var bounds: CGRect {
|
||||
return self.renderPath.bounds.offsetBy(dx: self.translation.x, dy: self.translation.y)
|
||||
}
|
||||
var points: [CGPoint] = []
|
||||
|
||||
var _points: [Polyline.Point] = []
|
||||
var points: [Polyline.Point] {
|
||||
return self._points.map { $0.offsetBy(self.translation) }
|
||||
}
|
||||
|
||||
weak var metalView: DrawingMetalView?
|
||||
|
||||
func containsPoint(_ point: CGPoint) -> Bool {
|
||||
return self.renderPath.contains(point.offsetBy(CGPoint(x: -self.translation.x, y: -self.translation.y)))
|
||||
}
|
||||
|
||||
func hasPointsInsidePath(_ path: UIBezierPath) -> Bool {
|
||||
let pathBoundingBox = path.bounds
|
||||
if self.bounds.intersects(pathBoundingBox) {
|
||||
for point in self._points {
|
||||
if path.contains(point.location.offsetBy(self.translation)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
required init(drawingSize: CGSize, color: DrawingColor, lineWidth: CGFloat, arrow: Bool) {
|
||||
|
||||
required init(drawingSize: CGSize, color: DrawingColor, lineWidth: CGFloat) {
|
||||
self.uuid = UUID()
|
||||
self.drawingSize = drawingSize
|
||||
self.color = color
|
||||
self.lineWidth = lineWidth
|
||||
self.arrow = arrow
|
||||
|
||||
|
||||
let minLineWidth = max(10.0, max(drawingSize.width, drawingSize.height) * 0.01)
|
||||
let maxLineWidth = max(20.0, max(drawingSize.width, drawingSize.height) * 0.09)
|
||||
let lineWidth = minLineWidth + (maxLineWidth - minLineWidth) * lineWidth
|
||||
@ -60,43 +32,64 @@ final class MarkerTool: DrawingElement {
|
||||
self.renderLineWidth = lineWidth
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case uuid
|
||||
case drawingSize
|
||||
case color
|
||||
case renderLineWidth
|
||||
case points
|
||||
}
|
||||
|
||||
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.color = try container.decode(DrawingColor.self, forKey: .color)
|
||||
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.color, forKey: .color)
|
||||
try container.encode(self.renderLineWidth, forKey: .renderLineWidth)
|
||||
try container.encode(self.points, forKey: .points)
|
||||
}
|
||||
|
||||
func setupRenderLayer() -> DrawingRenderLayer? {
|
||||
return nil
|
||||
}
|
||||
|
||||
private var hot = false
|
||||
private var didSetup = false
|
||||
func updatePath(_ path: DrawingGesturePipeline.DrawingResult, state: DrawingGesturePipeline.DrawingGestureState) {
|
||||
guard case let .location(point) = path else {
|
||||
return
|
||||
}
|
||||
|
||||
if self._points.isEmpty {
|
||||
self.renderPath.move(to: point.location)
|
||||
} else {
|
||||
self.renderPath.addLine(to: point.location)
|
||||
}
|
||||
self._points.append(point)
|
||||
self.points.append(point.location)
|
||||
|
||||
self.hot = true
|
||||
self.didSetup = true
|
||||
self.metalView?.updated(point, state: state, brush: .marker, color: self.color, size: self.renderLineWidth)
|
||||
}
|
||||
|
||||
func draw(in context: CGContext, size: CGSize) {
|
||||
guard !self._points.isEmpty else {
|
||||
guard !self.points.isEmpty else {
|
||||
return
|
||||
}
|
||||
context.saveGState()
|
||||
|
||||
context.translateBy(x: self.translation.x, y: self.translation.y)
|
||||
|
||||
let hot = self.hot
|
||||
if hot {
|
||||
self.hot = false
|
||||
let didSetup = self.didSetup
|
||||
if didSetup {
|
||||
self.didSetup = false
|
||||
} else {
|
||||
self.metalView?.setup(self._points.map { $0.location }, brush: .marker, color: self.color, size: self.renderLineWidth)
|
||||
self.metalView?.setup(self.points, brush: .marker, color: self.color, size: self.renderLineWidth)
|
||||
}
|
||||
self.metalView?.drawInContext(context)
|
||||
if !hot {
|
||||
if !didSetup {
|
||||
self.metalView?.clear()
|
||||
}
|
||||
|
||||
@ -104,7 +97,7 @@ final class MarkerTool: DrawingElement {
|
||||
}
|
||||
}
|
||||
|
||||
final class NeonTool: DrawingElement {
|
||||
final class NeonTool: DrawingElement, Codable {
|
||||
class RenderLayer: SimpleLayer, DrawingRenderLayer {
|
||||
var lineWidth: CGFloat = 0.0
|
||||
|
||||
@ -132,7 +125,6 @@ final class NeonTool: DrawingElement {
|
||||
self.shadowLayer.shadowOpacity = 1.0
|
||||
self.shadowLayer.shadowOffset = .zero
|
||||
|
||||
|
||||
self.borderLayer.frame = bounds
|
||||
self.borderLayer.contentsScale = 1.0
|
||||
self.borderLayer.lineWidth = strokeWidth
|
||||
@ -141,7 +133,6 @@ final class NeonTool: DrawingElement {
|
||||
self.borderLayer.fillColor = UIColor.clear.cgColor
|
||||
self.borderLayer.strokeColor = UIColor.white.mixedWith(color.toUIColor(), alpha: 0.25).cgColor
|
||||
|
||||
|
||||
self.fillLayer.frame = bounds
|
||||
self.fillLayer.contentsScale = 1.0
|
||||
self.fillLayer.fillColor = UIColor.white.cgColor
|
||||
@ -158,16 +149,11 @@ final class NeonTool: DrawingElement {
|
||||
}
|
||||
}
|
||||
|
||||
let uuid = UUID()
|
||||
let uuid: UUID
|
||||
|
||||
let drawingSize: CGSize
|
||||
let color: DrawingColor
|
||||
let lineWidth: CGFloat
|
||||
let arrow: Bool
|
||||
|
||||
var path: BezierPath?
|
||||
var boundingBox: CGRect?
|
||||
|
||||
|
||||
var renderPath: CGPath?
|
||||
let renderStrokeWidth: CGFloat
|
||||
let renderShadowRadius: CGFloat
|
||||
@ -176,70 +162,11 @@ final class NeonTool: DrawingElement {
|
||||
var translation = CGPoint()
|
||||
|
||||
private var currentRenderLayer: DrawingRenderLayer?
|
||||
|
||||
var bounds: CGRect {
|
||||
return self.path?.path.bounds.offsetBy(dx: self.translation.x, dy: self.translation.y) ?? .zero
|
||||
}
|
||||
|
||||
var points: [Polyline.Point] {
|
||||
guard let linePath = self.path else {
|
||||
return []
|
||||
}
|
||||
var points: [Polyline.Point] = []
|
||||
for element in linePath.elements {
|
||||
if case .moveTo = element.type {
|
||||
points.append(element.startPoint.offsetBy(self.translation))
|
||||
} else {
|
||||
points.append(element.endPoint.offsetBy(self.translation))
|
||||
}
|
||||
}
|
||||
return points
|
||||
}
|
||||
|
||||
func containsPoint(_ point: CGPoint) -> Bool {
|
||||
return self.renderPath?.contains(point.offsetBy(CGPoint(x: -self.translation.x, y: -self.translation.y))) ?? false
|
||||
}
|
||||
|
||||
func hasPointsInsidePath(_ path: UIBezierPath) -> Bool {
|
||||
if let linePath = self.path {
|
||||
let pathBoundingBox = path.bounds
|
||||
if self.bounds.intersects(pathBoundingBox) {
|
||||
for element in linePath.elements {
|
||||
if case .moveTo = element.type {
|
||||
if path.contains(element.startPoint.location.offsetBy(self.translation)) {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
if path.contains(element.startPoint.location.offsetBy(self.translation)) {
|
||||
return true
|
||||
}
|
||||
if path.contains(element.endPoint.location.offsetBy(self.translation)) {
|
||||
return true
|
||||
}
|
||||
if case .cubicCurve = element.type {
|
||||
if path.contains(element.controlPoints[0].offsetBy(self.translation)) {
|
||||
return true
|
||||
}
|
||||
if path.contains(element.controlPoints[1].offsetBy(self.translation)) {
|
||||
return true
|
||||
}
|
||||
} else if case .quadCurve = element.type {
|
||||
if path.contains(element.controlPoints[0].offsetBy(self.translation)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
required init(drawingSize: CGSize, color: DrawingColor, lineWidth: CGFloat, arrow: Bool) {
|
||||
|
||||
required init(drawingSize: CGSize, color: DrawingColor, lineWidth: CGFloat) {
|
||||
self.uuid = UUID()
|
||||
self.drawingSize = drawingSize
|
||||
self.color = color
|
||||
self.lineWidth = lineWidth
|
||||
self.arrow = arrow
|
||||
|
||||
let strokeWidth = min(drawingSize.width, drawingSize.height) * 0.008
|
||||
let shadowRadius = min(drawingSize.width, drawingSize.height) * 0.03
|
||||
@ -253,6 +180,37 @@ final class NeonTool: DrawingElement {
|
||||
self.renderLineWidth = lineWidth
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case uuid
|
||||
case drawingSize
|
||||
case color
|
||||
case renderStrokeWidth
|
||||
case renderShadowRadius
|
||||
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.color = try container.decode(DrawingColor.self, forKey: .color)
|
||||
self.renderStrokeWidth = try container.decode(CGFloat.self, forKey: .renderStrokeWidth)
|
||||
self.renderShadowRadius = try container.decode(CGFloat.self, forKey: .renderShadowRadius)
|
||||
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.color, forKey: .color)
|
||||
try container.encode(self.renderStrokeWidth, forKey: .renderStrokeWidth)
|
||||
try container.encode(self.renderShadowRadius, forKey: .renderShadowRadius)
|
||||
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, color: self.color, lineWidth: self.renderLineWidth, strokeWidth: self.renderStrokeWidth, shadowRadius: self.renderShadowRadius)
|
||||
@ -265,8 +223,6 @@ final class NeonTool: DrawingElement {
|
||||
return
|
||||
}
|
||||
|
||||
self.path = bezierPath
|
||||
|
||||
let cgPath = bezierPath.path.cgPath.copy(strokingWithWidth: self.renderLineWidth, lineCap: .round, lineJoin: .round, miterLimit: 0.0)
|
||||
self.renderPath = cgPath
|
||||
|
||||
@ -310,126 +266,42 @@ final class NeonTool: DrawingElement {
|
||||
}
|
||||
}
|
||||
|
||||
final class PencilTool: DrawingElement {
|
||||
let uuid = UUID()
|
||||
|
||||
let drawingSize: CGSize
|
||||
let color: DrawingColor
|
||||
let lineWidth: CGFloat
|
||||
let arrow: Bool
|
||||
|
||||
var translation = CGPoint()
|
||||
|
||||
let renderLineWidth: CGFloat
|
||||
var renderPath = UIBezierPath()
|
||||
var renderAngle: CGFloat = 0.0
|
||||
|
||||
var bounds: CGRect {
|
||||
return self.renderPath.bounds.offsetBy(dx: self.translation.x, dy: self.translation.y)
|
||||
}
|
||||
|
||||
var _points: [Polyline.Point] = []
|
||||
var points: [Polyline.Point] {
|
||||
return self._points.map { $0.offsetBy(self.translation) }
|
||||
}
|
||||
|
||||
weak var metalView: DrawingMetalView?
|
||||
|
||||
func containsPoint(_ point: CGPoint) -> Bool {
|
||||
return self.renderPath.contains(point.offsetBy(dx: -self.translation.x, dy: -self.translation.y))
|
||||
}
|
||||
|
||||
func hasPointsInsidePath(_ path: UIBezierPath) -> Bool {
|
||||
let pathBoundingBox = path.bounds
|
||||
if self.bounds.intersects(pathBoundingBox) {
|
||||
for point in self._points {
|
||||
if path.contains(point.location.offsetBy(self.translation)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
required init(drawingSize: CGSize, color: DrawingColor, lineWidth: CGFloat, arrow: Bool) {
|
||||
self.drawingSize = drawingSize
|
||||
self.color = color
|
||||
self.lineWidth = lineWidth
|
||||
self.arrow = arrow
|
||||
|
||||
let minLineWidth = max(10.0, max(drawingSize.width, drawingSize.height) * 0.01)
|
||||
let maxLineWidth = max(20.0, max(drawingSize.width, drawingSize.height) * 0.09)
|
||||
let lineWidth = minLineWidth + (maxLineWidth - minLineWidth) * lineWidth
|
||||
|
||||
self.renderLineWidth = lineWidth
|
||||
}
|
||||
|
||||
func setupRenderLayer() -> DrawingRenderLayer? {
|
||||
return nil
|
||||
}
|
||||
|
||||
private var hot = false
|
||||
func updatePath(_ path: DrawingGesturePipeline.DrawingResult, state: DrawingGesturePipeline.DrawingGestureState) {
|
||||
guard case let .location(point) = path else {
|
||||
return
|
||||
}
|
||||
|
||||
if self._points.isEmpty {
|
||||
self.renderPath.move(to: point.location)
|
||||
} else {
|
||||
self.renderPath.addLine(to: point.location)
|
||||
}
|
||||
self._points.append(point)
|
||||
|
||||
self.hot = true
|
||||
self.metalView?.updated(point, state: state, brush: .pencil, color: self.color, size: self.renderLineWidth)
|
||||
}
|
||||
|
||||
func draw(in context: CGContext, size: CGSize) {
|
||||
guard !self._points.isEmpty else {
|
||||
return
|
||||
}
|
||||
context.saveGState()
|
||||
|
||||
context.translateBy(x: self.translation.x, y: self.translation.y)
|
||||
|
||||
let hot = self.hot
|
||||
if hot {
|
||||
self.hot = false
|
||||
} else {
|
||||
self.metalView?.setup(self._points.map { $0.location }, brush: .pencil, color: self.color, size: self.renderLineWidth)
|
||||
}
|
||||
self.metalView?.drawInContext(context)
|
||||
if !hot {
|
||||
self.metalView?.clear()
|
||||
}
|
||||
|
||||
context.restoreGState()
|
||||
}
|
||||
}
|
||||
|
||||
final class FillTool: DrawingElement {
|
||||
let uuid = UUID()
|
||||
final class FillTool: DrawingElement, Codable {
|
||||
let uuid: UUID
|
||||
|
||||
let drawingSize: CGSize
|
||||
let color: DrawingColor
|
||||
let renderLineWidth: CGFloat = 0.0
|
||||
|
||||
var bounds: CGRect {
|
||||
return .zero
|
||||
}
|
||||
|
||||
var points: [Polyline.Point] {
|
||||
return []
|
||||
}
|
||||
|
||||
var translation = CGPoint()
|
||||
|
||||
required init(drawingSize: CGSize, color: DrawingColor, lineWidth: CGFloat, arrow: Bool) {
|
||||
required init(drawingSize: CGSize, color: DrawingColor) {
|
||||
self.uuid = UUID()
|
||||
self.drawingSize = drawingSize
|
||||
self.color = color
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case uuid
|
||||
case drawingSize
|
||||
case color
|
||||
}
|
||||
|
||||
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.color = try container.decode(DrawingColor.self, forKey: .color)
|
||||
// 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.color, forKey: .color)
|
||||
// try container.encode(self.points, forKey: .points)
|
||||
}
|
||||
|
||||
func setupRenderLayer() -> DrawingRenderLayer? {
|
||||
return nil
|
||||
}
|
||||
@ -458,14 +330,14 @@ final class FillTool: DrawingElement {
|
||||
}
|
||||
|
||||
|
||||
final class BlurTool: DrawingElement {
|
||||
final class BlurTool: DrawingElement, Codable {
|
||||
class RenderLayer: SimpleLayer, DrawingRenderLayer {
|
||||
var lineWidth: CGFloat = 0.0
|
||||
|
||||
let blurLayer = SimpleLayer()
|
||||
let fillLayer = SimpleShapeLayer()
|
||||
|
||||
func setup(size: CGSize, color: DrawingColor, lineWidth: CGFloat, image: UIImage?) {
|
||||
func setup(size: CGSize, lineWidth: CGFloat, image: UIImage?) {
|
||||
self.contentsScale = 1.0
|
||||
self.lineWidth = lineWidth
|
||||
|
||||
@ -497,15 +369,11 @@ final class BlurTool: DrawingElement {
|
||||
|
||||
var getFullImage: () -> UIImage? = { return nil }
|
||||
|
||||
let uuid = UUID()
|
||||
let uuid: UUID
|
||||
|
||||
let drawingSize: CGSize
|
||||
let color: DrawingColor
|
||||
let lineWidth: CGFloat
|
||||
let arrow: Bool
|
||||
|
||||
var path: BezierPath?
|
||||
var boundingBox: CGRect?
|
||||
|
||||
var renderPath: CGPath?
|
||||
let renderLineWidth: CGFloat
|
||||
@ -514,69 +382,9 @@ final class BlurTool: DrawingElement {
|
||||
|
||||
private var currentRenderLayer: DrawingRenderLayer?
|
||||
|
||||
var bounds: CGRect {
|
||||
return self.path?.path.bounds.offsetBy(dx: self.translation.x, dy: self.translation.y) ?? .zero
|
||||
}
|
||||
|
||||
var points: [Polyline.Point] {
|
||||
guard let linePath = self.path else {
|
||||
return []
|
||||
}
|
||||
var points: [Polyline.Point] = []
|
||||
for element in linePath.elements {
|
||||
if case .moveTo = element.type {
|
||||
points.append(element.startPoint.offsetBy(self.translation))
|
||||
} else {
|
||||
points.append(element.endPoint.offsetBy(self.translation))
|
||||
}
|
||||
}
|
||||
return points
|
||||
}
|
||||
|
||||
func containsPoint(_ point: CGPoint) -> Bool {
|
||||
return self.renderPath?.contains(point.offsetBy(CGPoint(x: -self.translation.x, y: -self.translation.y))) ?? false
|
||||
}
|
||||
|
||||
func hasPointsInsidePath(_ path: UIBezierPath) -> Bool {
|
||||
if let linePath = self.path {
|
||||
let pathBoundingBox = path.bounds
|
||||
if self.bounds.intersects(pathBoundingBox) {
|
||||
for element in linePath.elements {
|
||||
if case .moveTo = element.type {
|
||||
if path.contains(element.startPoint.location.offsetBy(self.translation)) {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
if path.contains(element.startPoint.location.offsetBy(self.translation)) {
|
||||
return true
|
||||
}
|
||||
if path.contains(element.endPoint.location.offsetBy(self.translation)) {
|
||||
return true
|
||||
}
|
||||
if case .cubicCurve = element.type {
|
||||
if path.contains(element.controlPoints[0].offsetBy(self.translation)) {
|
||||
return true
|
||||
}
|
||||
if path.contains(element.controlPoints[1].offsetBy(self.translation)) {
|
||||
return true
|
||||
}
|
||||
} else if case .quadCurve = element.type {
|
||||
if path.contains(element.controlPoints[0].offsetBy(self.translation)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
required init(drawingSize: CGSize, color: DrawingColor, lineWidth: CGFloat, arrow: Bool) {
|
||||
required init(drawingSize: CGSize, lineWidth: CGFloat) {
|
||||
self.uuid = UUID()
|
||||
self.drawingSize = drawingSize
|
||||
self.color = color
|
||||
self.lineWidth = lineWidth
|
||||
self.arrow = arrow
|
||||
|
||||
let minLineWidth = max(1.0, max(drawingSize.width, drawingSize.height) * 0.003)
|
||||
let maxLineWidth = max(10.0, max(drawingSize.width, drawingSize.height) * 0.09)
|
||||
@ -585,9 +393,31 @@ final class BlurTool: DrawingElement {
|
||||
self.renderLineWidth = lineWidth
|
||||
}
|
||||
|
||||
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, color: self.color, lineWidth: self.renderLineWidth, image: self.getFullImage())
|
||||
layer.setup(size: self.drawingSize, lineWidth: self.renderLineWidth, image: self.getFullImage())
|
||||
self.currentRenderLayer = layer
|
||||
return layer
|
||||
}
|
||||
@ -621,14 +451,14 @@ final class BlurTool: DrawingElement {
|
||||
}
|
||||
|
||||
|
||||
final class EraserTool: DrawingElement {
|
||||
final class EraserTool: DrawingElement, Codable {
|
||||
class RenderLayer: SimpleLayer, DrawingRenderLayer {
|
||||
var lineWidth: CGFloat = 0.0
|
||||
|
||||
let blurLayer = SimpleLayer()
|
||||
let fillLayer = SimpleShapeLayer()
|
||||
|
||||
func setup(size: CGSize, color: DrawingColor, lineWidth: CGFloat, image: UIImage?) {
|
||||
func setup(size: CGSize, lineWidth: CGFloat, image: UIImage?) {
|
||||
self.contentsScale = 1.0
|
||||
self.lineWidth = lineWidth
|
||||
|
||||
@ -661,15 +491,10 @@ final class EraserTool: DrawingElement {
|
||||
|
||||
var getFullImage: () -> UIImage? = { return nil }
|
||||
|
||||
let uuid = UUID()
|
||||
|
||||
let uuid: UUID
|
||||
let drawingSize: CGSize
|
||||
let color: DrawingColor
|
||||
let lineWidth: CGFloat
|
||||
let arrow: Bool
|
||||
|
||||
var path: BezierPath?
|
||||
var boundingBox: CGRect?
|
||||
|
||||
var renderPath: CGPath?
|
||||
let renderLineWidth: CGFloat
|
||||
@ -678,70 +503,9 @@ final class EraserTool: DrawingElement {
|
||||
|
||||
private var currentRenderLayer: DrawingRenderLayer?
|
||||
|
||||
var bounds: CGRect {
|
||||
return self.path?.path.bounds.offsetBy(dx: self.translation.x, dy: self.translation.y) ?? .zero
|
||||
}
|
||||
|
||||
var points: [Polyline.Point] {
|
||||
guard let linePath = self.path else {
|
||||
return []
|
||||
}
|
||||
var points: [Polyline.Point] = []
|
||||
for element in linePath.elements {
|
||||
if case .moveTo = element.type {
|
||||
points.append(element.startPoint.offsetBy(self.translation))
|
||||
} else {
|
||||
points.append(element.endPoint.offsetBy(self.translation))
|
||||
}
|
||||
}
|
||||
return points
|
||||
}
|
||||
|
||||
func containsPoint(_ point: CGPoint) -> Bool {
|
||||
return false
|
||||
// return self.renderPath?.contains(point.offsetBy(CGPoint(x: -self.translation.x, y: -self.translation.y))) ?? false
|
||||
}
|
||||
|
||||
func hasPointsInsidePath(_ path: UIBezierPath) -> Bool {
|
||||
if let linePath = self.path {
|
||||
let pathBoundingBox = path.bounds
|
||||
if self.bounds.intersects(pathBoundingBox) {
|
||||
for element in linePath.elements {
|
||||
if case .moveTo = element.type {
|
||||
if path.contains(element.startPoint.location.offsetBy(self.translation)) {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
if path.contains(element.startPoint.location.offsetBy(self.translation)) {
|
||||
return true
|
||||
}
|
||||
if path.contains(element.endPoint.location.offsetBy(self.translation)) {
|
||||
return true
|
||||
}
|
||||
if case .cubicCurve = element.type {
|
||||
if path.contains(element.controlPoints[0].offsetBy(self.translation)) {
|
||||
return true
|
||||
}
|
||||
if path.contains(element.controlPoints[1].offsetBy(self.translation)) {
|
||||
return true
|
||||
}
|
||||
} else if case .quadCurve = element.type {
|
||||
if path.contains(element.controlPoints[0].offsetBy(self.translation)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
required init(drawingSize: CGSize, color: DrawingColor, lineWidth: CGFloat, arrow: Bool) {
|
||||
required init(drawingSize: CGSize, lineWidth: CGFloat) {
|
||||
self.uuid = UUID()
|
||||
self.drawingSize = drawingSize
|
||||
self.color = color
|
||||
self.lineWidth = lineWidth
|
||||
self.arrow = arrow
|
||||
|
||||
let minLineWidth = max(1.0, max(drawingSize.width, drawingSize.height) * 0.003)
|
||||
let maxLineWidth = max(10.0, max(drawingSize.width, drawingSize.height) * 0.09)
|
||||
@ -750,9 +514,31 @@ final class EraserTool: DrawingElement {
|
||||
self.renderLineWidth = lineWidth
|
||||
}
|
||||
|
||||
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, color: self.color, lineWidth: self.renderLineWidth, image: self.getFullImage())
|
||||
layer.setup(size: self.drawingSize, lineWidth: self.renderLineWidth, image: self.getFullImage())
|
||||
self.currentRenderLayer = layer
|
||||
return layer
|
||||
}
|
||||
@ -784,3 +570,106 @@ final class EraserTool: DrawingElement {
|
||||
renderLayer?.render(in: context)
|
||||
}
|
||||
}
|
||||
|
||||
enum CodableDrawingElement {
|
||||
case pen(PenTool)
|
||||
case marker(MarkerTool)
|
||||
case neon(NeonTool)
|
||||
case eraser(EraserTool)
|
||||
case blur(BlurTool)
|
||||
case fill(FillTool)
|
||||
|
||||
init?(element: DrawingElement) {
|
||||
if let element = element as? PenTool {
|
||||
self = .pen(element)
|
||||
} else if let element = element as? MarkerTool {
|
||||
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 {
|
||||
self = .fill(element)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var entity: DrawingElement {
|
||||
switch self {
|
||||
case let .pen(element):
|
||||
return element
|
||||
case let .marker(element):
|
||||
return element
|
||||
case let .neon(element):
|
||||
return element
|
||||
case let .eraser(element):
|
||||
return element
|
||||
case let .blur(element):
|
||||
return element
|
||||
case let .fill(element):
|
||||
return element
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension CodableDrawingElement: Codable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case type
|
||||
case element
|
||||
}
|
||||
|
||||
private enum ElementType: Int, Codable {
|
||||
case pen
|
||||
case marker
|
||||
case neon
|
||||
case eraser
|
||||
case blur
|
||||
case fill
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
let type = try container.decode(ElementType.self, forKey: .type)
|
||||
switch type {
|
||||
case .pen:
|
||||
self = .pen(try container.decode(PenTool.self, forKey: .element))
|
||||
case .marker:
|
||||
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:
|
||||
self = .fill(try container.decode(FillTool.self, forKey: .element))
|
||||
}
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
switch self {
|
||||
case let .pen(payload):
|
||||
try container.encode(ElementType.pen, forKey: .type)
|
||||
try container.encode(payload, forKey: .element)
|
||||
case let .marker(payload):
|
||||
try container.encode(ElementType.marker, forKey: .type)
|
||||
try container.encode(payload, forKey: .element)
|
||||
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)
|
||||
case let .fill(payload):
|
||||
try container.encode(ElementType.fill, forKey: .type)
|
||||
try container.encode(payload, forKey: .element)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,36 +9,18 @@ import ImageBlur
|
||||
|
||||
protocol DrawingElement: AnyObject {
|
||||
var uuid: UUID { get }
|
||||
var bounds: CGRect { get }
|
||||
var points: [Polyline.Point] { get }
|
||||
|
||||
var translation: CGPoint { get set }
|
||||
|
||||
var renderLineWidth: CGFloat { get }
|
||||
|
||||
func containsPoint(_ point: CGPoint) -> Bool
|
||||
func hasPointsInsidePath(_ path: UIBezierPath) -> Bool
|
||||
|
||||
init(drawingSize: CGSize, color: DrawingColor, lineWidth: CGFloat, arrow: Bool)
|
||||
|
||||
|
||||
func setupRenderLayer() -> DrawingRenderLayer?
|
||||
func updatePath(_ path: DrawingGesturePipeline.DrawingResult, state: DrawingGesturePipeline.DrawingGestureState)
|
||||
|
||||
func draw(in: CGContext, size: CGSize)
|
||||
}
|
||||
|
||||
enum DrawingCommand {
|
||||
enum DrawingElementTransform {
|
||||
case move(offset: CGPoint)
|
||||
}
|
||||
|
||||
case addStroke(DrawingElement)
|
||||
case updateStrokes([UUID], DrawingElementTransform)
|
||||
case removeStroke(DrawingElement)
|
||||
case addEntity(DrawingEntity)
|
||||
case updateEntity(UUID, DrawingEntity)
|
||||
enum DrawingOperation {
|
||||
case element(DrawingElement)
|
||||
case addEntity(UUID)
|
||||
case removeEntity(DrawingEntity)
|
||||
case updateEntityZOrder(UUID, Int32)
|
||||
}
|
||||
|
||||
public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDrawingView {
|
||||
@ -63,10 +45,8 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
||||
case pen
|
||||
case marker
|
||||
case neon
|
||||
case pencil
|
||||
case eraser
|
||||
case lasso
|
||||
case objectRemover
|
||||
case blur
|
||||
}
|
||||
|
||||
@ -82,7 +62,8 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
||||
var getFullImage: (Bool) -> UIImage? = { _ in return nil }
|
||||
|
||||
private var elements: [DrawingElement] = []
|
||||
private var redoElements: [DrawingElement] = []
|
||||
private var undoStack: [DrawingOperation] = []
|
||||
private var redoStack: [DrawingOperation] = []
|
||||
fileprivate var uncommitedElement: DrawingElement?
|
||||
|
||||
private(set) var drawingImage: UIImage?
|
||||
@ -109,6 +90,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
||||
private var strokeRecognitionTimer: SwiftSignalKit.Timer?
|
||||
|
||||
private var isDrawing = false
|
||||
private var drawingGestureStartTimestamp: Double?
|
||||
|
||||
private func loadTemplates() {
|
||||
func load(_ name: String) {
|
||||
@ -134,7 +116,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
||||
load("shape_arrow")
|
||||
}
|
||||
|
||||
public init(size: CGSize) {
|
||||
init(size: CGSize) {
|
||||
self.imageSize = size
|
||||
|
||||
let format = UIGraphicsImageRendererFormat()
|
||||
@ -203,166 +185,120 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if case .objectRemover = strongSelf.tool {
|
||||
if case let .location(point) = path {
|
||||
var elementsToRemove: [DrawingElement] = []
|
||||
for element in strongSelf.elements {
|
||||
if element.containsPoint(point.location) {
|
||||
elementsToRemove.append(element)
|
||||
}
|
||||
}
|
||||
|
||||
for element in elementsToRemove {
|
||||
strongSelf.removeElement(element)
|
||||
}
|
||||
switch state {
|
||||
case .began:
|
||||
strongSelf.isDrawing = true
|
||||
strongSelf.previousStrokePoint = nil
|
||||
strongSelf.drawingGestureStartTimestamp = CACurrentMediaTime()
|
||||
|
||||
if strongSelf.uncommitedElement != nil {
|
||||
strongSelf.finishDrawing()
|
||||
}
|
||||
} else if case .lasso = strongSelf.tool {
|
||||
if case let .smoothCurve(bezierPath) = path {
|
||||
let scale = strongSelf.bounds.width / strongSelf.imageSize.width
|
||||
|
||||
switch state {
|
||||
case .began:
|
||||
strongSelf.lassoView.setup(scale: scale)
|
||||
strongSelf.lassoView.updatePath(bezierPath)
|
||||
case .changed:
|
||||
strongSelf.lassoView.updatePath(bezierPath)
|
||||
case .ended:
|
||||
let closedPath = bezierPath.closedCopy()
|
||||
|
||||
var selectedElements: [DrawingElement] = []
|
||||
var selectedPoints: [CGPoint] = []
|
||||
var maxLineWidth: CGFloat = 0.0
|
||||
for element in strongSelf.elements {
|
||||
if element.hasPointsInsidePath(closedPath.path) {
|
||||
maxLineWidth = max(maxLineWidth, element.renderLineWidth)
|
||||
selectedElements.append(element)
|
||||
selectedPoints.append(contentsOf: element.points.map { $0.location })
|
||||
}
|
||||
}
|
||||
|
||||
if selectedPoints.count > 0 {
|
||||
strongSelf.lassoView.apply(scale: scale, points: selectedPoints, selectedElements: selectedElements.map { $0.uuid }, expand: maxLineWidth)
|
||||
} else {
|
||||
strongSelf.lassoView.reset()
|
||||
}
|
||||
case .cancelled:
|
||||
strongSelf.lassoView.reset()
|
||||
}
|
||||
|
||||
guard let newElement = strongSelf.prepareNewElement() else {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
switch state {
|
||||
case .began:
|
||||
strongSelf.isDrawing = true
|
||||
strongSelf.previousStrokePoint = nil
|
||||
|
||||
if strongSelf.uncommitedElement != nil {
|
||||
strongSelf.finishDrawing()
|
||||
|
||||
if newElement is MarkerTool {
|
||||
self?.metalView.isHidden = false
|
||||
}
|
||||
|
||||
if let renderLayer = newElement.setupRenderLayer() {
|
||||
if let currentDrawingLayer = strongSelf.currentDrawingLayer {
|
||||
strongSelf.currentDrawingLayer = nil
|
||||
currentDrawingLayer.removeFromSuperlayer()
|
||||
}
|
||||
|
||||
guard let newElement = strongSelf.prepareNewElement() else {
|
||||
return
|
||||
}
|
||||
|
||||
if newElement is MarkerTool || newElement is PencilTool {
|
||||
self?.metalView.isHidden = false
|
||||
}
|
||||
|
||||
if let renderLayer = newElement.setupRenderLayer() {
|
||||
if let currentDrawingLayer = strongSelf.currentDrawingLayer {
|
||||
strongSelf.currentDrawingLayer = nil
|
||||
currentDrawingLayer.removeFromSuperlayer()
|
||||
strongSelf.currentDrawingView.layer.addSublayer(renderLayer)
|
||||
strongSelf.currentDrawingLayer = renderLayer
|
||||
}
|
||||
newElement.updatePath(path, state: state)
|
||||
strongSelf.uncommitedElement = newElement
|
||||
strongSelf.updateInternalState()
|
||||
case .changed:
|
||||
strongSelf.uncommitedElement?.updatePath(path, state: state)
|
||||
|
||||
if case let .polyline(line) = path, let lastPoint = line.points.last {
|
||||
if let previousStrokePoint = strongSelf.previousStrokePoint, line.points.count > 10 {
|
||||
let currentTimestamp = CACurrentMediaTime()
|
||||
if lastPoint.location.distance(to: previousStrokePoint) > 10.0 {
|
||||
strongSelf.previousStrokePoint = lastPoint.location
|
||||
|
||||
strongSelf.strokeRecognitionTimer?.invalidate()
|
||||
strongSelf.strokeRecognitionTimer = nil
|
||||
}
|
||||
strongSelf.currentDrawingView.layer.addSublayer(renderLayer)
|
||||
strongSelf.currentDrawingLayer = renderLayer
|
||||
}
|
||||
newElement.updatePath(path, state: state)
|
||||
strongSelf.uncommitedElement = newElement
|
||||
strongSelf.updateInternalState()
|
||||
case .changed:
|
||||
strongSelf.uncommitedElement?.updatePath(path, state: state)
|
||||
|
||||
if case let .polyline(line) = path, let lastPoint = line.points.last {
|
||||
if let previousStrokePoint = strongSelf.previousStrokePoint, line.points.count > 10 {
|
||||
if lastPoint.location.distance(to: previousStrokePoint) > 10.0 {
|
||||
strongSelf.previousStrokePoint = lastPoint.location
|
||||
|
||||
strongSelf.strokeRecognitionTimer?.invalidate()
|
||||
strongSelf.strokeRecognitionTimer = nil
|
||||
}
|
||||
|
||||
if strongSelf.strokeRecognitionTimer == nil {
|
||||
strongSelf.strokeRecognitionTimer = SwiftSignalKit.Timer(timeout: 0.85, repeat: false, completion: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if let previousStrokePoint = strongSelf.previousStrokePoint, lastPoint.location.distance(to: previousStrokePoint) <= 10.0 {
|
||||
let strokeRecognizer = Unistroke(points: line.points.map { $0.location })
|
||||
if let template = strokeRecognizer.match(templates: strongSelf.loadedTemplates, minThreshold: 0.5) {
|
||||
|
||||
let edges = line.bounds
|
||||
let bounds = CGRect(origin: edges.origin, size: CGSize(width: edges.width - edges.minX, height: edges.height - edges.minY))
|
||||
|
||||
var entity: DrawingEntity?
|
||||
if template == "shape_rectangle" {
|
||||
let shapeEntity = DrawingSimpleShapeEntity(shapeType: .rectangle, drawType: .stroke, color: strongSelf.toolColor, lineWidth: 0.25)
|
||||
shapeEntity.referenceDrawingSize = strongSelf.imageSize
|
||||
shapeEntity.position = bounds.center
|
||||
shapeEntity.size = bounds.size
|
||||
entity = shapeEntity
|
||||
} else if template == "shape_circle" {
|
||||
let shapeEntity = DrawingSimpleShapeEntity(shapeType: .ellipse, drawType: .stroke, color: strongSelf.toolColor, lineWidth: 0.25)
|
||||
shapeEntity.referenceDrawingSize = strongSelf.imageSize
|
||||
shapeEntity.position = bounds.center
|
||||
shapeEntity.size = bounds.size
|
||||
entity = shapeEntity
|
||||
} else if template == "shape_star" {
|
||||
let shapeEntity = DrawingSimpleShapeEntity(shapeType: .star, drawType: .stroke, color: strongSelf.toolColor, lineWidth: 0.25)
|
||||
shapeEntity.referenceDrawingSize = strongSelf.imageSize
|
||||
shapeEntity.position = bounds.center
|
||||
shapeEntity.size = CGSize(width: max(bounds.width, bounds.height), height: max(bounds.width, bounds.height))
|
||||
entity = shapeEntity
|
||||
} else if template == "shape_arrow" {
|
||||
let arrowEntity = DrawingVectorEntity(type: .oneSidedArrow, color: strongSelf.toolColor, lineWidth: 0.2)
|
||||
arrowEntity.referenceDrawingSize = strongSelf.imageSize
|
||||
arrowEntity.start = line.points.first?.location ?? .zero
|
||||
arrowEntity.end = line.points[line.points.count - 4].location
|
||||
entity = arrowEntity
|
||||
}
|
||||
|
||||
if let entity = entity {
|
||||
strongSelf.entitiesView?.add(entity)
|
||||
strongSelf.cancelDrawing()
|
||||
strongSelf.drawingGesturePipeline?.gestureRecognizer?.isEnabled = false
|
||||
strongSelf.drawingGesturePipeline?.gestureRecognizer?.isEnabled = true
|
||||
}
|
||||
|
||||
if strongSelf.strokeRecognitionTimer == nil, let startTimestamp = strongSelf.drawingGestureStartTimestamp, currentTimestamp - startTimestamp < 3.0 {
|
||||
strongSelf.strokeRecognitionTimer = SwiftSignalKit.Timer(timeout: 0.85, repeat: false, completion: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if let previousStrokePoint = strongSelf.previousStrokePoint, lastPoint.location.distance(to: previousStrokePoint) <= 10.0 {
|
||||
let strokeRecognizer = Unistroke(points: line.points.map { $0.location })
|
||||
if let template = strokeRecognizer.match(templates: strongSelf.loadedTemplates, minThreshold: 0.5) {
|
||||
let edges = line.bounds
|
||||
let bounds = CGRect(origin: edges.origin, size: CGSize(width: edges.width - edges.minX, height: edges.height - edges.minY))
|
||||
|
||||
var entity: DrawingEntity?
|
||||
if template == "shape_rectangle" {
|
||||
let shapeEntity = DrawingSimpleShapeEntity(shapeType: .rectangle, drawType: .stroke, color: strongSelf.toolColor, lineWidth: strongSelf.toolBrushSize)
|
||||
shapeEntity.referenceDrawingSize = strongSelf.imageSize
|
||||
shapeEntity.position = bounds.center
|
||||
shapeEntity.size = CGSize(width: bounds.size.width * 1.1, height: bounds.size.height * 1.1)
|
||||
entity = shapeEntity
|
||||
} else if template == "shape_circle" {
|
||||
let shapeEntity = DrawingSimpleShapeEntity(shapeType: .ellipse, drawType: .stroke, color: strongSelf.toolColor, lineWidth: strongSelf.toolBrushSize)
|
||||
shapeEntity.referenceDrawingSize = strongSelf.imageSize
|
||||
shapeEntity.position = bounds.center
|
||||
shapeEntity.size = CGSize(width: bounds.size.width * 1.1, height: bounds.size.height * 1.1)
|
||||
entity = shapeEntity
|
||||
} else if template == "shape_star" {
|
||||
let shapeEntity = DrawingSimpleShapeEntity(shapeType: .star, drawType: .stroke, color: strongSelf.toolColor, lineWidth: strongSelf.toolBrushSize)
|
||||
shapeEntity.referenceDrawingSize = strongSelf.imageSize
|
||||
shapeEntity.position = bounds.center
|
||||
shapeEntity.size = CGSize(width: max(bounds.width, bounds.height) * 1.1, height: max(bounds.width, bounds.height) * 1.1)
|
||||
entity = shapeEntity
|
||||
} else if template == "shape_arrow" {
|
||||
let arrowEntity = DrawingVectorEntity(type: .oneSidedArrow, color: strongSelf.toolColor, lineWidth: strongSelf.toolBrushSize)
|
||||
arrowEntity.referenceDrawingSize = strongSelf.imageSize
|
||||
arrowEntity.start = line.points.first?.location ?? .zero
|
||||
arrowEntity.end = line.points[line.points.count - 4].location
|
||||
entity = arrowEntity
|
||||
}
|
||||
|
||||
if let entity = entity {
|
||||
strongSelf.entitiesView?.add(entity)
|
||||
strongSelf.entitiesView?.selectEntity(entity)
|
||||
strongSelf.cancelDrawing()
|
||||
strongSelf.drawingGesturePipeline?.gestureRecognizer?.isEnabled = false
|
||||
strongSelf.drawingGesturePipeline?.gestureRecognizer?.isEnabled = true
|
||||
}
|
||||
}
|
||||
strongSelf.strokeRecognitionTimer?.invalidate()
|
||||
strongSelf.strokeRecognitionTimer = nil
|
||||
}, queue: Queue.mainQueue())
|
||||
strongSelf.strokeRecognitionTimer?.start()
|
||||
}
|
||||
} else {
|
||||
strongSelf.previousStrokePoint = lastPoint.location
|
||||
}
|
||||
strongSelf.strokeRecognitionTimer?.invalidate()
|
||||
strongSelf.strokeRecognitionTimer = nil
|
||||
}, queue: Queue.mainQueue())
|
||||
strongSelf.strokeRecognitionTimer?.start()
|
||||
}
|
||||
} else {
|
||||
strongSelf.previousStrokePoint = lastPoint.location
|
||||
}
|
||||
|
||||
case .ended:
|
||||
strongSelf.isDrawing = false
|
||||
strongSelf.strokeRecognitionTimer?.invalidate()
|
||||
strongSelf.strokeRecognitionTimer = nil
|
||||
strongSelf.uncommitedElement?.updatePath(path, state: state)
|
||||
Queue.mainQueue().after(0.05) {
|
||||
strongSelf.finishDrawing()
|
||||
}
|
||||
strongSelf.updateInternalState()
|
||||
case .cancelled:
|
||||
strongSelf.isDrawing = false
|
||||
strongSelf.strokeRecognitionTimer?.invalidate()
|
||||
strongSelf.strokeRecognitionTimer = nil
|
||||
strongSelf.cancelDrawing()
|
||||
strongSelf.updateInternalState()
|
||||
}
|
||||
|
||||
case .ended:
|
||||
strongSelf.isDrawing = false
|
||||
strongSelf.strokeRecognitionTimer?.invalidate()
|
||||
strongSelf.strokeRecognitionTimer = nil
|
||||
strongSelf.uncommitedElement?.updatePath(path, state: state)
|
||||
Queue.mainQueue().after(0.05) {
|
||||
strongSelf.finishDrawing()
|
||||
}
|
||||
strongSelf.updateInternalState()
|
||||
case .cancelled:
|
||||
strongSelf.isDrawing = false
|
||||
strongSelf.strokeRecognitionTimer?.invalidate()
|
||||
strongSelf.strokeRecognitionTimer = nil
|
||||
strongSelf.cancelDrawing()
|
||||
strongSelf.updateInternalState()
|
||||
}
|
||||
}
|
||||
self.drawingGesturePipeline = drawingGesturePipeline
|
||||
@ -423,6 +359,23 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
||||
self.strokeRecognitionTimer?.invalidate()
|
||||
}
|
||||
|
||||
public func setup(withDrawing drawingData: Data!) {
|
||||
|
||||
}
|
||||
|
||||
var drawingData: Data? {
|
||||
guard !self.elements.isEmpty 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
|
||||
@ -437,6 +390,10 @@ 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:
|
||||
@ -448,7 +405,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
||||
if let strongSelf = self {
|
||||
strongSelf.cancelDrawing()
|
||||
|
||||
let newElement = FillTool(drawingSize: strongSelf.imageSize, color: strongSelf.toolColor, lineWidth: 0.0, arrow: false)
|
||||
let newElement = FillTool(drawingSize: strongSelf.imageSize, color: strongSelf.toolColor)
|
||||
strongSelf.uncommitedElement = newElement
|
||||
strongSelf.finishDrawing()
|
||||
}
|
||||
@ -561,15 +518,16 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
||||
let complete: (Bool) -> Void = { synchronous in
|
||||
self.commit(interactive: true, synchronous: synchronous)
|
||||
|
||||
self.redoElements.removeAll()
|
||||
self.redoStack.removeAll()
|
||||
if let uncommitedElement = self.uncommitedElement {
|
||||
self.elements.append(uncommitedElement)
|
||||
self.undoStack.append(.element(uncommitedElement))
|
||||
self.uncommitedElement = nil
|
||||
}
|
||||
|
||||
self.updateInternalState()
|
||||
}
|
||||
if let uncommitedElement = self.uncommitedElement as? PenTool, uncommitedElement.arrow {
|
||||
if let uncommitedElement = self.uncommitedElement as? PenTool, uncommitedElement.hasArrow {
|
||||
uncommitedElement.finishArrow({
|
||||
complete(true)
|
||||
})
|
||||
@ -584,7 +542,8 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
||||
|
||||
self.uncommitedElement = nil
|
||||
self.elements.removeAll()
|
||||
self.redoElements.removeAll()
|
||||
self.undoStack.removeAll()
|
||||
self.redoStack.removeAll()
|
||||
|
||||
let snapshotView = UIImageView(image: self.drawingImage)
|
||||
snapshotView.frame = self.bounds
|
||||
@ -605,38 +564,89 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
||||
}
|
||||
|
||||
private func undo() {
|
||||
guard let lastElement = self.elements.last else {
|
||||
guard let lastOperation = self.undoStack.last else {
|
||||
return
|
||||
}
|
||||
self.uncommitedElement = nil
|
||||
self.redoElements.append(lastElement)
|
||||
self.elements.removeLast()
|
||||
|
||||
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()
|
||||
})
|
||||
switch lastOperation {
|
||||
case let .element(element):
|
||||
self.uncommitedElement = nil
|
||||
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()
|
||||
})
|
||||
}
|
||||
case let .addEntity(uuid):
|
||||
if let entityView = self.entitiesView?.getView(for: uuid) {
|
||||
self.entitiesView?.remove(uuid: uuid, animated: true, announce: false)
|
||||
self.redoStack.append(.removeEntity(entityView.entity))
|
||||
}
|
||||
case let .removeEntity(entity):
|
||||
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)
|
||||
}
|
||||
}
|
||||
self.redoStack.append(.addEntity(entity.uuid))
|
||||
}
|
||||
|
||||
self.undoStack.removeLast()
|
||||
|
||||
self.updateInternalState()
|
||||
}
|
||||
|
||||
private func redo() {
|
||||
guard let lastElement = self.redoElements.last else {
|
||||
guard let lastOperation = self.redoStack.last else {
|
||||
return
|
||||
}
|
||||
self.uncommitedElement = nil
|
||||
self.elements.append(lastElement)
|
||||
self.redoElements.removeLast()
|
||||
self.uncommitedElement = lastElement
|
||||
|
||||
self.commit(reset: false)
|
||||
self.uncommitedElement = nil
|
||||
|
||||
switch lastOperation {
|
||||
case let .element(element):
|
||||
self.uncommitedElement = nil
|
||||
self.elements.append(element)
|
||||
self.undoStack.append(.element(element))
|
||||
self.uncommitedElement = element
|
||||
|
||||
self.commit(reset: false)
|
||||
self.uncommitedElement = nil
|
||||
case let .addEntity(uuid):
|
||||
if let entityView = self.entitiesView?.getView(for: uuid) {
|
||||
self.entitiesView?.remove(uuid: uuid, animated: true, announce: false)
|
||||
self.undoStack.append(.removeEntity(entityView.entity))
|
||||
}
|
||||
case let .removeEntity(entity):
|
||||
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)
|
||||
}
|
||||
}
|
||||
self.undoStack.append(.addEntity(entity.uuid))
|
||||
}
|
||||
|
||||
self.redoStack.removeLast()
|
||||
|
||||
self.updateInternalState()
|
||||
}
|
||||
|
||||
func onEntityAdded(_ entity: DrawingEntity) {
|
||||
self.redoStack.removeAll()
|
||||
self.undoStack.append(.addEntity(entity.uuid))
|
||||
|
||||
self.updateInternalState()
|
||||
}
|
||||
|
||||
func onEntityRemoved(_ entity: DrawingEntity) {
|
||||
self.redoStack.removeAll()
|
||||
self.undoStack.append(.removeEntity(entity))
|
||||
|
||||
self.updateInternalState()
|
||||
}
|
||||
@ -723,9 +733,9 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
||||
|
||||
private func updateInternalState() {
|
||||
self.stateUpdated(NavigationState(
|
||||
canUndo: !self.elements.isEmpty,
|
||||
canRedo: !self.redoElements.isEmpty,
|
||||
canClear: !self.elements.isEmpty,
|
||||
canUndo: !self.elements.isEmpty || !self.undoStack.isEmpty,
|
||||
canRedo: !self.redoStack.isEmpty,
|
||||
canClear: !self.elements.isEmpty || !(self.entitiesView?.entities.isEmpty ?? true),
|
||||
canZoomOut: self.zoomScale > 1.0 + .ulpOfOne,
|
||||
isDrawing: self.isDrawing
|
||||
))
|
||||
@ -745,15 +755,14 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
||||
drawingSize: self.imageSize,
|
||||
color: self.toolColor,
|
||||
lineWidth: self.toolBrushSize * scale,
|
||||
arrow: self.toolHasArrow
|
||||
hasArrow: self.toolHasArrow
|
||||
)
|
||||
element = penTool
|
||||
case .marker:
|
||||
let markerTool = MarkerTool(
|
||||
drawingSize: self.imageSize,
|
||||
color: self.toolColor,
|
||||
lineWidth: self.toolBrushSize * scale,
|
||||
arrow: self.toolHasArrow
|
||||
lineWidth: self.toolBrushSize * scale
|
||||
)
|
||||
markerTool.metalView = self.metalView
|
||||
element = markerTool
|
||||
@ -761,24 +770,13 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
||||
element = NeonTool(
|
||||
drawingSize: self.imageSize,
|
||||
color: self.toolColor,
|
||||
lineWidth: self.toolBrushSize * scale,
|
||||
arrow: self.toolHasArrow
|
||||
lineWidth: self.toolBrushSize * scale
|
||||
)
|
||||
case .pencil:
|
||||
let pencilTool = PencilTool(
|
||||
drawingSize: self.imageSize,
|
||||
color: self.toolColor,
|
||||
lineWidth: self.toolBrushSize * scale,
|
||||
arrow: self.toolHasArrow
|
||||
)
|
||||
pencilTool.metalView = self.metalView
|
||||
element = pencilTool
|
||||
case .blur:
|
||||
let blurTool = BlurTool(
|
||||
drawingSize: self.imageSize,
|
||||
color: self.toolColor,
|
||||
lineWidth: self.toolBrushSize * scale,
|
||||
arrow: false)
|
||||
lineWidth: self.toolBrushSize * scale
|
||||
)
|
||||
blurTool.getFullImage = { [weak self] in
|
||||
return self?.preparredEraserImage
|
||||
}
|
||||
@ -786,9 +784,8 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
||||
case .eraser:
|
||||
let eraserTool = EraserTool(
|
||||
drawingSize: self.imageSize,
|
||||
color: self.toolColor,
|
||||
lineWidth: self.toolBrushSize * scale,
|
||||
arrow: false)
|
||||
lineWidth: self.toolBrushSize * scale
|
||||
)
|
||||
eraserTool.getFullImage = { [weak self] in
|
||||
return self?.preparredEraserImage
|
||||
}
|
||||
|
@ -2,30 +2,56 @@ import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
|
||||
final class PenTool: DrawingElement {
|
||||
final class PenTool: DrawingElement, Codable {
|
||||
class RenderLayer: SimpleLayer, DrawingRenderLayer {
|
||||
private weak var element: PenTool?
|
||||
|
||||
private var segmentsCount = 0
|
||||
private var velocity: CGFloat?
|
||||
private var previousRect: CGRect?
|
||||
|
||||
var displayLink: ConstantDisplayLinkAnimator?
|
||||
func setup(size: CGSize) {
|
||||
self.shouldRasterize = true
|
||||
self.contentsScale = 1.0
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
private var color: UIColor?
|
||||
private var line: StrokeLine?
|
||||
fileprivate func draw(line: StrokeLine, color: UIColor, rect: CGRect) {
|
||||
self.line = line
|
||||
self.color = color
|
||||
self.setNeedsDisplay(rect.insetBy(dx: -50.0, dy: -50.0))
|
||||
|
||||
fileprivate func draw(element: PenTool, velocity: CGFloat, rect: CGRect) {
|
||||
self.element = element
|
||||
|
||||
self.previousRect = rect
|
||||
if let previous = self.velocity {
|
||||
self.velocity = velocity * 0.4 + previous * 0.6
|
||||
} else {
|
||||
self.velocity = velocity
|
||||
}
|
||||
self.setNeedsDisplay(rect.insetBy(dx: -80.0, dy: -80.0))
|
||||
}
|
||||
|
||||
func animateArrowPaths(leftArrowPath: UIBezierPath, rightArrowPath: UIBezierPath, lineWidth: CGFloat, completion: @escaping () -> Void) {
|
||||
let leftArrowShape = CAShapeLayer()
|
||||
leftArrowShape.path = leftArrowPath.cgPath
|
||||
leftArrowShape.lineWidth = lineWidth
|
||||
leftArrowShape.strokeColor = self.color?.cgColor
|
||||
leftArrowShape.strokeColor = self.element?.color.toCGColor()
|
||||
leftArrowShape.lineCap = .round
|
||||
leftArrowShape.frame = self.bounds
|
||||
self.addSublayer(leftArrowShape)
|
||||
@ -33,7 +59,7 @@ final class PenTool: DrawingElement {
|
||||
let rightArrowShape = CAShapeLayer()
|
||||
rightArrowShape.path = rightArrowPath.cgPath
|
||||
rightArrowShape.lineWidth = lineWidth
|
||||
rightArrowShape.strokeColor = self.color?.cgColor
|
||||
rightArrowShape.strokeColor = self.element?.color.toCGColor()
|
||||
rightArrowShape.lineCap = .round
|
||||
rightArrowShape.frame = self.bounds
|
||||
self.addSublayer(rightArrowShape)
|
||||
@ -48,84 +74,79 @@ final class PenTool: DrawingElement {
|
||||
}
|
||||
|
||||
override func draw(in ctx: CGContext) {
|
||||
self.line?.drawInContext(ctx)
|
||||
self.element?.drawSegments(in: ctx, upTo: self.segmentsCount)
|
||||
}
|
||||
}
|
||||
|
||||
let uuid = UUID()
|
||||
|
||||
let uuid: UUID
|
||||
let drawingSize: CGSize
|
||||
let color: DrawingColor
|
||||
let lineWidth: CGFloat
|
||||
let arrow: Bool
|
||||
|
||||
var path: Polyline?
|
||||
var boundingBox: CGRect?
|
||||
|
||||
private var renderLine: StrokeLine
|
||||
let renderLineWidth: CGFloat
|
||||
let renderMinLineWidth: CGFloat
|
||||
|
||||
let hasArrow: Bool
|
||||
let renderArrowLength: CGFloat
|
||||
let renderArrowLineWidth: CGFloat
|
||||
|
||||
var didSetupArrow = false
|
||||
|
||||
var arrowLeftPath: UIBezierPath?
|
||||
var arrowLeftPoint: CGPoint?
|
||||
var arrowRightPath: UIBezierPath?
|
||||
var arrowRightPoint: CGPoint?
|
||||
|
||||
var translation = CGPoint()
|
||||
var translation: CGPoint = .zero
|
||||
|
||||
private var currentRenderLayer: DrawingRenderLayer?
|
||||
private weak var currentRenderLayer: DrawingRenderLayer?
|
||||
|
||||
var bounds: CGRect {
|
||||
return self.path?.bounds.offsetBy(dx: self.translation.x, dy: self.translation.y) ?? .zero
|
||||
}
|
||||
|
||||
var points: [Polyline.Point] {
|
||||
guard let linePath = self.path else {
|
||||
return []
|
||||
}
|
||||
var points: [Polyline.Point] = []
|
||||
for point in linePath.points {
|
||||
points.append(point.offsetBy(self.translation))
|
||||
}
|
||||
return points
|
||||
}
|
||||
|
||||
func containsPoint(_ point: CGPoint) -> Bool {
|
||||
return false
|
||||
// return self.renderPath?.contains(point.offsetBy(CGPoint(x: -self.translation.x, y: -self.translation.y))) ?? false
|
||||
}
|
||||
|
||||
func hasPointsInsidePath(_ path: UIBezierPath) -> Bool {
|
||||
if let linePath = self.path {
|
||||
let pathBoundingBox = path.bounds
|
||||
if self.bounds.intersects(pathBoundingBox) {
|
||||
for point in linePath.points {
|
||||
if path.contains(point.location.offsetBy(self.translation)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
required init(drawingSize: CGSize, color: DrawingColor, lineWidth: CGFloat, arrow: Bool) {
|
||||
required init(drawingSize: CGSize, color: DrawingColor, lineWidth: CGFloat, hasArrow: Bool) {
|
||||
self.uuid = UUID()
|
||||
self.drawingSize = drawingSize
|
||||
self.color = color
|
||||
self.lineWidth = lineWidth
|
||||
self.arrow = arrow
|
||||
self.hasArrow = hasArrow
|
||||
|
||||
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
|
||||
|
||||
self.renderLineWidth = lineWidth
|
||||
self.renderMinLineWidth = minLineWidth + (lineWidth - minLineWidth) * 0.3
|
||||
self.renderArrowLength = lineWidth * 3.0
|
||||
self.renderArrowLineWidth = lineWidth * 0.8
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case uuid
|
||||
case drawingSize
|
||||
case color
|
||||
case hasArrow
|
||||
|
||||
self.renderLine = StrokeLine(color: color.toUIColor(), minLineWidth: minLineWidth + (lineWidth - minLineWidth) * 0.3, lineWidth: lineWidth)
|
||||
case renderLineWidth
|
||||
case renderMinLineWidth
|
||||
case renderArrowLength
|
||||
case renderArrowLineWidth
|
||||
|
||||
case renderSegments
|
||||
}
|
||||
|
||||
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.color = try container.decode(DrawingColor.self, forKey: .color)
|
||||
self.hasArrow = try container.decode(Bool.self, forKey: .hasArrow)
|
||||
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)
|
||||
}
|
||||
|
||||
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.color, forKey: .color)
|
||||
try container.encode(self.hasArrow, forKey: .hasArrow)
|
||||
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)
|
||||
}
|
||||
|
||||
func finishArrow(_ completion: @escaping () -> Void) {
|
||||
@ -150,70 +171,78 @@ final class PenTool: DrawingElement {
|
||||
guard case let .polyline(line) = path, let point = line.points.last else {
|
||||
return
|
||||
}
|
||||
self.path = line
|
||||
|
||||
|
||||
let filterDistance: CGFloat
|
||||
if point.velocity > 1200 {
|
||||
if point.velocity > 1200.0 {
|
||||
filterDistance = 75.0
|
||||
} else {
|
||||
filterDistance = 35.0
|
||||
}
|
||||
|
||||
if let previousPoint, point.location.distance(to: previousPoint) < filterDistance, state == .changed, self.renderLine.ready {
|
||||
if let previousPoint, point.location.distance(to: previousPoint) < filterDistance, state == .changed, self.segments.count > 0 {
|
||||
return
|
||||
}
|
||||
self.previousPoint = point.location
|
||||
|
||||
let rect = self.renderLine.draw(at: point)
|
||||
if let currentRenderLayer = self.currentRenderLayer as? RenderLayer {
|
||||
currentRenderLayer.draw(line: self.renderLine, color: self.color.toUIColor(), rect: rect)
|
||||
var velocity = point.velocity
|
||||
if velocity.isZero {
|
||||
velocity = 600.0
|
||||
}
|
||||
|
||||
var effectiveRenderLineWidth = max(self.renderMinLineWidth, min(self.renderLineWidth + 1.0 - (velocity / 180.0), self.renderLineWidth))
|
||||
if let previousRenderLineWidth = self.previousRenderLineWidth {
|
||||
effectiveRenderLineWidth = effectiveRenderLineWidth * 0.2 + previousRenderLineWidth * 0.8
|
||||
}
|
||||
self.previousRenderLineWidth = effectiveRenderLineWidth
|
||||
|
||||
let rect = append(point: Point(position: point.location, width: effectiveRenderLineWidth))
|
||||
|
||||
if let currentRenderLayer = self.currentRenderLayer as? RenderLayer, let rect = rect {
|
||||
currentRenderLayer.draw(element: self, velocity: point.velocity, rect: rect)
|
||||
}
|
||||
|
||||
if state == .ended {
|
||||
if self.arrow {
|
||||
let points = self.path?.points ?? []
|
||||
|
||||
if self.hasArrow {
|
||||
var direction: CGFloat?
|
||||
if points.count > 4 {
|
||||
let p2 = points[points.count - 1].location
|
||||
for i in 1 ..< min(points.count - 2, 12) {
|
||||
let p1 = points[points.count - 1 - i].location
|
||||
if p1.distance(to: p2) > renderArrowLength * 0.5 {
|
||||
if self.smoothPoints.count > 4 {
|
||||
let p2 = self.smoothPoints[self.smoothPoints.count - 1].position
|
||||
for i in 1 ..< min(self.smoothPoints.count - 2, 200) {
|
||||
let p1 = self.smoothPoints[self.smoothPoints.count - 1 - i].position
|
||||
if p1.distance(to: p2) > self.renderArrowLength * 0.5 {
|
||||
direction = p2.angle(to: p1)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let point = points.last?.location, let direction {
|
||||
if let point = self.smoothPoints.last?.position, let direction {
|
||||
let arrowLeftPath = UIBezierPath()
|
||||
arrowLeftPath.move(to: point)
|
||||
let leftPoint = point.pointAt(distance: self.renderArrowLength, angle: direction - 0.45)
|
||||
arrowLeftPath.addLine(to: leftPoint)
|
||||
arrowLeftPath.addLine(to: point.pointAt(distance: self.renderArrowLength, angle: direction - 0.45))
|
||||
|
||||
let arrowRightPath = UIBezierPath()
|
||||
arrowRightPath.move(to: point)
|
||||
let rightPoint = point.pointAt(distance: self.renderArrowLength, angle: direction + 0.45)
|
||||
arrowRightPath.addLine(to: rightPoint)
|
||||
arrowRightPath.addLine(to: point.pointAt(distance: self.renderArrowLength, angle: direction + 0.45))
|
||||
|
||||
self.arrowLeftPath = arrowLeftPath
|
||||
self.arrowLeftPoint = leftPoint
|
||||
|
||||
self.arrowRightPath = arrowRightPath
|
||||
self.arrowRightPoint = rightPoint
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func draw(in context: CGContext, size: CGSize) {
|
||||
guard !self.segments.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
context.saveGState()
|
||||
|
||||
context.translateBy(x: self.translation.x, y: self.translation.y)
|
||||
|
||||
context.setShouldAntialias(true)
|
||||
|
||||
self.renderLine.drawInContext(context)
|
||||
self.drawSegments(in: context, upTo: self.segments.count)
|
||||
|
||||
if let arrowLeftPath, let arrowRightPath {
|
||||
context.setStrokeColor(self.color.toCGColor())
|
||||
@ -229,74 +258,37 @@ final class PenTool: DrawingElement {
|
||||
|
||||
context.restoreGState()
|
||||
}
|
||||
}
|
||||
|
||||
private class StrokeLine {
|
||||
struct Segment {
|
||||
|
||||
private struct Segment {
|
||||
let a: CGPoint
|
||||
let b: CGPoint
|
||||
let c: CGPoint
|
||||
let d: CGPoint
|
||||
let abWidth: CGFloat
|
||||
let cdWidth: CGFloat
|
||||
let radius1: CGFloat
|
||||
let radius2: CGFloat
|
||||
let rect: CGRect
|
||||
}
|
||||
|
||||
struct Point {
|
||||
private struct Point {
|
||||
let position: CGPoint
|
||||
let width: CGFloat
|
||||
|
||||
init(position: CGPoint, width: CGFloat) {
|
||||
init(
|
||||
position: CGPoint,
|
||||
width: CGFloat
|
||||
) {
|
||||
self.position = position
|
||||
self.width = width
|
||||
}
|
||||
}
|
||||
|
||||
private(set) var points: [Point] = []
|
||||
private var points: [Point] = []
|
||||
private var smoothPoints: [Point] = []
|
||||
private var segments: [Segment] = []
|
||||
|
||||
private let minLineWidth: CGFloat
|
||||
let lineWidth: CGFloat
|
||||
private var lastWidth: CGFloat?
|
||||
|
||||
var ready = false
|
||||
private var previousRenderLineWidth: CGFloat?
|
||||
|
||||
let color: UIColor
|
||||
|
||||
init(color: UIColor, minLineWidth: CGFloat, lineWidth: CGFloat) {
|
||||
self.color = color
|
||||
self.minLineWidth = minLineWidth
|
||||
self.lineWidth = lineWidth
|
||||
}
|
||||
|
||||
func draw(at point: Polyline.Point) -> CGRect {
|
||||
var velocity = point.velocity
|
||||
if velocity.isZero {
|
||||
velocity = 600.0
|
||||
}
|
||||
let width = extractLineWidth(from: velocity)
|
||||
self.lastWidth = width
|
||||
|
||||
let point = Point(position: point.location, width: width)
|
||||
return appendPoint(point)
|
||||
}
|
||||
|
||||
func drawInContext(_ context: CGContext) {
|
||||
self.drawSegments(self.segments, inContext: context)
|
||||
}
|
||||
|
||||
func extractLineWidth(from velocity: CGFloat) -> CGFloat {
|
||||
let minValue = self.minLineWidth
|
||||
let maxValue = self.lineWidth
|
||||
|
||||
var width = max(minValue, min(maxValue + 1.0 - (velocity / 180.0), maxValue))
|
||||
if let lastWidth = self.lastWidth {
|
||||
width = width * 0.2 + lastWidth * 0.8
|
||||
}
|
||||
return width
|
||||
}
|
||||
|
||||
func appendPoint(_ point: Point) -> CGRect {
|
||||
private func append(point: Point) -> CGRect? {
|
||||
self.points.append(point)
|
||||
|
||||
guard self.points.count > 2 else { return .null }
|
||||
@ -313,9 +305,11 @@ private class StrokeLine {
|
||||
)
|
||||
|
||||
let lastOldSmoothPoint = smoothPoints.last
|
||||
smoothPoints.append(contentsOf: newSmoothPoints)
|
||||
self.smoothPoints.append(contentsOf: newSmoothPoints)
|
||||
|
||||
guard smoothPoints.count > 1 else { return .null }
|
||||
guard self.smoothPoints.count > 1 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let newSegments: ([Segment], CGRect) = {
|
||||
guard let lastOldSmoothPoint = lastOldSmoothPoint else {
|
||||
@ -323,20 +317,18 @@ private class StrokeLine {
|
||||
}
|
||||
return segments(fromSmoothPoints: [lastOldSmoothPoint] + newSmoothPoints)
|
||||
}()
|
||||
segments.append(contentsOf: newSegments.0)
|
||||
|
||||
self.ready = true
|
||||
|
||||
self.segments.append(contentsOf: newSegments.0)
|
||||
|
||||
return newSegments.1
|
||||
}
|
||||
|
||||
func smoothPoints(fromPoint0 point0: Point, point1: Point, point2: Point) -> [Point] {
|
||||
var smoothPoints = [Point]()
|
||||
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 segmentDistance = 3.0
|
||||
let segmentDistance: CGFloat = 3.0
|
||||
let distance = midPoint1.distance(to: midPoint2)
|
||||
let numberOfSegments = min(128, max(floor(distance / segmentDistance), 32))
|
||||
|
||||
@ -354,7 +346,33 @@ private class StrokeLine {
|
||||
return smoothPoints
|
||||
}
|
||||
|
||||
func segments(fromSmoothPoints smoothPoints: [Point]) -> ([Segment], CGRect) {
|
||||
fileprivate func boundingRect(from: Int, to: Int) -> CGRect {
|
||||
var minX: CGFloat = .greatestFiniteMagnitude
|
||||
var minY: CGFloat = .greatestFiniteMagnitude
|
||||
var maxX: CGFloat = 0.0
|
||||
var maxY: CGFloat = 0.0
|
||||
|
||||
for i in from ..< to {
|
||||
let segment = self.segments[i]
|
||||
|
||||
if segment.rect.minX < minX {
|
||||
minX = segment.rect.minX
|
||||
}
|
||||
if segment.rect.maxX > maxX {
|
||||
maxX = segment.rect.maxX
|
||||
}
|
||||
if segment.rect.minY < minY {
|
||||
minY = segment.rect.minY
|
||||
}
|
||||
if segment.rect.maxY > maxY {
|
||||
maxY = segment.rect.maxY
|
||||
}
|
||||
}
|
||||
|
||||
return CGRect(x: minX, y: minY, width: maxX - minX, height: maxY - minY)
|
||||
}
|
||||
|
||||
private func segments(fromSmoothPoints smoothPoints: [Point]) -> ([Segment], CGRect) {
|
||||
var segments: [Segment] = []
|
||||
var updateRect = CGRect.null
|
||||
for i in 1 ..< smoothPoints.count {
|
||||
@ -395,38 +413,44 @@ private class StrokeLine {
|
||||
let maxX = max(a.x, b.x, c.x, d.x, ab.x, cd.x)
|
||||
let maxY = max(a.y, b.y, c.y, d.y, ab.y, cd.y)
|
||||
|
||||
updateRect = updateRect.union(CGRect(x: minX, y: minY, width: maxX - minX, height: maxY - minY))
|
||||
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, abWidth: previousWidth, cdWidth: currentWidth))
|
||||
segments.append(Segment(a: a, b: b, c: c, d: d, radius1: previousWidth / 2.0, radius2: currentWidth / 2.0, rect: segmentRect))
|
||||
}
|
||||
return (segments, updateRect)
|
||||
}
|
||||
|
||||
func drawSegments(_ segments: [Segment], inContext context: CGContext) {
|
||||
for segment in segments {
|
||||
private func drawSegments(in context: CGContext, upTo: Int) {
|
||||
context.setStrokeColor(self.color.toCGColor())
|
||||
context.setFillColor(self.color.toCGColor())
|
||||
|
||||
for i in 0 ..< upTo {
|
||||
let segment = self.segments[i]
|
||||
context.beginPath()
|
||||
|
||||
//let color = [UIColor.red, UIColor.green, UIColor.blue, UIColor.yellow].randomElement()!
|
||||
|
||||
context.setStrokeColor(color.cgColor)
|
||||
context.setFillColor(color.cgColor)
|
||||
|
||||
|
||||
context.move(to: segment.b)
|
||||
|
||||
let abStartAngle = atan2(segment.b.y - segment.a.y, segment.b.x - segment.a.x)
|
||||
let abStartAngle = atan2(
|
||||
segment.b.y - segment.a.y,
|
||||
segment.b.x - segment.a.x
|
||||
)
|
||||
context.addArc(
|
||||
center: (segment.a + segment.b)/2,
|
||||
radius: segment.abWidth/2,
|
||||
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)
|
||||
let cdStartAngle = atan2(
|
||||
segment.c.y - segment.d.y,
|
||||
segment.c.x - segment.d.x
|
||||
)
|
||||
context.addArc(
|
||||
center: (segment.c + segment.d) / 2,
|
||||
radius: segment.cdWidth/2,
|
||||
radius: segment.radius2,
|
||||
startAngle: cdStartAngle,
|
||||
endAngle: cdStartAngle + .pi,
|
||||
clockwise: true
|
||||
@ -438,3 +462,4 @@ private class StrokeLine {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -44,11 +44,12 @@ enum DrawingTextAlignment: Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
enum DrawingTextFont: Equatable, CaseIterable {
|
||||
enum DrawingTextFont: Equatable, Hashable {
|
||||
case sanFrancisco
|
||||
case newYork
|
||||
case monospaced
|
||||
case round
|
||||
case custom(String, String)
|
||||
|
||||
init(font: DrawingTextEntity.Font) {
|
||||
switch font {
|
||||
@ -60,6 +61,8 @@ enum DrawingTextFont: Equatable, CaseIterable {
|
||||
self = .monospaced
|
||||
case .round:
|
||||
self = .round
|
||||
case let .custom(font, name):
|
||||
self = .custom(font, name)
|
||||
}
|
||||
}
|
||||
|
||||
@ -73,6 +76,8 @@ enum DrawingTextFont: Equatable, CaseIterable {
|
||||
return .monospaced
|
||||
case .round:
|
||||
return .round
|
||||
case let .custom(font, name):
|
||||
return .custom(font, name)
|
||||
}
|
||||
}
|
||||
|
||||
@ -86,6 +91,8 @@ enum DrawingTextFont: Equatable, CaseIterable {
|
||||
return "Monospaced"
|
||||
case .round:
|
||||
return "Rounded"
|
||||
case let .custom(_, name):
|
||||
return name
|
||||
}
|
||||
}
|
||||
|
||||
@ -99,6 +106,8 @@ enum DrawingTextFont: Equatable, CaseIterable {
|
||||
return Font.with(size: 13.0, design: .monospace, weight: .semibold)
|
||||
case .round:
|
||||
return Font.with(size: 13.0, design: .round, weight: .semibold)
|
||||
case let .custom(font, _):
|
||||
return UIFont(name: font, size: 13.0) ?? Font.semibold(13.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -355,7 +364,10 @@ final class TextFontComponent: Component {
|
||||
|
||||
contentWidth += 36.0
|
||||
|
||||
var validIds = Set<DrawingTextFont>()
|
||||
for value in component.values {
|
||||
validIds.insert(value)
|
||||
|
||||
contentWidth += 12.0
|
||||
let button: HighlightableButton
|
||||
if let current = self.buttons[value] {
|
||||
@ -387,6 +399,13 @@ final class TextFontComponent: Component {
|
||||
}
|
||||
contentWidth += 12.0
|
||||
|
||||
for (font, button) in self.buttons {
|
||||
if !validIds.contains(font) {
|
||||
button.removeFromSuperview()
|
||||
self.buttons[font] = nil
|
||||
}
|
||||
}
|
||||
|
||||
if self.scrollView.contentSize.width != contentWidth {
|
||||
self.scrollView.contentSize = CGSize(width: contentWidth, height: 30.0)
|
||||
}
|
||||
@ -590,6 +609,16 @@ final class TextSettingsComponent: CombinedComponent {
|
||||
if component.color != nil {
|
||||
fontAvailableWidth -= 72.0
|
||||
}
|
||||
|
||||
var fonts: [DrawingTextFont] = [
|
||||
.sanFrancisco,
|
||||
.newYork,
|
||||
.monospaced,
|
||||
.round
|
||||
]
|
||||
if case .custom = component.font {
|
||||
fonts.insert(component.font, at: 0)
|
||||
}
|
||||
|
||||
let font = font.update(
|
||||
component: TextFontComponent(
|
||||
@ -617,7 +646,7 @@ final class TextSettingsComponent: CombinedComponent {
|
||||
}
|
||||
).minSize(CGSize(width: 44.0, height: 44.0))
|
||||
),
|
||||
values: DrawingTextFont.allCases,
|
||||
values: fonts,
|
||||
selectedValue: component.font,
|
||||
tag: component.tag,
|
||||
updated: { font in
|
||||
|
@ -1012,43 +1012,10 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
}
|
||||
|
||||
func setupItem(_ item: UniversalVideoGalleryItem) {
|
||||
if self.item?.content.id != item.content.id {
|
||||
func parseChapters(_ string: NSAttributedString) -> [MediaPlayerScrubbingChapter] {
|
||||
var existingTimecodes = Set<Double>()
|
||||
var timecodeRanges: [(NSRange, TelegramTimecode)] = []
|
||||
var lineRanges: [NSRange] = []
|
||||
string.enumerateAttributes(in: NSMakeRange(0, string.length), options: [], using: { attributes, range, _ in
|
||||
if let timecode = attributes[NSAttributedString.Key(TelegramTextAttributes.Timecode)] as? TelegramTimecode {
|
||||
if !existingTimecodes.contains(timecode.time) {
|
||||
timecodeRanges.append((range, timecode))
|
||||
existingTimecodes.insert(timecode.time)
|
||||
}
|
||||
}
|
||||
})
|
||||
(string.string as NSString).enumerateSubstrings(in: NSMakeRange(0, string.length), options: .byLines, using: { _, range, _, _ in
|
||||
lineRanges.append(range)
|
||||
})
|
||||
|
||||
var chapters: [MediaPlayerScrubbingChapter] = []
|
||||
for (timecodeRange, timecode) in timecodeRanges {
|
||||
inner: for lineRange in lineRanges {
|
||||
if lineRange.contains(timecodeRange.location) {
|
||||
if lineRange.length > timecodeRange.length && timecodeRange.location < lineRange.location + 4 {
|
||||
var title = ((string.string as NSString).substring(with: lineRange) as NSString).replacingCharacters(in: NSMakeRange(timecodeRange.location - lineRange.location, timecodeRange.length), with: "")
|
||||
title = title.trimmingCharacters(in: .whitespacesAndNewlines).trimmingCharacters(in: .punctuationCharacters)
|
||||
chapters.append(MediaPlayerScrubbingChapter(title: title, start: timecode.time))
|
||||
}
|
||||
break inner
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return chapters
|
||||
}
|
||||
|
||||
var chapters = parseChapters(item.caption)
|
||||
if self.item?.content.id != item.content.id {
|
||||
var chapters = parseMediaPlayerChapters(item.caption)
|
||||
if chapters.isEmpty, let description = item.description {
|
||||
chapters = parseChapters(description)
|
||||
chapters = parseMediaPlayerChapters(description)
|
||||
}
|
||||
let scrubberView = ChatVideoGalleryItemScrubberView(chapters: chapters)
|
||||
self.scrubberView = scrubberView
|
||||
@ -2710,7 +2677,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
}
|
||||
let baseNavigationController = strongSelf.baseNavigationController()
|
||||
baseNavigationController?.view.endEditing(true)
|
||||
let controller = StickerPackScreen(context: strongSelf.context, mainStickerPack: packs[0], stickerPacks: Array(packs.prefix(1)), sendSticker: nil, actionPerformed: { actions in
|
||||
let controller = StickerPackScreen(context: strongSelf.context, mainStickerPack: packs[0], stickerPacks: packs, sendSticker: nil, actionPerformed: { actions in
|
||||
if let (info, items, action) = actions.first {
|
||||
let animateInAsReplacement = false
|
||||
switch action {
|
||||
|
@ -54,6 +54,9 @@ typedef enum {
|
||||
- (void)forceStatusBarAppearanceUpdate;
|
||||
- (bool)prefersLightStatusBar;
|
||||
|
||||
- (void)lockPortrait;
|
||||
- (void)unlockPortrait;
|
||||
|
||||
- (TGNavigationBarPallete *)navigationBarPallete;
|
||||
- (TGMenuSheetPallete *)menuSheetPallete;
|
||||
- (TGMenuSheetPallete *)darkMenuSheetPallete;
|
||||
|
@ -19,9 +19,9 @@
|
||||
|
||||
@property (nonatomic, readonly) bool hasAnimation;
|
||||
|
||||
+ (instancetype)dataWithDrawingData:(NSData *)data entitiesData:(NSData *)entitiesData image:(UIImage *)image stillImage:(UIImage *)stillImage hasAnimation:(bool)hasAnimation;
|
||||
+ (instancetype)dataWithDrawingData:(NSData *)data entitiesData:(NSData *)entitiesData image:(UIImage *)image stillImage:(UIImage *)stillImage hasAnimation:(bool)hasAnimation stickers:(NSArray *)stickers;
|
||||
|
||||
+ (instancetype)dataWithPaintingImagePath:(NSString *)imagePath entitiesData:(NSData *)entitiesData hasAnimation:(bool)hasAnimation;
|
||||
+ (instancetype)dataWithPaintingImagePath:(NSString *)imagePath entitiesData:(NSData *)entitiesData hasAnimation:(bool)hasAnimation stickers:(NSArray *)stickers;
|
||||
|
||||
+ (instancetype)dataWithPaintingImagePath:(NSString *)imagePath;
|
||||
|
||||
|
@ -71,10 +71,15 @@
|
||||
|
||||
- (void)updateZoomScale:(CGFloat)scale;
|
||||
|
||||
- (void)setupWithDrawingData:(NSData *)drawingData;
|
||||
|
||||
@end
|
||||
|
||||
@protocol TGPhotoDrawingEntitiesView <NSObject>
|
||||
|
||||
@property (nonatomic, copy) CGPoint (^getEntityCenterPosition)(void);
|
||||
@property (nonatomic, copy) CGFloat (^getEntityInitialRotation)(void);
|
||||
|
||||
@property (nonatomic, copy) void(^hasSelectionChanged)(bool);
|
||||
@property (nonatomic, readonly) BOOL hasSelection;
|
||||
|
||||
@ -107,6 +112,7 @@
|
||||
safeInsets:(UIEdgeInsets)safeInsets
|
||||
statusBarHeight:(CGFloat)statusBarHeight
|
||||
inputHeight:(CGFloat)inputHeight
|
||||
orientation:(UIInterfaceOrientation)orientation
|
||||
animated:(BOOL)animated;
|
||||
|
||||
@end
|
||||
|
@ -158,12 +158,24 @@
|
||||
if (strongController == nil)
|
||||
return;
|
||||
|
||||
if (strongSelf.didFinishWithVideo != nil)
|
||||
strongSelf.didFinishWithVideo(image, asset, adjustments);
|
||||
|
||||
commit();
|
||||
|
||||
[strongController dismissAnimated:false];
|
||||
if (strongSelf.willFinishWithVideo != nil) {
|
||||
strongSelf.willFinishWithVideo(image, ^{
|
||||
if (strongSelf.didFinishWithVideo != nil)
|
||||
strongSelf.didFinishWithVideo(image, asset, adjustments);
|
||||
|
||||
commit();
|
||||
|
||||
[strongController dismissAnimated:false];
|
||||
});
|
||||
} else {
|
||||
if (strongSelf.didFinishWithVideo != nil)
|
||||
strongSelf.didFinishWithVideo(image,
|
||||
asset, adjustments);
|
||||
|
||||
commit();
|
||||
|
||||
[strongController dismissAnimated:false];
|
||||
}
|
||||
};
|
||||
[itemViews addObject:carouselItem];
|
||||
|
||||
|
@ -20,7 +20,7 @@
|
||||
|
||||
@implementation TGPaintingData
|
||||
|
||||
+ (instancetype)dataWithDrawingData:(NSData *)data entitiesData:(NSData *)entitiesData image:(UIImage *)image stillImage:(UIImage *)stillImage hasAnimation:(bool)hasAnimation
|
||||
+ (instancetype)dataWithDrawingData:(NSData *)data entitiesData:(NSData *)entitiesData image:(UIImage *)image stillImage:(UIImage *)stillImage hasAnimation:(bool)hasAnimation stickers:(NSArray *)stickers
|
||||
{
|
||||
TGPaintingData *paintingData = [[TGPaintingData alloc] init];
|
||||
paintingData->_drawingData = data;
|
||||
@ -28,14 +28,16 @@
|
||||
paintingData->_stillImage = stillImage;
|
||||
paintingData->_entitiesData = entitiesData;
|
||||
paintingData->_hasAnimation = hasAnimation;
|
||||
paintingData->_stickers = stickers;
|
||||
return paintingData;
|
||||
}
|
||||
|
||||
+ (instancetype)dataWithPaintingImagePath:(NSString *)imagePath entitiesData:(NSData *)entitiesData hasAnimation:(bool)hasAnimation {
|
||||
+ (instancetype)dataWithPaintingImagePath:(NSString *)imagePath entitiesData:(NSData *)entitiesData hasAnimation:(bool)hasAnimation stickers:(NSArray *)stickers {
|
||||
TGPaintingData *paintingData = [[TGPaintingData alloc] init];
|
||||
paintingData->_imagePath = imagePath;
|
||||
paintingData->_entitiesData = entitiesData;
|
||||
paintingData->_hasAnimation = hasAnimation;
|
||||
paintingData->_stickers = stickers;
|
||||
return paintingData;
|
||||
}
|
||||
|
||||
@ -51,6 +53,7 @@
|
||||
TGPaintingData *paintingData = [[TGPaintingData alloc] init];
|
||||
paintingData->_entitiesData = _entitiesData;
|
||||
paintingData->_hasAnimation = _hasAnimation;
|
||||
paintingData->_stickers = _stickers;
|
||||
return paintingData;
|
||||
}
|
||||
|
||||
@ -122,18 +125,6 @@
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSArray *)stickers
|
||||
{
|
||||
return @[];
|
||||
// NSMutableSet *stickers = [[NSMutableSet alloc] init];
|
||||
// for (TGPhotoPaintEntity *entity in self.entities)
|
||||
// {
|
||||
// if ([entity isKindOfClass:[TGPhotoPaintStickerEntity class]])
|
||||
// [stickers addObject:((TGPhotoPaintStickerEntity *)entity).document];
|
||||
// }
|
||||
// return [stickers allObjects];
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)object
|
||||
{
|
||||
if (object == self)
|
||||
|
@ -118,7 +118,7 @@ const CGSize TGPhotoPaintingMaxSize = { 2560.0f, 2560.0f };
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
NSLog(@"");
|
||||
[_context unlockPortrait];
|
||||
}
|
||||
|
||||
- (void)loadView
|
||||
@ -209,6 +209,8 @@ const CGSize TGPhotoPaintingMaxSize = { 2560.0f, 2560.0f };
|
||||
[strongSelf->_scrollView setZoomScale:strongSelf->_scrollView.normalZoomScale animated:true];
|
||||
};
|
||||
[_paintingWrapperView addSubview:_drawingView];
|
||||
|
||||
[_drawingView setupWithDrawingData:_photoEditor.paintingData.drawingData];
|
||||
}
|
||||
|
||||
_entitiesView.hasSelectionChanged = ^(bool hasSelection) {
|
||||
@ -219,6 +221,22 @@ const CGSize TGPhotoPaintingMaxSize = { 2560.0f, 2560.0f };
|
||||
strongSelf->_scrollView.pinchGestureRecognizer.enabled = !hasSelection;
|
||||
};
|
||||
|
||||
_entitiesView.getEntityCenterPosition = ^CGPoint {
|
||||
__strong TGPhotoDrawingController *strongSelf = weakSelf;
|
||||
if (strongSelf == nil)
|
||||
return CGPointZero;
|
||||
|
||||
return [strongSelf entityCenterPoint];
|
||||
};
|
||||
|
||||
_entitiesView.getEntityInitialRotation = ^CGFloat {
|
||||
__strong TGPhotoDrawingController *strongSelf = weakSelf;
|
||||
if (strongSelf == nil)
|
||||
return 0.0f;
|
||||
|
||||
return [strongSelf entityInitialRotation];
|
||||
};
|
||||
|
||||
[self.view setNeedsLayout];
|
||||
}
|
||||
|
||||
@ -455,6 +473,7 @@ const CGSize TGPhotoPaintingMaxSize = { 2560.0f, 2560.0f };
|
||||
#pragma mark - Transitions
|
||||
|
||||
- (void)transitionIn {
|
||||
[_context lockPortrait];
|
||||
// if (self.presentedForAvatarCreation) {
|
||||
// _drawingView.hidden = true;
|
||||
// }
|
||||
@ -771,6 +790,7 @@ const CGSize TGPhotoPaintingMaxSize = { 2560.0f, 2560.0f };
|
||||
safeInsets:UIEdgeInsetsMake(0.0, _context.safeAreaInset.left, 0.0, _context.safeAreaInset.right)
|
||||
statusBarHeight:[_context statusBarFrame].size.height
|
||||
inputHeight:_keyboardHeight
|
||||
orientation:self.effectiveOrientation
|
||||
animated:animated];
|
||||
|
||||
}
|
||||
@ -878,6 +898,16 @@ const CGSize TGPhotoPaintingMaxSize = { 2560.0f, 2560.0f };
|
||||
return UIRectEdgeTop | UIRectEdgeBottom;
|
||||
}
|
||||
|
||||
- (CGPoint)entityCenterPoint
|
||||
{
|
||||
return [_previewView convertPoint:TGPaintCenterOfRect(_previewView.bounds) toView:_entitiesView];
|
||||
}
|
||||
|
||||
- (CGFloat)entityInitialRotation
|
||||
{
|
||||
return TGCounterRotationForOrientation(_photoEditor.cropOrientation) - _photoEditor.cropRotation;
|
||||
}
|
||||
|
||||
+ (CGSize)maximumPaintingSize
|
||||
{
|
||||
static dispatch_once_t onceToken;
|
||||
|
@ -4,8 +4,6 @@
|
||||
|
||||
#import <objc/runtime.h>
|
||||
|
||||
#import <LegacyComponents/ASWatcher.h>
|
||||
|
||||
#import <Photos/Photos.h>
|
||||
|
||||
#import <LegacyComponents/TGPhotoEditorAnimation.h>
|
||||
@ -52,7 +50,7 @@
|
||||
#import <LegacyComponents/AVURLAsset+TGMediaItem.h>
|
||||
#import "TGCameraCapturedVideo.h"
|
||||
|
||||
@interface TGPhotoEditorController () <ASWatcher, TGViewControllerNavigationBarAppearance, TGMediaPickerGalleryVideoScrubberDataSource, TGMediaPickerGalleryVideoScrubberDelegate, UIDocumentInteractionControllerDelegate>
|
||||
@interface TGPhotoEditorController () <TGViewControllerNavigationBarAppearance, TGMediaPickerGalleryVideoScrubberDataSource, TGMediaPickerGalleryVideoScrubberDelegate, UIDocumentInteractionControllerDelegate>
|
||||
{
|
||||
bool _switchingTab;
|
||||
TGPhotoEditorTab _availableTabs;
|
||||
@ -102,7 +100,6 @@
|
||||
bool _hasOpenedPhotoTools;
|
||||
bool _hiddenToolbarView;
|
||||
|
||||
TGMenuContainerView *_menuContainerView;
|
||||
UIDocumentInteractionController *_documentController;
|
||||
|
||||
bool _dismissed;
|
||||
@ -136,16 +133,13 @@
|
||||
|
||||
@implementation TGPhotoEditorController
|
||||
|
||||
@synthesize actionHandle = _actionHandle;
|
||||
|
||||
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context item:(id<TGMediaEditableItem>)item intent:(TGPhotoEditorControllerIntent)intent adjustments:(id<TGMediaEditAdjustments>)adjustments caption:(NSAttributedString *)caption screenImage:(UIImage *)screenImage availableTabs:(TGPhotoEditorTab)availableTabs selectedTab:(TGPhotoEditorTab)selectedTab
|
||||
{
|
||||
self = [super initWithContext:context];
|
||||
if (self != nil)
|
||||
{
|
||||
_context = context;
|
||||
_actionHandle = [[ASHandle alloc] initWithDelegate:self releaseOnMainThread:true];
|
||||
|
||||
|
||||
self.automaticallyManageScrollViewInsets = false;
|
||||
self.autoManageStatusBarBackground = false;
|
||||
self.isImportant = true;
|
||||
@ -195,7 +189,6 @@
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[_actionHandle reset];
|
||||
[_faceDetectorDisposable dispose];
|
||||
[_thumbnailsDisposable dispose];
|
||||
}
|
||||
@ -255,11 +248,6 @@
|
||||
|
||||
void(^toolbarDoneLongPressed)(id) = ^(id sender)
|
||||
{
|
||||
__strong TGPhotoEditorController *strongSelf = weakSelf;
|
||||
if (strongSelf == nil)
|
||||
return;
|
||||
|
||||
[strongSelf doneButtonLongPressed:sender];
|
||||
};
|
||||
|
||||
void(^toolbarTabPressed)(TGPhotoEditorTab) = ^(TGPhotoEditorTab tab)
|
||||
@ -2270,45 +2258,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (void)doneButtonLongPressed:(UIButton *)sender
|
||||
{
|
||||
if (_intent == TGPhotoEditorControllerVideoIntent)
|
||||
return;
|
||||
|
||||
if (_menuContainerView != nil)
|
||||
{
|
||||
[_menuContainerView removeFromSuperview];
|
||||
_menuContainerView = nil;
|
||||
}
|
||||
|
||||
_menuContainerView = [[TGMenuContainerView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, self.view.frame.size.width, self.view.frame.size.height)];
|
||||
[self.view addSubview:_menuContainerView];
|
||||
|
||||
NSMutableArray *actions = [[NSMutableArray alloc] init];
|
||||
[actions addObject:@{ @"title": @"Save to Camera Roll", @"action": @"save" }];
|
||||
if ([_context canOpenURL:[NSURL URLWithString:@"instagram://"]])
|
||||
[actions addObject:@{ @"title": @"Share on Instagram", @"action": @"instagram" }];
|
||||
|
||||
[_menuContainerView.menuView setButtonsAndActions:actions watcherHandle:_actionHandle];
|
||||
[_menuContainerView.menuView sizeToFit];
|
||||
|
||||
CGRect titleLockIconViewFrame = [sender.superview convertRect:sender.frame toView:_menuContainerView];
|
||||
titleLockIconViewFrame.origin.y += 16.0f;
|
||||
[_menuContainerView showMenuFromRect:titleLockIconViewFrame animated:false];
|
||||
}
|
||||
|
||||
- (void)actionStageActionRequested:(NSString *)action options:(id)options
|
||||
{
|
||||
if ([action isEqualToString:@"menuAction"])
|
||||
{
|
||||
NSString *menuAction = options[@"action"];
|
||||
if ([menuAction isEqualToString:@"save"])
|
||||
[self _saveToCameraRoll];
|
||||
else if ([menuAction isEqualToString:@"instagram"])
|
||||
[self _openInInstagram];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - External Export
|
||||
|
||||
- (void)_saveToCameraRoll
|
||||
|
@ -77,7 +77,7 @@ const NSTimeInterval TGVideoEditMaximumGifDuration = 30.5;
|
||||
if (dictionary[@"originalSize"])
|
||||
adjustments->_originalSize = [dictionary[@"originalSize"] CGSizeValue];
|
||||
if (dictionary[@"entitiesData"]) {
|
||||
adjustments->_paintingData = [TGPaintingData dataWithPaintingImagePath:dictionary[@"paintingImagePath"] entitiesData:dictionary[@"entitiesData"] hasAnimation:[dictionary[@"hasAnimation"] boolValue]];
|
||||
adjustments->_paintingData = [TGPaintingData dataWithPaintingImagePath:dictionary[@"paintingImagePath"] entitiesData:dictionary[@"entitiesData"] hasAnimation:[dictionary[@"hasAnimation"] boolValue] stickers:dictionary[@"stickersData"]];
|
||||
} else if (dictionary[@"paintingImagePath"]) {
|
||||
adjustments->_paintingData = [TGPaintingData dataWithPaintingImagePath:dictionary[@"paintingImagePath"]];
|
||||
}
|
||||
|
@ -109,6 +109,19 @@ public final class LegacyControllerContext: NSObject, LegacyComponentsContext {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func lockPortrait() {
|
||||
if let controller = self.controller as? LegacyController {
|
||||
controller.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
|
||||
}
|
||||
}
|
||||
|
||||
public func unlockPortrait() {
|
||||
if let controller = self.controller as? LegacyController {
|
||||
controller.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .allButUpsideDown)
|
||||
}
|
||||
}
|
||||
|
||||
public func keyCommandController() -> TGKeyCommandController! {
|
||||
return nil
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ swift_library(
|
||||
"//submodules/RingBuffer:RingBuffer",
|
||||
"//submodules/YuvConversion:YuvConversion",
|
||||
"//submodules/Utils/RangeSet:RangeSet",
|
||||
"//submodules/TextFormat:TextFormat",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -3,6 +3,7 @@ import AsyncDisplayKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import RangeSet
|
||||
import TextFormat
|
||||
|
||||
public enum MediaPlayerScrubbingNodeCap {
|
||||
case square
|
||||
@ -29,6 +30,39 @@ public struct MediaPlayerScrubbingChapter: Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
public func parseMediaPlayerChapters(_ string: NSAttributedString) -> [MediaPlayerScrubbingChapter] {
|
||||
var existingTimecodes = Set<Double>()
|
||||
var timecodeRanges: [(NSRange, TelegramTimecode)] = []
|
||||
var lineRanges: [NSRange] = []
|
||||
string.enumerateAttributes(in: NSMakeRange(0, string.length), options: [], using: { attributes, range, _ in
|
||||
if let timecode = attributes[NSAttributedString.Key(TelegramTextAttributes.Timecode)] as? TelegramTimecode {
|
||||
if !existingTimecodes.contains(timecode.time) {
|
||||
timecodeRanges.append((range, timecode))
|
||||
existingTimecodes.insert(timecode.time)
|
||||
}
|
||||
}
|
||||
})
|
||||
(string.string as NSString).enumerateSubstrings(in: NSMakeRange(0, string.length), options: .byLines, using: { _, range, _, _ in
|
||||
lineRanges.append(range)
|
||||
})
|
||||
|
||||
var chapters: [MediaPlayerScrubbingChapter] = []
|
||||
for (timecodeRange, timecode) in timecodeRanges {
|
||||
inner: for lineRange in lineRanges {
|
||||
if lineRange.contains(timecodeRange.location) {
|
||||
if lineRange.length > timecodeRange.length && timecodeRange.location < lineRange.location + 4 {
|
||||
var title = ((string.string as NSString).substring(with: lineRange) as NSString).replacingCharacters(in: NSMakeRange(timecodeRange.location - lineRange.location, timecodeRange.length), with: "")
|
||||
title = title.trimmingCharacters(in: .whitespacesAndNewlines).trimmingCharacters(in: .punctuationCharacters)
|
||||
chapters.append(MediaPlayerScrubbingChapter(title: title, start: timecode.time))
|
||||
}
|
||||
break inner
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return chapters
|
||||
}
|
||||
|
||||
private final class MediaPlayerScrubbingNodeButton: ASDisplayNode, UIGestureRecognizerDelegate {
|
||||
var beginScrubbing: (() -> Void)?
|
||||
var endScrubbing: ((Bool) -> Void)?
|
||||
|
@ -846,8 +846,13 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr
|
||||
self?.dismissImmediately()
|
||||
})
|
||||
}))
|
||||
|
||||
var isFallback = false
|
||||
if case let .image(_, _, _, _, _, _, _, _, _, _, isFallbackValue) = rawEntry {
|
||||
isFallback = isFallbackValue
|
||||
}
|
||||
|
||||
if self.peer.id == self.context.account.peerId, let position = rawEntry.indexData?.position, position > 0 {
|
||||
if self.peer.id == self.context.account.peerId, let position = rawEntry.indexData?.position, position > 0 || isFallback {
|
||||
let title: String
|
||||
if let _ = rawEntry.videoRepresentations.last {
|
||||
title = self.presentationData.strings.ProfilePhoto_SetMainVideo
|
||||
@ -907,11 +912,14 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr
|
||||
}
|
||||
}
|
||||
}
|
||||
case let .image(_, reference, _, _, _, _, _, messageId, _, _, _):
|
||||
case let .image(_, reference, _, _, _, _, _, messageId, _, _, isFallback):
|
||||
if self.peer.id == self.context.account.peerId {
|
||||
if let reference = reference {
|
||||
if isFallback {
|
||||
let _ = self.context.engine.accountData.updateFallbackPhoto(resource: nil, videoResource: nil, videoStartTimestamp: nil, mapResourceToAvatarSizes: { _, _ in .single([:]) }).start()
|
||||
} else if let reference = reference {
|
||||
let _ = self.context.engine.accountData.removeAccountPhoto(reference: reference).start()
|
||||
}
|
||||
|
||||
if entry == self.entries.first {
|
||||
dismiss = true
|
||||
} else {
|
||||
|
@ -107,12 +107,14 @@ final class AvatarGalleryItemFooterContentNode: GalleryFooterContentNode {
|
||||
var buttonText: String?
|
||||
var canShare = true
|
||||
switch entry {
|
||||
case let .image(_, _, _, videoRepresentations, peer, date, _, _, _, _, _):
|
||||
if date != 0 {
|
||||
case let .image(_, _, _, videoRepresentations, peer, date, _, _, _, _, isFallback):
|
||||
if date != 0 || isFallback {
|
||||
nameText = peer.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? ""
|
||||
}
|
||||
if let date = date, date != 0 {
|
||||
dateText = humanReadableStringForTimestamp(strings: self.strings, dateTimeFormat: self.dateTimeFormat, timestamp: date).string
|
||||
} else if isFallback {
|
||||
dateText = !videoRepresentations.isEmpty ? self.strings.ProfilePhoto_PublicVideo : self.strings.ProfilePhoto_PublicPhoto
|
||||
}
|
||||
|
||||
if (!videoRepresentations.isEmpty) {
|
||||
|
@ -1595,7 +1595,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
||||
let isPremium = state?.isPremium == true
|
||||
|
||||
var dismissImpl: (() -> Void)?
|
||||
let controller = PremiumLimitsListScreen(context: accountContext, subject: demoSubject, source: .intro(state?.price), order: state?.configuration.perks, buttonText: isPremium ? strings.Common_OK : (state?.isAnnual == true ? strings.Premium_SubscribeForAnnual(state?.price ?? "–").string : strings.Premium_SubscribeFor(state?.price ?? "–").string), isPremium: isPremium)
|
||||
let controller = PremiumLimitsListScreen(context: accountContext, subject: demoSubject, source: .intro(state?.price), order: state?.configuration.perks, buttonText: isPremium ? strings.Common_OK : (state?.isAnnual == true ? strings.Premium_SubscribeForAnnual(state?.price ?? "—").string : strings.Premium_SubscribeFor(state?.price ?? "–").string), isPremium: isPremium)
|
||||
controller.action = { [weak state] in
|
||||
dismissImpl?()
|
||||
if state?.isPremium == false {
|
||||
@ -1611,23 +1611,6 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
||||
}
|
||||
updateIsFocused(true)
|
||||
|
||||
// let controller = PremiumDemoScreen(
|
||||
// context: accountContext,
|
||||
// subject: demoSubject,
|
||||
// source: .intro(state?.price),
|
||||
// order: state?.configuration.perks,
|
||||
// action: {
|
||||
// if state?.isPremium == false {
|
||||
// buy()
|
||||
// }
|
||||
// }
|
||||
// )
|
||||
// controller.disposed = {
|
||||
// updateIsFocused(false)
|
||||
// }
|
||||
// present(controller)
|
||||
// updateIsFocused(true)
|
||||
|
||||
addAppLogEvent(postbox: accountContext.account.postbox, type: "premium.promo_screen_tap", data: ["item": perk.identifier])
|
||||
}
|
||||
))
|
||||
|
@ -628,7 +628,6 @@ public class PremiumLimitsListScreen: ViewController {
|
||||
|
||||
self.wrappingView = UIView()
|
||||
self.containerView = UIView()
|
||||
// self.scrollView = UIScrollView()
|
||||
self.backgroundView = ComponentHostView()
|
||||
self.pagerView = ComponentHostView()
|
||||
self.closeView = ComponentHostView()
|
||||
@ -636,10 +635,7 @@ public class PremiumLimitsListScreen: ViewController {
|
||||
self.footerNode = FooterNode(theme: self.presentationData.theme, title: buttonTitle, gloss: gloss)
|
||||
|
||||
super.init()
|
||||
|
||||
// self.scrollView.delegate = self
|
||||
// self.scrollView.showsVerticalScrollIndicator = false
|
||||
|
||||
|
||||
self.containerView.clipsToBounds = true
|
||||
self.containerView.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
|
||||
|
||||
@ -651,8 +647,7 @@ public class PremiumLimitsListScreen: ViewController {
|
||||
self.containerView.addSubview(self.pagerView)
|
||||
self.containerView.addSubnode(self.footerNode)
|
||||
self.containerView.addSubview(self.closeView)
|
||||
// self.scrollView.addSubview(self.hostView)
|
||||
|
||||
|
||||
self.footerNode.action = { [weak self] in
|
||||
self?.controller?.action()
|
||||
}
|
||||
@ -889,7 +884,7 @@ public class PremiumLimitsListScreen: ViewController {
|
||||
} else {
|
||||
self.dim.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.4)
|
||||
self.containerView.layer.cornerRadius = 10.0
|
||||
|
||||
|
||||
let verticalInset: CGFloat = 44.0
|
||||
|
||||
let maxSide = max(layout.size.width, layout.size.height)
|
||||
@ -899,7 +894,6 @@ public class PremiumLimitsListScreen: ViewController {
|
||||
}
|
||||
|
||||
transition.setFrame(view: self.containerView, frame: clipFrame)
|
||||
// transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: clipFrame.size), completion: nil)
|
||||
|
||||
var clipLayout = layout.withUpdatedSize(clipFrame.size)
|
||||
if case .regular = layout.metrics.widthClass {
|
||||
@ -914,10 +908,12 @@ public class PremiumLimitsListScreen: ViewController {
|
||||
}
|
||||
|
||||
func updated(transition: Transition) {
|
||||
guard let controller = self.controller, let layout = self.currentLayout else {
|
||||
guard let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
|
||||
let contentSize = self.containerView.bounds.size
|
||||
|
||||
let backgroundSize = self.backgroundView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
@ -929,9 +925,9 @@ public class PremiumLimitsListScreen: ViewController {
|
||||
])
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: layout.size.width, height: layout.size.width)
|
||||
containerSize: CGSize(width: contentSize.width, height: contentSize.width)
|
||||
)
|
||||
self.backgroundView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - backgroundSize.width) / 2.0), y: 0.0), size: backgroundSize)
|
||||
self.backgroundView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((contentSize.width - backgroundSize.width) / 2.0), y: 0.0), size: backgroundSize)
|
||||
|
||||
var isStandalone = false
|
||||
if case .other = controller.source {
|
||||
@ -1215,9 +1211,9 @@ public class PremiumLimitsListScreen: ViewController {
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: layout.size.width, height: self.containerView.frame.height)
|
||||
containerSize: contentSize
|
||||
)
|
||||
self.pagerView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - pagerSize.width) / 2.0), y: 0.0), size: pagerSize)
|
||||
self.pagerView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((contentSize.width - pagerSize.width) / 2.0), y: 0.0), size: pagerSize)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1228,7 +1224,6 @@ public class PremiumLimitsListScreen: ViewController {
|
||||
closeImage = generateCloseButtonImage(backgroundColor: .clear, foregroundColor: UIColor(rgb: 0xffffff))!
|
||||
self.cachedCloseImage = closeImage
|
||||
}
|
||||
|
||||
|
||||
let closeSize = self.closeView.update(
|
||||
transition: .immediate,
|
||||
@ -1259,7 +1254,7 @@ public class PremiumLimitsListScreen: ViewController {
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 30.0, height: 30.0)
|
||||
)
|
||||
self.closeView.frame = CGRect(origin: CGPoint(x: layout.size.width - closeSize.width * 1.5, y: 28.0 - closeSize.height / 2.0), size: closeSize)
|
||||
self.closeView.frame = CGRect(origin: CGPoint(x: contentSize.width - closeSize.width * 1.5, y: 28.0 - closeSize.height / 2.0), size: closeSize)
|
||||
}
|
||||
private var cachedCloseImage: UIImage?
|
||||
|
||||
|
@ -824,6 +824,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
strongSelf.push(controller)
|
||||
return true
|
||||
case let .suggestedProfilePhoto(image):
|
||||
strongSelf.chatDisplayNode.dismissInput()
|
||||
if let image = image {
|
||||
if message.effectivelyIncoming(strongSelf.context.account.peerId) {
|
||||
var selectedNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?
|
||||
|
@ -106,6 +106,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
|
||||
private let scrubberNode: MediaPlayerScrubbingNode
|
||||
private let leftDurationLabel: MediaPlayerTimeTextNode
|
||||
private let rightDurationLabel: MediaPlayerTimeTextNode
|
||||
private let infoNode: ASTextNode
|
||||
|
||||
private let backwardButton: IconButtonNode
|
||||
private let forwardButton: IconButtonNode
|
||||
@ -149,6 +150,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
|
||||
private var scrubbingDisposable: Disposable?
|
||||
private var leftDurationLabelPushed = false
|
||||
private var rightDurationLabelPushed = false
|
||||
private var infoNodePushed = false
|
||||
|
||||
private var currentDuration: Double = 0.0
|
||||
private var currentPosition: Double = 0.0
|
||||
@ -196,6 +198,11 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
|
||||
self.rightDurationLabel.alignment = .right
|
||||
self.rightDurationLabel.keepPreviousValueOnEmptyState = true
|
||||
|
||||
self.infoNode = ASTextNode()
|
||||
self.infoNode.maximumNumberOfLines = 1
|
||||
self.infoNode.isUserInteractionEnabled = false
|
||||
self.infoNode.displaysAsynchronously = false
|
||||
|
||||
self.rateButton = HighlightableButtonNode()
|
||||
self.rateButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -4.0, bottom: -8.0, right: -4.0)
|
||||
self.rateButton.displaysAsynchronously = false
|
||||
@ -238,6 +245,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
|
||||
|
||||
self.addSubnode(self.leftDurationLabel)
|
||||
self.addSubnode(self.rightDurationLabel)
|
||||
self.addSubnode(self.infoNode)
|
||||
self.addSubnode(self.rateButton)
|
||||
self.addSubnode(self.scrubberNode)
|
||||
|
||||
@ -283,16 +291,20 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
|
||||
}
|
||||
let leftDurationLabelPushed: Bool
|
||||
let rightDurationLabelPushed: Bool
|
||||
let infoNodePushed: Bool
|
||||
if let value = value {
|
||||
leftDurationLabelPushed = value < 0.16
|
||||
rightDurationLabelPushed = value > (strongSelf.rateButton.isHidden ? 0.84 : 0.74)
|
||||
infoNodePushed = value >= 0.16 && value <= 0.84
|
||||
} else {
|
||||
leftDurationLabelPushed = false
|
||||
rightDurationLabelPushed = false
|
||||
infoNodePushed = false
|
||||
}
|
||||
if leftDurationLabelPushed != strongSelf.leftDurationLabelPushed || rightDurationLabelPushed != strongSelf.rightDurationLabelPushed {
|
||||
if leftDurationLabelPushed != strongSelf.leftDurationLabelPushed || rightDurationLabelPushed != strongSelf.rightDurationLabelPushed || infoNodePushed != strongSelf.infoNodePushed {
|
||||
strongSelf.leftDurationLabelPushed = leftDurationLabelPushed
|
||||
strongSelf.rightDurationLabelPushed = rightDurationLabelPushed
|
||||
strongSelf.infoNodePushed = infoNodePushed
|
||||
|
||||
if let layout = strongSelf.validLayout {
|
||||
let _ = strongSelf.updateLayout(width: layout.0, leftInset: layout.1, rightInset: layout.2, maxHeight: layout.3, transition: .animated(duration: 0.35, curve: .spring))
|
||||
@ -778,6 +790,13 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
|
||||
let rightLabelVerticalOffset: CGFloat = self.rightDurationLabelPushed ? 6.0 : 0.0
|
||||
transition.updateFrame(node: self.rightDurationLabel, frame: CGRect(origin: CGPoint(x: width - sideInset - rightInset - 100.0, y: scrubberVerticalOrigin + 14.0 + rightLabelVerticalOffset), size: CGSize(width: 100.0, height: 20.0)))
|
||||
|
||||
let infoLabelVerticalOffset: CGFloat = self.infoNodePushed ? 6.0 : 0.0
|
||||
|
||||
let infoSize = self.infoNode.measure(CGSize(width: width - 60.0 * 2.0 - 100.0, height: 100.0))
|
||||
self.infoNode.bounds = CGRect(origin: CGPoint(), size: infoSize)
|
||||
transition.updatePosition(node: self.infoNode, position: CGPoint(x: width / 2.0, y: scrubberVerticalOrigin + 14.0 + infoLabelVerticalOffset + infoSize.height / 2.0))
|
||||
|
||||
|
||||
let rateRightOffset = timestampLabelWidthForDuration(self.currentDuration)
|
||||
transition.updateFrame(node: self.rateButton, frame: CGRect(origin: CGPoint(x: width - sideInset - rightInset - rateRightOffset - 28.0, y: scrubberVerticalOrigin + 10.0 + rightLabelVerticalOffset), size: CGSize(width: 24.0, height: 24.0)))
|
||||
|
||||
|
@ -1378,17 +1378,33 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL
|
||||
interaction.suggestPhoto()
|
||||
}))
|
||||
|
||||
items[.peerDataSettings]!.append(PeerInfoScreenActionItem(id: ItemCustom, text: presentationData.strings.UserInfo_SetCustomPhoto(compactName).string, color: .accent, icon: UIImage(bundleImageName: "Settings/SetAvatar"), action: {
|
||||
let setText: String
|
||||
if user.photo.first?.isPersonal == true {
|
||||
setText = presentationData.strings.UserInfo_ChangeCustomPhoto(compactName).string
|
||||
} else {
|
||||
setText = presentationData.strings.UserInfo_SetCustomPhoto(compactName).string
|
||||
}
|
||||
|
||||
items[.peerDataSettings]!.append(PeerInfoScreenActionItem(id: ItemCustom, text: setText, color: .accent, icon: UIImage(bundleImageName: "Settings/SetAvatar"), action: {
|
||||
interaction.setCustomPhoto()
|
||||
}))
|
||||
|
||||
if user.photo.first?.isPersonal == true || state.updatingAvatar != nil {
|
||||
var representation: TelegramMediaImageRepresentation?
|
||||
var originalIsVideo: Bool?
|
||||
if let cachedData = data.cachedData as? CachedUserData, case let .known(photo) = cachedData.photo {
|
||||
representation = photo?.representationForDisplayAtSize(PixelDimensions(width: 28, height: 28))
|
||||
originalIsVideo = !(photo?.videoRepresentations.isEmpty ?? true)
|
||||
}
|
||||
|
||||
items[.peerDataSettings]!.append(PeerInfoScreenActionItem(id: ItemReset, text: presentationData.strings.UserInfo_ResetCustomPhoto, 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 removeText: String
|
||||
if let originalIsVideo {
|
||||
removeText = originalIsVideo ? presentationData.strings.UserInfo_ResetCustomVideo : presentationData.strings.UserInfo_ResetCustomPhoto
|
||||
} else {
|
||||
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: {
|
||||
interaction.resetCustomPhoto()
|
||||
}))
|
||||
}
|
||||
@ -3489,11 +3505,18 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
|
||||
return
|
||||
}
|
||||
|
||||
var isPersonal = false
|
||||
var currentIsVideo = false
|
||||
let item = strongSelf.headerNode.avatarListNode.listContainerNode.currentItemNode?.item
|
||||
if let item = item, case let .image(_, _, videoRepresentations, _, _) = item {
|
||||
if let item = item, case let .image(_, representations, videoRepresentations, _, _) = item {
|
||||
if representations.first?.representation.isPersonal == true {
|
||||
isPersonal = true
|
||||
}
|
||||
currentIsVideo = !videoRepresentations.isEmpty
|
||||
}
|
||||
guard !isPersonal else {
|
||||
return
|
||||
}
|
||||
|
||||
let items: [ContextMenuItem] = [
|
||||
.action(ContextMenuActionItem(text: currentIsVideo ? strongSelf.presentationData.strings.PeerInfo_ReportProfileVideo : strongSelf.presentationData.strings.PeerInfo_ReportProfilePhoto, icon: { theme in
|
||||
@ -6816,6 +6839,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
|
||||
}
|
||||
if let topController = self.controller?.navigationController?.topViewController as? ViewController {
|
||||
topController.presentInGlobalOverlay(statusController)
|
||||
} else if let topController = self.controller?.parentController?.topViewController as? ViewController {
|
||||
topController.presentInGlobalOverlay(statusController)
|
||||
} else {
|
||||
self.controller?.presentInGlobalOverlay(statusController)
|
||||
}
|
||||
@ -6991,6 +7016,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
|
||||
}
|
||||
if let topController = self.controller?.navigationController?.topViewController as? ViewController {
|
||||
topController.presentInGlobalOverlay(statusController)
|
||||
} else if let topController = self.controller?.parentController?.topViewController as? ViewController {
|
||||
topController.presentInGlobalOverlay(statusController)
|
||||
} else {
|
||||
self.controller?.presentInGlobalOverlay(statusController)
|
||||
}
|
||||
|
@ -36,6 +36,7 @@ private enum ApplicationSpecificSharedDataKeyValues: Int32 {
|
||||
case webBrowserSettings = 16
|
||||
case intentsSettings = 17
|
||||
case translationSettings = 18
|
||||
case drawingSettings = 19
|
||||
}
|
||||
|
||||
public struct ApplicationSpecificSharedDataKeys {
|
||||
@ -58,6 +59,7 @@ public struct ApplicationSpecificSharedDataKeys {
|
||||
public static let webBrowserSettings = applicationSpecificPreferencesKey(ApplicationSpecificSharedDataKeyValues.webBrowserSettings.rawValue)
|
||||
public static let intentsSettings = applicationSpecificPreferencesKey(ApplicationSpecificSharedDataKeyValues.intentsSettings.rawValue)
|
||||
public static let translationSettings = applicationSpecificPreferencesKey(ApplicationSpecificSharedDataKeyValues.translationSettings.rawValue)
|
||||
public static let drawingSettings = applicationSpecificPreferencesKey(ApplicationSpecificSharedDataKeyValues.drawingSettings.rawValue)
|
||||
}
|
||||
|
||||
private enum ApplicationSpecificItemCacheCollectionIdValues: Int8 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user