// // TextLinkCaret.swift // VDS // // Created by Matt Bruce on 11/1/22. // import Foundation import UIKit import VDSColorTokens import VDSFormControlsTokens import Combine @objc(VDSTextLinkCaret) open class TextLinkCaret: ButtonBase { //-------------------------------------------------- // MARK: - Enums //-------------------------------------------------- public enum IconPosition: String, CaseIterable { case left, right } //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- open override var textStyle: TextStyle { TextStyle.boldBodyLarge } private var caretView = CaretView().with { $0.size = CaretView.Size.small(.vertical) $0.lineWidth = 2 } private var imageAttribute: ImageLabelAttribute? open override var attributes: [any LabelAttributeModel]? { guard let imageAttribute else { return nil } return [imageAttribute] } //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- public override var availableSizes: [ButtonSize] { [.large] } open var iconPosition: IconPosition = .right { didSet { didChange() } } private var height: CGFloat { 44 } private var _text: String? open override var text: String? { get{ _text } set { var updatedText = newValue ?? "" updatedText = iconPosition == .right ? "\(updatedText) " : " \(updatedText)" _text = updatedText didChange() } } open override var textColor: UIColor { textColorConfiguration.getColor(self) } private var textColorConfiguration = ControlColorConfiguration().with { $0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .normal) $0.setSurfaceColors(VDSColor.elementsSecondaryOnlight, VDSColor.elementsSecondaryOndark, forState: .disabled) $0.setSurfaceColors(VDSColor.interactiveActiveOnlight, VDSColor.interactiveActiveOndark, forState: .highlighted) } //-------------------------------------------------- // MARK: - Initializers //-------------------------------------------------- required public init() { super.init(frame: .zero) } public override init(frame: CGRect) { super.init(frame: .zero) } public required init?(coder: NSCoder) { super.init(coder: coder) } //-------------------------------------------------- // MARK: - Public Functions //-------------------------------------------------- open override func setup() { super.setup() let size = caretView.size!.dimensions() caretView.frame = .init(x: 0, y: 0, width: size.width, height: size.height) } open override func reset() { super.reset() iconPosition = .right } //-------------------------------------------------- // MARK: - Overrides //-------------------------------------------------- override open var intrinsicContentSize: CGSize { //get the labels size, if not the button let size = titleLabel?.intrinsicContentSize ?? super.intrinsicContentSize var itemWidth = size.width if let caretWidth = caretView.size?.dimensions().width { itemWidth += caretWidth } return CGSize(width: itemWidth, height: size.height) } open override func updateView() { let updatedText = text ?? "" caretView.surface = surface caretView.disabled = disabled caretView.direction = iconPosition == .right ? CaretView.Direction.right : CaretView.Direction.left let image = caretView.getImage() let location = iconPosition == .right ? updatedText.count : 0 imageAttribute = ImageLabelAttribute(location: location, image: image, tintColor: textColor) super.updateView() } } extension UIView { public func getImage() -> UIImage { let renderer = UIGraphicsImageRenderer(size: self.bounds.size) let image = renderer.image { ctx in self.drawHierarchy(in: self.bounds, afterScreenUpdates: true) } return image } } internal class CaretView: View { //------------------------------------------------------ // MARK: - Properties //------------------------------------------------------ private var caretPath: UIBezierPath = UIBezierPath() public var lineWidth: CGFloat = 1 { didSet{ didChange() } } public var direction: Direction = .right { didSet{ didChange() } } public var size: Size? { didSet{ didChange() } } public var colorConfiguration: AnyColorable = ViewColorConfiguration().with { $0.setSurfaceColors(VDSColor.elementsSecondaryOnlight, VDSColor.elementsSecondaryOndark, forDisabled: true) $0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forDisabled: false) }.eraseToAnyColorable() //------------------------------------------------------ // MARK: - Constraints //------------------------------------------------------ /// Sizes of CaretView are derived from InVision design specs. They are provided for convenience. public enum Size { case small(Orientation) case medium(Orientation) case large(Orientation) /// Orientation based on the longest line of the view. public enum Orientation { case vertical case horizontal } /// Dimensions of container; provided by InVision design. func dimensions() -> CGSize { switch self { case .small(let o): return o == .vertical ? CGSize(width: 6.9, height: 10.96) : CGSize(width: 10.96, height: 6.9) case .medium(let o): return o == .vertical ? CGSize(width: 9.9, height: 16.96) : CGSize(width: 16.96, height: 9.9) case .large(let o): return o == .vertical ? CGSize(width: 14.9, height: 24.96) : CGSize(width: 24.96, height: 14.9) } } } //------------------------------------------------------ // MARK: - Initialization //------------------------------------------------------ public override init(frame: CGRect) { super.init(frame: frame) } public convenience init(lineWidth: CGFloat) { self.init(frame: .zero) self.lineWidth = lineWidth } required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) fatalError("CaretView xib not supported.") } required public convenience init() { self.init(frame: .zero) } public convenience init(size: Size){ let dimensions = size.dimensions() self.init(frame: .init(x: 0, y: 0, width: dimensions.width, height: dimensions.height)) self.size = size } //------------------------------------------------------ // MARK: - Setup //------------------------------------------------------ override open func setup() { super.setup() defaultState() } //------------------------------------------------------ // MARK: - Drawing //------------------------------------------------------ /// The direction the caret will be pointing to. public enum Direction: Int { case left case right case down case up } override func draw(_ rect: CGRect) { super.draw(rect) caretPath.removeAllPoints() caretPath.lineJoinStyle = .miter caretPath.lineWidth = lineWidth let inset = lineWidth / 2 let halfWidth = frame.size.width / 2 let halfHeight = frame.size.height / 2 switch direction { case .up: caretPath.move(to: CGPoint(x: inset, y: frame.size.height - inset)) caretPath.addLine(to: CGPoint(x: halfWidth, y: inset)) caretPath.addLine(to: CGPoint(x: frame.size.width, y: frame.size.height)) case .right: caretPath.move(to: CGPoint(x: inset, y: inset)) caretPath.addLine(to: CGPoint(x: frame.size.width - inset, y: halfHeight)) caretPath.addLine(to: CGPoint(x: inset, y: frame.size.height - inset)) case .down: caretPath.move(to: CGPoint(x: inset, y: inset)) caretPath.addLine(to: CGPoint(x: halfWidth, y: frame.size.height - inset)) caretPath.addLine(to: CGPoint(x: frame.size.width - inset, y: inset)) case .left: caretPath.move(to: CGPoint(x: frame.size.width - inset, y: inset)) caretPath.addLine(to: CGPoint(x: inset, y: halfHeight)) caretPath.addLine(to: CGPoint(x: frame.size.width - inset, y: frame.size.height - inset)) } let color = colorConfiguration.getColor(self) color.setStroke() caretPath.stroke() } override func updateView() { setNeedsDisplay() } //------------------------------------------------------ // MARK: - Methods //------------------------------------------------------ public func setLineColor(_ color: UIColor) { setNeedsDisplay() } public func defaultState() { isOpaque = false isHidden = false backgroundColor = .clear } /// Ensure you have defined a CaretSize with Orientation before calling. public func setConstraints() { guard let dimensions = size?.dimensions() else { return } heightAnchor.constraint(equalToConstant: dimensions.height).isActive = true widthAnchor.constraint(equalToConstant: dimensions.width).isActive = true } }