diff --git a/VDS/Components/Buttons/TextLinkCaret/TextLinkCaret.swift b/VDS/Components/Buttons/TextLinkCaret/TextLinkCaret.swift index 9ea3f4e5..c3103cc7 100644 --- a/VDS/Components/Buttons/TextLinkCaret/TextLinkCaret.swift +++ b/VDS/Components/Buttons/TextLinkCaret/TextLinkCaret.swift @@ -11,7 +11,7 @@ import VDSColorTokens import VDSFormControlsTokens import Combine -public enum TextLinkIconPosition: String, CaseIterable { +public enum TextLinkCaretPosition: String, CaseIterable { case left, right } @@ -22,14 +22,26 @@ open class TextLinkCaret: Control { // MARK: - Private Properties //-------------------------------------------------- private var heightConstraint: NSLayoutConstraint? - private var label = Label() + + private let containerView = UIView().with{ + $0.translatesAutoresizingMaskIntoConstraints = false + } + + private var label = Label().with { + $0.typograpicalStyle = TypographicalStyle.BoldBodyLarge + } + + private var caretView = CaretView().with { + $0.size = CaretView.CaretSize.small(.vertical) + $0.lineWidth = 2 + } //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- open var text: String? { didSet { didChange() } } - open var iconPosition: TextLinkIconPosition = .right { didSet { didChange() } } + open var iconPosition: TextLinkCaretPosition = .right { didSet { didChange() } } private var height: CGFloat { 44 @@ -62,21 +74,55 @@ open class TextLinkCaret: Control { open override func setup() { super.setup() - - addSubview(label) - //add tapGesture to self publisher(for: UITapGestureRecognizer()).sink { [weak self] _ in self?.sendActions(for: .touchUpInside) }.store(in: &subscribers) - - //pin stackview to edges - label.topAnchor.constraint(equalTo: topAnchor).isActive = true - label.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true - label.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true - label.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true + + addSubview(containerView) + containerView.addSubview(label) + containerView.addSubview(caretView) + + //constraints heightAnchor.constraint(equalToConstant: height).isActive = true + + containerView.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true + containerView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true + containerView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true + containerView.heightAnchor.constraint(lessThanOrEqualToConstant: height).isActive = true + + label.topAnchor.constraint(equalTo: containerView.topAnchor).isActive = true + label.bottomAnchor.constraint(equalTo: containerView.bottomAnchor).isActive = true + label.widthAnchor.constraint(lessThanOrEqualTo: containerView.widthAnchor, multiplier: 0.90).isActive = true + caretView.bottomAnchor.constraint(lessThanOrEqualTo: label.bottomAnchor, constant: -3).isActive = true + caretView.setConstraints() + } + + //-------------------------------------------------- + // MARK: - Constraints + //-------------------------------------------------- + private var caretLeadingConstraint: NSLayoutConstraint? + private var caretTrailingConstraint: NSLayoutConstraint? + private var labelConstraint: NSLayoutConstraint? + + private func setConstraints(){ + caretLeadingConstraint?.isActive = false + caretTrailingConstraint?.isActive = false + labelConstraint?.isActive = false + if iconPosition == .right { + labelConstraint = label.leadingAnchor.constraint(equalTo: containerView.leadingAnchor) + caretLeadingConstraint = caretView.leadingAnchor.constraint(equalTo: label.trailingAnchor, constant: 4) + caretTrailingConstraint = caretView.trailingAnchor.constraint(lessThanOrEqualTo: containerView.trailingAnchor) + } else { + labelConstraint = label.trailingAnchor.constraint(lessThanOrEqualTo: containerView.trailingAnchor) + caretLeadingConstraint = caretView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor) + caretTrailingConstraint = caretView.trailingAnchor.constraint(equalTo: label.leadingAnchor, constant: -4) + } + caretTrailingConstraint?.isActive = true + caretLeadingConstraint?.isActive = true + labelConstraint?.isActive = true + self.layoutIfNeeded() } open override func reset() { @@ -91,8 +137,170 @@ open class TextLinkCaret: Control { open override func updateView() { label.surface = surface label.disabled = disabled - label.typograpicalStyle = TypographicalStyle.BoldBodyLarge - label.text = text ?? "" + label.text = text + + caretView.direction = iconPosition == .right ? CaretView.Direction.right : CaretView.Direction.left + setConstraints() } } + +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: CaretSize? { didSet{ didChange() } } + + public var colorConfiguration: AnyColorable = DisabledSurfaceColorConfiguration().with { + $0.disabled.lightColor = VDSColor.elementsSecondaryOnlight + $0.disabled.darkColor = VDSColor.elementsSecondaryOndark + $0.enabled.lightColor = VDSColor.elementsPrimaryOnlight + $0.enabled.darkColor = VDSColor.elementsPrimaryOndark + }.eraseToAnyColorable() + + + //------------------------------------------------------ + // MARK: - Constraints + //------------------------------------------------------ + + /// Sizes of CaretView are derived from InVision design specs. They are provided for convenience. + public enum CaretSize { + 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) + } + + //------------------------------------------------------ + // 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 + } +}