From 441c784146e6f716f468702cf25a4b06190836c5 Mon Sep 17 00:00:00 2001
From: Matthew Chen <matthew@signal.org>
Date: Thu, 14 Mar 2019 12:16:38 -0400
Subject: [PATCH] Add preview view to the color palette control.

---
 SignalMessaging/Views/GalleryRailView.swift   |  10 --
 .../ImageEditor/ImageEditorPaletteView.swift  | 136 +++++++++++++++++-
 SignalMessaging/categories/UIView+OWS.swift   |  23 +++
 3 files changed, 157 insertions(+), 12 deletions(-)

diff --git a/SignalMessaging/Views/GalleryRailView.swift b/SignalMessaging/Views/GalleryRailView.swift
index 6f797ea6b..4227c108c 100644
--- a/SignalMessaging/Views/GalleryRailView.swift
+++ b/SignalMessaging/Views/GalleryRailView.swift
@@ -264,13 +264,3 @@ public class GalleryRailView: UIView, GalleryRailCellViewDelegate {
         }
     }
 }
-
-public extension CGSize {
-    var aspectRatio: CGFloat {
-        guard self.height > 0 else {
-            return 0
-        }
-
-        return self.width / self.height
-    }
-}
diff --git a/SignalMessaging/Views/ImageEditor/ImageEditorPaletteView.swift b/SignalMessaging/Views/ImageEditor/ImageEditorPaletteView.swift
index 7101b333d..fc0f11032 100644
--- a/SignalMessaging/Views/ImageEditor/ImageEditorPaletteView.swift
+++ b/SignalMessaging/Views/ImageEditor/ImageEditorPaletteView.swift
@@ -62,6 +62,118 @@ public class ImageEditorColor: NSObject {
 
 // MARK: -
 
+private class PalettePreviewView: OWSLayerView {
+
+    private static let innerRadius: CGFloat = 32
+    private static let shadowMargin: CGFloat = 0
+    // The distance from the "inner circle" to the "teardrop".
+    private static let circleMargin: CGFloat = 3
+    private static let teardropTipRadius: CGFloat = 4
+    private static let teardropPointiness: CGFloat = 12
+
+    private let teardropColor = UIColor.white
+    public var selectedColor = UIColor.white {
+        didSet {
+            circleLayer.fillColor = selectedColor.cgColor
+        }
+    }
+
+    private let circleLayer: CAShapeLayer
+    private let teardropLayer: CAShapeLayer
+
+    override init() {
+        let circleLayer = CAShapeLayer()
+        let teardropLayer = CAShapeLayer()
+        self.circleLayer = circleLayer
+        self.teardropLayer = teardropLayer
+
+        super.init()
+
+        circleLayer.strokeColor = nil
+        teardropLayer.strokeColor = nil
+        // Layer order matters.
+        layer.addSublayer(teardropLayer)
+        layer.addSublayer(circleLayer)
+
+        teardropLayer.fillColor = teardropColor.cgColor
+        teardropLayer.shadowColor = UIColor.black.cgColor
+        teardropLayer.shadowRadius = 2.0
+        teardropLayer.shadowOpacity = 0.33
+        teardropLayer.shadowOffset = .zero
+
+        layoutCallback = { (view) in
+            PalettePreviewView.updateLayers(view: view,
+                                            circleLayer: circleLayer,
+                                            teardropLayer: teardropLayer)
+        }
+
+        // The bounding rect of the teardrop + shadow is non-trivial, so
+        // we use a generous size that reserves plenty of space.
+        //
+        // The size doesn't matter since this view is
+        // mostly transparent and isn't hot.
+        autoSetDimensions(to: CGSize(width: PalettePreviewView.innerRadius * 4,
+                                     height: PalettePreviewView.innerRadius * 4))
+    }
+
+    @available(*, unavailable, message: "use other init() instead.")
+    required public init?(coder aDecoder: NSCoder) {
+        notImplemented()
+    }
+
+    static func updateLayers(view: UIView,
+                             circleLayer: CAShapeLayer,
+                             teardropLayer: CAShapeLayer) {
+        let bounds = view.bounds
+        let outerRadius = innerRadius + circleMargin
+        let rightEdge = CGPoint(x: bounds.width,
+                                y: bounds.height * 0.5)
+        let teardropTipCenter = rightEdge.minus(CGPoint(x: teardropTipRadius + shadowMargin, y: 0))
+        let circleCenter = teardropTipCenter.minus(CGPoint(x: teardropPointiness + innerRadius, y: 0))
+
+        // The "teardrop" shape is bounded by 2 circles, joined by their tangents.
+        //
+        // UIBezierPath can be used to draw this using 2 arcs, if we
+        // have the angle of the tangents.
+        //
+        // Finding the tangent between two circles of known distance + radius
+        // is pretty straightforward.  We solve for the right triangle that
+        // defines the tangents and atan() that triangle to get the angle.
+        //
+        // 1. Find the length of the hypotenuse.
+        let circleCenterDistance = teardropTipCenter.minus(circleCenter).length
+        // 2. Fine the length of the first side.
+        let radiusDiff = outerRadius - teardropTipRadius
+        // 2. Fine the length of the second side.
+        let tangentLength = (circleCenterDistance.square - radiusDiff.square).squareRoot()
+        let angle = atan2(tangentLength, radiusDiff)
+
+        let teardropPath = UIBezierPath()
+        teardropPath.addArc(withCenter: circleCenter,
+                            radius: outerRadius,
+                            startAngle: +angle,
+                            endAngle: -angle,
+                            clockwise: true)
+        teardropPath.addArc(withCenter: teardropTipCenter,
+                            radius: teardropTipRadius,
+                            startAngle: -angle,
+                            endAngle: +angle,
+                            clockwise: true)
+
+        teardropLayer.path = teardropPath.cgPath
+        teardropLayer.frame = bounds
+
+        let innerCircleSize = CGSize(width: innerRadius * 2,
+                                height: innerRadius * 2)
+        let circleFrame = CGRect(origin: circleCenter.minus(innerCircleSize.asPoint.times(0.5)),
+                                 size: innerCircleSize)
+        circleLayer.path = UIBezierPath(ovalIn: circleFrame).cgPath
+        circleLayer.frame = bounds
+    }
+}
+
+// MARK: -
+
 public class ImageEditorPaletteView: UIView {
 
     public weak var delegate: ImageEditorPaletteViewDelegate?
@@ -89,6 +201,8 @@ public class ImageEditorPaletteView: UIView {
     private let imageWrapper = OWSLayerView()
     private let shadowView = UIView()
     private var selectionConstraint: NSLayoutConstraint?
+    private let previewView = PalettePreviewView()
+    private var previewConstraint: NSLayoutConstraint?
 
     private func createContents() {
         self.backgroundColor = .clear
@@ -140,6 +254,14 @@ public class ImageEditorPaletteView: UIView {
         selectionConstraint.autoInstall()
         self.selectionConstraint = selectionConstraint
 
+        previewView.isHidden = true
+        addSubview(previewView)
+        previewView.autoPinEdge(.trailing, to: .leading, of: imageView, withOffset: -24)
+        let previewConstraint = NSLayoutConstraint(item: previewView,
+                                                     attribute: .centerY, relatedBy: .equal, toItem: imageWrapper, attribute: .top, multiplier: 1, constant: 0)
+        previewConstraint.autoInstall()
+        self.previewConstraint = previewConstraint
+
         isUserInteractionEnabled = true
         addGestureRecognizer(PaletteGestureRecognizer(target: self, action: #selector(didTouch)))
 
@@ -208,6 +330,7 @@ public class ImageEditorPaletteView: UIView {
 
     private func updateState() {
         selectionView.backgroundColor = selectedValue.color
+        previewView.selectedColor = selectedValue.color
 
         guard let selectionConstraint = selectionConstraint else {
             owsFailDebug("Missing selectionConstraint.")
@@ -215,6 +338,12 @@ public class ImageEditorPaletteView: UIView {
         }
         let selectionY = imageWrapper.height() * selectedValue.palettePhase
         selectionConstraint.constant = selectionY
+
+        guard let previewConstraint = previewConstraint else {
+            owsFailDebug("Missing previewConstraint.")
+            return
+        }
+        previewConstraint.constant = selectionY
     }
 
     // MARK: Events
@@ -222,9 +351,12 @@ public class ImageEditorPaletteView: UIView {
     @objc
     func didTouch(gesture: UIGestureRecognizer) {
         switch gesture.state {
-        case .began, .changed, .ended:
-            break
+        case .began, .changed:
+            previewView.isHidden = false
+        case .ended:
+            previewView.isHidden = true
         default:
+            previewView.isHidden = true
             return
         }
 
diff --git a/SignalMessaging/categories/UIView+OWS.swift b/SignalMessaging/categories/UIView+OWS.swift
index 4c2d93ec7..a05025f0c 100644
--- a/SignalMessaging/categories/UIView+OWS.swift
+++ b/SignalMessaging/categories/UIView+OWS.swift
@@ -149,6 +149,10 @@ public extension CGFloat {
     public func fuzzyEquals(_ other: CGFloat, tolerance: CGFloat = 0.001) -> Bool {
         return abs(self - other) < tolerance
     }
+
+    public var square: CGFloat {
+        return self * self
+    }
 }
 
 public extension Int {
@@ -222,6 +226,25 @@ public extension CGPoint {
         return (x.fuzzyEquals(other.x, tolerance: tolerance) &&
             y.fuzzyEquals(other.y, tolerance: tolerance))
     }
+
+    public static func tan(angle: CGFloat) -> CGPoint {
+        return CGPoint(x: sin(angle),
+                       y: cos(angle))
+    }
+}
+
+public extension CGSize {
+    var aspectRatio: CGFloat {
+        guard self.height > 0 else {
+            return 0
+        }
+
+        return self.width / self.height
+    }
+
+    var asPoint: CGPoint {
+        return CGPoint(x: width, y: height)
+    }
 }
 
 public extension CGRect {