import Foundation import UIKit import Display import ComponentFlow import AccountContext import TelegramPresentationData import AsyncDisplayKit import TelegramCore import ComponentDisplayAdapters public final class TokenListTextField: Component { public final class ExternalState { public fileprivate(set) var isFocused: Bool = false public fileprivate(set) var text: String = "" public init() { } } public final class Token: Equatable { public enum Content: Equatable { case peer(EnginePeer) case category(UIImage?) case emoji(String) public static func ==(lhs: Content, rhs: Content) -> Bool { switch lhs { case let .peer(peer): if case .peer(peer) = rhs { return true } else { return false } case let .category(lhsImage): if case let .category(rhsImage) = rhs, lhsImage === rhsImage { return true } else { return false } case let .emoji(lhsEmoji): if case let .emoji(rhsEmoji) = rhs, lhsEmoji == rhsEmoji { return true } else { return false } } } } public let id: AnyHashable public let title: String public let fixedPosition: Int? public let content: Content public init( id: AnyHashable, title: String, fixedPosition: Int?, content: Content ) { self.id = id self.title = title self.fixedPosition = fixedPosition self.content = content } public static func ==(lhs: Token, rhs: Token) -> Bool { if lhs.id != rhs.id { return false } if lhs.title != rhs.title { return false } if lhs.fixedPosition != rhs.fixedPosition { return false } if lhs.content != rhs.content { return false } return true } } public let externalState: ExternalState public let context: AccountContext public let theme: PresentationTheme public let placeholder: String public let tokens: [Token] public let sideInset: CGFloat public let deleteToken: (AnyHashable) -> Void public let isFocusedUpdated: (Bool) -> Void public init( externalState: ExternalState, context: AccountContext, theme: PresentationTheme, placeholder: String, tokens: [Token], sideInset: CGFloat, deleteToken: @escaping (AnyHashable) -> Void, isFocusedUpdated: @escaping (Bool) -> Void = { _ in } ) { self.externalState = externalState self.context = context self.theme = theme self.placeholder = placeholder self.tokens = tokens self.sideInset = sideInset self.deleteToken = deleteToken self.isFocusedUpdated = isFocusedUpdated } public static func ==(lhs: TokenListTextField, rhs: TokenListTextField) -> Bool { if lhs.externalState !== rhs.externalState { return false } if lhs.context !== rhs.context { return false } if lhs.theme !== rhs.theme { return false } if lhs.placeholder != rhs.placeholder { return false } if lhs.tokens != rhs.tokens { return false } if lhs.sideInset != rhs.sideInset { return false } return true } public final class View: UIView { private var tokenListNode: EditableTokenListNode? private var tokenListText: String = "" private var component: TokenListTextField? private weak var componentState: EmptyComponentState? override init(frame: CGRect) { super.init(frame: frame) } required init(coder: NSCoder) { preconditionFailure() } override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { let result = super.hitTest(point, with: event) if result != nil { return result } return nil } public func clearText() { if let tokenListNode = self.tokenListNode { tokenListNode.setText("") } } func update(component: TokenListTextField, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { self.component = component self.componentState = state let tokenListNode: EditableTokenListNode if let current = self.tokenListNode { tokenListNode = current } else { tokenListNode = EditableTokenListNode( context: component.context, presentationTheme: component.theme, theme: EditableTokenListNodeTheme( backgroundColor: .clear, separatorColor: component.theme.rootController.navigationBar.separatorColor, placeholderTextColor: component.theme.list.itemPlaceholderTextColor, primaryTextColor: component.theme.list.itemPrimaryTextColor, tokenBackgroundColor: component.theme.list.itemCheckColors.strokeColor.withAlphaComponent(0.25), selectedTextColor: component.theme.list.itemCheckColors.foregroundColor, selectedBackgroundColor: component.theme.list.itemCheckColors.fillColor, accentColor: component.theme.list.itemAccentColor, keyboardColor: component.theme.rootController.keyboardColor ), placeholder: component.placeholder ) self.tokenListNode = tokenListNode self.addSubnode(tokenListNode) tokenListNode.isFirstResponderChanged = { [weak self] in guard let self else { return } self.component?.isFocusedUpdated(self.tokenListNode?.isFocused ?? false) self.componentState?.updated(transition: Transition(animation: .curve(duration: 0.35, curve: .spring))) } tokenListNode.textUpdated = { [weak self] text in guard let self else { return } self.tokenListText = text self.componentState?.updated(transition: .immediate) } tokenListNode.textReturned = { [weak self] in guard let self else { return } self.tokenListNode?.view.endEditing(true) } tokenListNode.deleteToken = { [weak self] id in guard let self, let component = self.component else { return } component.deleteToken(id) } } let mappedTokens = component.tokens.map { token -> EditableTokenListToken in let mappedSubject: EditableTokenListToken.Subject switch token.content { case let .peer(peer): mappedSubject = .peer(peer) case let .category(image): mappedSubject = .category(image) case let .emoji(emoji): mappedSubject = .emoji(emoji) } return EditableTokenListToken( id: token.id, title: token.title, fixedPosition: token.fixedPosition, subject: mappedSubject ) } let height = tokenListNode.updateLayout( tokens: mappedTokens, width: availableSize.width, leftInset: component.sideInset, rightInset: component.sideInset, transition: transition.containedViewLayoutTransition ) let size = CGSize(width: availableSize.width, height: height) transition.containedViewLayoutTransition.updateFrame(node: tokenListNode, frame: CGRect(origin: CGPoint(), size: size)) component.externalState.isFocused = tokenListNode.isFocused component.externalState.text = self.tokenListText return size } } public func makeView() -> View { return View(frame: CGRect()) } public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } }