Files
ichmagmaus111andClaude Opus 5.5 0622057a4f Ghostgram: open-source release
Ghostgram for iOS, rebuilt from scratch on Telegram iOS 13.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 17:57:09 +01:00

781 lines
32 KiB
Swift

import Foundation
import UIKit
import CoreText
import Display
import ComponentFlow
import AnimatedTextComponent
import MultilineTextComponent
import TelegramPresentationData
import PresentationDataUtils
import TelegramStringFormatting
import WalletContext
func walletSendInputText(
amount: Int64,
mode: WalletSendInputMode,
rate: Double?,
dateTimeFormat: PresentationDateTimeFormat
) -> String {
guard amount > 0 else {
return ""
}
switch mode {
case .gram:
return formatTonAmountText(
amount,
dateTimeFormat: PresentationDateTimeFormat(
timeFormat: dateTimeFormat.timeFormat,
dateFormat: dateTimeFormat.dateFormat,
dateSeparator: "",
dateSuffix: "",
requiresFullYear: false,
decimalSeparator: dateTimeFormat.decimalSeparator,
groupingSeparator: ""
),
maxDecimalPositions: 9
)
case .fiat:
guard let rate, rate.isFinite, rate > 0.0 else {
return ""
}
let value = Double(amount) / 1_000_000_000.0 * rate
guard value.isFinite else {
return ""
}
var text = String(
format: "%.2f",
locale: Locale(identifier: "en_US_POSIX"),
value
)
while text.hasSuffix("0") {
text.removeLast()
}
if text.hasSuffix(".") {
text.removeLast()
}
return text.replacingOccurrences(
of: ".",
with: dateTimeFormat.decimalSeparator
)
}
}
func walletSendGroupedAmountText(_ text: String, dateTimeFormat: PresentationDateTimeFormat) -> String {
return walletSendGroupedAmountText(text, decimalSeparator: dateTimeFormat.decimalSeparator, groupingSeparator: dateTimeFormat.groupingSeparator)
}
struct WalletSendAmountTextLayout {
static let paragraphStyle: NSParagraphStyle = {
let style = NSMutableParagraphStyle()
style.alignment = .left
style.baseWritingDirection = .leftToRight
return style
}()
let attributedText: NSAttributedString
let groupingSeparator: NSAttributedString
let groupingSeparatorSize: CGSize
let groupingPositions: [CGFloat]
}
final class WalletSendAmountTextField: UITextField {
private let groupingView = UIView()
private var groupingLabels: [UILabel] = []
private var textLayout: WalletSendAmountTextLayout?
var interactionBegan: (() -> Void)?
var emptyDeletion: (() -> Void)?
var caretColor: UIColor = .clear {
didSet {
self.updateSelectionTintColor()
}
}
var usesCustomCaret = false {
didSet {
self.updateSelectionTintColor()
self.setNeedsLayout()
}
}
var displaysNativeCaret = true {
didSet {
guard self.displaysNativeCaret != oldValue else { return }
self.updateSelectionTintColor()
self.setNeedsLayout()
}
}
override var selectedTextRange: UITextRange? {
didSet {
self.updateSelectionTintColor()
}
}
var rendersText = true {
didSet {
guard self.rendersText != oldValue, let layout = self.textLayout else { return }
self.update(layout: layout, selection: self.selectionRange)
}
}
private var displaysNativeSelection: Bool {
return self.displaysNativeCaret && (!self.usesCustomCaret || self.selectedTextRange?.isEmpty == false)
}
private func updateSelectionTintColor() {
let color: UIColor = self.displaysNativeSelection ? self.caretColor : .clear
if self.tintColor != color {
self.tintColor = color
}
}
override func caretRect(for position: UITextPosition) -> CGRect {
return self.displaysNativeSelection ? self.amountCaretRect(for: position) : .zero
}
func nativeCaretRect(for position: UITextPosition) -> CGRect {
return super.caretRect(for: position)
}
func amountTextBaseline(font: UIFont) -> CGFloat {
let centerY: CGFloat
if (self.text ?? "").isEmpty {
centerY = self.placeholderRect(forBounds: self.bounds).midY
} else {
let caret = self.nativeCaretRect(for: self.beginningOfDocument)
if !caret.isNull, !caret.isInfinite, caret.height > 0.0 {
centerY = caret.midY
} else {
let textRect = self.isEditing ? self.editingRect(forBounds: self.bounds) : self.textRect(forBounds: self.bounds)
centerY = textRect.midY
}
}
return centerY + (font.ascender + font.descender) / 2.0
}
func amountCaretRect(for position: UITextPosition) -> CGRect {
var rect = self.nativeCaretRect(for: position)
if !rect.isNull, !rect.isInfinite {
let width: CGFloat = 3.0
var offset: CGFloat = self.offset(from: self.beginningOfDocument, to: position) == 0 ? 0.0 : 5.0
if (self.text ?? "").isEmpty, let placeholder = self.attributedPlaceholder, placeholder.length > 0 {
let line = CTLineCreateWithAttributedString(placeholder)
rect.origin.x += CGFloat(CTLineGetOffsetForStringIndex(line, placeholder.length, nil))
offset = 3.0
} else if self.text == "0", offset > 0.0 {
offset = 4.0
}
rect.origin.x = floorToScreenPixels(rect.midX - width / 2.0) + offset
rect.size.width = width
}
let integralFont: UIFont?
if let text = self.textLayout?.attributedText, text.length > 0 {
integralFont = text.attribute(.font, at: 0, effectiveRange: nil) as? UIFont ?? self.font
} else {
integralFont = self.font
}
guard !rect.isNull, !rect.isInfinite, rect.height > 0.0,
let integralFont else { return rect }
var caretFont = integralFont
if let text = self.textLayout?.attributedText, text.length > 0 {
let offset = self.offset(from: self.beginningOfDocument, to: position)
let index = min(text.length - 1, max(0, offset - 1))
caretFont = text.attribute(.font, at: index, effectiveRange: nil) as? UIFont ?? integralFont
}
let baseline = self.amountTextBaseline(font: integralFont)
let height = caretFont.capHeight * 1.24
return CGRect(
x: rect.minX,
y: baseline - caretFont.capHeight / 2.0 - height / 2.0,
width: rect.width,
height: height
)
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.point(inside: point, with: event), event?.type == .touches {
self.interactionBegan?()
}
return super.hitTest(point, with: event)
}
var selectionRange: NSRange? {
guard let selection = self.selectedTextRange else { return nil }
let start = self.offset(from: self.beginningOfDocument, to: selection.start)
let end = self.offset(from: self.beginningOfDocument, to: selection.end)
return NSRange(location: start, length: end - start)
}
override init(frame: CGRect) {
super.init(frame: frame)
self.semanticContentAttribute = .forceLeftToRight
self.defaultTextAttributes[.paragraphStyle] = WalletSendAmountTextLayout.paragraphStyle
self.groupingView.isUserInteractionEnabled = false
self.groupingView.accessibilityElementsHidden = true
self.addSubview(self.groupingView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func baseWritingDirection(for position: UITextPosition, in direction: UITextStorageDirection) -> NSWritingDirection {
return .leftToRight
}
override func setBaseWritingDirection(_ writingDirection: NSWritingDirection, for range: UITextRange) {
super.setBaseWritingDirection(.leftToRight, for: range)
}
override func deleteBackward() {
if (self.text ?? "").isEmpty {
self.emptyDeletion?()
} else {
super.deleteBackward()
}
}
override func buildMenu(with builder: UIMenuBuilder) {
super.buildMenu(with: builder)
builder.remove(menu: .replace)
builder.remove(menu: .lookup)
builder.remove(menu: .learn)
builder.remove(menu: .share)
if #available(iOS 17.0, *) {
builder.remove(menu: .autoFill)
}
}
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
if #available(iOS 15.0, *), action == #selector(captureTextFromCamera(_:)) {
return false
}
return super.canPerformAction(action, withSender: sender)
}
func update(layout: WalletSendAmountTextLayout, selection: NSRange?) {
self.textLayout = layout
let renderedText = NSMutableAttributedString(attributedString: layout.attributedText)
if !self.rendersText {
renderedText.addAttribute(.foregroundColor, value: UIColor.clear, range: NSRange(location: 0, length: renderedText.length))
}
if self.attributedText?.isEqual(to: renderedText) != true {
self.attributedText = renderedText
}
self.groupingView.isHidden = !self.rendersText
if let selection {
let textLength = layout.attributedText.length
let start = min(textLength, max(0, selection.location))
let end = start + min(textLength - start, max(0, selection.length))
if self.selectionRange != NSRange(location: start, length: end - start),
let startPosition = self.position(from: self.beginningOfDocument, offset: start),
let endPosition = self.position(from: self.beginningOfDocument, offset: end) {
self.selectedTextRange = self.textRange(from: startPosition, to: endPosition)
}
}
self.setNeedsLayout()
}
override func layoutSubviews() {
super.layoutSubviews()
self.groupingView.frame = self.bounds
self.bringSubviewToFront(self.groupingView)
guard let textLayout = self.textLayout else { return }
while self.groupingLabels.count > textLayout.groupingPositions.count {
self.groupingLabels.removeLast().removeFromSuperview()
}
while self.groupingLabels.count < textLayout.groupingPositions.count {
let label = UILabel()
label.isUserInteractionEnabled = false
label.isAccessibilityElement = false
self.groupingView.addSubview(label)
self.groupingLabels.append(label)
}
let startCaret = self.nativeCaretRect(for: self.beginningOfDocument)
let textOriginX: CGFloat
let centerY: CGFloat
if !startCaret.isNull, !startCaret.isInfinite, startCaret.height > 0.0 {
textOriginX = startCaret.minX
centerY = startCaret.midY
} else {
let textRect = self.isEditing ? self.editingRect(forBounds: self.bounds) : self.textRect(forBounds: self.bounds)
textOriginX = textRect.minX
centerY = textRect.midY
}
for (index, position) in textLayout.groupingPositions.enumerated() {
let label = self.groupingLabels[index]
label.attributedText = textLayout.groupingSeparator
label.frame = CGRect(
x: textOriginX + position - textLayout.groupingSeparatorSize.width,
y: centerY - textLayout.groupingSeparatorSize.height / 2.0,
width: textLayout.groupingSeparatorSize.width,
height: textLayout.groupingSeparatorSize.height
)
}
}
}
class WalletSendAmountField: UIView, UITextFieldDelegate {
let contentView = UIView()
var gramIcon = ComponentView<Empty>()
let fiatIcon = ComponentView<Empty>()
let textField = WalletSendAmountTextField(frame: .zero)
let suffix = ComponentView<Empty>()
let integralFont = WalletSendAmountFonts.integral
let fractionalFont = WalletSendAmountFonts.fractional
private let gramIconLayoutSize = CGSize(width: 44.0, height: 44.0)
var gramAnimationSize: CGSize { return CGSize(width: 48.0, height: 48.0) }
var fiatSymbolFont: UIFont { return Font.with(size: 48.0, design: .round, weight: .bold, traits: [.alternateDollarSign]) }
private var fiatIconSize: CGSize = .zero
private var fiatSymbolInkBounds: CGRect = .zero
private var suffixSize: CGSize = .zero
private(set) var mode: WalletSendInputMode = .gram
private var amount: Int64 = 0
private var rate: Double?
private(set) var dateTimeFormat: PresentationDateTimeFormat?
private(set) var isApplyingText = false
private(set) var amountTextColor: UIColor = .black
var usesAnimatedPresentation: Bool { return false }
var amountUpdated: ((Int64) -> Void)?
var focusUpdated: ((Bool) -> Void)?
var isInputActive: Bool {
return self.textField.isFirstResponder
}
var hasInputText: Bool {
return !(self.textField.text ?? "").isEmpty
}
private var canEditAmount: Bool {
guard self.isUserInteractionEnabled,
let dateTimeFormat = self.dateTimeFormat, !dateTimeFormat.decimalSeparator.isEmpty else {
return false
}
if self.mode == .fiat {
guard let rate = self.rate, rate.isFinite, rate > 0.0 else { return false }
}
return true
}
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(self.contentView)
self.textField.font = self.integralFont
self.textField.delegate = self
self.textField.inputView = UIView(frame: .zero)
self.textField.inputAssistantItem.leadingBarButtonGroups = []
self.textField.inputAssistantItem.trailingBarButtonGroups = []
self.textField.autocorrectionType = .no
self.textField.autocapitalizationType = .none
self.textField.spellCheckingType = .no
self.textField.smartQuotesType = .no
self.textField.smartDashesType = .no
self.textField.smartInsertDeleteType = .no
self.textField.textContentType = nil
if #available(iOS 17.0, *) {
self.textField.inlinePredictionType = .no
}
if #available(iOS 18.0, *) {
self.textField.writingToolsBehavior = .none
self.textField.mathExpressionCompletionType = .no
}
self.textField.textAlignment = .left
self.textField.addTarget(self, action: #selector(self.textChanged), for: .editingChanged)
self.textField.emptyDeletion = { [weak self] in
let _ = self?.deleteBackward()
}
self.contentView.addSubview(self.textField)
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.activateInput))
self.addGestureRecognizer(tapGesture)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc func activateInput() {
guard self.isUserInteractionEnabled else { return }
self.textField.becomeFirstResponder()
}
@discardableResult
func insertText(_ text: String) -> Bool {
guard self.canEditAmount else { return false }
self.activateInput()
let range = self.textField.selectionRange ?? NSRange(location: (self.textField.text ?? "").utf16.count, length: 0)
return self.replaceText(in: range, with: text)
}
@discardableResult
func deleteBackward() -> Bool {
guard self.canEditAmount else { return false }
self.activateInput()
let text = (self.textField.text ?? "") as NSString
var range = self.textField.selectionRange ?? NSRange(location: text.length, length: 0)
guard range.location != NSNotFound, range.location >= 0, range.location <= text.length,
range.length >= 0, range.length <= text.length - range.location else { return false }
if range.length == 0 {
guard range.location > 0 else {
if text.length == 0 {
self.inputRejected()
}
return false
}
range = text.rangeOfComposedCharacterSequence(at: range.location - 1)
}
return self.replaceText(in: range, with: "")
}
func inputRejected() {
}
func amountTextLayout(_ text: String) -> WalletSendAmountTextLayout {
let textColor = self.amountTextColor
let decimalSeparator = self.dateTimeFormat?.decimalSeparator ?? "."
let groupingSeparator = self.dateTimeFormat?.groupingSeparator ?? ""
let attributedText = NSMutableAttributedString(attributedString: tonAmountAttributedString(
text,
integralFont: self.integralFont,
fractionalFont: self.fractionalFont,
color: textColor,
decimalSeparator: decimalSeparator
))
attributedText.addAttribute(.paragraphStyle, value: WalletSendAmountTextLayout.paragraphStyle, range: NSRange(location: 0, length: attributedText.length))
let separatorText = NSAttributedString(string: groupingSeparator, font: self.integralFont, textColor: textColor)
let separatorLine = CTLineCreateWithAttributedString(separatorText)
let separatorSize = CGSize(
width: ceil(CGFloat(CTLineGetTypographicBounds(separatorLine, nil, nil, nil))),
height: ceil(self.integralFont.lineHeight)
)
let decimalRange = (text as NSString).range(of: decimalSeparator)
let integralLength = decimalRange.location == NSNotFound ? attributedText.length : decimalRange.location
var groupingOffsets: [Int] = []
if !groupingSeparator.isEmpty, integralLength > 3 {
groupingOffsets = Array(stride(from: integralLength - 3, through: 1, by: -3).reversed())
for offset in groupingOffsets {
attributedText.addAttribute(.kern, value: separatorSize.width, range: NSRange(location: offset - 1, length: 1))
}
}
var groupingPositions: [CGFloat] = []
if !groupingOffsets.isEmpty {
let line = CTLineCreateWithAttributedString(attributedText)
for runValue in CTLineGetGlyphRuns(line) as NSArray {
let run = runValue as! CTRun
let glyphCount = CTRunGetGlyphCount(run)
guard glyphCount > 0 else { continue }
var positions = [CGPoint](repeating: .zero, count: glyphCount)
var indices = [CFIndex](repeating: 0, count: glyphCount)
let range = CFRangeMake(0, glyphCount)
CTRunGetPositions(run, range, &positions)
CTRunGetStringIndices(run, range, &indices)
for index in 0 ..< glyphCount where groupingOffsets.contains(indices[index]) {
groupingPositions.append(positions[index].x)
}
}
}
return WalletSendAmountTextLayout(
attributedText: attributedText,
groupingSeparator: separatorText,
groupingSeparatorSize: separatorSize,
groupingPositions: groupingPositions
)
}
func willApplyText(_ text: String, selection: NSRange?) {
}
func inputAccepted(_ insertedText: String) {
}
private func applyText(_ text: String, selection: NSRange?) {
self.isApplyingText = true
self.willApplyText(text, selection: selection)
self.textField.update(layout: self.amountTextLayout(text), selection: selection)
self.isApplyingText = false
self.setNeedsLayout()
}
@objc private func textChanged() {
guard !self.isApplyingText, let dateTimeFormat = self.dateTimeFormat else {
return
}
self.applyText(self.textField.text ?? "", selection: self.textField.selectionRange)
if let amount = walletSendNanograms(
text: self.textField.text ?? "",
mode: self.mode,
rate: self.rate,
decimalSeparator: dateTimeFormat.decimalSeparator
) {
self.amount = amount
self.amountUpdated?(amount)
}
self.setNeedsLayout()
}
func setAmount(_ amount: Int64) {
self.amount = amount
guard let dateTimeFormat = self.dateTimeFormat else {
return
}
let inputText = walletSendInputText(
amount: amount,
mode: self.mode,
rate: self.rate,
dateTimeFormat: dateTimeFormat
)
self.applyText(inputText, selection: NSRange(location: inputText.utf16.count, length: 0))
}
func update(
mode: WalletSendInputMode,
amount: Int64,
rate: Double?,
fiatCurrency: WalletContext.FiatCurrency,
dateTimeFormat: PresentationDateTimeFormat,
theme: PresentationTheme,
isVisible: Bool,
transition: ComponentTransition
) {
let modeChanged = self.mode != mode
let amountChanged = self.amount != amount
let rateChanged = self.rate != rate
let previousDecimalSeparator = self.dateTimeFormat?.decimalSeparator
let decimalSeparatorChanged = self.dateTimeFormat?.decimalSeparator != dateTimeFormat.decimalSeparator
let groupingSeparatorChanged = self.dateTimeFormat?.groupingSeparator != dateTimeFormat.groupingSeparator
let textColorChanged = !self.amountTextColor.isEqual(theme.list.itemPrimaryTextColor)
let previousSelection = self.textField.selectionRange
self.mode = mode
self.amount = amount
self.rate = rate
self.dateTimeFormat = dateTimeFormat
if textColorChanged {
self.amountTextColor = theme.list.itemPrimaryTextColor
self.textField.textColor = theme.list.itemPrimaryTextColor
}
let currencyColor = UIColor(rgb: mode == .gram ? (theme.overallDarkAppearance ? 0x30A1F5 : 0x0088FF) : 0x219949)
self.textField.caretColor = currencyColor
self.textField.attributedPlaceholder = self.amountTextLayout("0").attributedText
let suffixText: String
switch mode {
case .gram:
suffixText = "GRAM"
case .fiat:
suffixText = fiatCurrency.code
}
let currencyTransition: ComponentTransition = self.gramIcon.view == nil ? .immediate : transition
let iconBlurRadius: CGFloat = 6.0
self.updateGramIcon(theme: theme, isVisible: isVisible, transition: currencyTransition)
let currencySymbol = fiatCurrency.symbol
self.fiatSymbolInkBounds = WalletSendAmountGlyphMetrics.inkBounds(currencySymbol, font: self.fiatSymbolFont)
self.fiatIconSize = self.fiatIcon.update(
transition: transition,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: currencySymbol,
font: self.fiatSymbolFont,
textColor: UIColor(rgb: 0x219949)
)),
maximumNumberOfLines: 1
)),
environment: {},
containerSize: CGSize(width: 40.0, height: 74.0)
)
if let fiatIconView = self.fiatIcon.view {
if fiatIconView.superview == nil {
self.contentView.addSubview(fiatIconView)
}
if !self.usesAnimatedPresentation {
currencyTransition.setAlpha(view: fiatIconView, alpha: mode == .fiat ? 1.0 : 0.0)
currencyTransition.setBlur(layer: fiatIconView.layer, radius: mode == .fiat ? 0.0 : iconBlurRadius)
}
}
self.suffixSize = self.suffix.update(
transition: currencyTransition,
component: AnyComponent(AnimatedTextComponent(
font: self.fractionalFont,
color: currencyColor,
items: [
AnimatedTextComponent.Item(id: "currency", content: .text(suffixText))
],
noDelay: true,
blur: true
)),
environment: {},
containerSize: CGSize(width: 150.0, height: 74.0)
)
if let suffixView = self.suffix.view, suffixView.superview == nil {
self.contentView.addSubview(suffixView)
}
if modeChanged || ((amountChanged || rateChanged) && !self.textField.isFirstResponder) {
self.setAmount(amount)
} else if textColorChanged || decimalSeparatorChanged || groupingSeparatorChanged {
var text = self.textField.text ?? ""
var selection = previousSelection
if decimalSeparatorChanged, let previousDecimalSeparator, !previousDecimalSeparator.isEmpty {
let range = (text as NSString).range(of: previousDecimalSeparator)
if range.location != NSNotFound {
text = (text as NSString).replacingCharacters(in: range, with: dateTimeFormat.decimalSeparator)
if let previousSelection = selection {
let replacementLength = dateTimeFormat.decimalSeparator.utf16.count
func updatedOffset(_ offset: Int) -> Int {
if offset <= range.location { return offset }
if offset < NSMaxRange(range) { return range.location + replacementLength }
return offset + replacementLength - range.length
}
let start = updatedOffset(previousSelection.location)
let end = updatedOffset(NSMaxRange(previousSelection))
selection = NSRange(location: start, length: end - start)
}
}
}
self.applyText(text, selection: selection)
}
self.setNeedsLayout()
}
func updateGramIcon(theme: PresentationTheme, isVisible: Bool, transition: ComponentTransition) {
}
override func layoutSubviews() {
super.layoutSubviews()
var iconLayoutSize = CGSize(width: 40.0, height: 40.0)
var iconSpacing: CGFloat = self.mode == .fiat ? 0.0 : 2.0
let suffixSpacing: CGFloat = 3.0
let displayText = (self.textField.text ?? "").isEmpty ? "0" : (self.textField.text ?? "")
let displayTextBounds = self.amountTextLayout(displayText).attributedText.boundingRect(
with: CGSize(width: CGFloat.greatestFiniteMagnitude, height: self.bounds.height),
options: [],
context: nil
)
let textWidth = max(31.0, ceil(displayTextBounds.width) + 5.0)
let iconWidth = self.mode == .gram ? self.gramIconLayoutSize.width : self.fiatIconSize.width
let usesFiatInkLayout = self.usesAnimatedPresentation && self.mode == .fiat && !self.fiatSymbolInkBounds.isEmpty
if usesFiatInkLayout {
self.textField.bounds = CGRect(x: 0.0, y: 0.0, width: textWidth, height: self.bounds.height)
self.textField.layoutIfNeeded()
let caret = self.textField.nativeCaretRect(for: self.textField.beginningOfDocument)
let textRect = self.textField.isEditing ? self.textField.editingRect(forBounds: self.textField.bounds) : self.textField.textRect(forBounds: self.textField.bounds)
let textInset = !caret.isNull && !caret.isInfinite && caret.height > 0.0 ? caret.minX : textRect.minX
let firstDigitBounds = WalletSendAmountGlyphMetrics.inkBounds(String(displayText.prefix(1)), font: self.integralFont)
iconLayoutSize.width = self.fiatSymbolInkBounds.width
iconSpacing = 8.3 - max(0.0, firstDigitBounds.minX) - textInset
}
let iconLeadingInset = usesFiatInkLayout ? 0.0 : max(0.0, -floorToScreenPixels((iconLayoutSize.width - iconWidth) / 2.0))
let totalWidth = iconLeadingInset + iconLayoutSize.width + iconSpacing + textWidth + suffixSpacing + self.suffixSize.width
let scale = min(1.0, self.bounds.width / totalWidth)
self.contentView.bounds = CGRect(origin: .zero, size: CGSize(width: totalWidth, height: self.bounds.height))
self.contentView.center = CGPoint(x: self.bounds.midX, y: self.bounds.midY)
self.contentView.transform = CGAffineTransform(scaleX: scale, y: scale)
var x = iconLeadingInset
let centerY = self.bounds.height / 2.0
if let gramIconView = self.gramIcon.view {
gramIconView.frame = CGRect(
origin: CGPoint(
x: x + floorToScreenPixels((iconLayoutSize.width - self.gramAnimationSize.width) / 2.0) - 1.0,
y: floorToScreenPixels(centerY - self.gramAnimationSize.height / 2.0) - 1.0
),
size: self.gramAnimationSize
)
}
if let fiatIconView = self.fiatIcon.view {
fiatIconView.frame = CGRect(
origin: CGPoint(
x: usesFiatInkLayout ? x - self.fiatSymbolInkBounds.minX : x + floorToScreenPixels((iconLayoutSize.width - self.fiatIconSize.width) / 2.0),
y: floorToScreenPixels(centerY - self.fiatIconSize.height / 2.0)
),
size: self.fiatIconSize
)
}
x += iconLayoutSize.width + iconSpacing
self.textField.frame = CGRect(
origin: CGPoint(x: x, y: 0.0),
size: CGSize(width: textWidth, height: self.bounds.height)
)
if self.usesAnimatedPresentation, let fiatIconView = self.fiatIcon.view as? TextView,
let line = fiatIconView.cachedLayout?.linesRects().first {
let amountBaseline = self.textField.frame.minY + self.textField.amountTextBaseline(font: self.integralFont)
let symbolBaseline = amountBaseline - self.integralFont.capHeight / 2.0 + self.fiatSymbolFont.capHeight / 2.0 + 3.45
fiatIconView.frame.origin.y = floorToScreenPixels(symbolBaseline - line.minY)
if usesFiatInkLayout {
fiatIconView.frame.origin.x -= line.minX
}
}
x += textWidth + suffixSpacing
if let suffixView = self.suffix.view {
suffixView.frame = CGRect(
origin: CGPoint(x: x, y: floorToScreenPixels(centerY - self.suffixSize.height / 2.0 + 6.0)),
size: self.suffixSize
)
}
}
func textFieldDidBeginEditing(_ textField: UITextField) {
textField.setNeedsLayout()
self.focusUpdated?(true)
}
func textFieldDidEndEditing(_ textField: UITextField) {
textField.setNeedsLayout()
self.focusUpdated?(false)
}
func textFieldDidChangeSelection(_ textField: UITextField) {
textField.setNeedsLayout()
}
func textField(
_ textField: UITextField,
shouldChangeCharactersIn range: NSRange,
replacementString string: String
) -> Bool {
self.replaceText(in: range, with: string)
return false
}
@discardableResult
private func replaceText(in range: NSRange, with string: String) -> Bool {
guard self.canEditAmount, let dateTimeFormat = self.dateTimeFormat else { return false }
let text = self.textField.text ?? ""
let length = (text as NSString).length
guard range.location != NSNotFound, range.location >= 0, range.location <= length,
range.length >= 0, range.length <= length - range.location,
!string.isEmpty || range.length > 0 else { return false }
if text == "0" + dateTimeFormat.decimalSeparator, range.location == length, range.length == 0,
string == dateTimeFormat.decimalSeparator || string == "." || string == "," {
return false
}
guard let edit = walletSendReplacingAmountText(
text, range: range, replacement: string,
mode: self.mode, rate: self.rate, decimalSeparator: dateTimeFormat.decimalSeparator
) else {
self.inputRejected()
return false
}
self.applyText(edit.text, selection: edit.selection)
self.inputAccepted(string)
self.textChanged()
return true
}
}