Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/SignalUI/ImageEditor/ImageEditorTextItem.swift
1 views
//
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import UIKit

final class ImageEditorTextItem: ImageEditorItem, ImageEditorTransformable {

    let text: String

    let color: ColorPickerBarColor

    let decorationStyle: MediaTextView.DecorationStyle

    let textStyle: MediaTextView.TextStyle

    let fontSize: CGFloat
    static let defaultFontSize: CGFloat = 36
    var font: UIFont {
        MediaTextView.font(for: textStyle, withPointSize: fontSize)
    }

    var textForegroundColor: UIColor {
        switch decorationStyle {
        case .none, .whiteBackground: return color.color

        case .coloredBackground:
            let backgroundColor = color.color
            return backgroundColor.isCloseToColor(.white) ? .black : .white

        case .outline, .underline: return .white
        }
    }

    var textBackgroundColor: UIColor? {
        switch decorationStyle {
        case .none, .underline, .outline: return nil

        case .whiteBackground:
            let textColor = color.color
            return textColor.isCloseToColor(.white) ? .black : .white

        case .coloredBackground: return color.color
        }
    }

    var textDecorationColor: UIColor? {
        switch decorationStyle {
        case .none, .whiteBackground, .coloredBackground: return nil
        case .outline, .underline: return color.color
        }
    }

    // In order to render the text at a consistent size
    // in very differently sized contexts (canvas in
    // portrait, landscape, in the crop tool, before and
    // after cropping, while rendering output),
    // we need to scale the font size to reflect the
    // view width.
    //
    // We use the image's rendering width as the reference value,
    // since we want to be consistent with regard to the image's
    // content.
    let fontReferenceImageWidth: CGFloat

    let unitCenter: ImageEditorSample

    // Leave some margins against the edge of the image.
    static let kDefaultUnitWidth: CGFloat = 0.9

    // The max width of the text as a fraction of the image width.
    //
    // This provides continuity of text layout before/after cropping.
    //
    // NOTE: When you scale the text with with a pinch gesture, that
    // affects _scaling_, not the _unit width_, since we don't want
    // to change how the text wraps when scaling.
    let unitWidth: CGFloat

    // 0 = no rotation.
    // CGFloat.pi * 0.5 = rotation 90 degrees clockwise.
    let rotationRadians: CGFloat

    static let kMaxScaling: CGFloat = 4.0

    static let kMinScaling: CGFloat = 0.5

    let scaling: CGFloat

    init(
        text: String,
        color: ColorPickerBarColor,
        fontSize: CGFloat,
        textStyle: MediaTextView.TextStyle = .regular,
        decorationStyle: MediaTextView.DecorationStyle = .none,
        fontReferenceImageWidth: CGFloat,
        unitCenter: ImageEditorSample = ImageEditorSample(x: 0.5, y: 0.5),
        unitWidth: CGFloat = ImageEditorTextItem.kDefaultUnitWidth,
        rotationRadians: CGFloat = 0.0,
        scaling: CGFloat = 1.0,
    ) {
        self.text = text
        self.color = color
        self.fontSize = fontSize
        self.textStyle = textStyle
        self.decorationStyle = decorationStyle
        self.fontReferenceImageWidth = fontReferenceImageWidth
        self.unitCenter = unitCenter
        self.unitWidth = unitWidth
        self.rotationRadians = rotationRadians
        self.scaling = scaling

        super.init(itemType: .text)
    }

    init(
        itemId: String,
        text: String,
        color: ColorPickerBarColor,
        fontSize: CGFloat,
        textStyle: MediaTextView.TextStyle,
        decorationStyle: MediaTextView.DecorationStyle,
        fontReferenceImageWidth: CGFloat,
        unitCenter: ImageEditorSample,
        unitWidth: CGFloat,
        rotationRadians: CGFloat,
        scaling: CGFloat,
    ) {
        self.text = text
        self.color = color
        self.fontSize = fontSize
        self.textStyle = textStyle
        self.decorationStyle = decorationStyle
        self.fontReferenceImageWidth = fontReferenceImageWidth
        self.unitCenter = unitCenter
        self.unitWidth = unitWidth
        self.rotationRadians = rotationRadians
        self.scaling = scaling

        super.init(itemId: itemId, itemType: .text)
    }

    class func empty(
        withColor color: ColorPickerBarColor,
        textStyle: MediaTextView.TextStyle,
        decorationStyle: MediaTextView.DecorationStyle,
        unitWidth: CGFloat,
        fontReferenceImageWidth: CGFloat,
        scaling: CGFloat,
        rotationRadians: CGFloat,
    ) -> ImageEditorTextItem {
        return ImageEditorTextItem(
            text: "",
            color: color,
            fontSize: ImageEditorTextItem.defaultFontSize,
            textStyle: textStyle,
            decorationStyle: decorationStyle,
            fontReferenceImageWidth: fontReferenceImageWidth,
            unitWidth: unitWidth,
            rotationRadians: rotationRadians,
            scaling: scaling,
        )
    }

