import Foundation import UIKit import Display import SwiftSignalKit import ComponentFlow import LegacyComponents import TelegramCore import AccountContext import TelegramPresentationData import SheetComponent import ViewControllerComponent import BlurredBackgroundComponent import SegmentedControlNode import MultilineTextComponent import HexColor import MediaEditor private let palleteColors: [UInt32] = [ 0xffffff, 0xebebeb, 0xd6d6d6, 0xc2c2c2, 0xadadad, 0x999999, 0x858585, 0x707070, 0x5c5c5c, 0x474747, 0x333333, 0x000000, 0x00374a, 0x011d57, 0x11053b, 0x2e063d, 0x3c071b, 0x5c0701, 0x5a1c00, 0x583300, 0x563d00, 0x666100, 0x4f5504, 0x263e0f, 0x004d65, 0x012f7b, 0x1a0a52, 0x450d59, 0x551029, 0x831100, 0x7b2900, 0x7a4a00, 0x785800, 0x8d8602, 0x6f760a, 0x38571a, 0x016e8f, 0x0042a9, 0x2c0977, 0x61187c, 0x791a3d, 0xb51a00, 0xad3e00, 0xa96800, 0xa67b01, 0xc4bc00, 0x9ba50e, 0x4e7a27, 0x008cb4, 0x0056d6, 0x371a94, 0x7a219e, 0x99244f, 0xe22400, 0xda5100, 0xd38301, 0xd19d01, 0xf5ec00, 0xc3d117, 0x669d34, 0x00a1d8, 0x0061fe, 0x4d22b2, 0x982abc, 0xb92d5d, 0xff4015, 0xff6a00, 0xffab01, 0xfdc700, 0xfefb41, 0xd9ec37, 0x76bb40, 0x01c7fc, 0x3a87fe, 0x5e30eb, 0xbe38f3, 0xe63b7a, 0xff6250, 0xff8648, 0xfeb43f, 0xfecb3e, 0xfff76b, 0xe4ef65, 0x96d35f, 0x52d6fc, 0x74a7ff, 0x864ffe, 0xd357fe, 0xee719e, 0xff8c82, 0xffa57d, 0xffc777, 0xffd977, 0xfff994, 0xeaf28f, 0xb1dd8b, 0x93e3fd, 0xa7c6ff, 0xb18cfe, 0xe292fe, 0xf4a4c0, 0xffb5af, 0xffc5ab, 0xffd9a8, 0xfee4a8, 0xfffbb9, 0xf2f7b7, 0xcde8b5, 0xcbf0ff, 0xd3e2ff, 0xd9c9fe, 0xefcaff, 0xf9d3e0, 0xffdbd8, 0xffe2d6, 0xffecd4, 0xfff2d5, 0xfefcdd, 0xf7fadb, 0xdfeed4 ] private class GradientLayer: CAGradientLayer { override func action(forKey event: String) -> CAAction? { return nullAction } } private struct ColorSelectionImage: Equatable { var _image: UIImage? let size: CGSize let topLeftRadius: CGFloat let topRightRadius: CGFloat let bottomLeftRadius: CGFloat let bottomRightRadius: CGFloat let isLight: Bool init(size: CGSize, topLeftRadius: CGFloat, topRightRadius: CGFloat, bottomLeftRadius: CGFloat, bottomRightRadius: CGFloat, isLight: Bool) { self.size = size self.topLeftRadius = topLeftRadius self.topRightRadius = topRightRadius self.bottomLeftRadius = bottomLeftRadius self.bottomRightRadius = bottomRightRadius self.isLight = isLight } public static func ==(lhs: ColorSelectionImage, rhs: ColorSelectionImage) -> Bool { if lhs.size != rhs.size { return false } if lhs.topLeftRadius != rhs.topLeftRadius { return false } if lhs.topRightRadius != rhs.topRightRadius { return false } if lhs.bottomLeftRadius != rhs.bottomLeftRadius { return false } if lhs.bottomRightRadius != rhs.bottomRightRadius { return false } if lhs.isLight != rhs.isLight { return false } return true } mutating func getImage() -> UIImage { if self._image == nil { self._image = generateColorSelectionImage(size: self.size, topLeftRadius: self.topLeftRadius, topRightRadius: self.topRightRadius, bottomLeftRadius: self.bottomLeftRadius, bottomRightRadius: self.bottomRightRadius, isLight: self.isLight) } return self._image! } } private func generateColorSelectionImage(size: CGSize, topLeftRadius: CGFloat, topRightRadius: CGFloat, bottomLeftRadius: CGFloat, bottomRightRadius: CGFloat, isLight: Bool) -> UIImage? { let margin: CGFloat = 10.0 let realSize = size let image = generateImage(CGSize(width: size.width + margin * 2.0, height: size.height + margin * 2.0), opaque: false, rotatedContext: { size, context in context.clear(CGRect(origin: .zero, size: size)) let path = UIBezierPath(roundRect: CGRect(origin: CGPoint(x: margin, y: margin), size: realSize), topLeftRadius: topLeftRadius, topRightRadius: topRightRadius, bottomLeftRadius: bottomLeftRadius, bottomRightRadius: bottomRightRadius) context.addPath(path.cgPath) context.setShadow(offset: CGSize(), blur: 9.0, color: UIColor(rgb: 0x000000, alpha: 0.15).cgColor) context.setLineWidth(3.0 - UIScreenPixel) context.setStrokeColor(UIColor(rgb: isLight ? 0xffffff : 0x1a1a1c).cgColor) context.strokePath() }) return image } private func generateColorGridImage(size: CGSize) -> UIImage? { return generateImage(size, opaque: true, rotatedContext: { size, context in let squareSize = floorToScreenPixels(size.width / 12.0) var index = 0 for row in 0 ..< 10 { for col in 0 ..< 12 { let color = palleteColors[index] var correctedSize = squareSize if col == 11 { correctedSize = size.width - squareSize * 11.0 } let rect = CGRect(origin: CGPoint(x: CGFloat(col) * squareSize, y: CGFloat(row) * squareSize), size: CGSize(width: correctedSize, height: squareSize)) context.setFillColor(UIColor(rgb: color).cgColor) context.fill(rect) index += 1 } } }) } private func generateCheckeredImage(size: CGSize, whiteColor: UIColor, blackColor: UIColor, length: CGFloat) -> UIImage? { return generateImage(size, opaque: false, rotatedContext: { size, context in context.clear(CGRect(origin: .zero, size: size)) let w = Int(ceil(size.width / length)) let h = Int(ceil(size.height / length)) for i in 0 ..< w { for j in 0 ..< h { if (i % 2) != (j % 2) { context.setFillColor(whiteColor.cgColor) } else { context.setFillColor(blackColor.cgColor) } context.fill(CGRect(origin: CGPoint(x: CGFloat(i) * length, y: CGFloat(j) * length), size: CGSize(width: length, height: length))) } } }) } private func generateKnobImage() -> UIImage? { let side: CGFloat = 32.0 let margin: CGFloat = 10.0 let image = generateImage(CGSize(width: side + margin * 2.0, height: side + margin * 2.0), opaque: false, rotatedContext: { size, context in context.clear(CGRect(origin: .zero, size: size)) context.setShadow(offset: CGSize(width: 0.0, height: 0.0), blur: 9.0, color: UIColor(rgb: 0x000000, alpha: 0.3).cgColor) context.setFillColor(UIColor(rgb: 0x1a1a1c).cgColor) context.fillEllipse(in: CGRect(origin: CGPoint(x: margin, y: margin), size: CGSize(width: side, height: side))) }) return image } private class ColorSliderComponent: Component { let leftColor: DrawingColor let rightColor: DrawingColor let currentColor: DrawingColor let value: CGFloat let updated: (CGFloat) -> Void public init( leftColor: DrawingColor, rightColor: DrawingColor, currentColor: DrawingColor, value: CGFloat, updated: @escaping (CGFloat) -> Void ) { self.leftColor = leftColor self.rightColor = rightColor self.currentColor = currentColor self.value = value self.updated = updated } public static func ==(lhs: ColorSliderComponent, rhs: ColorSliderComponent) -> Bool { if lhs.leftColor != rhs.leftColor { return false } if lhs.rightColor != rhs.rightColor { return false } if lhs.currentColor != rhs.currentColor { return false } if lhs.value != rhs.value { return false } return true } final class View: UIView, UIGestureRecognizerDelegate { private var validSize: CGSize? private let wrapper = UIView(frame: CGRect()) private let transparencyLayer = SimpleLayer() private let gradientLayer = GradientLayer() private let knob = SimpleLayer() private let circle = SimpleShapeLayer() fileprivate var updated: (CGFloat) -> Void = { _ in } @objc func handlePress(_ gestureRecognizer: UILongPressGestureRecognizer) { let side: CGFloat = 36.0 let location = gestureRecognizer.location(in: self).offsetBy(dx: -side * 0.5, dy: 0.0) guard self.frame.width > 0.0, case .began = gestureRecognizer.state else { return } let value = max(0.0, min(1.0, location.x / (self.frame.width - side))) self.updated(value) } @objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { if gestureRecognizer.state == .changed { let side: CGFloat = 36.0 let location = gestureRecognizer.location(in: self).offsetBy(dx: -side * 0.5, dy: 0.0) guard self.frame.width > 0.0 else { return } let value = max(0.0, min(1.0, location.x / (self.frame.width - side))) self.updated(value) } } func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { return true } func updateLayout(size: CGSize, leftColor: DrawingColor, rightColor: DrawingColor, currentColor: DrawingColor, value: CGFloat) -> CGSize { let previousSize = self.validSize let sliderSize = CGSize(width: size.width, height: 36.0) self.validSize = sliderSize self.gradientLayer.type = .axial self.gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5) self.gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5) self.gradientLayer.colors = [leftColor.toUIColor().cgColor, rightColor.toUIColor().cgColor] if leftColor.alpha < 1.0 || rightColor.alpha < 1.0 { self.transparencyLayer.isHidden = false } else { self.transparencyLayer.isHidden = true } if previousSize != sliderSize { self.wrapper.frame = CGRect(origin: .zero, size: sliderSize) if self.wrapper.superview == nil { self.addSubview(self.wrapper) } self.transparencyLayer.frame = CGRect(origin: .zero, size: sliderSize) if self.transparencyLayer.superlayer == nil { self.wrapper.layer.addSublayer(self.transparencyLayer) } self.gradientLayer.frame = CGRect(origin: .zero, size: sliderSize) if self.gradientLayer.superlayer == nil { self.wrapper.layer.addSublayer(self.gradientLayer) } if self.knob.superlayer == nil { self.layer.addSublayer(self.knob) } if self.circle.superlayer == nil { self.circle.path = UIBezierPath(ovalIn: CGRect(origin: .zero, size: CGSize(width: 26.0, height: 26.0))).cgPath self.layer.addSublayer(self.circle) } if previousSize == nil { self.isUserInteractionEnabled = true self.wrapper.clipsToBounds = true self.wrapper.layer.cornerRadius = 18.0 let pressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handlePress(_:))) pressGestureRecognizer.minimumPressDuration = 0.01 pressGestureRecognizer.delegate = self self.addGestureRecognizer(pressGestureRecognizer) self.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))) if !self.transparencyLayer.isHidden { self.transparencyLayer.contents = generateCheckeredImage(size: sliderSize, whiteColor: UIColor(rgb: 0xffffff, alpha: 1.0), blackColor: .clear, length: 12.0)?.cgImage } self.knob.contents = generateKnobImage()?.cgImage } } let margin: CGFloat = 10.0 let knobSize = CGSize(width: 32.0, height: 32.0) let knobFrame = CGRect(origin: CGPoint(x: 2.0 + floorToScreenPixels((sliderSize.width - 4.0 - knobSize.width) * value), y: 2.0), size: knobSize) self.knob.frame = knobFrame.insetBy(dx: -margin, dy: -margin) self.circle.fillColor = currentColor.toUIColor().cgColor self.circle.frame = knobFrame.insetBy(dx: 3.0, dy: 3.0) return sliderSize } } func makeView() -> View { return View(frame: CGRect()) } func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { view.updated = self.updated return view.updateLayout(size: availableSize, leftColor: self.leftColor, rightColor: self.rightColor, currentColor: self.currentColor, value: self.value) } } private class ColorFieldComponent: Component { enum FieldType { case number case text } let backgroundColor: UIColor let textColor: UIColor let type: FieldType let value: String let suffix: String? let updated: (String) -> Void let shouldUpdate: (String) -> Bool public init( backgroundColor: UIColor, textColor: UIColor, type: FieldType, value: String, suffix: String? = nil, updated: @escaping (String) -> Void, shouldUpdate: @escaping (String) -> Bool ) { self.backgroundColor = backgroundColor self.textColor = textColor self.type = type self.value = value self.suffix = suffix self.updated = updated self.shouldUpdate = shouldUpdate } public static func ==(lhs: ColorFieldComponent, rhs: ColorFieldComponent) -> Bool { if lhs.backgroundColor != rhs.backgroundColor { return false } if lhs.textColor != rhs.textColor { return false } if lhs.type != rhs.type { return false } if lhs.value != rhs.value { return false } if lhs.suffix != rhs.suffix { return false } return true } final class View: UIView, UITextFieldDelegate { private var validSize: CGSize? private let backgroundNode = NavigationBackgroundNode(color: .clear) private let textField = UITextField(frame: CGRect()) private let suffixLabel = UITextField(frame: CGRect()) fileprivate var updated: (String) -> Void = { _ in } fileprivate var shouldUpdate: (String) -> Bool = { _ in return true } func updateLayout(size: CGSize, component: ColorFieldComponent) -> CGSize { let previousSize = self.validSize self.updated = component.updated self.shouldUpdate = component.shouldUpdate self.validSize = size self.backgroundNode.frame = CGRect(origin: .zero, size: size) self.backgroundNode.update(size: size, cornerRadius: 9.0, transition: .immediate) self.backgroundNode.updateColor(color: component.backgroundColor, transition: .immediate) if previousSize == nil { self.insertSubview(self.backgroundNode.view, at: 0) self.addSubview(self.textField) self.textField.textAlignment = component.suffix != nil ? .right : .center self.textField.delegate = self self.textField.font = Font.with(size: 17.0, design: .regular, weight: .semibold, traits: .monospacedNumbers) self.textField.addTarget(self, action: #selector(self.textDidChange(_:)), for: .editingChanged) self.textField.keyboardAppearance = .dark self.textField.autocorrectionType = .no self.textField.autocapitalizationType = .allCharacters switch component.type { case .number: self.textField.keyboardType = .numberPad case .text: self.textField.keyboardType = .asciiCapable } } self.textField.textColor = component.textColor var textFieldOffset: CGFloat = 0.0 if let suffix = component.suffix { if self.suffixLabel.superview == nil { self.suffixLabel.isUserInteractionEnabled = false self.suffixLabel.text = suffix self.suffixLabel.font = self.textField.font self.suffixLabel.textColor = self.textField.textColor self.addSubview(self.suffixLabel) self.suffixLabel.sizeToFit() self.suffixLabel.frame = CGRect(origin: CGPoint(x: size.width - self.suffixLabel.frame.width - 14.0, y: floorToScreenPixels((size.height - self.suffixLabel.frame.size.height) / 2.0)), size: self.suffixLabel.frame.size) } textFieldOffset = -33.0 } else { self.suffixLabel.removeFromSuperview() } self.textField.frame = CGRect(origin: CGPoint(x: textFieldOffset, y: 0.0), size: size) self.textField.text = component.value return size } @objc private func textDidChange(_ textField: UITextField) { self.updated(textField.text ?? "") } func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { var updated = textField.text ?? "" updated.replaceSubrange(updated.index(updated.startIndex, offsetBy: range.lowerBound) ..< updated.index(updated.startIndex, offsetBy: range.upperBound), with: string) if self.shouldUpdate(updated) { return true } else { return false } } func textFieldDidBeginEditing(_ textField: UITextField) { textField.selectAll(nil) } func textFieldShouldReturn(_ textField: UITextField) -> Bool { return false } } func makeView() -> View { return View(frame: CGRect()) } func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { view.updated = self.updated return view.updateLayout(size: availableSize, component: self) } } private func generatePreviewBackgroundImage(size: CGSize) -> UIImage? { return generateImage(size, opaque: true, rotatedContext: { size, context in context.move(to: .zero) context.addLine(to: CGPoint(x: size.width, y: 0.0)) context.addLine(to: CGPoint(x: 0.0, y: size.height)) context.closePath() context.setFillColor(UIColor.black.cgColor) context.fillPath() context.move(to: CGPoint(x: size.width, y: 0.0)) context.addLine(to: CGPoint(x: size.width, y: size.height)) context.addLine(to: CGPoint(x: 0.0, y: size.height)) context.closePath() context.setFillColor(UIColor.white.cgColor) context.fillPath() }) } private class ColorPreviewComponent: Component { let color: DrawingColor public init( color: DrawingColor ) { self.color = color } public static func ==(lhs: ColorPreviewComponent, rhs: ColorPreviewComponent) -> Bool { if lhs.color != rhs.color { return false } return true } final class View: UIView { private var validSize: CGSize? private let wrapper = UIView(frame: CGRect()) private let background = SimpleLayer() private let color = SimpleLayer() func updateLayout(size: CGSize, color: DrawingColor) -> CGSize { let previousSize = self.validSize self.validSize = size if previousSize != size { self.wrapper.frame = CGRect(origin: .zero, size: size) if self.wrapper.superview == nil { self.addSubview(self.wrapper) } self.background.frame = CGRect(origin: .zero, size: size) if self.background.superlayer == nil { self.wrapper.layer.addSublayer(self.background) } self.color.frame = CGRect(origin: .zero, size: size) if self.color.superlayer == nil { self.wrapper.layer.addSublayer(self.color) } if previousSize == nil { self.isUserInteractionEnabled = true self.wrapper.clipsToBounds = true self.wrapper.layer.cornerRadius = 12.0 if #available(iOS 13.0, *) { self.wrapper.layer.cornerCurve = .continuous } } self.background.contents = generatePreviewBackgroundImage(size: size)?.cgImage } self.color.backgroundColor = color.toUIColor().cgColor return size } } func makeView() -> View { return View(frame: CGRect()) } func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.updateLayout(size: availableSize, color: self.color) } } final class ColorGridComponent: Component { let color: DrawingColor? let selected: (DrawingColor) -> Void init( color: DrawingColor?, selected: @escaping (DrawingColor) -> Void ) { self.color = color self.selected = selected } static func ==(lhs: ColorGridComponent, rhs: ColorGridComponent) -> Bool { if lhs.color != rhs.color { return false } return true } final class View: UIView, UIGestureRecognizerDelegate { private var validSize: CGSize? private var selectedColor: DrawingColor? private var selectedColorIndex: Int? private var wrapper = UIView(frame: CGRect()) private var image = UIImageView(image: nil) private var selectionKnob = UIImageView(image: nil) private var selectionKnobImage: ColorSelectionImage? fileprivate var selected: (DrawingColor) -> Void = { _ in } func getColor(at point: CGPoint) -> DrawingColor? { guard let size = self.validSize, point.x >= 0 && point.x <= size.width, point.y >= 0 && point.y <= size.height else { return nil } let row = max(0, min(10, Int(point.y / size.height * 10.0))) let col = max(0, min(12, Int(point.x / size.width * 12.0))) let index = row * 12 + col if index < palleteColors.count { return DrawingColor(rgb: palleteColors[index]) } else { return DrawingColor(rgb: 0x000000) } } @objc func handlePress(_ gestureRecognizer: UILongPressGestureRecognizer) { guard case .began = gestureRecognizer.state else { return } let location = gestureRecognizer.location(in: self) if let color = self.getColor(at: location), color != self.selectedColor { self.selected(color) } } @objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { if gestureRecognizer.state == .changed { let location = gestureRecognizer.location(in: self) if let color = self.getColor(at: location), color != self.selectedColor { self.selected(color) } } } func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { return true } func updateLayout(size: CGSize, selectedColor: DrawingColor?) -> CGSize { let previousSize = self.validSize let squareSize = floorToScreenPixels(size.width / 12.0) let imageSize = CGSize(width: size.width, height: squareSize * 10.0) self.validSize = imageSize let previousColor = self.selectedColor self.selectedColor = selectedColor if previousSize != imageSize { if previousSize == nil { self.isUserInteractionEnabled = true self.wrapper.clipsToBounds = true self.wrapper.layer.cornerRadius = 10.0 let pressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handlePress(_:))) pressGestureRecognizer.delegate = self pressGestureRecognizer.minimumPressDuration = 0.01 self.addGestureRecognizer(pressGestureRecognizer) self.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))) } self.wrapper.frame = CGRect(origin: .zero, size: imageSize) if self.wrapper.superview == nil { self.addSubview(self.wrapper) } self.image.image = generateColorGridImage(size: imageSize) self.image.frame = CGRect(origin: .zero, size: imageSize) if self.image.superview == nil { self.wrapper.addSubview(self.image) } } if previousColor != selectedColor { if let selectedColor = selectedColor { let color = selectedColor.toUIColor().rgb if let index = palleteColors.firstIndex(where: { $0 == color }) { self.selectedColorIndex = index } else { self.selectedColorIndex = nil } } else { self.selectedColorIndex = nil } } if let selectedColorIndex = self.selectedColorIndex { if self.selectionKnob.superview == nil { self.addSubview(self.selectionKnob) } let smallCornerRadius: CGFloat = 2.0 let largeCornerRadius: CGFloat = 10.0 var topLeftRadius = smallCornerRadius var topRightRadius = smallCornerRadius var bottomLeftRadius = smallCornerRadius var bottomRightRadius = smallCornerRadius if selectedColorIndex == 0 { topLeftRadius = largeCornerRadius } else if selectedColorIndex == 11 { topRightRadius = largeCornerRadius } else if selectedColorIndex == palleteColors.count - 12 { bottomLeftRadius = largeCornerRadius } else if selectedColorIndex == palleteColors.count - 1 { bottomRightRadius = largeCornerRadius } let isLight = (selectedColor?.toUIColor().lightness ?? 1.0) < 0.5 ? true : false var selectionKnobImage = ColorSelectionImage(size: CGSize(width: squareSize, height: squareSize), topLeftRadius: topLeftRadius, topRightRadius: topRightRadius, bottomLeftRadius: bottomLeftRadius, bottomRightRadius: bottomRightRadius, isLight: isLight) if selectionKnobImage != self.selectionKnobImage { self.selectionKnob.image = selectionKnobImage.getImage() self.selectionKnobImage = selectionKnobImage } let row = Int(floor(CGFloat(selectedColorIndex) / 12.0)) let col = selectedColorIndex % 12 let margin: CGFloat = 10.0 var selectionFrame = CGRect(origin: CGPoint(x: CGFloat(col) * squareSize, y: CGFloat(row) * squareSize), size: CGSize(width: squareSize, height: squareSize)) selectionFrame = selectionFrame.insetBy(dx: -margin, dy: -margin) self.selectionKnob.frame = selectionFrame } else { self.selectionKnob.image = nil } return CGSize(width: size.width, height: squareSize * 10.0) } } func makeView() -> View { return View(frame: CGRect()) } func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { view.selected = self.selected return view.updateLayout(size: availableSize, selectedColor: self.color) } } private func generateSpectrumImage(size: CGSize) -> UIImage? { return generateImage(size, contextGenerator: { size, context in if let image = UIImage(bundleImageName: "Media Editor/Spectrum") { context.draw(image.cgImage!, in: CGRect(origin: .zero, size: size)) } if let image = UIImage(bundleImageName: "Media Editor/Grayscale") { context.draw(image.cgImage!, in: CGRect(origin: .zero, size: size)) } }) } final class ColorSpectrumComponent: Component { let color: DrawingColor? let selected: (DrawingColor) -> Void init( color: DrawingColor?, selected: @escaping (DrawingColor) -> Void ) { self.color = color self.selected = selected } static func ==(lhs: ColorSpectrumComponent, rhs: ColorSpectrumComponent) -> Bool { if lhs.color != rhs.color { return false } return true } final class View: UIView, UIGestureRecognizerDelegate { private var validSize: CGSize? private var selectedColor: DrawingColor? private var wrapper = UIView(frame: CGRect()) private var image = UIImageView(image: nil) private let knob = SimpleLayer() private let circle = SimpleShapeLayer() fileprivate var selected: (DrawingColor) -> Void = { _ in } private var bitmapData: UnsafeMutableRawPointer? func getColor(at point: CGPoint) -> DrawingColor? { guard let size = self.validSize, point.x >= 0 && point.x <= size.width, point.y >= 0 && point.y <= size.height else { return nil } let position = CGPoint(x: point.x / size.width, y: point.y / size.height) let scale = self.image.image?.scale ?? 1.0 let point = CGPoint(x: point.x * scale, y: point.y * scale) guard let image = self.image.image?.cgImage else { return nil } var redComponent: CGFloat? var greenComponent: CGFloat? var blueComponent: CGFloat? let imageWidth = image.width let imageHeight = image.height let bitmapBytesForRow = Int(imageWidth * 4) let bitmapByteCount = bitmapBytesForRow * Int(imageHeight) if self.bitmapData == nil { let imageRect = CGRect(origin: .zero, size: CGSize(width: imageWidth, height: imageHeight)) let colorSpace = CGColorSpaceCreateDeviceRGB() let bitmapData = malloc(bitmapByteCount) let bitmapInformation = CGImageAlphaInfo.premultipliedFirst.rawValue let colorContext = CGContext( data: bitmapData, width: imageWidth, height: imageHeight, bitsPerComponent: 8, bytesPerRow: bitmapBytesForRow, space: colorSpace, bitmapInfo: bitmapInformation ) colorContext?.clear(imageRect) colorContext?.draw(image, in: imageRect) self.bitmapData = bitmapData } self.bitmapData?.withMemoryRebound(to: UInt8.self, capacity: bitmapByteCount) { pointer in let offset = 4 * ((Int(imageWidth) * Int(point.y)) + Int(point.x)) redComponent = CGFloat(pointer[offset + 1]) / 255.0 greenComponent = CGFloat(pointer[offset + 2]) / 255.0 blueComponent = CGFloat(pointer[offset + 3]) / 255.0 } if let redComponent = redComponent, let greenComponent = greenComponent, let blueComponent = blueComponent { return DrawingColor(rgb: UIColor(red: redComponent, green: greenComponent, blue: blueComponent, alpha: 1.0).rgb).withUpdatedPosition(position) } else { return nil } } deinit { if let bitmapData = self.bitmapData { free(bitmapData) } } @objc func handlePress(_ gestureRecognizer: UILongPressGestureRecognizer) { guard case .began = gestureRecognizer.state else { return } let location = gestureRecognizer.location(in: self) if let color = self.getColor(at: location), color != self.selectedColor { self.selected(color) } } @objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { if gestureRecognizer.state == .changed { let location = gestureRecognizer.location(in: self) if let color = self.getColor(at: location), color != self.selectedColor { self.selected(color) } } } func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { return true } func updateLayout(size: CGSize, selectedColor: DrawingColor?) -> CGSize { let previousSize = self.validSize let imageSize = size self.validSize = imageSize self.selectedColor = selectedColor if previousSize != imageSize { if previousSize == nil { self.layer.allowsGroupOpacity = true self.isUserInteractionEnabled = true self.wrapper.clipsToBounds = true self.wrapper.layer.cornerRadius = 10.0 let pressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handlePress(_:))) pressGestureRecognizer.delegate = self pressGestureRecognizer.minimumPressDuration = 0.01 self.addGestureRecognizer(pressGestureRecognizer) self.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))) } self.wrapper.frame = CGRect(origin: .zero, size: imageSize) if self.wrapper.superview == nil { self.addSubview(self.wrapper) } if let bitmapData = self.bitmapData { free(bitmapData) } self.image.image = generateSpectrumImage(size: imageSize) self.image.frame = CGRect(origin: .zero, size: imageSize) if self.image.superview == nil { self.wrapper.addSubview(self.image) } } if let color = selectedColor, let position = color.position { if self.knob.superlayer == nil { self.knob.contents = generateKnobImage()?.cgImage self.layer.addSublayer(self.knob) } if self.circle.superlayer == nil { self.circle.path = UIBezierPath(ovalIn: CGRect(origin: .zero, size: CGSize(width: 26.0, height: 26.0))).cgPath self.layer.addSublayer(self.circle) } self.knob.isHidden = false self.circle.isHidden = false let margin: CGFloat = 10.0 let knobSize = CGSize(width: 32.0, height: 32.0) let knobFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(size.width * position.x - knobSize.width / 2.0), y: floorToScreenPixels(size.height * position.y - knobSize.height / 2.0)), size: knobSize) self.knob.frame = knobFrame.insetBy(dx: -margin, dy: -margin) self.circle.fillColor = color.toUIColor().cgColor self.circle.frame = knobFrame.insetBy(dx: 3.0, dy: 3.0) } else { self.knob.isHidden = true self.circle.isHidden = true } return size } } func makeView() -> View { return View(frame: CGRect()) } func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { view.selected = self.selected return view.updateLayout(size: availableSize, selectedColor: self.color) } } public final class ColorSpectrumPickerView: UIView, UIGestureRecognizerDelegate { private var validSize: CGSize? private var selectedColor: DrawingColor? private var wrapper = UIView(frame: CGRect()) private var image = UIImageView(image: nil) private let knob = SimpleLayer() private let circle = SimpleShapeLayer() private var circleMaskView = UIView() private let maskCircle = SimpleShapeLayer() public var selected: (DrawingColor) -> Void = { _ in } private var bitmapData: UnsafeMutableRawPointer? func getColor(at point: CGPoint) -> DrawingColor? { guard let size = self.validSize, point.x >= 0 && point.x <= size.width, point.y >= 0 && point.y <= size.height else { return nil } let position = CGPoint(x: point.x / size.width, y: point.y / size.height) let scale = self.image.image?.scale ?? 1.0 let point = CGPoint(x: point.x * scale, y: point.y * scale) guard let image = self.image.image?.cgImage else { return nil } var redComponent: CGFloat? var greenComponent: CGFloat? var blueComponent: CGFloat? let imageWidth = image.width let imageHeight = image.height let bitmapBytesForRow = Int(imageWidth * 4) let bitmapByteCount = bitmapBytesForRow * Int(imageHeight) if self.bitmapData == nil { let imageRect = CGRect(origin: .zero, size: CGSize(width: imageWidth, height: imageHeight)) let colorSpace = CGColorSpaceCreateDeviceRGB() let bitmapData = malloc(bitmapByteCount) let bitmapInformation = CGImageAlphaInfo.premultipliedFirst.rawValue let colorContext = CGContext( data: bitmapData, width: imageWidth, height: imageHeight, bitsPerComponent: 8, bytesPerRow: bitmapBytesForRow, space: colorSpace, bitmapInfo: bitmapInformation ) colorContext?.clear(imageRect) colorContext?.draw(image, in: imageRect) self.bitmapData = bitmapData } self.bitmapData?.withMemoryRebound(to: UInt8.self, capacity: bitmapByteCount) { pointer in let offset = 4 * ((Int(imageWidth) * Int(point.y)) + Int(point.x)) redComponent = CGFloat(pointer[offset + 1]) / 255.0 greenComponent = CGFloat(pointer[offset + 2]) / 255.0 blueComponent = CGFloat(pointer[offset + 3]) / 255.0 } if let redComponent = redComponent, let greenComponent = greenComponent, let blueComponent = blueComponent { return DrawingColor(rgb: UIColor(red: redComponent, green: greenComponent, blue: blueComponent, alpha: 1.0).rgb).withUpdatedPosition(position) } else { return nil } } override init(frame: CGRect) { super.init(frame: frame) self.isUserInteractionEnabled = false } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { if let bitmapData = self.bitmapData { free(bitmapData) } } @objc func handlePan(point: CGPoint) { guard let size = self.validSize else { return } var location = self.convert(point, from: nil) location.x = max(0.0, min(size.width - 1.0, location.x)) location.y = max(0.0, min(size.height - 1.0, location.y)) if let color = self.getColor(at: location), color != self.selectedColor { self.selected(color) let _ = self.updateLayout(size: size, selectedColor: color) } } private var animatingIn = false private var scheduledAnimateOut: (() -> Void)? public func animateIn() { self.animatingIn = true Queue.mainQueue().after(0.15) { self.selected(DrawingColor(rgb: 0xffffff)) } self.wrapper.mask = self.circleMaskView self.circleMaskView.frame = self.bounds self.maskCircle.fillColor = UIColor.red.cgColor self.circleMaskView.layer.addSublayer(self.maskCircle) self.maskCircle.path = UIBezierPath(ovalIn: CGRect(origin: .zero, size: CGSize(width: 300.0, height: 300.0))).cgPath self.maskCircle.frame = CGRect(origin: .zero, size: CGSize(width: 300.0, height: 300.0)) self.maskCircle.position = CGPoint(x: 15.0, y: self.bounds.height - 15.0) self.maskCircle.transform = CATransform3DMakeScale(3.0, 3.0, 1.0) self.maskCircle.animateScale(from: 0.05, to: 3.0, duration: 0.35, completion: { _ in self.animatingIn = false self.wrapper.mask = nil if let scheduledAnimateOut = self.scheduledAnimateOut { self.scheduledAnimateOut = nil self.animateOut(completion: scheduledAnimateOut) } }) } func animateOut(completion: @escaping () -> Void) { guard !self.animatingIn else { self.scheduledAnimateOut = completion return } if let selectedColor = self.selectedColor { self.selected(selectedColor) } self.knob.opacity = 0.0 self.knob.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) self.circle.opacity = 0.0 self.circle.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) let filler = UIView(frame: self.bounds) filler.backgroundColor = self.selectedColor?.toUIColor() ?? .white self.wrapper.addSubview(filler) filler.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) self.wrapper.mask = self.circleMaskView self.maskCircle.animatePosition(from: self.maskCircle.position, to: CGPoint(x: 16.0, y: self.bounds.height - 16.0), duration: 0.25, removeOnCompletion: false) self.maskCircle.animateScale(from: 3.0, to: 0.06333, duration: 0.35, removeOnCompletion: false, completion: { _ in completion() }) } public func updateLayout(size: CGSize, selectedColor: DrawingColor?) -> CGSize { let previousSize = self.validSize let imageSize = size self.validSize = imageSize self.selectedColor = selectedColor if previousSize != imageSize { if previousSize == nil { self.layer.allowsGroupOpacity = true self.isUserInteractionEnabled = true self.wrapper.clipsToBounds = true self.wrapper.layer.cornerRadius = 17.0 } self.wrapper.frame = CGRect(origin: .zero, size: imageSize) if self.wrapper.superview == nil { self.addSubview(self.wrapper) } if let bitmapData = self.bitmapData { free(bitmapData) } self.image.image = generateSpectrumImage(size: imageSize) self.image.frame = CGRect(origin: .zero, size: imageSize) if self.image.superview == nil { self.wrapper.addSubview(self.image) } } if let color = selectedColor, let position = color.position { if self.knob.superlayer == nil { self.knob.contents = generateKnobImage()?.cgImage self.layer.addSublayer(self.knob) self.knob.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } if self.circle.superlayer == nil { self.circle.path = UIBezierPath(ovalIn: CGRect(origin: .zero, size: CGSize(width: 26.0, height: 26.0))).cgPath self.layer.addSublayer(self.circle) self.circle.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } self.knob.isHidden = false self.circle.isHidden = false let margin: CGFloat = 10.0 let knobSize = CGSize(width: 32.0, height: 32.0) let knobFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(size.width * position.x - knobSize.width / 2.0), y: floorToScreenPixels(size.height * position.y - knobSize.height / 2.0) - 33.0), size: knobSize) self.knob.frame = knobFrame.insetBy(dx: -margin, dy: -margin) self.circle.fillColor = color.toUIColor().cgColor self.circle.frame = knobFrame.insetBy(dx: 3.0, dy: 3.0) } else { self.knob.isHidden = true self.circle.isHidden = true } return size } } private final class ColorSlidersComponent: CombinedComponent { typealias EnvironmentType = ComponentFlow.Empty let color: DrawingColor let updated: (DrawingColor) -> Void init( color: DrawingColor, updated: @escaping (DrawingColor) -> Void ) { self.color = color self.updated = updated } static func ==(lhs: ColorSlidersComponent, rhs: ColorSlidersComponent) -> Bool { if lhs.color != rhs.color { return false } return true } static var body: Body { let redTitle = Child(MultilineTextComponent.self) let redSlider = Child(ColorSliderComponent.self) let redField = Child(ColorFieldComponent.self) let greenTitle = Child(MultilineTextComponent.self) let greenSlider = Child(ColorSliderComponent.self) let greenField = Child(ColorFieldComponent.self) let blueTitle = Child(MultilineTextComponent.self) let blueSlider = Child(ColorSliderComponent.self) let blueField = Child(ColorFieldComponent.self) let hexTitle = Child(MultilineTextComponent.self) let hexField = Child(ColorFieldComponent.self) return { context in let component = context.component var contentHeight: CGFloat = 0.0 let redTitle = redTitle.update( component: MultilineTextComponent( text: .plain(NSAttributedString( string: "RED", font: Font.semibold(13.0), textColor: UIColor(rgb: 0x9b9da5), paragraphAlignment: .center )), horizontalAlignment: .center, maximumNumberOfLines: 1 ), availableSize: CGSize(width: context.availableSize.width, height: CGFloat.greatestFiniteMagnitude), transition: .immediate ) context.add(redTitle .position(CGPoint(x: 5.0 + redTitle.size.width / 2.0, y: contentHeight + redTitle.size.height / 2.0)) ) contentHeight += redTitle.size.height contentHeight += 8.0 let currentColor = component.color let updateColor = component.updated let redSlider = redSlider.update( component: ColorSliderComponent( leftColor: component.color.withUpdatedRed(0.0).withUpdatedAlpha(1.0), rightColor: component.color.withUpdatedRed(1.0).withUpdatedAlpha(1.0), currentColor: component.color, value: component.color.red, updated: { value in updateColor(currentColor.withUpdatedRed(value)) } ), availableSize: CGSize(width: context.availableSize.width - 89.0, height: CGFloat.greatestFiniteMagnitude), transition: .immediate ) context.add(redSlider .position(CGPoint(x: redSlider.size.width / 2.0, y: contentHeight + redSlider.size.height / 2.0)) ) let redField = redField.update( component: ColorFieldComponent( backgroundColor: UIColor(rgb: 0x000000, alpha: 0.6), textColor: .white, type: .number, value: "\(Int(component.color.red * 255.0))", updated: { value in let intValue = Int(value) ?? 0 updateColor(currentColor.withUpdatedRed(CGFloat(intValue) / 255.0)) }, shouldUpdate: { value in if let intValue = Int(value), intValue >= 0 && intValue <= 255 { return true } else if value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { return true } else { return false } } ), availableSize: CGSize(width: 77.0, height: 36.0), transition: .immediate ) context.add(redField .position(CGPoint(x: context.availableSize.width - redField.size.width / 2.0, y: contentHeight + redField.size.height / 2.0)) ) contentHeight += redSlider.size.height contentHeight += 28.0 let greenTitle = greenTitle.update( component: MultilineTextComponent( text: .plain(NSAttributedString( string: "GREEN", font: Font.semibold(13.0), textColor: UIColor(rgb: 0x9b9da5), paragraphAlignment: .center )), horizontalAlignment: .center, maximumNumberOfLines: 1 ), availableSize: CGSize(width: context.availableSize.width, height: CGFloat.greatestFiniteMagnitude), transition: .immediate ) context.add(greenTitle .position(CGPoint(x: 5.0 + greenTitle.size.width / 2.0, y: contentHeight + greenTitle.size.height / 2.0)) ) contentHeight += greenTitle.size.height contentHeight += 8.0 let greenSlider = greenSlider.update( component: ColorSliderComponent( leftColor: component.color.withUpdatedGreen(0.0).withUpdatedAlpha(1.0), rightColor: component.color.withUpdatedGreen(1.0).withUpdatedAlpha(1.0), currentColor: component.color, value: component.color.green, updated: { value in updateColor(currentColor.withUpdatedGreen(value)) } ), availableSize: CGSize(width: context.availableSize.width - 89.0, height: CGFloat.greatestFiniteMagnitude), transition: .immediate ) context.add(greenSlider .position(CGPoint(x: greenSlider.size.width / 2.0, y: contentHeight + greenSlider.size.height / 2.0)) ) let greenField = greenField.update( component: ColorFieldComponent( backgroundColor: UIColor(rgb: 0x000000, alpha: 0.6), textColor: .white, type: .number, value: "\(Int(component.color.green * 255.0))", updated: { value in let intValue = Int(value) ?? 0 updateColor(currentColor.withUpdatedGreen(CGFloat(intValue) / 255.0)) }, shouldUpdate: { value in if let intValue = Int(value), intValue >= 0 && intValue <= 255 { return true } else if value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { return true } else { return false } } ), availableSize: CGSize(width: 77.0, height: 36.0), transition: .immediate ) context.add(greenField .position(CGPoint(x: context.availableSize.width - greenField.size.width / 2.0, y: contentHeight + greenField.size.height / 2.0)) ) contentHeight += greenSlider.size.height contentHeight += 28.0 let blueTitle = blueTitle.update( component: MultilineTextComponent( text: .plain(NSAttributedString( string: "BLUE", font: Font.semibold(13.0), textColor: UIColor(rgb: 0x9b9da5), paragraphAlignment: .center )), horizontalAlignment: .center, maximumNumberOfLines: 1 ), availableSize: CGSize(width: context.availableSize.width, height: CGFloat.greatestFiniteMagnitude), transition: .immediate ) context.add(blueTitle .position(CGPoint(x: 5.0 + blueTitle.size.width / 2.0, y: contentHeight + blueTitle.size.height / 2.0)) ) contentHeight += blueTitle.size.height contentHeight += 8.0 let blueSlider = blueSlider.update( component: ColorSliderComponent( leftColor: component.color.withUpdatedBlue(0.0).withUpdatedAlpha(1.0), rightColor: component.color.withUpdatedBlue(1.0).withUpdatedAlpha(1.0), currentColor: component.color, value: component.color.blue, updated: { value in updateColor(currentColor.withUpdatedBlue(value)) } ), availableSize: CGSize(width: context.availableSize.width - 89.0, height: CGFloat.greatestFiniteMagnitude), transition: .immediate ) context.add(blueSlider .position(CGPoint(x: blueSlider.size.width / 2.0, y: contentHeight + blueSlider.size.height / 2.0)) ) let blueField = blueField.update( component: ColorFieldComponent( backgroundColor: UIColor(rgb: 0x000000, alpha: 0.6), textColor: .white, type: .number, value: "\(Int(component.color.blue * 255.0))", updated: { value in let intValue = Int(value) ?? 0 updateColor(currentColor.withUpdatedBlue(CGFloat(intValue) / 255.0)) }, shouldUpdate: { value in if let intValue = Int(value), intValue >= 0 && intValue <= 255 { return true } else if value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { return true } else { return false } } ), availableSize: CGSize(width: 77.0, height: 36.0), transition: .immediate ) context.add(blueField .position(CGPoint(x: context.availableSize.width - blueField.size.width / 2.0, y: contentHeight + blueField.size.height / 2.0)) ) contentHeight += blueSlider.size.height contentHeight += 28.0 let hexField = hexField.update( component: ColorFieldComponent( backgroundColor: UIColor(rgb: 0x000000, alpha: 0.6), textColor: .white, type: .text, value: component.color.toUIColor().hexString.uppercased(), updated: { value in if value.count == 6, let uiColor = UIColor(hexString: value) { updateColor(DrawingColor(color: uiColor).withUpdatedAlpha(currentColor.alpha)) } }, shouldUpdate: { value in if value.count <= 6 && value.rangeOfCharacter(from: CharacterSet(charactersIn: "0123456789abcdefABCDEF").inverted) == nil { return true } else { return false } } ), availableSize: CGSize(width: 77.0, height: 36.0), transition: .immediate ) context.add(hexField .position(CGPoint(x: context.availableSize.width - hexField.size.width / 2.0, y: contentHeight + hexField.size.height / 2.0)) ) let hexTitle = hexTitle.update( component: MultilineTextComponent( text: .plain(NSAttributedString( string: "Hex Color #", font: Font.regular(17.0), textColor: UIColor(rgb: 0xffffff), paragraphAlignment: .center )), horizontalAlignment: .center, maximumNumberOfLines: 1 ), availableSize: CGSize(width: context.availableSize.width, height: CGFloat.greatestFiniteMagnitude), transition: .immediate ) context.add(hexTitle .position(CGPoint(x: context.availableSize.width - hexField.size.width - 12.0 - hexTitle.size.width / 2.0, y: contentHeight + hexField.size.height / 2.0)) ) contentHeight += hexField.size.height contentHeight += 8.0 return CGSize(width: context.availableSize.width, height: contentHeight) } } } private func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? { return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setFillColor(backgroundColor.cgColor) context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) context.setLineWidth(2.0) context.setLineCap(.round) context.setStrokeColor(foregroundColor.cgColor) context.move(to: CGPoint(x: 10.0, y: 10.0)) context.addLine(to: CGPoint(x: 20.0, y: 20.0)) context.strokePath() context.move(to: CGPoint(x: 20.0, y: 10.0)) context.addLine(to: CGPoint(x: 10.0, y: 20.0)) context.strokePath() }) } private class SegmentedControlComponent: Component { let values: [String] let selectedIndex: Int let selectionChanged: (Int) -> Void init(values: [String], selectedIndex: Int, selectionChanged: @escaping (Int) -> Void) { self.values = values self.selectedIndex = selectedIndex self.selectionChanged = selectionChanged } static func ==(lhs: SegmentedControlComponent, rhs: SegmentedControlComponent) -> Bool { if lhs.values != rhs.values { return false } if lhs.selectedIndex != rhs.selectedIndex { return false } return true } final class View: UIView { private let backgroundNode: NavigationBackgroundNode private let node: SegmentedControlNode init() { self.backgroundNode = NavigationBackgroundNode(color: UIColor(rgb: 0x888888, alpha: 0.1)) self.node = SegmentedControlNode(theme: SegmentedControlTheme(backgroundColor: .clear, foregroundColor: UIColor(rgb: 0x6f7075, alpha: 0.6), shadowColor: .black, textColor: UIColor(rgb: 0xffffff), dividerColor: UIColor(rgb: 0x505155, alpha: 0.6)), items: [], selectedIndex: 0) super.init(frame: CGRect()) self.addSubview(self.backgroundNode.view) self.addSubview(self.node.view) } required init?(coder aDecoder: NSCoder) { preconditionFailure() } func update(component: SegmentedControlComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { self.node.items = component.values.map { SegmentedControlItem(title: $0) } self.node.selectedIndex = component.selectedIndex let selectionChanged = component.selectionChanged self.node.selectedIndexChanged = { [weak self] index in self?.window?.endEditing(true) selectionChanged(index) } let size = self.node.updateLayout(.stretchToFill(width: availableSize.width), transition: transition.containedViewLayoutTransition) transition.setFrame(view: self.node.view, frame: CGRect(origin: CGPoint(), size: size)) transition.setFrame(view: self.backgroundNode.view, frame: CGRect(origin: CGPoint(), size: size)) self.backgroundNode.update(size: size, cornerRadius: 10.0, transition: .immediate) return size } } func makeView() -> View { return View() } func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } final class ColorSwatchComponent: Component { enum SwatchType: Equatable { case main case pallete(Bool) } let type: SwatchType let color: DrawingColor? let tag: AnyObject? let action: () -> Void let holdAction: (() -> Void)? let pan: ((CGPoint) -> Void)? let release: (() -> Void)? init( type: SwatchType, color: DrawingColor?, tag: AnyObject? = nil, action: @escaping () -> Void, holdAction: (() -> Void)? = nil, pan: ((CGPoint) -> Void)? = nil, release: (() -> Void)? = nil ) { self.type = type self.color = color self.tag = tag self.action = action self.holdAction = holdAction self.pan = pan self.release = release } static func == (lhs: ColorSwatchComponent, rhs: ColorSwatchComponent) -> Bool { return lhs.type == rhs.type && lhs.color == rhs.color } final class View: UIButton, ComponentTaggedView { private var component: ColorSwatchComponent? private var contentView: UIView private var ringLayer: CALayer? private var ringMaskLayer: SimpleShapeLayer? private let circleLayer: SimpleShapeLayer private let fastCircleLayer: SimpleShapeLayer private var currentIsHighlighted: Bool = false { didSet { if self.currentIsHighlighted != oldValue { self.contentView.alpha = self.currentIsHighlighted ? 0.6 : 1.0 } } } private var holdActionTriggerred: Bool = false private var holdActionTimer: Foundation.Timer? override init(frame: CGRect) { self.contentView = UIView(frame: CGRect(origin: .zero, size: frame.size)) self.contentView.isUserInteractionEnabled = false self.circleLayer = SimpleShapeLayer() self.fastCircleLayer = SimpleShapeLayer() self.fastCircleLayer.fillColor = UIColor.white.cgColor self.fastCircleLayer.isHidden = true super.init(frame: frame) self.addSubview(self.contentView) self.contentView.layer.addSublayer(self.circleLayer) self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func matches(tag: Any) -> Bool { if let component = self.component, let componentTag = component.tag { let tag = tag as AnyObject if componentTag === tag { return true } } return false } @objc private func pressed() { if self.holdActionTriggerred { self.holdActionTriggerred = false } else { self.component?.action() } } override public func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { self.currentIsHighlighted = true self.holdActionTriggerred = false if self.component?.holdAction != nil { Queue.mainQueue().after(0.15, { if self.currentIsHighlighted { self.fastCircleLayer.isHidden = false self.fastCircleLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) self.fastCircleLayer.animateScale(from: 0.57575, to: 1.0, duration: 0.25) } }) self.holdActionTimer?.invalidate() if #available(iOS 10.0, *) { let holdActionTimer = Timer(timeInterval: 0.4, repeats: false, block: { [weak self] _ in guard let strongSelf = self else { return } strongSelf.holdActionTriggerred = true strongSelf.holdActionTimer?.invalidate() strongSelf.component?.holdAction?() Queue.mainQueue().after(0.1, { strongSelf.fastCircleLayer.isHidden = true }) }) self.holdActionTimer = holdActionTimer RunLoop.main.add(holdActionTimer, forMode: .common) } } return super.beginTracking(touch, with: event) } override public func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { if self.holdActionTriggerred { let location = touch.location(in: nil) self.component?.pan?(location) } return true } override public func endTracking(_ touch: UITouch?, with event: UIEvent?) { if self.holdActionTriggerred { self.component?.release?() } self.currentIsHighlighted = false Queue.mainQueue().after(0.1) { self.holdActionTriggerred = false } if !self.fastCircleLayer.isHidden { let currentAlpha: CGFloat = CGFloat(self.fastCircleLayer.presentation()?.opacity ?? 1.0) self.fastCircleLayer.animateAlpha(from: currentAlpha, to: 0.0, duration: 0.1, completion: { _ in self.fastCircleLayer.isHidden = true }) } self.holdActionTimer?.invalidate() self.holdActionTimer = nil super.endTracking(touch, with: event) } override public func cancelTracking(with event: UIEvent?) { if self.holdActionTriggerred { self.component?.release?() } self.currentIsHighlighted = false self.holdActionTriggerred = false self.holdActionTimer?.invalidate() self.holdActionTimer = nil super.cancelTracking(with: event) } func animateIn() { self.contentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) self.contentView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3) } func animateOut() { self.contentView.alpha = 0.0 self.contentView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) self.contentView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3) } func update(component: ColorSwatchComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component let contentSize: CGSize if case .pallete = component.type { contentSize = availableSize } else { contentSize = CGSize(width: 24.0, height: 24.0) } self.contentView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - contentSize.width) / 2.0), y: floor((availableSize.height - contentSize.height) / 2.0)), size: contentSize) let bounds = CGRect(origin: .zero, size: contentSize) switch component.type { case .main: self.circleLayer.frame = bounds if self.circleLayer.path == nil { self.circleLayer.path = UIBezierPath(ovalIn: bounds.insetBy(dx: 3.0, dy: 3.0)).cgPath } let ringFrame = bounds.insetBy(dx: -1.0, dy: -1.0) if self.ringLayer == nil { let ringLayer = SimpleLayer() ringLayer.contents = UIImage(bundleImageName: "Media Editor/RoundSpectrum")?.cgImage ringLayer.frame = ringFrame self.contentView.layer.addSublayer(ringLayer) self.ringLayer = ringLayer let ringMaskLayer = SimpleShapeLayer() ringMaskLayer.frame = CGRect(origin: .zero, size: ringFrame.size) ringMaskLayer.strokeColor = UIColor.white.cgColor ringMaskLayer.fillColor = UIColor.clear.cgColor self.ringMaskLayer = ringMaskLayer self.ringLayer?.mask = ringMaskLayer } if let ringMaskLayer = self.ringMaskLayer { if component.color == nil { transition.setShapeLayerPath(layer: ringMaskLayer, path: UIBezierPath(ovalIn: CGRect(origin: .zero, size: ringFrame.size).insetBy(dx: 7.0, dy: 7.0)).cgPath) transition.setShapeLayerLineWidth(layer: ringMaskLayer, lineWidth: 12.0) } else { transition.setShapeLayerPath(layer: ringMaskLayer, path: UIBezierPath(ovalIn: CGRect(origin: .zero, size: ringFrame.size).insetBy(dx: 1.0, dy: 1.0)).cgPath) transition.setShapeLayerLineWidth(layer: ringMaskLayer, lineWidth: 2.0) } } if self.fastCircleLayer.path == nil { self.fastCircleLayer.path = UIBezierPath(ovalIn: bounds).cgPath self.fastCircleLayer.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - bounds.size.width) / 2.0), y: floorToScreenPixels((availableSize.height - bounds.size.height) / 2.0)), size: bounds.size) self.layer.addSublayer(self.fastCircleLayer) } case let .pallete(selected): self.layer.allowsGroupOpacity = true self.contentView.layer.allowsGroupOpacity = true self.circleLayer.frame = bounds if self.ringLayer == nil { let ringLayer = SimpleLayer() ringLayer.backgroundColor = UIColor.clear.cgColor ringLayer.cornerRadius = contentSize.width / 2.0 ringLayer.borderWidth = 3.0 ringLayer.frame = CGRect(origin: .zero, size: contentSize) self.contentView.layer.insertSublayer(ringLayer, at: 0) self.ringLayer = ringLayer } if selected { transition.setShapeLayerPath(layer: self.circleLayer, path: CGPath(ellipseIn: bounds.insetBy(dx: 5.0, dy: 5.0), transform: nil)) } else { transition.setShapeLayerPath(layer: self.circleLayer, path: CGPath(ellipseIn: bounds, transform: nil)) } } if let color = component.color { self.circleLayer.fillColor = color.toCGColor() if case .pallete = component.type { if color.toUIColor().rgb == 0x000000 { self.circleLayer.strokeColor = UIColor(rgb: 0x1f1f1f).cgColor self.circleLayer.lineWidth = 1.0 self.ringLayer?.borderColor = UIColor(rgb: 0x1f1f1f).cgColor } else { self.ringLayer?.borderColor = color.toCGColor() } } } if let screenTransition = transition.userData(DrawingScreenTransition.self) { switch screenTransition { case .animateIn: self.animateIn() case .animateOut: self.animateOut() } } return availableSize } } public func makeView() -> View { return View(frame: CGRect()) } public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } private final class ColorPickerContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext let initialColor: DrawingColor let colorChanged: (DrawingColor) -> Void let eyedropper: () -> Void let dismiss: () -> Void init( context: AccountContext, initialColor: DrawingColor, colorChanged: @escaping (DrawingColor) -> Void, eyedropper: @escaping () -> Void, dismiss: @escaping () -> Void ) { self.context = context self.initialColor = initialColor self.colorChanged = colorChanged self.eyedropper = eyedropper self.dismiss = dismiss } static func ==(lhs: ColorPickerContent, rhs: ColorPickerContent) -> Bool { if lhs.context !== rhs.context { return false } return true } final class State: ComponentState { var cachedEyedropperImage: UIImage? var eyedropperImage: UIImage { let eyedropperImage: UIImage if let image = self.cachedEyedropperImage { eyedropperImage = image } else { eyedropperImage = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Eyedropper"), color: .white)! self.cachedEyedropperImage = eyedropperImage } return eyedropperImage } var cachedCloseImage: UIImage? var closeImage: UIImage { let closeImage: UIImage if let image = self.cachedCloseImage { closeImage = image } else { closeImage = generateCloseButtonImage(backgroundColor: .clear, foregroundColor: UIColor(rgb: 0xa8aab1))! self.cachedCloseImage = closeImage } return closeImage } var selectedMode: Int = 0 var selectedColor: DrawingColor var savedColors: [DrawingColor] = [] var colorChanged: (DrawingColor) -> Void = { _ in } init(initialColor: DrawingColor) { self.selectedColor = initialColor self.savedColors = [DrawingColor(color: .red), DrawingColor(color: .green), DrawingColor(color: .blue)] } func updateColor(_ color: DrawingColor, keepAlpha: Bool = false) { self.selectedColor = keepAlpha ? color.withUpdatedAlpha(self.selectedColor.alpha) : color self.colorChanged(self.selectedColor) self.updated(transition: .immediate) } func updateAlpha(_ alpha: CGFloat) { self.selectedColor = self.selectedColor.withUpdatedAlpha(alpha) self.colorChanged(self.selectedColor) self.updated(transition: .immediate) } func updateSelectedMode(_ mode: Int) { self.selectedMode = mode self.updated(transition: .easeInOut(duration: 0.2)) } func saveCurrentColor() { self.savedColors.append(self.selectedColor) self.updated(transition: .easeInOut(duration: 0.2)) } } func makeState() -> State { return State(initialColor: self.initialColor) } static var body: Body { let eyedropperButton = Child(Button.self) let closeButton = Child(Button.self) let title = Child(MultilineTextComponent.self) let modeControl = Child(SegmentedControlComponent.self) let colorGrid = Child(ColorGridComponent.self) let colorSpectrum = Child(ColorSpectrumComponent.self) let colorSliders = Child(ColorSlidersComponent.self) let opacityTitle = Child(MultilineTextComponent.self) let opacitySlider = Child(ColorSliderComponent.self) let opacityField = Child(ColorFieldComponent.self) let divider = Child(Rectangle.self) let preview = Child(ColorPreviewComponent.self) let swatch1Button = Child(ColorSwatchComponent.self) let swatch2Button = Child(ColorSwatchComponent.self) let swatch3Button = Child(ColorSwatchComponent.self) let swatch4Button = Child(ColorSwatchComponent.self) let swatch5Button = Child(ColorSwatchComponent.self) return { context in let environment = context.environment[ViewControllerComponentContainer.Environment.self].value let component = context.component let state = context.state let strings = environment.strings state.colorChanged = component.colorChanged let sideInset: CGFloat = 16.0 let eyedropperButton = eyedropperButton.update( component: Button( content: AnyComponent( Image(image: state.eyedropperImage) ), action: { [weak component] in component?.eyedropper() } ).minSize(CGSize(width: 30.0, height: 30.0)), availableSize: CGSize(width: 19.0, height: 19.0), transition: .immediate ) context.add(eyedropperButton .position(CGPoint(x: environment.safeInsets.left + eyedropperButton.size.width + 1.0, y: 29.0)) ) let closeButton = closeButton.update( component: Button( content: AnyComponent(ZStack([ AnyComponentWithIdentity( id: "background", component: AnyComponent( BlurredBackgroundComponent( color: UIColor(rgb: 0x888888, alpha: 0.1) ) ) ), AnyComponentWithIdentity( id: "icon", component: AnyComponent( Image(image: state.closeImage) ) ), ])), action: { [weak component] in component?.dismiss() } ), availableSize: CGSize(width: 30.0, height: 30.0), transition: .immediate ) context.add(closeButton .position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - closeButton.size.width - 1.0, y: 29.0)) .clipsToBounds(true) .cornerRadius(15.0) ) let title = title.update( component: MultilineTextComponent( text: .plain(NSAttributedString( string: strings.Paint_ColorTitle, font: Font.semibold(17.0), textColor: .white, paragraphAlignment: .center )), horizontalAlignment: .center, maximumNumberOfLines: 1 ), availableSize: CGSize(width: context.availableSize.width - 100.0, height: CGFloat.greatestFiniteMagnitude), transition: .immediate ) context.add(title .position(CGPoint(x: context.availableSize.width / 2.0, y: 29.0)) ) var contentHeight: CGFloat = 58.0 let modeControl = modeControl.update( component: SegmentedControlComponent( values: [strings.Paint_ColorGrid, strings.Paint_ColorSpectrum, strings.Paint_ColorSliders], selectedIndex: 0, selectionChanged: { [weak state] index in state?.updateSelectedMode(index) } ), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height), transition: .immediate ) context.add(modeControl .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + modeControl.size.height / 2.0)) ) contentHeight += modeControl.size.height contentHeight += 20.0 let squareSize = floorToScreenPixels((context.availableSize.width - sideInset * 2.0) / 12.0) let fieldSize = CGSize(width: context.availableSize.width - sideInset * 2.0, height: squareSize * 10.0) if state.selectedMode == 0 { let colorGrid = colorGrid.update( component: ColorGridComponent( color: state.selectedColor, selected: { [weak state] color in state?.updateColor(color, keepAlpha: true) } ), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height), transition: .immediate ) context.add(colorGrid .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + colorGrid.size.height / 2.0)) .appear(.default(alpha: true)) .disappear(.default()) ) } else if state.selectedMode == 1 { let colorSpectrum = colorSpectrum.update( component: ColorSpectrumComponent( color: state.selectedColor, selected: { [weak state] color in state?.updateColor(color, keepAlpha: true) } ), availableSize: fieldSize, transition: .immediate ) context.add(colorSpectrum .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + fieldSize.height / 2.0)) .appear(.default(alpha: true)) .disappear(.default()) ) } else if state.selectedMode == 2 { let colorSliders = colorSliders.update( component: ColorSlidersComponent( color: state.selectedColor, updated: { [weak state] color in state?.updateColor(color, keepAlpha: true) } ), availableSize: fieldSize, transition: .immediate ) context.add(colorSliders .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + colorSliders.size.height / 2.0)) .appear(.default(alpha: true)) .disappear(.default()) ) } contentHeight += fieldSize.height contentHeight += 21.0 let opacityTitle = opacityTitle.update( component: MultilineTextComponent( text: .plain(NSAttributedString( string: strings.Paint_ColorOpacity, font: Font.semibold(13.0), textColor: UIColor(rgb: 0x9b9da5), paragraphAlignment: .center )), horizontalAlignment: .center, maximumNumberOfLines: 1 ), availableSize: CGSize(width: context.availableSize.width, height: CGFloat.greatestFiniteMagnitude), transition: .immediate ) context.add(opacityTitle .position(CGPoint(x: sideInset + 5.0 + opacityTitle.size.width / 2.0, y: contentHeight + opacityTitle.size.height / 2.0)) ) contentHeight += opacityTitle.size.height contentHeight += 8.0 let opacitySlider = opacitySlider.update( component: ColorSliderComponent( leftColor: state.selectedColor.withUpdatedAlpha(0.0), rightColor: state.selectedColor.withUpdatedAlpha(1.0), currentColor: state.selectedColor, value: state.selectedColor.alpha, updated: { value in state.updateAlpha(value) } ), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 89.0, height: CGFloat.greatestFiniteMagnitude), transition: .immediate ) context.add(opacitySlider .position(CGPoint(x: sideInset + opacitySlider.size.width / 2.0, y: contentHeight + opacitySlider.size.height / 2.0)) ) let opacityField = opacityField.update( component: ColorFieldComponent( backgroundColor: UIColor(rgb: 0x000000, alpha: 0.6), textColor: .white, type: .number, value: "\(Int(state.selectedColor.alpha * 100.0))", suffix: "%", updated: { value in let intValue = Int(value) ?? 0 state.updateAlpha(CGFloat(intValue) / 100.0) }, shouldUpdate: { value in if let intValue = Int(value), intValue >= 0 && intValue <= 100 { return true } else if value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { return true } else { return false } } ), availableSize: CGSize(width: 77.0, height: 36.0), transition: .immediate ) context.add(opacityField .position(CGPoint(x: context.availableSize.width - sideInset - opacityField.size.width / 2.0, y: contentHeight + opacityField.size.height / 2.0)) ) contentHeight += opacitySlider.size.height contentHeight += 24.0 let divider = divider.update( component: Rectangle(color: UIColor(rgb: 0x48484a)), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 1.0), transition: .immediate ) context.add(divider .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight)) ) contentHeight += divider.size.height contentHeight += 22.0 let preview = preview.update( component: ColorPreviewComponent( color: state.selectedColor ), availableSize: CGSize(width: 82.0, height: 82.0), transition: .immediate ) context.add(preview .position(CGPoint(x: sideInset + preview.size.width / 2.0, y: contentHeight + preview.size.height / 2.0)) ) var swatchOffset: CGFloat = sideInset + preview.size.width + 38.0 let swatchSpacing: CGFloat = 20.0 let swatch1Button = swatch1Button.update( component: ColorSwatchComponent( type: .pallete(state.selectedColor == DrawingColor(color: .black)), color: DrawingColor(color: .black), action: { state.updateColor(DrawingColor(color: .black)) } ), availableSize: CGSize(width: 30.0, height: 30.0), transition: context.transition ) context.add(swatch1Button .position(CGPoint(x: swatchOffset, y: contentHeight + swatch1Button.size.height / 2.0)) ) swatchOffset += swatch1Button.size.width + swatchSpacing let swatch2Button = swatch2Button.update( component: ColorSwatchComponent( type: .pallete(state.selectedColor == DrawingColor(rgb: 0x0161fd)), color: DrawingColor(rgb: 0x0161fd), action: { state.updateColor(DrawingColor(rgb: 0x0161fd)) } ), availableSize: CGSize(width: 30.0, height: 30.0), transition: context.transition ) context.add(swatch2Button .position(CGPoint(x: swatchOffset, y: contentHeight + swatch2Button.size.height / 2.0)) ) swatchOffset += swatch2Button.size.width + swatchSpacing let swatch3Button = swatch3Button.update( component: ColorSwatchComponent( type: .pallete(state.selectedColor == DrawingColor(rgb: 0x32c759)), color: DrawingColor(rgb: 0x32c759), action: { state.updateColor(DrawingColor(rgb: 0x32c759)) } ), availableSize: CGSize(width: 30.0, height: 30.0), transition: context.transition ) context.add(swatch3Button .position(CGPoint(x: swatchOffset, y: contentHeight + swatch3Button.size.height / 2.0)) ) swatchOffset += swatch3Button.size.width + swatchSpacing let swatch4Button = swatch4Button.update( component: ColorSwatchComponent( type: .pallete(state.selectedColor == DrawingColor(rgb: 0xffcc02)), color: DrawingColor(rgb: 0xffcc02), action: { state.updateColor(DrawingColor(rgb: 0xffcc02)) } ), availableSize: CGSize(width: 30.0, height: 30.0), transition: context.transition ) context.add(swatch4Button .position(CGPoint(x: swatchOffset, y: contentHeight + swatch4Button.size.height / 2.0)) ) swatchOffset += swatch4Button.size.width + swatchSpacing let swatch5Button = swatch5Button.update( component: ColorSwatchComponent( type: .pallete(state.selectedColor == DrawingColor(rgb: 0xff3a30)), color: DrawingColor(rgb: 0xff3a30), action: { state.updateColor(DrawingColor(rgb: 0xff3a30)) } ), availableSize: CGSize(width: 30.0, height: 30.0), transition: context.transition ) context.add(swatch5Button .position(CGPoint(x: swatchOffset, y: contentHeight + swatch5Button.size.height / 2.0)) ) contentHeight += preview.size.height contentHeight += 10.0 let bottomPanelPadding: CGFloat = 12.0 var bottomInset: CGFloat if case .regular = environment.metrics.widthClass { bottomInset = bottomPanelPadding } else { bottomInset = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding } if environment.inputHeight > 0.0 { bottomInset += environment.inputHeight - bottomInset - 120.0 } return CGSize(width: context.availableSize.width, height: contentHeight + bottomInset) } } } private final class ColorPickerSheetComponent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment private let context: AccountContext private let initialColor: DrawingColor private let updated: (DrawingColor) -> Void private let openEyedropper: () -> Void private let dismissed: () -> Void init(context: AccountContext, initialColor: DrawingColor, updated: @escaping (DrawingColor) -> Void, openEyedropper: @escaping () -> Void, dismissed: @escaping () -> Void) { self.context = context self.initialColor = initialColor self.updated = updated self.openEyedropper = openEyedropper self.dismissed = dismissed } static func ==(lhs: ColorPickerSheetComponent, rhs: ColorPickerSheetComponent) -> Bool { if lhs.context !== rhs.context { return false } return true } static var body: Body { let sheet = Child(SheetComponent<(EnvironmentType)>.self) let animateOut = StoredActionSlot(Action.self) return { context in let environment = context.environment[EnvironmentType.self] let controller = environment.controller let updated = context.component.updated let openEyedropper = context.component.openEyedropper let dismissed = context.component.dismissed let sheet = sheet.update( component: SheetComponent( content: AnyComponent(ColorPickerContent( context: context.component.context, initialColor: context.component.initialColor, colorChanged: { color in updated(color) }, eyedropper: { openEyedropper() animateOut.invoke(Action { _ in if let controller = controller() { controller.dismiss(completion: nil) } }) }, dismiss: { dismissed() animateOut.invoke(Action { _ in if let controller = controller() { controller.dismiss(completion: nil) } }) } )), backgroundColor: .blur(.dark), animateOut: animateOut ), environment: { environment SheetComponentEnvironment( isDisplaying: environment.value.isVisible, isCentered: environment.metrics.widthClass == .regular, hasInputHeight: !environment.inputHeight.isZero, regularMetricsSize: CGSize(width: 430.0, height: 900.0), dismiss: { animated in if animated { animateOut.invoke(Action { _ in if let controller = controller() { controller.dismiss(completion: nil) } }) } else { if let controller = controller() { controller.dismiss(completion: nil) } } } ) }, availableSize: context.availableSize, transition: context.transition ) context.add(sheet .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) ) return context.availableSize } } } public final class ColorPickerScreen: ViewControllerComponentContainer { private var dismissed: () -> Void public init(context: AccountContext, initialColor: DrawingColor, updated: @escaping (DrawingColor) -> Void, openEyedropper: @escaping () -> Void, dismissed: @escaping () -> Void = {}) { self.dismissed = dismissed super.init(context: context, component: ColorPickerSheetComponent(context: context, initialColor: initialColor, updated: updated, openEyedropper: openEyedropper, dismissed: dismissed), navigationBarAppearance: .none) self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) self.navigationPresentation = .flatModal } deinit { self.dismissed() } required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } }