283 lines
11 KiB
Swift
283 lines
11 KiB
Swift
//
|
|
// Copyright (c) 2022 Open Whisper Systems. All rights reserved.
|
|
//
|
|
|
|
import UIKit
|
|
|
|
public class MediaTextView: UITextView {
|
|
|
|
public enum DecorationStyle: Int {
|
|
case none = 0
|
|
case inverted
|
|
case underline
|
|
case outline
|
|
}
|
|
|
|
public enum TextStyle: Int {
|
|
case regular = 0
|
|
case bold
|
|
case serif
|
|
case script
|
|
case condensed
|
|
}
|
|
|
|
class func font(forTextStyle textStyle: TextStyle, pointSize: CGFloat) -> UIFont {
|
|
// TODO: this is a copy-paste code from TextAttachmentView that needs to be consolidated in one place.
|
|
let attributes: [UIFontDescriptor.AttributeName: Any]
|
|
|
|
switch textStyle {
|
|
case .regular:
|
|
attributes = [.name: "Inter-Regular_Medium"]
|
|
case .bold:
|
|
attributes = [.name: "Inter-Regular_Black"]
|
|
case .serif:
|
|
attributes = [.name: "EBGaramond-Regular"]
|
|
case .script:
|
|
attributes = [.name: "Parisienne-Regular"]
|
|
case .condensed:
|
|
// TODO: Ideally we could set an attribute to make this font
|
|
// all caps, but iOS deprecated that ability and didn't add
|
|
// a new equivalent function.
|
|
attributes = [.name: "BarlowCondensed-Medium"]
|
|
}
|
|
|
|
// TODO: Eventually we'll want to provide a cascadeList here to fallback
|
|
// to different fonts for different scripts rather than just relying on
|
|
// the built in OS fallbacks that don't tend to match the desired style.
|
|
let descriptor = UIFontDescriptor(fontAttributes: attributes)
|
|
|
|
return UIFont(descriptor: descriptor, size: pointSize)
|
|
}
|
|
|
|
private var kvoObservation: NSKeyValueObservation?
|
|
|
|
override init(frame: CGRect, textContainer: NSTextContainer?) {
|
|
super.init(frame: frame, textContainer: textContainer)
|
|
|
|
backgroundColor = .clear
|
|
isOpaque = false
|
|
isScrollEnabled = false
|
|
keyboardAppearance = .dark
|
|
scrollsToTop = false
|
|
textAlignment = .center
|
|
tintColor = .white
|
|
self.textContainer.lineFragmentPadding = 0
|
|
|
|
kvoObservation = observe(\.contentSize, options: [.new]) { [weak self] _, _ in
|
|
guard let self = self else { return }
|
|
self.adjustFontSizeIfNecessary()
|
|
}
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
private func adjustFontSizeIfNecessary() {
|
|
// TODO: Figure out correct way to handle long text and implement it.
|
|
}
|
|
|
|
public func update(using textStylingToolbar: TextStylingToolbar,
|
|
fontPointSize: CGFloat,
|
|
textAlignment: NSTextAlignment = .center) {
|
|
let font = MediaTextView.font(forTextStyle: textStylingToolbar.textStyle, pointSize: fontPointSize)
|
|
updateWith(textForegroundColor: textStylingToolbar.textForegroundColor,
|
|
font: font,
|
|
textAlignment: textAlignment,
|
|
textDecorationColor: textStylingToolbar.textDecorationColor,
|
|
decorationStyle: textStylingToolbar.decorationStyle)
|
|
}
|
|
|
|
public func updateWith(textForegroundColor: UIColor,
|
|
font: UIFont,
|
|
textAlignment: NSTextAlignment,
|
|
textDecorationColor: UIColor?,
|
|
decorationStyle: MediaTextView.DecorationStyle) {
|
|
var attributes: [NSAttributedString.Key: Any] = [ .font: font]
|
|
|
|
attributes[.foregroundColor] = textForegroundColor
|
|
|
|
if let paragraphStyle = NSParagraphStyle.default.mutableCopy() as? NSMutableParagraphStyle {
|
|
paragraphStyle.alignment = textAlignment
|
|
attributes[.paragraphStyle] = paragraphStyle
|
|
}
|
|
|
|
if let textDecorationColor = textDecorationColor {
|
|
switch decorationStyle {
|
|
case .underline:
|
|
attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue
|
|
attributes[.underlineColor] = textDecorationColor
|
|
|
|
case .outline:
|
|
attributes[.strokeWidth] = -3
|
|
attributes[.strokeColor] = textDecorationColor
|
|
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
attributedText = NSAttributedString(string: text, attributes: attributes)
|
|
|
|
// This makes UITextView apply text styling to the text that user enters.
|
|
typingAttributes = attributes
|
|
|
|
invalidateIntrinsicContentSize()
|
|
}
|
|
|
|
// MARK: - Key Commands
|
|
|
|
public override var keyCommands: [UIKeyCommand]? {
|
|
return [
|
|
UIKeyCommand(input: "\r", modifierFlags: .command, action: #selector(modifiedReturnPressed(sender:)), discoverabilityTitle: "Add Text"),
|
|
UIKeyCommand(input: "\r", modifierFlags: .alternate, action: #selector(modifiedReturnPressed(sender:)), discoverabilityTitle: "Add Text")
|
|
]
|
|
}
|
|
|
|
@objc
|
|
private func modifiedReturnPressed(sender: UIKeyCommand) {
|
|
Logger.verbose("")
|
|
|
|
acceptAutocorrectSuggestion()
|
|
resignFirstResponder()
|
|
}
|
|
}
|
|
|
|
public class TextStylingToolbar: UIControl {
|
|
|
|
public enum Layout {
|
|
case photoOverlay
|
|
case textStory
|
|
}
|
|
let layout: Layout
|
|
|
|
private let colorPickerView: ColorPickerBarView
|
|
|
|
// Photo Editor operates with ColorPickerBarColor hence the need to expose this value.
|
|
public var currentColorPickerValue: ColorPickerBarColor {
|
|
get { colorPickerView.selectedValue }
|
|
set { colorPickerView.selectedValue = newValue }
|
|
}
|
|
|
|
private static func defaultColor(forLayout layout: Layout) -> ColorPickerBarColor {
|
|
switch layout {
|
|
case .photoOverlay:
|
|
return ColorPickerBarColor.defaultColor()
|
|
case .textStory:
|
|
return ColorPickerBarColor.white
|
|
}
|
|
}
|
|
|
|
public let textStyleButton = RoundMediaButton(image: #imageLiteral(resourceName: "media-editor-text-font"), backgroundStyle: .blur)
|
|
public var textStyle: MediaTextView.TextStyle = .regular
|
|
|
|
public var textForegroundColor: UIColor {
|
|
switch decorationStyle {
|
|
case .none: return colorPickerView.color
|
|
case .inverted, .outline, .underline: return .white
|
|
}
|
|
}
|
|
public var textBackgroundColor: UIColor? {
|
|
switch decorationStyle {
|
|
case .none, .underline, .outline: return nil
|
|
case .inverted: return colorPickerView.color
|
|
}
|
|
}
|
|
public var textDecorationColor: UIColor? {
|
|
switch decorationStyle {
|
|
case .none, .inverted: return nil
|
|
case .outline, .underline: return colorPickerView.color
|
|
}
|
|
}
|
|
|
|
public let decorationStyleButton = RoundMediaButton(image: #imageLiteral(resourceName: "media-editor-text-style-1"), backgroundStyle: .blur)
|
|
public var decorationStyle: MediaTextView.DecorationStyle = .none {
|
|
didSet {
|
|
decorationStyleButton.isSelected = (decorationStyle != .none)
|
|
// Change default decoration style away from white to avoid white on white situation.
|
|
if decorationStyle != .none && colorPickerView.selectedValue == .white {
|
|
colorPickerView.selectedValue = .black
|
|
}
|
|
// Switch color picker color back to white because it now controls text color.
|
|
// The idea is that cycling through decoration styles should keep text white.
|
|
if decorationStyle == .none && colorPickerView.selectedValue == .black {
|
|
colorPickerView.selectedValue = .white
|
|
}
|
|
}
|
|
}
|
|
|
|
public lazy var doneButton = RoundMediaButton(image: UIImage(imageLiteralResourceName: "check-24"), backgroundStyle: .blur)
|
|
|
|
public init(layout: Layout, currentColor: ColorPickerBarColor? = nil) {
|
|
self.layout = layout
|
|
colorPickerView = ColorPickerBarView(currentColor: currentColor ?? TextStylingToolbar.defaultColor(forLayout: layout))
|
|
|
|
super.init(frame: .zero)
|
|
|
|
colorPickerView.delegate = self
|
|
|
|
decorationStyleButton.setContentCompressionResistancePriority(.required, for: .vertical)
|
|
decorationStyleButton.setImage(#imageLiteral(resourceName: "media-editor-text-style-2"), for: .selected)
|
|
|
|
// A container with width capped at a predefined size,
|
|
// centered in superview and constrained to layout margins.
|
|
let stackViewLayoutGuide = UILayoutGuide()
|
|
addLayoutGuide(stackViewLayoutGuide)
|
|
addConstraints([
|
|
stackViewLayoutGuide.centerXAnchor.constraint(equalTo: centerXAnchor),
|
|
stackViewLayoutGuide.leadingAnchor.constraint(greaterThanOrEqualTo: layoutMarginsGuide.leadingAnchor),
|
|
stackViewLayoutGuide.topAnchor.constraint(equalTo: topAnchor),
|
|
stackViewLayoutGuide.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -2) ])
|
|
addConstraint({
|
|
let constraint = stackViewLayoutGuide.widthAnchor.constraint(equalToConstant: ImageEditorViewController.preferredToolbarContentWidth)
|
|
constraint.priority = .defaultHigh
|
|
return constraint
|
|
}())
|
|
|
|
// I had to use a custom layout guide because stack view isn't centered
|
|
// but instead has slight offset towards the trailing edge.
|
|
let stackViewSubviews: [UIView] = {
|
|
switch layout {
|
|
case .photoOverlay:
|
|
return [ colorPickerView, textStyleButton, decorationStyleButton ]
|
|
case .textStory:
|
|
return [ textStyleButton, decorationStyleButton, colorPickerView, doneButton ]
|
|
}
|
|
}()
|
|
let stackView = UIStackView(arrangedSubviews: stackViewSubviews)
|
|
stackView.translatesAutoresizingMaskIntoConstraints = false
|
|
stackView.alignment = .center
|
|
stackView.spacing = 8
|
|
stackView.setCustomSpacing(0, after: textStyleButton)
|
|
addSubview(stackView)
|
|
|
|
// Round buttons have no-zero layout margins. Use values of those margins
|
|
// to offset button positions so that they appear properly aligned.
|
|
var leadingMargin: CGFloat = 0
|
|
var trailingMargin: CGFloat = 0
|
|
if let button = stackViewSubviews.first as? RoundMediaButton {
|
|
leadingMargin = button.layoutMargins.leading
|
|
}
|
|
if let button = stackViewSubviews.last as? RoundMediaButton {
|
|
trailingMargin = button.layoutMargins.trailing
|
|
}
|
|
addConstraints([
|
|
stackView.leadingAnchor.constraint(equalTo: stackViewLayoutGuide.leadingAnchor, constant: -leadingMargin),
|
|
stackView.trailingAnchor.constraint(equalTo: stackViewLayoutGuide.trailingAnchor, constant: trailingMargin),
|
|
stackView.topAnchor.constraint(equalTo: stackViewLayoutGuide.topAnchor),
|
|
stackView.bottomAnchor.constraint(equalTo: stackViewLayoutGuide.bottomAnchor) ])
|
|
}
|
|
|
|
@available(iOS, unavailable, message: "Use init(currentColor:)")
|
|
required init(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
}
|
|
|
|
extension TextStylingToolbar: ColorPickerBarViewDelegate {
|
|
|
|
public func colorPickerBarView(_ pickerView: ColorPickerBarView, didSelectColor color: ColorPickerBarColor) {
|
|
sendActions(for: .valueChanged)
|
|
}
|
|
}
|