    func copy(withText newText: String, color newColor: ColorPickerBarColor) -> ImageEditorTextItem {
        return ImageEditorTextItem(
            itemId: itemId,
            text: newText,
            color: newColor,
            fontSize: fontSize,
            textStyle: textStyle,
            decorationStyle: decorationStyle,
            fontReferenceImageWidth: fontReferenceImageWidth,
            unitCenter: unitCenter,
            unitWidth: unitWidth,
            rotationRadians: rotationRadians,
            scaling: scaling,
        )
    }

    func copy(unitCenter: CGPoint) -> ImageEditorTextItem {
        return ImageEditorTextItem(
            itemId: itemId,
            text: text,
            color: color,
            fontSize: fontSize,
            textStyle: textStyle,
            decorationStyle: decorationStyle,
            fontReferenceImageWidth: fontReferenceImageWidth,
            unitCenter: unitCenter,
            unitWidth: unitWidth,
            rotationRadians: rotationRadians,
            scaling: scaling,
        )
    }

    func copy(scaling: CGFloat, rotationRadians: CGFloat) -> ImageEditorTextItem {
        return ImageEditorTextItem(
            itemId: itemId,
            text: text,
            color: color,
            fontSize: fontSize,
            textStyle: textStyle,
            decorationStyle: decorationStyle,
            fontReferenceImageWidth: fontReferenceImageWidth,
            unitCenter: unitCenter,
            unitWidth: unitWidth,
            rotationRadians: rotationRadians,
            scaling: scaling,
        )
    }

    func copy(unitWidth: CGFloat) -> ImageEditorTextItem {
        return ImageEditorTextItem(
            itemId: itemId,
            text: text,
            color: color,
            fontSize: fontSize,
            textStyle: textStyle,
            decorationStyle: decorationStyle,
            fontReferenceImageWidth: fontReferenceImageWidth,
            unitCenter: unitCenter,
            unitWidth: unitWidth,
            rotationRadians: rotationRadians,
            scaling: scaling,
        )
    }

    func copy(fontSize: CGFloat) -> ImageEditorTextItem {
        return ImageEditorTextItem(
            itemId: itemId,
            text: text,
            color: color,
            fontSize: fontSize,
            textStyle: textStyle,
            decorationStyle: decorationStyle,
            fontReferenceImageWidth: fontReferenceImageWidth,
            unitCenter: unitCenter,
            unitWidth: unitWidth,
            rotationRadians: rotationRadians,
            scaling: scaling,
        )
    }

    func copy(color: ColorPickerBarColor) -> ImageEditorTextItem {
        return ImageEditorTextItem(
            itemId: itemId,
            text: text,
            color: color,
            fontSize: fontSize,
            textStyle: textStyle,
            decorationStyle: decorationStyle,
            fontReferenceImageWidth: fontReferenceImageWidth,
            unitCenter: unitCenter,
            unitWidth: unitWidth,
            rotationRadians: rotationRadians,
            scaling: scaling,
        )
    }

    func copy(textStyle: MediaTextView.TextStyle, decorationStyle: MediaTextView.DecorationStyle) -> ImageEditorTextItem {
        return ImageEditorTextItem(
            itemId: itemId,
            text: text,
            color: color,
            fontSize: fontSize,
            textStyle: textStyle,
            decorationStyle: decorationStyle,
            fontReferenceImageWidth: fontReferenceImageWidth,
            unitCenter: unitCenter,
            unitWidth: unitWidth,
            rotationRadians: rotationRadians,
            scaling: scaling,
        )
    }

    override func outputScale() -> CGFloat {
        return scaling
    }

    static func ==(left: ImageEditorTextItem, right: ImageEditorTextItem) -> Bool {
        return left.text == right.text &&
            left.color == right.color &&
            left.textStyle == right.textStyle &&
            left.decorationStyle == right.decorationStyle &&
            left.fontSize.fuzzyEquals(right.fontSize) &&
            left.fontReferenceImageWidth.fuzzyEquals(right.fontReferenceImageWidth) &&
            left.unitCenter.fuzzyEquals(right.unitCenter) &&
            left.unitWidth.fuzzyEquals(right.unitWidth) &&
            left.rotationRadians.fuzzyEquals(right.rotationRadians) &&
            left.scaling.fuzzyEquals(right.scaling)
    }
}