Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
@@ -0,0 +1,739 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import TelegramPresentationData
import ProgressNavigationButtonNode
public enum ItemListNavigationButtonStyle {
case regular
case bold
case activity
public var barButtonItemStyle: UIBarButtonItem.Style {
switch self {
case .regular, .activity:
return .plain
case .bold:
return .done
}
}
}
public enum ItemListNavigationButtonContentIcon {
case search
case add
case action
}
public enum ItemListNavigationButtonContent: Equatable {
case none
case text(String)
case icon(ItemListNavigationButtonContentIcon)
case node(ASDisplayNode)
}
public struct ItemListNavigationButton {
public let content: ItemListNavigationButtonContent
public let style: ItemListNavigationButtonStyle
public let enabled: Bool
public let action: () -> Void
public init(content: ItemListNavigationButtonContent, style: ItemListNavigationButtonStyle, enabled: Bool, action: @escaping () -> Void) {
self.content = content
self.style = style
self.enabled = enabled
self.action = action
}
}
public struct ItemListBackButton: Equatable {
public let title: String
public init(title: String) {
self.title = title
}
}
public enum ItemListControllerTitle: Equatable {
case text(String)
case textWithSubtitle(String, String)
case sectionControl([String], Int)
case textWithTabs(String, [String], Int)
}
public final class ItemListControllerTabBarItem: Equatable {
let title: String
let image: UIImage?
let selectedImage: UIImage?
let tintImages: Bool
let badgeValue: String?
public init(title: String, image: UIImage?, selectedImage: UIImage?, tintImages: Bool = true, badgeValue: String? = nil) {
self.title = title
self.image = image
self.selectedImage = selectedImage
self.tintImages = tintImages
self.badgeValue = badgeValue
}
public static func ==(lhs: ItemListControllerTabBarItem, rhs: ItemListControllerTabBarItem) -> Bool {
return lhs.title == rhs.title && lhs.image === rhs.image && lhs.selectedImage === rhs.selectedImage && lhs.tintImages == rhs.tintImages && lhs.badgeValue == rhs.badgeValue
}
}
public struct ItemListControllerState {
let presentationData: ItemListPresentationData
let title: ItemListControllerTitle
let leftNavigationButton: ItemListNavigationButton?
let rightNavigationButton: ItemListNavigationButton?
let secondaryRightNavigationButton: ItemListNavigationButton?
let backNavigationButton: ItemListBackButton?
let tabBarItem: ItemListControllerTabBarItem?
let animateChanges: Bool
public init(presentationData: ItemListPresentationData, title: ItemListControllerTitle, leftNavigationButton: ItemListNavigationButton?, rightNavigationButton: ItemListNavigationButton?, secondaryRightNavigationButton: ItemListNavigationButton? = nil, backNavigationButton: ItemListBackButton?, tabBarItem: ItemListControllerTabBarItem? = nil, animateChanges: Bool = true) {
self.presentationData = presentationData
self.title = title
self.leftNavigationButton = leftNavigationButton
self.rightNavigationButton = rightNavigationButton
self.secondaryRightNavigationButton = secondaryRightNavigationButton
self.backNavigationButton = backNavigationButton
self.tabBarItem = tabBarItem
self.animateChanges = animateChanges
}
}
open class ItemListController: ViewController, KeyShortcutResponder, PresentableController {
var controllerNode: ItemListControllerNode {
return (self.displayNode as! ItemListControllerNode)
}
private let state: Signal<(ItemListControllerState, (ItemListNodeState, Any)), NoError>
private var leftNavigationButtonTitleAndStyle: (ItemListNavigationButtonContent, ItemListNavigationButtonStyle)?
private var rightNavigationButtonTitleAndStyle: [(ItemListNavigationButtonContent, ItemListNavigationButtonStyle)] = []
private var backNavigationButton: ItemListBackButton?
private var tabBarItemInfo: ItemListControllerTabBarItem?
private var navigationButtonActions: (left: (() -> Void)?, right: (() -> Void)?, secondaryRight: (() -> Void)?) = (nil, nil, nil)
private var segmentedTitleView: ItemListControllerSegmentedTitleView?
private var tabsNavigationContentNode: ItemListControllerTabsContentNode?
private var presentationData: ItemListPresentationData
private let hideNavigationBarBackground: Bool
private var validLayout: ContainerViewLayout?
public var additionalInsets: UIEdgeInsets = UIEdgeInsets()
private var didPlayPresentationAnimation = false
public private(set) var didAppearOnce = false
public var didAppear: ((Bool) -> Void)?
private var isDismissed = false
public var titleControlValueChanged: ((Int) -> Void)?
private var tabBarItemDisposable: Disposable?
private let _ready = Promise<Bool>()
override open var ready: Promise<Bool> {
return self._ready
}
public var experimentalSnapScrollToItem: Bool = false {
didSet {
if self.isNodeLoaded {
self.controllerNode.listNode.experimentalSnapScrollToItem = self.experimentalSnapScrollToItem
}
}
}
public var enableInteractiveDismiss = false {
didSet {
if self.isNodeLoaded {
self.controllerNode.enableInteractiveDismiss = self.enableInteractiveDismiss
}
}
}
public var alwaysSynchronous = false {
didSet {
if self.isNodeLoaded {
self.controllerNode.alwaysSynchronous = self.alwaysSynchronous
}
}
}
public var visibleEntriesUpdated: ((ItemListNodeVisibleEntries) -> Void)? {
didSet {
if self.isNodeLoaded {
self.controllerNode.visibleEntriesUpdated = self.visibleEntriesUpdated
}
}
}
public var beganInteractiveDragging: (() -> Void)? {
didSet {
if self.isNodeLoaded {
self.controllerNode.beganInteractiveDragging = self.beganInteractiveDragging
}
}
}
public var visibleBottomContentOffset: ListViewVisibleContentOffset {
if self.isNodeLoaded {
return self.controllerNode.listNode.visibleBottomContentOffset()
} else {
return .unknown
}
}
public var visibleBottomContentOffsetChanged: ((ListViewVisibleContentOffset) -> Void)? {
didSet {
if self.isNodeLoaded {
self.controllerNode.visibleBottomContentOffsetChanged = self.visibleBottomContentOffsetChanged
}
}
}
public var contentOffsetChanged: ((ListViewVisibleContentOffset, Bool) -> Void)? {
didSet {
if self.isNodeLoaded {
self.controllerNode.contentOffsetChanged = self.contentOffsetChanged
}
}
}
public var contentScrollingEnded: ((ListView) -> Bool)? {
didSet {
if self.isNodeLoaded {
self.controllerNode.contentScrollingEnded = self.contentScrollingEnded
}
}
}
public var searchActivated: ((Bool) -> Void)? {
didSet {
if self.isNodeLoaded {
self.controllerNode.searchActivated = self.searchActivated
}
}
}
public var didScrollWithOffset: ((CGFloat, ContainedViewLayoutTransition, ListViewItemNode?, Bool) -> Void)? {
didSet {
if self.isNodeLoaded {
self.controllerNode.listNode.didScrollWithOffset = self.didScrollWithOffset
}
}
}
public var willScrollToTop: (() -> Void)?
public func setReorderEntry<T: ItemListNodeEntry>(_ f: @escaping (Int, Int, [T]) -> Signal<Bool, NoError>) {
self.reorderEntry = { a, b, list in
return f(a, b, list.map { $0 as! T })
}
}
private var reorderEntry: ((Int, Int, [ItemListNodeAnyEntry]) -> Signal<Bool, NoError>)? {
didSet {
if self.isNodeLoaded {
self.controllerNode.reorderEntry = self.reorderEntry
}
}
}
public func setReorderCompleted<T: ItemListNodeEntry>(_ f: @escaping ([T]) -> Void) {
self.reorderCompleted = { list in
f(list.map { $0 as! T })
}
}
private var reorderCompleted: (([ItemListNodeAnyEntry]) -> Void)? {
didSet {
if self.isNodeLoaded {
self.controllerNode.reorderCompleted = self.reorderCompleted
}
}
}
public var willDisappear: ((Bool) -> Void)?
public var didDisappear: ((Bool) -> Void)?
public var afterTransactionCompleted: (() -> Void)? {
didSet {
if self.isNodeLoaded {
self.controllerNode.afterTransactionCompleted = self.afterTransactionCompleted
}
}
}
public init<ItemGenerationArguments>(
presentationData: ItemListPresentationData,
updatedPresentationData: Signal<ItemListPresentationData, NoError>,
state: Signal<(ItemListControllerState, (ItemListNodeState, ItemGenerationArguments)), NoError>,
tabBarItem: Signal<ItemListControllerTabBarItem, NoError>?,
hideNavigationBarBackground: Bool = false
) {
self.state = state
|> map { controllerState, nodeStateAndArgument -> (ItemListControllerState, (ItemListNodeState, Any)) in
return (controllerState, (nodeStateAndArgument.0, nodeStateAndArgument.1))
}
self.presentationData = presentationData
self.hideNavigationBarBackground = hideNavigationBarBackground
super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: NavigationBarTheme(rootControllerTheme: presentationData.theme, hideBackground: hideNavigationBarBackground, hideSeparator: hideNavigationBarBackground), strings: NavigationBarStrings(presentationStrings: presentationData.strings)))
self.isOpaqueWhenInOverlay = true
self.blocksBackgroundWhenInOverlay = true
self.automaticallyControlPresentationContextLayout = false
self.statusBar.statusBarStyle = presentationData.theme.rootController.statusBarStyle.style
self.scrollToTop = { [weak self] in
self?.willScrollToTop?()
(self?.displayNode as! ItemListControllerNode).scrollToTop()
}
if let tabBarItem = tabBarItem {
self.tabBarItemDisposable = (tabBarItem |> deliverOnMainQueue).start(next: { [weak self] tabBarItemInfo in
if let strongSelf = self {
if strongSelf.tabBarItemInfo != tabBarItemInfo {
strongSelf.tabBarItemInfo = tabBarItemInfo
strongSelf.tabBarItem.title = tabBarItemInfo.title
strongSelf.tabBarItem.image = tabBarItemInfo.image
strongSelf.tabBarItem.selectedImage = tabBarItemInfo.selectedImage
strongSelf.tabBarItem.badgeValue = tabBarItemInfo.badgeValue
}
}
})
}
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.tabBarItemDisposable?.dispose()
}
override open func loadDisplayNode() {
let previousControllerState = Atomic<ItemListControllerState?>(value: nil)
let nodeState = self.state
|> deliverOnMainQueue
|> afterNext { [weak self] controllerState, state in
Queue.mainQueue().async {
if let strongSelf = self {
let previousState = previousControllerState.swap(controllerState)
if previousState?.title != controllerState.title {
switch controllerState.title {
case let .text(text):
strongSelf.title = text
strongSelf.navigationItem.titleView = nil
strongSelf.segmentedTitleView = nil
strongSelf.navigationBar?.setContentNode(nil, animated: false)
if strongSelf.isNodeLoaded {
strongSelf.controllerNode.panRecognizer?.isEnabled = false
}
case let .textWithSubtitle(title, subtitle):
strongSelf.title = ""
strongSelf.navigationItem.titleView = ItemListTextWithSubtitleTitleView(theme: controllerState.presentationData.theme, title: title, subtitle: subtitle)
strongSelf.segmentedTitleView = nil
strongSelf.navigationBar?.setContentNode(nil, animated: false)
if strongSelf.isNodeLoaded {
strongSelf.controllerNode.panRecognizer?.isEnabled = false
}
case let .sectionControl(sections, index):
strongSelf.title = ""
if let segmentedTitleView = strongSelf.segmentedTitleView, segmentedTitleView.segments == sections {
segmentedTitleView.index = index
} else {
let segmentedTitleView = ItemListControllerSegmentedTitleView(theme: controllerState.presentationData.theme, segments: sections, selectedIndex: index)
strongSelf.segmentedTitleView = segmentedTitleView
strongSelf.navigationItem.titleView = strongSelf.segmentedTitleView
segmentedTitleView.indexUpdated = { [weak self] index in
if let strongSelf = self {
strongSelf.titleControlValueChanged?(index)
}
}
}
strongSelf.navigationBar?.setContentNode(nil, animated: false)
if strongSelf.isNodeLoaded {
strongSelf.controllerNode.panRecognizer?.isEnabled = false
}
case let .textWithTabs(title, sections, index):
strongSelf.title = title
if let tabsNavigationContentNode = strongSelf.tabsNavigationContentNode, tabsNavigationContentNode.segments == sections {
tabsNavigationContentNode.index = index
} else {
let tabsNavigationContentNode = ItemListControllerTabsContentNode(theme: controllerState.presentationData.theme, segments: sections, selectedIndex: index)
strongSelf.tabsNavigationContentNode = tabsNavigationContentNode
strongSelf.navigationBar?.setContentNode(tabsNavigationContentNode, animated: false)
tabsNavigationContentNode.indexUpdated = { [weak self] index in
if let strongSelf = self {
strongSelf.titleControlValueChanged?(index)
}
}
if let validLayout = strongSelf.validLayout {
strongSelf.updateNavigationBarLayout(validLayout, transition: .immediate)
}
strongSelf.navigationBar?.updateBackgroundAlpha(1.0, transition: .immediate)
}
if strongSelf.isNodeLoaded {
strongSelf.controllerNode.panTransitionFractionChanged = { [weak self] transitionFraction in
if let strongSelf = self {
strongSelf.tabsNavigationContentNode?.transitionFraction = transitionFraction
}
}
strongSelf.controllerNode.panGestureAllowedDirections = {
if index == 0 {
return [.leftCenter]
} else if index == sections.count - 1 {
return [.rightCenter]
} else {
return [.leftCenter, .rightCenter]
}
}
strongSelf.controllerNode.panRecognizer?.isEnabled = true
}
}
}
strongSelf.navigationButtonActions = (left: controllerState.leftNavigationButton?.action, right: controllerState.rightNavigationButton?.action, secondaryRight: controllerState.secondaryRightNavigationButton?.action)
let themeUpdated = strongSelf.presentationData != controllerState.presentationData
if strongSelf.leftNavigationButtonTitleAndStyle?.0 != controllerState.leftNavigationButton?.content || strongSelf.leftNavigationButtonTitleAndStyle?.1 != controllerState.leftNavigationButton?.style || themeUpdated {
if let leftNavigationButton = controllerState.leftNavigationButton {
let item: UIBarButtonItem
switch leftNavigationButton.content {
case .none:
item = UIBarButtonItem(title: "", style: leftNavigationButton.style.barButtonItemStyle, target: strongSelf, action: #selector(strongSelf.leftNavigationButtonPressed))
case let .text(value):
item = UIBarButtonItem(title: value, style: leftNavigationButton.style.barButtonItemStyle, target: strongSelf, action: #selector(strongSelf.leftNavigationButtonPressed))
case let .icon(icon):
var image: UIImage?
switch icon {
case .search:
image = PresentationResourcesRootController.navigationCompactSearchIcon(controllerState.presentationData.theme)
case .add:
image = PresentationResourcesRootController.navigationAddIcon(controllerState.presentationData.theme)
case .action:
image = PresentationResourcesRootController.navigationShareIcon(controllerState.presentationData.theme)
}
item = UIBarButtonItem(image: image, style: leftNavigationButton.style.barButtonItemStyle, target: strongSelf, action: #selector(strongSelf.leftNavigationButtonPressed))
case let .node(node):
item = UIBarButtonItem(customDisplayNode: node)
item.setCustomAction({ [weak self] in
self?.navigationButtonActions.0?()
})
}
strongSelf.leftNavigationButtonTitleAndStyle = (leftNavigationButton.content, leftNavigationButton.style)
strongSelf.navigationItem.setLeftBarButton(item, animated: false)
item.isEnabled = leftNavigationButton.enabled
} else {
strongSelf.leftNavigationButtonTitleAndStyle = nil
strongSelf.navigationItem.setLeftBarButton(nil, animated: false)
}
} else if let barButtonItem = strongSelf.navigationItem.leftBarButtonItem, let leftNavigationButton = controllerState.leftNavigationButton, leftNavigationButton.enabled != barButtonItem.isEnabled {
barButtonItem.isEnabled = leftNavigationButton.enabled
}
var rightNavigationButtonTitleAndStyle: [(ItemListNavigationButtonContent, ItemListNavigationButtonStyle, Bool)] = []
if let secondaryRightNavigationButton = controllerState.secondaryRightNavigationButton {
rightNavigationButtonTitleAndStyle.append((secondaryRightNavigationButton.content, secondaryRightNavigationButton.style, secondaryRightNavigationButton.enabled))
}
if let rightNavigationButton = controllerState.rightNavigationButton {
rightNavigationButtonTitleAndStyle.append((rightNavigationButton.content, rightNavigationButton.style, rightNavigationButton.enabled))
}
var updateRightButtonItems = false
if rightNavigationButtonTitleAndStyle.count != strongSelf.rightNavigationButtonTitleAndStyle.count {
updateRightButtonItems = true
} else {
for i in 0 ..< rightNavigationButtonTitleAndStyle.count {
if rightNavigationButtonTitleAndStyle[i].0 != strongSelf.rightNavigationButtonTitleAndStyle[i].0 || rightNavigationButtonTitleAndStyle[i].1 != strongSelf.rightNavigationButtonTitleAndStyle[i].1 {
updateRightButtonItems = true
}
}
}
if updateRightButtonItems || themeUpdated {
strongSelf.rightNavigationButtonTitleAndStyle = rightNavigationButtonTitleAndStyle.map { ($0.0, $0.1) }
var items: [UIBarButtonItem] = []
var index = 0
for (content, style, _) in rightNavigationButtonTitleAndStyle {
let item: UIBarButtonItem
if case .activity = style {
item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: controllerState.presentationData.theme.rootController.navigationBar.controlColor))
} else {
let action: Selector = (index == 0 && rightNavigationButtonTitleAndStyle.count > 1) ? #selector(strongSelf.secondaryRightNavigationButtonPressed) : #selector(strongSelf.rightNavigationButtonPressed)
switch content {
case .none:
item = UIBarButtonItem(title: "", style: style.barButtonItemStyle, target: strongSelf, action: action)
case let .text(value):
item = UIBarButtonItem(title: value, style: style.barButtonItemStyle, target: strongSelf, action: action)
case let .icon(icon):
var image: UIImage?
switch icon {
case .search:
image = PresentationResourcesRootController.navigationCompactSearchIcon(controllerState.presentationData.theme)
case .add:
image = PresentationResourcesRootController.navigationAddIcon(controllerState.presentationData.theme)
case .action:
image = PresentationResourcesRootController.navigationShareIcon(controllerState.presentationData.theme)
}
item = UIBarButtonItem(image: image, style: style.barButtonItemStyle, target: strongSelf, action: action)
case let .node(node):
item = UIBarButtonItem(customDisplayNode: node)
item.setCustomAction({ [weak self] in
self?.navigationButtonActions.1?()
})
}
}
items.append(item)
index += 1
}
strongSelf.navigationItem.setRightBarButtonItems(items, animated: false)
index = 0
for (_, _, enabled) in rightNavigationButtonTitleAndStyle {
items[index].isEnabled = enabled
index += 1
}
} else {
for i in 0 ..< rightNavigationButtonTitleAndStyle.count {
strongSelf.navigationItem.rightBarButtonItems?[i].isEnabled = rightNavigationButtonTitleAndStyle[i].2
}
}
if strongSelf.backNavigationButton != controllerState.backNavigationButton {
strongSelf.backNavigationButton = controllerState.backNavigationButton
if let backNavigationButton = strongSelf.backNavigationButton {
strongSelf.navigationItem.backBarButtonItem = UIBarButtonItem(title: backNavigationButton.title, style: .plain, target: nil, action: nil)
} else {
strongSelf.navigationItem.backBarButtonItem = nil
}
}
if strongSelf.presentationData != controllerState.presentationData {
strongSelf.presentationData = controllerState.presentationData
strongSelf.navigationBar?.updatePresentationData(NavigationBarPresentationData(theme: NavigationBarTheme(rootControllerTheme: strongSelf.presentationData.theme, hideBackground: strongSelf.hideNavigationBarBackground, hideSeparator: strongSelf.hideNavigationBarBackground), strings: NavigationBarStrings(presentationStrings: strongSelf.presentationData.strings)))
strongSelf.statusBar.updateStatusBarStyle(strongSelf.presentationData.theme.rootController.statusBarStyle.style, animated: true)
strongSelf.segmentedTitleView?.theme = controllerState.presentationData.theme
if let titleView = strongSelf.navigationItem.titleView as? ItemListTextWithSubtitleTitleView {
titleView.updateTheme(theme: controllerState.presentationData.theme)
}
var items = strongSelf.navigationItem.rightBarButtonItems ?? []
for i in 0 ..< strongSelf.rightNavigationButtonTitleAndStyle.count {
if case .activity = strongSelf.rightNavigationButtonTitleAndStyle[i].1 {
items[i] = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: controllerState.presentationData.theme.rootController.navigationBar.controlColor))!
}
}
strongSelf.navigationItem.setRightBarButtonItems(items, animated: false)
}
}
}
}
|> map { ($0.presentationData, $1) }
let displayNode = ItemListControllerNode(controller: self, navigationBar: self.navigationBar!, state: nodeState)
displayNode.dismiss = { [weak self] in
self?.presentingViewController?.dismiss(animated: true, completion: nil)
}
displayNode.enableInteractiveDismiss = self.enableInteractiveDismiss
displayNode.alwaysSynchronous = self.alwaysSynchronous
displayNode.visibleEntriesUpdated = self.visibleEntriesUpdated
displayNode.beganInteractiveDragging = self.beganInteractiveDragging
displayNode.visibleBottomContentOffsetChanged = self.visibleBottomContentOffsetChanged
displayNode.contentOffsetChanged = self.contentOffsetChanged
displayNode.contentScrollingEnded = self.contentScrollingEnded
displayNode.searchActivated = self.searchActivated
displayNode.reorderEntry = self.reorderEntry
displayNode.reorderCompleted = self.reorderCompleted
displayNode.afterTransactionCompleted = self.afterTransactionCompleted
displayNode.listNode.experimentalSnapScrollToItem = self.experimentalSnapScrollToItem
displayNode.listNode.didScrollWithOffset = self.didScrollWithOffset
displayNode.requestLayout = { [weak self] transition in
self?.requestLayout(transition: transition)
}
self.displayNode = displayNode
super.displayNodeDidLoad()
self._ready.set(self.controllerNode.ready)
}
override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.validLayout = layout
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.cleanNavigationHeight, transition: transition, additionalInsets: self.additionalInsets)
}
@objc func leftNavigationButtonPressed() {
self.navigationButtonActions.left?()
}
@objc func rightNavigationButtonPressed() {
self.navigationButtonActions.right?()
}
@objc func secondaryRightNavigationButtonPressed() {
self.navigationButtonActions.secondaryRight?()
}
override open func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.viewDidAppear(completion: {})
}
public func viewDidAppear(completion: @escaping () -> Void) {
self.controllerNode.listNode.preloadPages = true
if let presentationArguments = self.presentationArguments as? ViewControllerPresentationArguments, !self.didPlayPresentationAnimation {
self.didPlayPresentationAnimation = true
if case .modalSheet = presentationArguments.presentationAnimation {
self.controllerNode.animateIn(completion: {
presentationArguments.completion?()
completion()
})
self.updateTransitionWhenPresentedAsModal?(1.0, .animated(duration: 0.5, curve: .spring))
} else {
completion()
}
} else {
completion()
}
let firstTime = !self.didAppearOnce
self.didAppearOnce = true
self.didAppear?(firstTime)
}
override open func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.willDisappear?(animated)
}
override open func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
self.didDisappear?(animated)
}
public var listInsets: UIEdgeInsets {
return self.controllerNode.listNode.insets
}
public func frameForItemNode(_ predicate: (ListViewItemNode) -> Bool) -> CGRect? {
var result: CGRect?
self.controllerNode.listNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ListViewItemNode {
if predicate(itemNode) {
result = itemNode.convert(itemNode.bounds, to: self.displayNode)
}
}
}
return result
}
public func forEachItemNode(_ f: (ListViewItemNode) -> Void) {
self.controllerNode.listNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ListViewItemNode {
f(itemNode)
}
}
}
public func ensureItemNodeVisible(_ itemNode: ListViewItemNode, animated: Bool = true, overflow: CGFloat = 0.0, atTop: Bool = false, curve: ListViewAnimationCurve = .Default(duration: 0.25)) {
self.controllerNode.listNode.ensureItemNodeVisible(itemNode, animated: animated, overflow: overflow, atTop: atTop, curve: curve)
}
public func afterLayout(_ f: @escaping () -> Void) {
self.controllerNode.afterLayout(f)
}
public func clearItemNodesHighlight(animated: Bool = false) {
self.controllerNode.listNode.clearHighlightAnimated(animated)
}
public var keyShortcuts: [KeyShortcut] {
return [KeyShortcut(input: UIKeyCommand.inputEscape, action: { [weak self] in
if !(self?.navigationController?.topViewController is TabBarController) {
_ = self?.navigationBar?.executeBack()
}
})]
}
}
private final class ItemListTextWithSubtitleTitleView: UIView, NavigationBarTitleView {
private let titleNode: ImmediateTextNode
private let subtitleNode: ImmediateTextNode
private var validLayout: (CGSize, CGRect)?
init(theme: PresentationTheme, title: String, subtitle: String) {
self.titleNode = ImmediateTextNode()
self.titleNode.displaysAsynchronously = false
self.titleNode.maximumNumberOfLines = 1
self.titleNode.isOpaque = false
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.medium(17.0), textColor: theme.rootController.navigationBar.primaryTextColor)
self.subtitleNode = ImmediateTextNode()
self.subtitleNode.displaysAsynchronously = false
self.subtitleNode.maximumNumberOfLines = 1
self.subtitleNode.isOpaque = false
self.subtitleNode.attributedText = NSAttributedString(string: subtitle, font: Font.regular(13.0), textColor: theme.rootController.navigationBar.secondaryTextColor)
super.init(frame: CGRect())
self.addSubnode(self.titleNode)
self.addSubnode(self.subtitleNode)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func updateTheme(theme: PresentationTheme) {
self.titleNode.attributedText = NSAttributedString(string: self.titleNode.attributedText?.string ?? "", font: Font.medium(17.0), textColor: theme.rootController.navigationBar.primaryTextColor)
self.subtitleNode.attributedText = NSAttributedString(string: self.subtitleNode.attributedText?.string ?? "", font: Font.regular(13.0), textColor: theme.rootController.navigationBar.secondaryTextColor)
if let (size, clearBounds) = self.validLayout {
let _ = self.updateLayout(size: size, clearBounds: clearBounds, transition: .immediate)
}
}
override func layoutSubviews() {
super.layoutSubviews()
if let (size, clearBounds) = self.validLayout {
let _ = self.updateLayout(size: size, clearBounds: clearBounds, transition: .immediate)
}
}
func updateLayout(size: CGSize, clearBounds: CGRect, transition: ContainedViewLayoutTransition) -> CGRect {
self.validLayout = (size, clearBounds)
let titleSize = self.titleNode.updateLayout(size)
let subtitleSize = self.subtitleNode.updateLayout(size)
let spacing: CGFloat = 0.0
let contentHeight = titleSize.height + spacing + subtitleSize.height
let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: floor((size.height - contentHeight) / 2.0)), size: titleSize)
let subtitleFrame = CGRect(origin: CGPoint(x: floor((size.width - subtitleSize.width) / 2.0), y: titleFrame.maxY + spacing), size: subtitleSize)
self.titleNode.frame = titleFrame
self.subtitleNode.frame = subtitleFrame
return titleFrame
}
func animateLayoutTransition() {
}
}
@@ -0,0 +1,14 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
public protocol ItemListControllerEmptyStateItem {
func isEqual(to: ItemListControllerEmptyStateItem) -> Bool
func node(current: ItemListControllerEmptyStateItemNode?) -> ItemListControllerEmptyStateItemNode
}
open class ItemListControllerEmptyStateItemNode: ASDisplayNode {
open func updateLayout(layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
}
}
@@ -0,0 +1,35 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
public protocol ItemListControllerFooterItem {
func isEqual(to: ItemListControllerFooterItem) -> Bool
func node(current: ItemListControllerFooterItemNode?) -> ItemListControllerFooterItemNode
}
open class ItemListControllerFooterItemNode: ASDisplayNode {
open func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) -> CGFloat {
return 0.0
}
open func updateBackgroundAlpha(_ alpha: CGFloat, transition: ContainedViewLayoutTransition) {
}
}
public protocol ItemListControllerHeaderItem {
func isEqual(to: ItemListControllerHeaderItem) -> Bool
func node(current: ItemListControllerHeaderItemNode?) -> ItemListControllerHeaderItemNode
}
open class ItemListControllerHeaderItemNode: ASDisplayNode {
open func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) -> CGFloat {
return 0.0
}
open func updateContentOffset(_ contentOffset: CGFloat, transition: ContainedViewLayoutTransition) {
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,41 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
public protocol ItemListControllerSearchNavigationContentNode {
func activate()
func deactivate()
func setQueryUpdated(_ f: @escaping (String) -> Void)
}
public protocol ItemListControllerSearch {
func isEqual(to: ItemListControllerSearch) -> Bool
func titleContentNode(current: (NavigationBarContentNode & ItemListControllerSearchNavigationContentNode)?) -> (NavigationBarContentNode & ItemListControllerSearchNavigationContentNode)?
func node(current: ItemListControllerSearchNode?, titleContentNode: (NavigationBarContentNode & ItemListControllerSearchNavigationContentNode)?) -> ItemListControllerSearchNode
}
open class ItemListControllerSearchNode: ASDisplayNode {
open var addedUnderNavigationBar: Bool = false
open func activate() {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
}
open func deactivate() {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in
self?.removeFromSupernode()
})
}
open func scrollToTop() {
}
open func queryUpdated(_ query: String) {
}
open func updateLayout(layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
}
}
@@ -0,0 +1,110 @@
import Foundation
import UIKit
import Display
import TelegramPresentationData
import ComponentFlow
import TabSelectorComponent
public final class ItemListControllerSegmentedTitleView: UIView {
private let tabSelector = ComponentView<Empty>()
public var theme: PresentationTheme {
didSet {
if self.theme !== oldValue {
self.setNeedsLayout()
}
}
}
public var segments: [String] {
didSet {
if self.segments != oldValue {
self.setNeedsLayout()
}
}
}
public var index: Int {
didSet {
if self.index != oldValue {
self.animateLayout = true
self.setNeedsLayout()
}
}
}
private var validLayout: CGSize?
private var animateLayout: Bool = false
public var indexUpdated: ((Int) -> Void)?
public init(theme: PresentationTheme, segments: [String], selectedIndex: Int) {
self.theme = theme
self.segments = segments
self.index = selectedIndex
super.init(frame: CGRect())
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func layoutSubviews() {
super.layoutSubviews()
let size = self.bounds.size
self.validLayout = size
self.update(transition: .immediate)
}
private func update(transition: ComponentTransition) {
guard let size = self.validLayout else {
return
}
let mappedItems = zip(0 ..< self.segments.count, self.segments).map { index, segment in
return TabSelectorComponent.Item(
id: AnyHashable(index),
title: segment
)
}
var transition = transition
if self.animateLayout {
transition = .spring(duration: 0.4)
self.animateLayout = false
}
let tabSelectorSize = self.tabSelector.update(
transition: transition,
component: AnyComponent(TabSelectorComponent(
colors: TabSelectorComponent.Colors(
foreground: self.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.8),
selection: self.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.05)
),
theme: self.theme,
customLayout: TabSelectorComponent.CustomLayout(
font: Font.medium(15.0),
spacing: 8.0
),
items: mappedItems,
selectedId: AnyHashable(self.index),
setSelectedId: { [weak self] id in
guard let self, let index = id.base as? Int else {
return
}
self.indexUpdated?(index)
}
)),
environment: {},
containerSize: CGSize(width: size.width, height: 44.0)
)
let tabSelectorFrame = CGRect(origin: CGPoint(x: floor((size.width - tabSelectorSize.width) / 2.0), y: floor((size.height - tabSelectorSize.height) / 2.0)), size: tabSelectorSize)
if let tabSelectorView = self.tabSelector.view {
if tabSelectorView.superview == nil {
self.addSubview(tabSelectorView)
}
transition.setFrame(view: tabSelectorView, frame: tabSelectorFrame)
}
}
}
@@ -0,0 +1,136 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import ComponentFlow
import ComponentDisplayAdapters
import TabSelectorComponent
private let searchBarFont = Font.regular(17.0)
final class ItemListControllerTabsContentNode: NavigationBarContentNode {
private let tabSelector = ComponentView<Empty>()
var theme: PresentationTheme {
didSet {
if self.theme !== oldValue {
self.update()
}
}
}
var segments: [String] {
didSet {
if self.segments != oldValue {
self.update()
}
}
}
var index: Int {
didSet {
if self.index != oldValue {
self.update(transition: .animated(duration: 0.35, curve: .spring))
}
}
}
var transitionFraction: CGFloat? {
didSet {
if self.transitionFraction != oldValue {
self.update(transition: self.transitionFraction == nil ? .animated(duration: 0.35, curve: .spring) : .immediate)
}
}
}
var indexUpdated: ((Int) -> Void)?
private var validLayout: (CGSize, CGFloat, CGFloat)?
init(theme: PresentationTheme, segments: [String], selectedIndex: Int) {
self.theme = theme
self.segments = segments
self.index = selectedIndex
super.init()
}
override func didLoad() {
super.didLoad()
}
private func update(transition: ContainedViewLayoutTransition = .immediate) {
guard let (size, leftInset, rightInset) = self.validLayout else {
return
}
self.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, transition: transition)
}
override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) {
let isFirstTime = self.validLayout == nil
self.validLayout = (size, leftInset, rightInset)
let mappedItems = zip(0 ..< self.segments.count, self.segments).map { index, segment in
return TabSelectorComponent.Item(
id: AnyHashable(index),
title: segment
)
}
let tabSelectorSize = self.tabSelector.update(
transition: ComponentTransition(transition),
component: AnyComponent(TabSelectorComponent(
colors: TabSelectorComponent.Colors(
foreground: self.theme.list.itemSecondaryTextColor,
selection: self.theme.list.itemAccentColor
),
theme: self.theme,
customLayout: TabSelectorComponent.CustomLayout(
font: Font.medium(14.0),
spacing: 48.0,
innerSpacing: 0.0,
lineSelection: true,
allowScroll: false
),
items: mappedItems,
selectedId: AnyHashable(self.index),
setSelectedId: { [weak self] id in
guard let self, let index = id.base as? Int else {
return
}
self.indexUpdated?(index)
},
transitionFraction: self.transitionFraction
)),
environment: {},
containerSize: CGSize(width: size.width, height: 44.0)
)
let tabSelectorFrame = CGRect(origin: CGPoint(x: floor((size.width - tabSelectorSize.width) / 2.0), y: floor((size.height - tabSelectorSize.height) / 2.0) + 3.0), size: tabSelectorSize)
if let tabSelectorView = self.tabSelector.view {
if tabSelectorView.superview == nil {
self.view.addSubview(tabSelectorView)
}
transition.updateFrame(view: tabSelectorView, frame: tabSelectorFrame)
}
if isFirstTime {
self.requestContainerLayout(.immediate)
}
}
override var height: CGFloat {
return self.nominalHeight
}
override var clippedHeight: CGFloat {
return self.nominalHeight
}
override var nominalHeight: CGFloat {
return 54.0// + self.additionalHeight
}
override var mode: NavigationBarContentMode {
return .expansion
}
}
@@ -0,0 +1,53 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
public final class ItemListEditableControlNode: ASDisplayNode {
public var tapped: (() -> Void)?
private let iconNode: ASImageNode
override public init() {
self.iconNode = ASImageNode()
self.iconNode.isLayerBacked = true
super.init()
self.addSubnode(self.iconNode)
}
override public func didLoad() {
super.didLoad()
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
}
public static func asyncLayout(_ node: ItemListEditableControlNode?) -> (_ theme: PresentationTheme, _ hidden: Bool) -> (CGFloat, (CGFloat) -> ItemListEditableControlNode) {
return { theme, hidden in
let image = PresentationResourcesItemList.itemListDeleteIndicatorIcon(theme)
let resultNode: ItemListEditableControlNode
if let node = node {
resultNode = node
} else {
resultNode = ItemListEditableControlNode()
}
resultNode.iconNode.image = image
return (38.0, { height in
if let image = image {
resultNode.iconNode.frame = CGRect(origin: CGPoint(x: 12.0, y: floor((height - image.size.height) / 2.0)), size: image.size)
resultNode.iconNode.isHidden = hidden
}
return resultNode
})
}
}
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.tapped?()
}
}
}
@@ -0,0 +1,43 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
public final class ItemListEditableReorderControlNode: ASDisplayNode {
public var tapped: (() -> Void)?
private let iconNode: ASImageNode
override public init() {
self.iconNode = ASImageNode()
self.iconNode.displayWithoutProcessing = true
self.iconNode.displaysAsynchronously = false
self.iconNode.isUserInteractionEnabled = false
super.init()
self.addSubnode(self.iconNode)
}
public static func asyncLayout(_ node: ItemListEditableReorderControlNode?) -> (_ theme: PresentationTheme) -> (CGFloat, (CGFloat, Bool, ContainedViewLayoutTransition) -> ItemListEditableReorderControlNode) {
return { theme in
let image = PresentationResourcesItemList.itemListReorderIndicatorIcon(theme)
let resultNode: ItemListEditableReorderControlNode
if let node = node {
resultNode = node
} else {
resultNode = ItemListEditableReorderControlNode()
}
resultNode.iconNode.image = image
return (40.0, { height, offsetForLabel, transition in
if let image = image {
transition.updateFrame(node: resultNode.iconNode, frame: CGRect(origin: CGPoint(x: 0.0, y: floor((height - image.size.height) / 2.0) - (offsetForLabel ? 6.0 : 0.0)), size: image.size))
}
return resultNode
})
}
}
}
@@ -0,0 +1,246 @@
import Foundation
import UIKit
import Display
import TelegramUIPreferences
import TelegramPresentationData
public protocol ItemListItemTag {
func isEqual(to other: ItemListItemTag) -> Bool
}
public protocol ItemListItem {
var sectionId: ItemListSectionId { get }
var tag: ItemListItemTag? { get }
var isAlwaysPlain: Bool { get }
var requestsNoInset: Bool { get }
}
public extension ItemListItem {
//let accessoryItem: ListViewAccessoryItem?
var isAlwaysPlain: Bool {
return false
}
var tag: ItemListItemTag? {
return nil
}
var requestsNoInset: Bool {
return false
}
}
public protocol ItemListItemNode {
var tag: ItemListItemTag? { get }
}
public protocol ItemListItemFocusableNode {
func focus()
func selectAll()
}
public enum ItemListInsetWithOtherSection {
case none
case full
case reduced
}
public enum ItemListNeighbor {
case none
case otherSection(ItemListInsetWithOtherSection)
case sameSection(alwaysPlain: Bool)
}
public struct ItemListNeighbors {
public var top: ItemListNeighbor
public var bottom: ItemListNeighbor
public init(top: ItemListNeighbor, bottom: ItemListNeighbor) {
self.top = top
self.bottom = bottom
}
}
public func itemListNeighbors(item: ItemListItem, topItem: ItemListItem?, bottomItem: ItemListItem?) -> ItemListNeighbors {
let topNeighbor: ItemListNeighbor
if let topItem = topItem {
if topItem.sectionId != item.sectionId {
let topInset: ItemListInsetWithOtherSection
if topItem.requestsNoInset {
topInset = .none
} else {
if topItem is ItemListTextItem {
topInset = .reduced
} else {
topInset = .full
}
}
topNeighbor = .otherSection(topInset)
} else {
topNeighbor = .sameSection(alwaysPlain: topItem.isAlwaysPlain)
}
} else {
topNeighbor = .none
}
let bottomNeighbor: ItemListNeighbor
if let bottomItem = bottomItem {
if bottomItem.sectionId != item.sectionId {
let bottomInset: ItemListInsetWithOtherSection
if bottomItem.requestsNoInset {
bottomInset = .none
} else {
bottomInset = .full
}
bottomNeighbor = .otherSection(bottomInset)
} else {
bottomNeighbor = .sameSection(alwaysPlain: bottomItem.isAlwaysPlain)
}
} else {
bottomNeighbor = .none
}
return ItemListNeighbors(top: topNeighbor, bottom: bottomNeighbor)
}
public func itemListNeighborsPlainInsets(_ neighbors: ItemListNeighbors) -> UIEdgeInsets {
var insets = UIEdgeInsets()
switch neighbors.top {
case .otherSection:
insets.top += 22.0
case .none, .sameSection:
break
}
switch neighbors.bottom {
case .none:
insets.bottom += 22.0
case .otherSection, .sameSection:
break
}
return insets
}
public func itemListNeighborsGroupedInsets(_ neighbors: ItemListNeighbors, _ params: ListViewItemLayoutParams) -> UIEdgeInsets {
if params.isStandalone {
return UIEdgeInsets()
}
let topInset: CGFloat
switch neighbors.top {
case .none:
if itemListHasRoundedBlockLayout(params) {
topInset = UIScreenPixel + 24.0
} else {
topInset = UIScreenPixel + 35.0
}
case .sameSection:
topInset = 0.0
case let .otherSection(otherInset):
switch otherInset {
case .none:
topInset = 0.0
case .full:
topInset = UIScreenPixel + 35.0
case .reduced:
topInset = UIScreenPixel + 16.0
}
}
let bottomInset: CGFloat
switch neighbors.bottom {
case .sameSection, .otherSection:
bottomInset = 0.0
case .none:
bottomInset = UIScreenPixel + 35.0
}
return UIEdgeInsets(top: topInset, left: 0.0, bottom: bottomInset, right: 0.0)
}
public func itemListHasRoundedBlockLayout(_ params: ListViewItemLayoutParams) -> Bool {
return params.width >= 350.0
}
public final class ItemListPresentationData: Equatable {
public let theme: PresentationTheme
public let fontSize: PresentationFontSize
public let strings: PresentationStrings
public let nameDisplayOrder: PresentationPersonNameOrder
public let dateTimeFormat: PresentationDateTimeFormat
public init(theme: PresentationTheme, fontSize: PresentationFontSize, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat) {
self.theme = theme
self.fontSize = fontSize
self.strings = strings
self.nameDisplayOrder = nameDisplayOrder
self.dateTimeFormat = dateTimeFormat
}
public static func ==(lhs: ItemListPresentationData, rhs: ItemListPresentationData) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.fontSize != rhs.fontSize {
return false
}
if lhs.nameDisplayOrder != rhs.nameDisplayOrder {
return false
}
if lhs.dateTimeFormat != rhs.dateTimeFormat {
return false
}
return true
}
}
public extension PresentationFontSize {
var itemListBaseHeaderFontSize: CGFloat {
return floor(self.itemListBaseFontSize * 13.0 / 17.0)
}
var itemListBaseFontSize: CGFloat {
switch self {
case .extraSmall:
return 14.0
case .small:
return 15.0
case .medium:
return 16.0
case .regular:
return 17.0
case .large:
return 19.0
case .extraLarge:
return 23.0
case .extraLargeX2:
return 26.0
}
}
var itemListBaseLabelFontSize: CGFloat {
switch self {
case .extraSmall:
return 11.0
case .small:
return 12.0
case .medium:
return 13.0
case .regular:
return 14.0
case .large:
return 16.0
case .extraLarge:
return 20.0
case .extraLargeX2:
return 23.0
}
}
}
public extension ItemListPresentationData {
convenience init(_ presentationData: PresentationData) {
self.init(theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat)
}
}
@@ -0,0 +1,57 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import ActivityIndicator
public final class ItemListLoadingIndicatorEmptyStateItem: ItemListControllerEmptyStateItem {
let theme: PresentationTheme
public init(theme: PresentationTheme) {
self.theme = theme
}
public func isEqual(to: ItemListControllerEmptyStateItem) -> Bool {
return to is ItemListLoadingIndicatorEmptyStateItem
}
public func node(current: ItemListControllerEmptyStateItemNode?) -> ItemListControllerEmptyStateItemNode {
if let current = current as? ItemListLoadingIndicatorEmptyStateItemNode {
current.theme = self.theme
return current
} else {
return ItemListLoadingIndicatorEmptyStateItemNode(theme: self.theme)
}
}
}
public final class ItemListLoadingIndicatorEmptyStateItemNode: ItemListControllerEmptyStateItemNode {
public var theme: PresentationTheme {
didSet {
self.indicator.type = .custom(self.theme.list.itemAccentColor, 40.0, 2.0, false)
}
}
private let indicator: ActivityIndicator
private var validLayout: (ContainerViewLayout, CGFloat)?
public init(theme: PresentationTheme) {
self.theme = theme
self.indicator = ActivityIndicator(type: .custom(theme.list.itemAccentColor, 22.0, 2.0, false))
super.init()
self.addSubnode(self.indicator)
}
override public func updateLayout(layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.validLayout = (layout, navigationBarHeight)
var insets = layout.insets(options: [.statusBar])
insets.top += navigationBarHeight
let size = CGSize(width: 22.0, height: 22.0)
transition.updateFrame(node: self.indicator, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - size.width) / 2.0), y: insets.top + floor((layout.size.height - insets.top - insets.bottom - size.height) / 2.0)), size: size))
}
}
@@ -0,0 +1,476 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ManagedAnimationNode
public enum ItemListRevealOptionIcon: Equatable {
case none
case image(image: UIImage)
case animation(animation: String, scale: CGFloat, offset: CGFloat, replaceColors: [UInt32]?, flip: Bool)
public static func ==(lhs: ItemListRevealOptionIcon, rhs: ItemListRevealOptionIcon) -> Bool {
switch lhs {
case .none:
if case .none = rhs {
return true
} else {
return false
}
case let .image(lhsImage):
if case let .image(rhsImage) = rhs, lhsImage == rhsImage {
return true
} else {
return false
}
case let .animation(lhsAnimation, lhsScale, lhsOffset, lhsKeysToColor, lhsFlip):
if case let .animation(rhsAnimation, rhsScale, rhsOffset, rhsKeysToColor, rhsFlip) = rhs, lhsAnimation == rhsAnimation, lhsScale == rhsScale, lhsOffset == rhsOffset, lhsKeysToColor == rhsKeysToColor, lhsFlip == rhsFlip {
return true
} else {
return false
}
}
}
}
public struct ItemListRevealOption: Equatable {
public let key: Int32
public let title: String
public let icon: ItemListRevealOptionIcon
public let color: UIColor
public let textColor: UIColor
public init(key: Int32, title: String, icon: ItemListRevealOptionIcon, color: UIColor, textColor: UIColor) {
self.key = key
self.title = title
self.icon = icon
self.color = color
self.textColor = textColor
}
public static func ==(lhs: ItemListRevealOption, rhs: ItemListRevealOption) -> Bool {
if lhs.key != rhs.key {
return false
}
if lhs.title != rhs.title {
return false
}
if !lhs.color.isEqual(rhs.color) {
return false
}
if !lhs.textColor.isEqual(rhs.textColor) {
return false
}
if lhs.icon != rhs.icon {
return false
}
return true
}
}
private let titleFontWithIcon = Font.medium(13.0)
private let titleFontWithoutIcon = Font.regular(17.0)
private enum ItemListRevealOptionAlignment {
case left
case right
}
private final class ItemListRevealOptionNode: ASDisplayNode {
private let backgroundNode: ASDisplayNode
private let highlightNode: ASDisplayNode
private let titleNode: ASTextNode
private let iconNode: ASImageNode?
private let animationNode: SimpleAnimationNode?
private let enableAnimations: Bool
private var animationScale: CGFloat = 1.0
private var animationNodeOffset: CGFloat = 0.0
private var animationNodeFlip = false
var alignment: ItemListRevealOptionAlignment?
var isExpanded: Bool = false
init(title: String, icon: ItemListRevealOptionIcon, color: UIColor, textColor: UIColor, enableAnimations: Bool) {
self.backgroundNode = ASDisplayNode()
self.highlightNode = ASDisplayNode()
self.titleNode = ASTextNode()
self.titleNode.attributedText = NSAttributedString(string: title, font: icon == .none ? titleFontWithoutIcon : titleFontWithIcon, textColor: textColor)
self.enableAnimations = enableAnimations
switch icon {
case let .image(image):
let iconNode = ASImageNode()
iconNode.image = generateTintedImage(image: image, color: textColor)
self.iconNode = iconNode
self.animationNode = nil
case let .animation(animation, scale, offset, replaceColors, flip):
self.animationScale = scale
self.iconNode = nil
var colors: [UInt32: UInt32] = [:]
if let replaceColors = replaceColors {
for colorToReplace in replaceColors {
colors[colorToReplace] = color.rgb
}
}
self.animationNode = SimpleAnimationNode(animationName: animation, replaceColors: colors, size: CGSize(width: 79.0, height: 79.0), playOnce: true)
if !enableAnimations {
self.animationNode!.seekToEnd()
}
if flip {
self.animationNode!.transform = CATransform3DMakeScale(1.0, -1.0, 1.0)
}
self.animationNodeOffset = offset
self.animationNodeFlip = flip
break
case .none:
self.iconNode = nil
self.animationNode = nil
}
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.titleNode)
if let iconNode = self.iconNode {
self.addSubnode(iconNode)
} else if let animationNode = self.animationNode {
self.addSubnode(animationNode)
}
self.backgroundNode.backgroundColor = color
self.highlightNode.backgroundColor = color.withMultipliedBrightnessBy(0.9)
}
func setHighlighted(_ highlighted: Bool) {
if highlighted {
self.insertSubnode(self.highlightNode, aboveSubnode: self.backgroundNode)
self.highlightNode.layer.animate(from: 0.0 as NSNumber, to: 1.0 as NSNumber, keyPath: "opacity", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.3)
self.highlightNode.alpha = 1.0
} else {
self.highlightNode.removeFromSupernode()
self.highlightNode.alpha = 0.0
}
}
func resetAnimation() {
self.animationNode?.reset()
}
func updateLayout(isFirst: Bool, isLeft: Bool, baseSize: CGSize, alignment: ItemListRevealOptionAlignment, isExpanded: Bool, extendedWidth: CGFloat, sideInset: CGFloat, transition: ContainedViewLayoutTransition, additive: Bool, revealFactor: CGFloat, animateIconMovement: Bool) {
self.highlightNode.frame = CGRect(origin: CGPoint(), size: baseSize)
var animateAdditive = false
if additive && transition.isAnimated && self.isExpanded != isExpanded {
animateAdditive = true
}
let backgroundFrame: CGRect
if isFirst {
backgroundFrame = CGRect(origin: CGPoint(x: isLeft ? -400.0 : 0.0, y: 0.0), size: CGSize(width: extendedWidth + 400.0, height: baseSize.height))
} else {
backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: extendedWidth, height: baseSize.height))
}
let deltaX: CGFloat
if animateAdditive {
let previousFrame = self.backgroundNode.frame
self.backgroundNode.frame = backgroundFrame
if isLeft {
deltaX = previousFrame.width - backgroundFrame.width
} else {
deltaX = -(previousFrame.width - backgroundFrame.width)
}
if !animateIconMovement {
transition.animatePositionAdditive(node: self.backgroundNode, offset: CGPoint(x: deltaX, y: 0.0))
}
} else {
deltaX = 0.0
transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame)
}
self.alignment = alignment
self.isExpanded = isExpanded
let titleSize = self.titleNode.calculatedSize
var contentRect = CGRect(origin: CGPoint(), size: baseSize)
switch alignment {
case .left:
contentRect.origin.x = 0.0
case .right:
contentRect.origin.x = extendedWidth - contentRect.width
}
if let animationNode = self.animationNode {
let imageSize = CGSize(width: animationNode.size.width * self.animationScale, height: animationNode.size.height * self.animationScale)
let iconOffset: CGFloat = -2.0 + self.animationNodeOffset
let titleIconSpacing: CGFloat = 11.0
let iconFrame = CGRect(origin: CGPoint(x: contentRect.minX + floor((baseSize.width - imageSize.width + sideInset) / 2.0), y: contentRect.midY - imageSize.height / 2.0 + iconOffset), size: imageSize)
if animateAdditive {
let iconOffsetX = animateIconMovement ? animationNode.frame.minX - iconFrame.minX : deltaX
animationNode.frame = iconFrame
transition.animatePositionAdditive(node: animationNode, offset: CGPoint(x: iconOffsetX, y: 0.0))
} else {
transition.updateFrame(node: animationNode, frame: iconFrame)
}
let titleFrame = CGRect(origin: CGPoint(x: contentRect.minX + floor((baseSize.width - titleSize.width + sideInset) / 2.0), y: contentRect.midY + titleIconSpacing), size: titleSize)
if animateAdditive {
let titleOffsetX = animateIconMovement ? self.titleNode.frame.minX - titleFrame.minX : deltaX
self.titleNode.frame = titleFrame
transition.animatePositionAdditive(node: self.titleNode, offset: CGPoint(x: titleOffsetX, y: 0.0))
} else {
transition.updateFrame(node: self.titleNode, frame: titleFrame)
}
if self.enableAnimations {
if (abs(revealFactor) >= 0.4) {
animationNode.play()
} else if abs(revealFactor) < CGFloat.ulpOfOne && !transition.isAnimated {
animationNode.reset()
}
}
} else if let iconNode = self.iconNode, let imageSize = iconNode.image?.size {
let iconOffset: CGFloat = -9.0
let titleIconSpacing: CGFloat = 11.0
let iconFrame = CGRect(origin: CGPoint(x: contentRect.minX + floor((baseSize.width - imageSize.width + sideInset) / 2.0), y: contentRect.midY - imageSize.height / 2.0 + iconOffset), size: imageSize)
if animateAdditive {
let iconOffsetX = animateIconMovement ? iconNode.frame.minX - iconFrame.minX : deltaX
iconNode.frame = iconFrame
transition.animatePositionAdditive(node: iconNode, offset: CGPoint(x: iconOffsetX, y: 0.0))
} else {
transition.updateFrame(node: iconNode, frame: iconFrame)
}
let titleFrame = CGRect(origin: CGPoint(x: contentRect.minX + floor((baseSize.width - titleSize.width + sideInset) / 2.0), y: contentRect.midY + titleIconSpacing), size: titleSize)
if animateAdditive {
let titleOffsetX = animateIconMovement ? self.titleNode.frame.minX - titleFrame.minX : deltaX
self.titleNode.frame = titleFrame
transition.animatePositionAdditive(node: self.titleNode, offset: CGPoint(x: titleOffsetX, y: 0.0))
} else {
transition.updateFrame(node: self.titleNode, frame: titleFrame)
}
} else {
let titleFrame = CGRect(origin: CGPoint(x: contentRect.minX + floor((baseSize.width - titleSize.width + sideInset) / 2.0), y: contentRect.minY + floor((baseSize.height - titleSize.height) / 2.0)), size: titleSize)
if animateAdditive {
let titleOffsetX = animateIconMovement ? self.titleNode.frame.minX - titleFrame.minX : deltaX
self.titleNode.frame = titleFrame
transition.animatePositionAdditive(node: self.titleNode, offset: CGPoint(x: titleOffsetX, y: 0.0))
} else {
transition.updateFrame(node: self.titleNode, frame: titleFrame)
}
}
}
override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
let titleSize = self.titleNode.measure(constrainedSize)
var maxWidth = titleSize.width
if let iconNode = self.iconNode, let image = iconNode.image {
maxWidth = max(image.size.width, maxWidth)
}
return CGSize(width: max(74.0, maxWidth + 20.0), height: constrainedSize.height)
}
}
public final class ItemListRevealOptionsNode: ASDisplayNode {
private let optionSelected: (ItemListRevealOption) -> Void
private let tapticAction: () -> Void
private var options: [ItemListRevealOption] = []
private var isLeft: Bool = false
private var optionNodes: [ItemListRevealOptionNode] = []
private var revealOffset: CGFloat = 0.0
private var sideInset: CGFloat = 0.0
public init(optionSelected: @escaping (ItemListRevealOption) -> Void, tapticAction: @escaping () -> Void) {
self.optionSelected = optionSelected
self.tapticAction = tapticAction
super.init()
}
override public func didLoad() {
super.didLoad()
let gestureRecognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
gestureRecognizer.highlight = { [weak self] location in
guard let strongSelf = self, let location = location else {
return
}
for node in strongSelf.optionNodes {
if node.frame.contains(location) {
//node.setHighlighted(true)
break
}
}
}
gestureRecognizer.tapActionAtPoint = { _ in
return .waitForSingleTap
}
self.view.addGestureRecognizer(gestureRecognizer)
}
public func setOptions(_ options: [ItemListRevealOption], isLeft: Bool, enableAnimations: Bool) {
if self.options != options || self.isLeft != isLeft {
self.options = options
self.isLeft = isLeft
for node in self.optionNodes {
node.removeFromSupernode()
}
self.optionNodes = options.map { option in
return ItemListRevealOptionNode(title: option.title, icon: option.icon, color: option.color, textColor: option.textColor, enableAnimations: enableAnimations)
}
if isLeft {
for node in self.optionNodes.reversed() {
self.addSubnode(node)
}
} else {
for node in self.optionNodes {
self.addSubnode(node)
}
}
self.invalidateCalculatedLayout()
}
}
override public func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
var maxWidth: CGFloat = 0.0
for node in self.optionNodes {
let nodeSize = node.measure(constrainedSize)
maxWidth = max(nodeSize.width, maxWidth)
}
return CGSize(width: maxWidth * CGFloat(self.optionNodes.count), height: constrainedSize.height)
}
public func updateRevealOffset(offset: CGFloat, sideInset: CGFloat, transition: ContainedViewLayoutTransition) {
self.revealOffset = offset
self.sideInset = sideInset
self.updateNodesLayout(transition: transition)
}
private func updateNodesLayout(transition: ContainedViewLayoutTransition) {
let size = self.bounds.size
if size.width.isLessThanOrEqualTo(0.0) || self.optionNodes.isEmpty {
return
}
let basicNodeWidth = floor((size.width - abs(self.sideInset)) / CGFloat(self.optionNodes.count))
let lastNodeWidth = size.width - basicNodeWidth * CGFloat(self.optionNodes.count - 1)
let revealFactor = self.revealOffset / size.width
let boundaryRevealFactor: CGFloat
if self.optionNodes.count > 2 {
boundaryRevealFactor = 1.0 + 16.0 / size.width
} else {
boundaryRevealFactor = 1.0 + basicNodeWidth / size.width
}
let startingOffset: CGFloat
if self.isLeft {
startingOffset = size.width + max(0.0, abs(revealFactor) - 1.0) * size.width
} else {
startingOffset = 0.0
}
let animated = transition.isAnimated
var completionCount = self.optionNodes.count
let intermediateCompletion = {
if completionCount == 0 && animated && abs(revealFactor) < CGFloat.ulpOfOne {
for node in self.optionNodes {
node.resetAnimation()
}
}
}
var i = self.isLeft ? (self.optionNodes.count - 1) : 0
while i >= 0 && i < self.optionNodes.count {
let node = self.optionNodes[i]
let nodeWidth = i == (self.optionNodes.count - 1) ? lastNodeWidth : basicNodeWidth
var nodeTransition = transition
var isExpanded = false
if (self.isLeft && i == 0) || (!self.isLeft && i == self.optionNodes.count - 1) {
if abs(revealFactor) > boundaryRevealFactor {
isExpanded = true
}
}
if let _ = node.alignment, node.isExpanded != isExpanded {
nodeTransition = transition.isAnimated ? transition : .animated(duration: 0.2, curve: .easeInOut)
if !transition.isAnimated {
self.tapticAction()
}
}
var sideInset: CGFloat = 0.0
if i == self.optionNodes.count - 1 {
sideInset = self.sideInset
}
let extendedWidth: CGFloat
let nodeLeftOffset: CGFloat
if isExpanded {
nodeLeftOffset = 0.0
extendedWidth = size.width * max(1.0, abs(revealFactor))
} else if self.isLeft {
let offset = basicNodeWidth * CGFloat(self.optionNodes.count - 1 - i)
extendedWidth = (size.width - offset) * max(1.0, abs(revealFactor))
nodeLeftOffset = startingOffset - extendedWidth - floorToScreenPixels(offset * abs(revealFactor))
} else {
let offset = basicNodeWidth * CGFloat(i)
extendedWidth = (size.width - offset) * max(1.0, abs(revealFactor))
nodeLeftOffset = startingOffset + floorToScreenPixels(offset * abs(revealFactor))
}
transition.updateFrame(node: node, frame: CGRect(origin: CGPoint(x: nodeLeftOffset, y: 0.0), size: CGSize(width: extendedWidth, height: size.height)), completion: { _ in
completionCount -= 1
intermediateCompletion()
})
var nodeAlignment: ItemListRevealOptionAlignment
if (self.optionNodes.count > 1) {
nodeAlignment = self.isLeft ? .right : .left
} else {
if (self.isLeft) {
nodeAlignment = isExpanded ? .right : .left
} else {
nodeAlignment = isExpanded ? .left : .right
}
}
let animateIconMovement = self.optionNodes.count == 1
node.updateLayout(isFirst: (self.isLeft && i == 0) || (!self.isLeft && i == self.optionNodes.count - 1), isLeft: self.isLeft, baseSize: CGSize(width: nodeWidth, height: size.height), alignment: nodeAlignment, isExpanded: isExpanded, extendedWidth: extendedWidth, sideInset: sideInset, transition: nodeTransition, additive: !transition.isAnimated, revealFactor: revealFactor, animateIconMovement: animateIconMovement)
if self.isLeft {
i -= 1
} else {
i += 1
}
}
}
@objc private func tapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
if case .ended = recognizer.state, let gesture = recognizer.lastRecognizedGestureAndLocation?.0, case .tap = gesture {
let location = recognizer.location(in: self.view)
var selectedOption: Int?
var i = self.isLeft ? 0 : (self.optionNodes.count - 1)
while i >= 0 && i < self.optionNodes.count {
self.optionNodes[i].setHighlighted(false)
if self.optionNodes[i].frame.contains(location) {
selectedOption = i
break
}
if self.isLeft {
i += 1
} else {
i -= 1
}
}
if let selectedOption = selectedOption {
self.optionSelected(self.options[selectedOption])
}
}
}
public func isDisplayingExtendedAction() -> Bool {
return self.optionNodes.contains(where: { $0.isExpanded })
}
}
@@ -0,0 +1,61 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import CheckNode
public final class ItemListSelectableControlNode: ASDisplayNode {
public enum Style {
case regular
case compact
case small
}
private let checkNode: CheckNode
public init(strokeColor: UIColor, fillColor: UIColor, foregroundColor: UIColor) {
self.checkNode = CheckNode(theme: CheckNodeTheme(backgroundColor: fillColor, strokeColor: foregroundColor, borderColor: strokeColor, overlayBorder: false, hasInset: true, hasShadow: false))
self.checkNode.isUserInteractionEnabled = false
super.init()
self.addSubnode(self.checkNode)
}
public static func asyncLayout(_ node: ItemListSelectableControlNode?) -> (_ strokeColor: UIColor, _ fillColor: UIColor, _ foregroundColor: UIColor, _ selected: Bool, _ style: Style) -> (CGFloat, (CGSize, Bool) -> ItemListSelectableControlNode) {
return { strokeColor, fillColor, foregroundColor, selected, style in
let resultNode: ItemListSelectableControlNode
if let node = node {
resultNode = node
} else {
resultNode = ItemListSelectableControlNode(strokeColor: strokeColor, fillColor: fillColor, foregroundColor: foregroundColor)
}
let offsetSize: CGFloat
switch style {
case .regular:
offsetSize = 45.0
case .compact:
offsetSize = 38.0
case .small:
offsetSize = 44.0
}
return (offsetSize, { size, animated in
let checkSize: CGSize
let checkOffset: CGFloat
switch style {
case .regular, .compact:
checkSize = CGSize(width: 26.0, height: 26.0)
checkOffset = style == .compact ? 11.0 : 13.0
case .small:
checkSize = CGSize(width: 22.0, height: 22.0)
checkOffset = 16.0
}
resultNode.checkNode.frame = CGRect(origin: CGPoint(x: checkOffset, y: floorToScreenPixels((size.height - checkSize.height) / 2.0)), size: checkSize)
resultNode.checkNode.setSelected(selected, animated: animated)
return resultNode
})
}
}
}
@@ -0,0 +1,347 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
public enum ItemListActionKind {
case generic
case destructive
case neutral
case disabled
}
public enum ItemListActionAlignment {
case natural
case center
}
public class ItemListActionItem: ListViewItem, ItemListItem {
let presentationData: ItemListPresentationData
let systemStyle: ItemListSystemStyle
let title: String
let kind: ItemListActionKind
let alignment: ItemListActionAlignment
public let sectionId: ItemListSectionId
let style: ItemListStyle
public let action: () -> Void
let longTapAction: (() -> Void)?
let clearHighlightAutomatically: Bool
public let tag: Any?
public init(presentationData: ItemListPresentationData, systemStyle: ItemListSystemStyle = .legacy, title: String, kind: ItemListActionKind, alignment: ItemListActionAlignment, sectionId: ItemListSectionId, style: ItemListStyle, action: @escaping () -> Void, longTapAction: (() -> Void)? = nil, clearHighlightAutomatically: Bool = true, tag: Any? = nil) {
self.presentationData = presentationData
self.systemStyle = systemStyle
self.title = title
self.kind = kind
self.alignment = alignment
self.sectionId = sectionId
self.style = style
self.action = action
self.longTapAction = longTapAction
self.clearHighlightAutomatically = clearHighlightAutomatically
self.tag = tag
}
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ItemListActionItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply(false) })
})
}
}
}
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? ItemListActionItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply(false)
})
}
}
}
}
}
public var selectable: Bool = true
public func selected(listView: ListView){
if self.clearHighlightAutomatically {
listView.clearHighlightAnimated(true)
}
self.action()
}
}
public class ItemListActionItemNode: ListViewItemNode, ItemListItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
private let maskNode: ASImageNode
private let titleNode: TextNode
private let activateArea: AccessibilityAreaNode
private var item: ItemListActionItem?
public var tag: ItemListItemTag? {
return self.item?.tag as? ItemListItemTag
}
public init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.backgroundColor = .white
self.maskNode = ASImageNode()
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
self.activateArea = AccessibilityAreaNode()
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.titleNode)
self.addSubnode(self.activateArea)
}
public func asyncLayout() -> (_ item: ItemListActionItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let currentItem = self.item
return { item, params, neighbors in
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize)
var updatedTheme: PresentationTheme?
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
}
let textColor: UIColor
switch item.kind {
case .destructive:
textColor = item.presentationData.theme.list.itemDestructiveColor
case .generic:
textColor = item.presentationData.theme.list.itemAccentColor
case .neutral:
textColor = item.presentationData.theme.list.itemPrimaryTextColor
case .disabled:
textColor = item.presentationData.theme.list.itemDisabledTextColor
}
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: textColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let contentSize: CGSize
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
let separatorRightInset: CGFloat = item.systemStyle == .glass ? 16.0 : 0.0
let verticalInset: CGFloat
switch item.systemStyle {
case .glass:
verticalInset = 15.0
case .legacy:
verticalInset = 11.0
}
let itemBackgroundColor: UIColor
let itemSeparatorColor: UIColor
switch item.style {
case .plain:
itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor
contentSize = CGSize(width: params.width, height: titleLayout.size.height + verticalInset * 2.0)
insets = itemListNeighborsPlainInsets(neighbors)
case .blocks:
itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor
contentSize = CGSize(width: params.width, height: titleLayout.size.height + verticalInset * 2.0)
insets = itemListNeighborsGroupedInsets(neighbors, params)
}
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
return (layout, { [weak self] _ in
if let strongSelf = self {
strongSelf.item = item
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
strongSelf.activateArea.accessibilityLabel = item.title
var accessibilityTraits: UIAccessibilityTraits = .button
switch item.kind {
case .disabled:
accessibilityTraits.insert(.notEnabled)
default:
break
}
strongSelf.activateArea.accessibilityTraits = accessibilityTraits
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
}
let _ = titleApply()
let leftInset = 16.0 + params.leftInset
switch item.style {
case .plain:
if strongSelf.backgroundNode.supernode != nil {
strongSelf.backgroundNode.removeFromSupernode()
}
if strongSelf.topStripeNode.supernode != nil {
strongSelf.topStripeNode.removeFromSupernode()
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0)
}
if strongSelf.maskNode.supernode != nil {
strongSelf.maskNode.removeFromSupernode()
}
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight))
case .blocks:
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = 16.0 + params.leftInset
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: params.width - params.rightInset - bottomStripeInset - separatorRightInset, height: separatorHeight))
}
switch item.alignment {
case .natural:
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floorToScreenPixels((contentSize.height - titleLayout.size.height) / 2.0) + 1.0), size: titleLayout.size)
case .center:
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: params.leftInset + floor((params.width - params.leftInset - params.rightInset - titleLayout.size.width) / 2.0), y: floorToScreenPixels((contentSize.height - titleLayout.size.height) / 2.0) + 1.0), size: titleLayout.size)
}
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel))
}
})
}
}
override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
super.setHighlighted(highlighted, at: point, animated: animated)
if highlighted && self.item?.kind != ItemListActionKind.disabled {
self.highlightedBackgroundNode.alpha = 1.0
if self.highlightedBackgroundNode.supernode == nil {
var anchorNode: ASDisplayNode?
if self.bottomStripeNode.supernode != nil {
anchorNode = self.bottomStripeNode
} else if self.topStripeNode.supernode != nil {
anchorNode = self.topStripeNode
} else if self.backgroundNode.supernode != nil {
anchorNode = self.backgroundNode
}
if let anchorNode = anchorNode {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
} else {
self.addSubnode(self.highlightedBackgroundNode)
}
}
} else {
if self.highlightedBackgroundNode.supernode != nil {
if animated {
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
if let strongSelf = self {
if completed {
strongSelf.highlightedBackgroundNode.removeFromSupernode()
}
}
})
self.highlightedBackgroundNode.alpha = 0.0
} else {
self.highlightedBackgroundNode.removeFromSupernode()
}
}
}
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
override public func longTapped() {
self.item?.longTapAction?()
}
override public var canBeLongTapped: Bool {
return self.item?.longTapAction != nil
}
}
@@ -0,0 +1,195 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import ActivityIndicator
import TextFormat
import Markdown
public class ItemListActivityTextItem: ListViewItem, ItemListItem {
public enum TextColor {
case generic
case constructive
case destructive
case warning
}
let displayActivity: Bool
let presentationData: ItemListPresentationData
let text: String
let color: TextColor
let linkAction: ((ItemListTextItemLinkAction) -> Void)?
public let sectionId: ItemListSectionId
public let isAlwaysPlain: Bool = true
public init(displayActivity: Bool, presentationData: ItemListPresentationData, text: String, color: TextColor, linkAction: ((ItemListTextItemLinkAction) -> Void)? = nil, sectionId: ItemListSectionId) {
self.displayActivity = displayActivity
self.presentationData = presentationData
self.text = text
self.color = color
self.linkAction = linkAction
self.sectionId = sectionId
}
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ItemListActivityTextItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
guard let nodeValue = node() as? ItemListActivityTextItemNode else {
assertionFailure()
return
}
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
public class ItemListActivityTextItemNode: ListViewItemNode {
private let titleNode: TextNode
private let activityIndicator: ActivityIndicator
private var item: ItemListActivityTextItem?
public init() {
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale
self.activityIndicator = ActivityIndicator(type: ActivityIndicatorType.custom(.black, 16.0, 2.0, false))
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.titleNode)
self.addSubnode(self.activityIndicator)
}
override public func didLoad() {
super.didLoad()
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
recognizer.tapActionAtPoint = { _ in
return .waitForSingleTap
}
self.view.addGestureRecognizer(recognizer)
}
public func asyncLayout() -> (_ item: ItemListActivityTextItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
return { item, params, neighbors in
let leftInset: CGFloat = 15.0 + params.leftInset
let verticalInset: CGFloat = 7.0
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseHeaderFontSize)
var activityWidth: CGFloat = 0.0
if item.displayActivity {
activityWidth = 25.0
}
let textColor: UIColor
switch item.color {
case .generic:
textColor = item.presentationData.theme.list.freeTextColor
case .constructive:
textColor = item.presentationData.theme.list.freeTextSuccessColor
case .destructive:
textColor = item.presentationData.theme.list.freeTextErrorColor
case .warning:
textColor = UIColor(rgb: 0xef8c00)
}
let attributedString = parseMarkdownIntoAttributedString(item.text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: textColor), bold: MarkdownAttributeSet(font: titleFont, textColor: item.presentationData.theme.list.freeTextErrorColor), link: MarkdownAttributeSet(font: titleFont, textColor: item.presentationData.theme.list.itemAccentColor), linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
}))
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: attributedString, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - 20.0 - 22.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: TextNodeCutout(topLeft: CGSize(width: activityWidth, height: 22.0)), insets: UIEdgeInsets()))
let contentSize: CGSize
let insets: UIEdgeInsets
contentSize = CGSize(width: params.width, height: titleLayout.size.height + verticalInset + verticalInset)
insets = itemListNeighborsPlainInsets(neighbors)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
let _ = titleApply()
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: titleLayout.size)
strongSelf.activityIndicator.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((contentSize.height - 16.0) / 2.0)), size: CGSize(width: 16.0, height: 16.0))
strongSelf.activityIndicator.type = .custom(item.presentationData.theme.list.itemAccentColor, 16.0, 2.0, false)
if item.displayActivity {
strongSelf.activityIndicator.isHidden = false
} else {
strongSelf.activityIndicator.isHidden = true
}
}
})
}
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
@objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
switch recognizer.state {
case .ended:
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
switch gesture {
case .tap:
let titleFrame = self.titleNode.frame
if let item = self.item, titleFrame.contains(location) {
if let (_, attributes) = self.titleNode.attributesAtPoint(CGPoint(x: location.x - titleFrame.minX, y: location.y - titleFrame.minY)) {
if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
item.linkAction?(.tap(url))
}
}
}
default:
break
}
}
default:
break
}
}
}
@@ -0,0 +1,461 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
public enum ItemListCheckboxItemStyle {
case left
case right
}
public enum ItemListCheckboxItemColor {
case accent
case secondary
}
public class ItemListCheckboxItem: ListViewItem, ItemListItem {
public enum IconPlacement {
case `default`
case check
}
public enum TextColor {
case primary
case accent
}
let presentationData: ItemListPresentationData
let systemStyle: ItemListSystemStyle
let icon: UIImage?
let iconSize: CGSize?
let iconPlacement: IconPlacement
let title: String
let subtitle: String?
let style: ItemListCheckboxItemStyle
let color: ItemListCheckboxItemColor
let textColor: TextColor
let checked: Bool
let enabled: Bool
let zeroSeparatorInsets: Bool
public let sectionId: ItemListSectionId
let action: () -> Void
let deleteAction: (() -> Void)?
public init(presentationData: ItemListPresentationData, systemStyle: ItemListSystemStyle = .legacy, icon: UIImage? = nil, iconSize: CGSize? = nil, iconPlacement: IconPlacement = .default, title: String, subtitle: String? = nil, style: ItemListCheckboxItemStyle, color: ItemListCheckboxItemColor = .accent, textColor: TextColor = .primary, checked: Bool, enabled: Bool = true, zeroSeparatorInsets: Bool, sectionId: ItemListSectionId, action: @escaping () -> Void, deleteAction: (() -> Void)? = nil) {
self.presentationData = presentationData
self.systemStyle = systemStyle
self.icon = icon
self.iconSize = iconSize
self.iconPlacement = iconPlacement
self.title = title
self.subtitle = subtitle
self.style = style
self.color = color
self.textColor = textColor
self.checked = checked
self.enabled = enabled
self.zeroSeparatorInsets = zeroSeparatorInsets
self.sectionId = sectionId
self.action = action
self.deleteAction = deleteAction
}
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ItemListCheckboxItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? ItemListCheckboxItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
public var selectable: Bool = true
public func selected(listView: ListView){
listView.clearHighlightAnimated(true)
if self.enabled {
self.action()
}
}
}
public class ItemListCheckboxItemNode: ItemListRevealOptionsItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
private let maskNode: ASImageNode
private let activateArea: AccessibilityAreaNode
private let contentParentNode: ASDisplayNode
private let contentContainerNode: ASDisplayNode
private let imageNode: ASImageNode
private let iconNode: ASImageNode
private let titleNode: TextNode
private let subtitleNode: TextNode
private var item: ItemListCheckboxItem?
override public var controlsContainer: ASDisplayNode {
return self.contentParentNode
}
public init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.maskNode.isUserInteractionEnabled = false
self.contentParentNode = ASDisplayNode()
self.contentContainerNode = ASDisplayNode()
self.imageNode = ASImageNode()
self.imageNode.isLayerBacked = true
self.imageNode.displayWithoutProcessing = true
self.imageNode.displaysAsynchronously = false
self.iconNode = ASImageNode()
self.iconNode.isLayerBacked = true
self.iconNode.displayWithoutProcessing = true
self.iconNode.displaysAsynchronously = false
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale
self.subtitleNode = TextNode()
self.subtitleNode.isUserInteractionEnabled = false
self.subtitleNode.contentMode = .left
self.subtitleNode.contentsScale = UIScreen.main.scale
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
self.activateArea = AccessibilityAreaNode()
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
self.addSubnode(self.contentParentNode)
self.contentParentNode.addSubnode(self.contentContainerNode)
self.contentContainerNode.addSubnode(self.imageNode)
self.contentContainerNode.addSubnode(self.iconNode)
self.contentContainerNode.addSubnode(self.titleNode)
self.contentContainerNode.addSubnode(self.subtitleNode)
self.addSubnode(self.activateArea)
self.activateArea.activate = { [weak self] in
self?.item?.action()
return true
}
}
public func asyncLayout() -> (_ item: ItemListCheckboxItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode)
let currentItem = self.item
return { item, params, neighbors in
var leftInset: CGFloat = params.leftInset
switch item.style {
case .left:
leftInset += 62.0
case .right:
if item.icon == nil {
leftInset += 16.0
}
}
let iconInset: CGFloat = 62.0
if item.icon != nil {
switch item.iconPlacement {
case .default:
leftInset += iconInset
case .check:
break
}
}
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize)
let subtitleFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0))
var titleColor: UIColor
let subtitleColor: UIColor = item.presentationData.theme.list.itemSecondaryTextColor
switch item.textColor {
case .primary:
titleColor = item.presentationData.theme.list.itemPrimaryTextColor
case .accent:
titleColor = item.presentationData.theme.list.itemAccentColor
}
if !item.enabled {
titleColor = item.presentationData.theme.list.itemDisabledTextColor
}
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 28.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.subtitle ?? "", font: subtitleFont, textColor: subtitleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 28.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let separatorHeight = UIScreenPixel
let separatorRightInset: CGFloat = item.systemStyle == .glass ? 16.0 : 0.0
let verticalInset: CGFloat
switch item.systemStyle {
case .glass:
verticalInset = 15.0
case .legacy:
verticalInset = 11.0
}
let insets = itemListNeighborsGroupedInsets(neighbors, params)
var contentSize = CGSize(width: params.width, height: titleLayout.size.height + verticalInset * 2.0)
if item.subtitle != nil {
contentSize.height += subtitleLayout.size.height
}
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
var updateCheckImage: UIImage?
var updatedTheme: PresentationTheme?
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
}
if currentItem?.presentationData.theme !== item.presentationData.theme || currentItem?.color != item.color || currentItem?.enabled != item.enabled {
if !item.enabled {
updateCheckImage = PresentationResourcesItemList.disabledCheckIconImage(item.presentationData.theme)
} else {
switch item.color {
case .accent:
updateCheckImage = PresentationResourcesItemList.checkIconImage(item.presentationData.theme)
case .secondary:
updateCheckImage = PresentationResourcesItemList.secondaryCheckIconImage(item.presentationData.theme)
}
}
}
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.contentParentNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: params.width, height: layout.contentSize.height))
strongSelf.contentContainerNode.frame = CGRect(origin: CGPoint(x: strongSelf.contentContainerNode.frame.minX, y: 0.0), size: CGSize(width: params.width, height: layout.contentSize.height))
strongSelf.activateArea.accessibilityLabel = item.title
if item.checked {
strongSelf.activateArea.accessibilityValue = "Selected"
} else {
strongSelf.activateArea.accessibilityValue = ""
}
if item.enabled {
strongSelf.activateArea.accessibilityTraits = []
} else {
strongSelf.activateArea.accessibilityTraits = [.notEnabled]
}
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
if let updateCheckImage = updateCheckImage {
strongSelf.iconNode.image = updateCheckImage
}
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
}
let _ = titleApply()
let _ = subtitleApply()
if let image = strongSelf.iconNode.image {
switch item.style {
case .left:
strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: params.leftInset + floor((leftInset - params.leftInset - image.size.width) / 2.0), y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size)
case .right:
strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - image.size.width - floor((44.0 - image.size.width) / 2.0), y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size)
}
}
strongSelf.iconNode.isHidden = !item.checked
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, aboveSubnode: strongSelf.contentParentNode)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
if item.zeroSeparatorInsets {
bottomStripeInset = 0.0
} else {
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - params.rightInset - bottomStripeInset - separatorRightInset, height: separatorHeight))
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset + 1.0), size: titleLayout.size)
strongSelf.subtitleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: strongSelf.titleNode.frame.maxY), size: subtitleLayout.size)
if let icon = item.icon {
let iconSize = item.iconSize ?? icon.size
strongSelf.imageNode.image = icon
let iconFrame: CGRect
switch item.iconPlacement {
case .default:
iconFrame = CGRect(origin: CGPoint(x: params.leftInset + floor((leftInset - params.leftInset - iconSize.width) / 2.0), y: floor((layout.contentSize.height - iconSize.height) / 2.0)), size: iconSize)
case .check:
iconFrame = CGRect(origin: CGPoint(x: params.leftInset + floor((leftInset - params.leftInset - iconSize.width) / 2.0), y: floor((contentSize.height - iconSize.height) / 2.0)), size: iconSize)
}
strongSelf.imageNode.frame = iconFrame
} else {
strongSelf.imageNode.image = nil
}
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: strongSelf.backgroundNode.frame.height + UIScreenPixel + UIScreenPixel))
strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset)
if item.deleteAction != nil {
strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)]))
} else {
strongSelf.setRevealOptions((left: [], right: []))
}
}
})
}
}
override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
super.setHighlighted(highlighted, at: point, animated: animated)
if highlighted && (self.item?.enabled ?? false) {
self.highlightedBackgroundNode.alpha = 1.0
if self.highlightedBackgroundNode.supernode == nil {
var anchorNode: ASDisplayNode?
if self.bottomStripeNode.supernode != nil {
anchorNode = self.bottomStripeNode
} else if self.topStripeNode.supernode != nil {
anchorNode = self.topStripeNode
} else if self.backgroundNode.supernode != nil {
anchorNode = self.backgroundNode
}
if let anchorNode = anchorNode {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
} else {
self.addSubnode(self.highlightedBackgroundNode)
}
}
} else {
if self.highlightedBackgroundNode.supernode != nil {
if animated {
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
if let strongSelf = self {
if completed {
strongSelf.highlightedBackgroundNode.removeFromSupernode()
}
}
})
self.highlightedBackgroundNode.alpha = 0.0
} else {
self.highlightedBackgroundNode.removeFromSupernode()
}
}
}
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
override public func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
super.updateRevealOffset(offset: offset, transition: transition)
transition.updateFrame(node: self.contentContainerNode, frame: CGRect(origin: CGPoint(x: offset, y: self.contentContainerNode.frame.minY), size: self.contentContainerNode.bounds.size))
}
override public func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) {
self.setRevealOptionsOpened(false, animated: true)
self.revealOptionsInteractivelyClosed()
if let item = self.item {
item.deleteAction?()
}
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event)
return result
}
}
@@ -0,0 +1,929 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import ShimmerEffect
import AvatarNode
import TelegramCore
import AccountContext
import TextNodeWithEntities
import ListItemComponentAdaptor
private let avatarFont = avatarPlaceholderFont(size: 16.0)
public enum ItemListDisclosureItemTitleColor {
case primary
case accent
}
public enum ItemListDisclosureItemTitleFont {
case regular
case bold
}
public enum ItemListDisclosureStyle {
case arrow
case optionArrows
case none
}
public enum ItemListDisclosureLabelStyle {
case text
case detailText
case coloredText(UIColor)
case textWithIcon(UIImage)
case multilineDetailText
case badge(UIColor)
case color(UIColor)
case semitransparentBadge(UIColor)
case image(image: UIImage, size: CGSize)
}
public enum ItemListDisclosureItemDetailLabelColor {
case generic
case constructive
case destructive
}
public class ItemListDisclosureItem: ListViewItem, ItemListItem, ListItemComponentAdaptor.ItemGenerator {
let presentationData: ItemListPresentationData
let systemStyle: ItemListSystemStyle
let icon: UIImage?
let context: AccountContext?
let iconPeer: EnginePeer?
let title: String
let attributedTitle: NSAttributedString?
let titleColor: ItemListDisclosureItemTitleColor
let titleFont: ItemListDisclosureItemTitleFont
let titleIcon: UIImage?
let titleBadge: String?
let enabled: Bool
let label: String
let attributedLabel: NSAttributedString?
let labelStyle: ItemListDisclosureLabelStyle
let additionalDetailLabel: String?
let additionalDetailLabelColor: ItemListDisclosureItemDetailLabelColor
public let sectionId: ItemListSectionId
let style: ItemListStyle
let disclosureStyle: ItemListDisclosureStyle
let noInsets: Bool
let action: (() -> Void)?
let clearHighlightAutomatically: Bool
public let tag: ItemListItemTag?
public let shimmeringIndex: Int?
public init(presentationData: ItemListPresentationData, systemStyle: ItemListSystemStyle = .legacy, icon: UIImage? = nil, context: AccountContext? = nil, iconPeer: EnginePeer? = nil, title: String, attributedTitle: NSAttributedString? = nil, enabled: Bool = true, titleColor: ItemListDisclosureItemTitleColor = .primary, titleFont: ItemListDisclosureItemTitleFont = .regular, titleIcon: UIImage? = nil, titleBadge: String? = nil, label: String, attributedLabel: NSAttributedString? = nil, labelStyle: ItemListDisclosureLabelStyle = .text, additionalDetailLabel: String? = nil, additionalDetailLabelColor: ItemListDisclosureItemDetailLabelColor = .generic, sectionId: ItemListSectionId, style: ItemListStyle, disclosureStyle: ItemListDisclosureStyle = .arrow, noInsets: Bool = false, action: (() -> Void)?, clearHighlightAutomatically: Bool = true, tag: ItemListItemTag? = nil, shimmeringIndex: Int? = nil) {
self.presentationData = presentationData
self.systemStyle = systemStyle
self.icon = icon
self.context = context
self.iconPeer = iconPeer
self.title = title
self.attributedTitle = attributedTitle
self.titleColor = titleColor
self.titleFont = titleFont
self.titleIcon = titleIcon
self.titleBadge = titleBadge
self.enabled = enabled
self.labelStyle = labelStyle
self.label = label
self.attributedLabel = attributedLabel
self.additionalDetailLabel = additionalDetailLabel
self.additionalDetailLabelColor = additionalDetailLabelColor
self.sectionId = sectionId
self.style = style
self.disclosureStyle = disclosureStyle
self.noInsets = noInsets
self.action = action
self.clearHighlightAutomatically = clearHighlightAutomatically
self.tag = tag
self.shimmeringIndex = shimmeringIndex
}
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ItemListDisclosureItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? ItemListDisclosureItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
public var selectable: Bool = true
public func selected(listView: ListView){
if self.clearHighlightAutomatically {
listView.clearHighlightAnimated(true)
}
if self.enabled {
self.action?()
}
}
public func item() -> ListViewItem {
return self
}
public static func ==(lhs: ItemListDisclosureItem, rhs: ItemListDisclosureItem) -> Bool {
if lhs.presentationData != rhs.presentationData {
return false
}
if lhs.context !== rhs.context {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.label != rhs.label {
return false
}
return true
}
}
private let badgeFont = Font.regular(15.0)
private let boldBadgeFont = Font.semibold(14.0)
public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
private let maskNode: ASImageNode
var avatarNode: AvatarNode?
let iconNode: ASImageNode
let titleNode: TextNodeWithEntities
let titleIconNode: ASImageNode
public let labelNode: TextNode
var additionalDetailLabelNode: TextNode?
let arrowNode: ASImageNode
let labelBadgeNode: ASImageNode
let labelImageNode: ASImageNode
var titleBadgeNode: ASImageNode?
var titleBadgeTextNode: TextNode?
private let activateArea: AccessibilityAreaNode
private var item: ItemListDisclosureItem?
override public var canBeSelected: Bool {
if let item = self.item, let _ = item.action {
return true
} else {
return false
}
}
public var tag: ItemListItemTag? {
return self.item?.tag
}
private var placeholderNode: ShimmerEffectNode?
private var absoluteLocation: (CGRect, CGSize)?
public init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.backgroundColor = .white
self.maskNode = ASImageNode()
self.maskNode.isUserInteractionEnabled = false
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.iconNode = ASImageNode()
self.iconNode.isLayerBacked = true
self.iconNode.displaysAsynchronously = false
self.titleNode = TextNodeWithEntities()
self.titleNode.textNode.isUserInteractionEnabled = false
self.titleIconNode = ASImageNode()
self.titleIconNode.displayWithoutProcessing = true
self.titleIconNode.displaysAsynchronously = false
self.labelNode = TextNode()
self.labelNode.isUserInteractionEnabled = false
self.arrowNode = ASImageNode()
self.arrowNode.displayWithoutProcessing = true
self.arrowNode.displaysAsynchronously = false
self.arrowNode.isLayerBacked = true
self.labelBadgeNode = ASImageNode()
self.labelImageNode = ASImageNode()
self.labelBadgeNode.displayWithoutProcessing = true
self.labelBadgeNode.displaysAsynchronously = false
self.labelBadgeNode.isLayerBacked = true
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
self.activateArea = AccessibilityAreaNode()
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.titleNode.textNode)
self.addSubnode(self.labelNode)
self.addSubnode(self.arrowNode)
self.addSubnode(self.activateArea)
}
override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
var rect = rect
rect.origin.y += self.insets.top
self.absoluteLocation = (rect, containerSize)
if let shimmerNode = self.placeholderNode {
shimmerNode.updateAbsoluteRect(rect, within: containerSize)
}
}
public func updateHasContextMenu(hasContextMenu: Bool) {
let transition: ContainedViewLayoutTransition
if hasContextMenu {
transition = .immediate
} else {
transition = .animated(duration: 0.3, curve: .easeInOut)
}
transition.updateAlpha(node: self.labelNode, alpha: hasContextMenu ? 0.5 : 1.0)
transition.updateAlpha(node: self.arrowNode, alpha: hasContextMenu ? 0.5 : 1.0)
}
public func asyncLayout() -> (_ item: ItemListDisclosureItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode.textNode)
let makeTitleWithEntitiesLayout = TextNodeWithEntities.asyncLayout(self.titleNode)
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
let makeAdditionalDetailLabelLayout = TextNode.asyncLayout(self.additionalDetailLabelNode)
let makeTitleBadgeTextNodeLayout = TextNode.asyncLayout(self.titleBadgeTextNode)
let currentItem = self.item
let currentHasBadge = self.labelBadgeNode.image != nil
return { item, params, neighbors in
var rightInset: CGFloat
switch item.disclosureStyle {
case .none:
rightInset = 16.0 + params.rightInset
case .arrow:
rightInset = 34.0 + params.rightInset
case .optionArrows:
rightInset = 34.0 + params.rightInset
}
var updateArrowImage: UIImage?
var updatedTheme: PresentationTheme?
var updatedLabelBadgeImage: UIImage?
var updatedLabelImage: UIImage?
var badgeDiameter: CGFloat = 20.0
var badgeColor: UIColor?
var badgeColorUpdated = false
if case let .badge(color) = item.labelStyle {
if item.label.count > 0 {
badgeColor = color
}
} else if case let .semitransparentBadge(color) = item.labelStyle {
badgeDiameter = 24.0
badgeColor = color.withAlphaComponent(0.1)
badgeColorUpdated = true
if let currentItem = currentItem, case let .semitransparentBadge(previousColor) = currentItem.labelStyle, color.isEqual(previousColor) {
badgeColorUpdated = false
}
}
if case let .color(color) = item.labelStyle {
var updatedColor = true
if let currentItem = currentItem, case let .color(previousColor) = currentItem.labelStyle, color.isEqual(previousColor) {
updatedColor = false
}
if updatedColor {
updatedLabelImage = generateFilledCircleImage(diameter: 17.0, color: color)
}
}
if case let .textWithIcon(image) = item.labelStyle {
updatedLabelImage = generateTintedImage(image: image, color: item.presentationData.theme.list.itemSecondaryTextColor)
} else if case let .image(image, _) = item.labelStyle {
updatedLabelImage = image
}
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
switch item.disclosureStyle {
case .none, .arrow:
updateArrowImage = PresentationResourcesItemList.disclosureArrowImage(item.presentationData.theme)
case .optionArrows:
updateArrowImage = PresentationResourcesItemList.disclosureOptionArrowsImage(item.presentationData.theme)
}
if let badgeColor = badgeColor {
updatedLabelBadgeImage = generateStretchableFilledCircleImage(diameter: badgeDiameter, color: badgeColor)
}
} else if let badgeColor = badgeColor, !currentHasBadge || badgeColorUpdated {
updatedLabelBadgeImage = generateStretchableFilledCircleImage(diameter: badgeDiameter, color: badgeColor)
}
var updateIcon = false
if currentItem?.icon != item.icon {
updateIcon = true
}
let contentSize: CGSize
var insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
let separatorRightInset: CGFloat = item.systemStyle == .glass ? 16.0 : 0.0
let itemBackgroundColor: UIColor
let itemSeparatorColor: UIColor
var leftInset = 16.0 + params.leftInset
if item.icon != nil {
leftInset += item.noInsets ? 49.0 : 43.0
} else if item.iconPeer != nil {
leftInset += 46.0
}
var additionalTextRightInset: CGFloat = 0.0
switch item.labelStyle {
case .badge, .semitransparentBadge:
additionalTextRightInset += 44.0
default:
break
}
let titleColor: UIColor
if item.enabled {
titleColor = item.titleColor == .accent ? item.presentationData.theme.list.itemAccentColor : item.presentationData.theme.list.itemPrimaryTextColor
} else {
titleColor = item.presentationData.theme.list.itemDisabledTextColor
}
let titleFont: UIFont
let defaultLabelFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize)
switch item.titleFont {
case .regular:
titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize)
case .bold:
titleFont = Font.medium(item.presentationData.fontSize.itemListBaseFontSize)
}
var maxTitleWidth: CGFloat = params.width - params.rightInset - 20.0 - leftInset - additionalTextRightInset
if item.iconPeer != nil {
maxTitleWidth -= 12.0
}
var titleBadgeTextNodeLayout: (TextNodeLayout, () -> TextNode)?
if let titleBadge = item.titleBadge {
let titleBadgeTextNodeLayoutValue = makeTitleBadgeTextNodeLayout(TextNodeLayoutArguments(attributedString: item.attributedTitle ?? NSAttributedString(string: titleBadge, font: Font.medium(11.0), textColor: item.presentationData.theme.list.itemCheckColors.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
titleBadgeTextNodeLayout = titleBadgeTextNodeLayoutValue
maxTitleWidth -= 5.0 + titleBadgeTextNodeLayoutValue.0.size.width
}
let titleArguments = TextNodeLayoutArguments(attributedString: item.attributedTitle ?? NSAttributedString(string: item.title, font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: item.attributedTitle != nil ? 0 : 1, truncationType: .end, constrainedSize: CGSize(width: maxTitleWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())
let (titleLayoutAndApply) = item.context == nil ? makeTitleLayout(titleArguments) : nil
let (titleWithEntitiesLayoutAndApply) = item.context != nil ? makeTitleWithEntitiesLayout(titleArguments) : nil
let titleLayout: TextNodeLayout = (titleWithEntitiesLayoutAndApply?.0 ?? titleLayoutAndApply?.0)!
let detailFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0))
let labelFont: UIFont
let labelBadgeColor: UIColor
var labelConstrain: CGFloat = params.width - params.rightInset - leftInset - 40.0 - titleLayout.size.width - 10.0
if item.iconPeer != nil {
labelConstrain -= 6.0
}
switch item.labelStyle {
case .badge:
labelBadgeColor = item.presentationData.theme.list.itemCheckColors.foregroundColor
labelFont = badgeFont
case let .semitransparentBadge(color):
labelBadgeColor = color
labelFont = boldBadgeFont
case .detailText, .multilineDetailText:
labelBadgeColor = item.presentationData.theme.list.itemSecondaryTextColor
labelFont = detailFont
labelConstrain = params.width - params.rightInset - 40.0 - leftInset
case let .coloredText(color):
labelBadgeColor = color
labelFont = defaultLabelFont
default:
labelBadgeColor = item.presentationData.theme.list.itemSecondaryTextColor
labelFont = defaultLabelFont
}
var multilineLabel = false
if case .multilineDetailText = item.labelStyle {
multilineLabel = true
}
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: item.attributedLabel ?? NSAttributedString(string: item.label, font: labelFont, textColor: labelBadgeColor), backgroundColor: nil, maximumNumberOfLines: multilineLabel ? 0 : 1, truncationType: .end, constrainedSize: CGSize(width: labelConstrain, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
var additionalDetailLabelInfo: (TextNodeLayout, () -> TextNode)?
if let additionalDetailLabel = item.additionalDetailLabel {
var detailRightInset: CGFloat = 20.0 + params.rightInset + additionalTextRightInset
if labelLayout.size.width != 0 {
detailRightInset += labelLayout.size.width + 7.0
}
let additionalDetailColor: UIColor
switch item.additionalDetailLabelColor {
case .generic:
additionalDetailColor = item.presentationData.theme.list.itemSecondaryTextColor
case .constructive:
additionalDetailColor = item.presentationData.theme.list.itemDisclosureActions.constructive.fillColor
case .destructive:
additionalDetailColor = item.presentationData.theme.list.itemDestructiveColor
}
additionalDetailLabelInfo = makeAdditionalDetailLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: additionalDetailLabel, font: detailFont, textColor: additionalDetailColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - detailRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
}
let verticalInset: CGFloat
if item.iconPeer != nil {
verticalInset = 6.0
} else {
switch item.systemStyle {
case .glass:
if !item.label.isEmpty {
switch item.labelStyle {
case .detailText, .multilineDetailText:
verticalInset = 13.0
default:
if let additionalDetailLabel = item.additionalDetailLabel, !additionalDetailLabel.isEmpty {
verticalInset = 13.0
} else {
verticalInset = 15.0
}
}
} else if let additionalDetailLabel = item.additionalDetailLabel, !additionalDetailLabel.isEmpty {
verticalInset = 13.0
} else {
verticalInset = 15.0
}
case .legacy:
verticalInset = 11.0
}
}
let titleSpacing: CGFloat = 1.0
var height: CGFloat
switch item.labelStyle {
case .detailText, .multilineDetailText:
height = verticalInset * 2.0 + titleLayout.size.height + titleSpacing + labelLayout.size.height
default:
height = verticalInset * 2.0 + titleLayout.size.height
}
if let additionalDetailLabelInfo = additionalDetailLabelInfo {
height += titleSpacing + additionalDetailLabelInfo.0.size.height
}
if item.iconPeer != nil {
height = max(height, 40.0 + verticalInset * 2.0)
}
switch item.style {
case .plain:
itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor
contentSize = CGSize(width: params.width, height: height)
insets = itemListNeighborsPlainInsets(neighbors)
if item.noInsets {
insets.top = 0.0
insets.bottom = 0.0
}
case .blocks:
itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor
contentSize = CGSize(width: params.width, height: height)
insets = itemListNeighborsGroupedInsets(neighbors, params)
}
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
strongSelf.activateArea.accessibilityLabel = item.title
strongSelf.activateArea.accessibilityValue = item.label
if item.enabled {
strongSelf.activateArea.accessibilityTraits = [.button]
} else {
strongSelf.activateArea.accessibilityTraits = [.button, .notEnabled]
}
if let icon = item.icon {
if strongSelf.iconNode.supernode == nil {
strongSelf.addSubnode(strongSelf.iconNode)
}
if updateIcon {
strongSelf.iconNode.image = icon
}
let iconY: CGFloat
if case .multilineDetailText = item.labelStyle {
iconY = 14.0
} else {
iconY = floor((layout.contentSize.height - icon.size.height) / 2.0)
}
strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: params.leftInset + floor((leftInset - params.leftInset - icon.size.width) / 2.0), y: iconY), size: icon.size)
} else if strongSelf.iconNode.supernode != nil {
strongSelf.iconNode.image = nil
strongSelf.iconNode.removeFromSupernode()
}
if let context = item.context, let iconPeer = item.iconPeer {
let avatarNode: AvatarNode
if let current = strongSelf.avatarNode {
avatarNode = current
} else {
avatarNode = AvatarNode(font: avatarFont)
strongSelf.avatarNode = avatarNode
strongSelf.addSubnode(avatarNode)
}
let avatarSize: CGFloat = 40.0
avatarNode.frame = CGRect(origin: CGPoint(x: params.leftInset + floor((leftInset - params.leftInset - avatarSize) / 2.0), y: floor((height - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize))
var clipStyle: AvatarNodeClipStyle = .round
if case let .channel(channel) = iconPeer, channel.isForumOrMonoForum {
clipStyle = .roundedRect
}
var overrideImage: AvatarNodeImageOverride?
if iconPeer.id == context.account.peerId {
overrideImage = .savedMessagesIcon
}
avatarNode.setPeer(context: context, theme: item.presentationData.theme, peer: iconPeer, overrideImage: overrideImage, clipStyle: clipStyle)
} else if let avatarNode = strongSelf.avatarNode {
strongSelf.avatarNode = nil
avatarNode.removeFromSupernode()
}
if let updateArrowImage = updateArrowImage {
strongSelf.arrowNode.image = updateArrowImage
}
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
}
if let titleWithEntitiesApply = titleWithEntitiesLayoutAndApply?.1, let context = item.context {
let _ = titleWithEntitiesApply(
TextNodeWithEntities.Arguments(
context: context,
cache: context.animationCache,
renderer: context.animationRenderer,
placeholderColor: item.presentationData.theme.chat.inputPanel.inputTextColor.withAlphaComponent(0.12),
attemptSynchronous: false
)
)
} else if let titleApply = titleLayoutAndApply?.1 {
let _ = titleApply()
}
let _ = labelApply()
switch item.style {
case .plain:
if strongSelf.backgroundNode.supernode != nil {
strongSelf.backgroundNode.removeFromSupernode()
}
if strongSelf.topStripeNode.supernode != nil {
strongSelf.topStripeNode.removeFromSupernode()
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0)
}
if strongSelf.maskNode.supernode != nil {
strongSelf.maskNode.removeFromSupernode()
}
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight))
case .blocks:
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - params.rightInset - bottomStripeInset - separatorRightInset, height: separatorHeight))
}
var centralContentHeight: CGFloat = titleLayout.size.height
switch item.labelStyle {
case .detailText, .multilineDetailText:
centralContentHeight += titleSpacing
centralContentHeight += labelLayout.size.height
default:
break
}
if let additionalDetailLabelInfo {
centralContentHeight += titleSpacing
centralContentHeight += additionalDetailLabelInfo.0.size.height
}
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - centralContentHeight) / 2.0)), size: titleLayout.size)
strongSelf.titleNode.textNode.frame = titleFrame
if let updateBadgeImage = updatedLabelBadgeImage {
if strongSelf.labelBadgeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.labelBadgeNode, belowSubnode: strongSelf.labelNode)
}
strongSelf.labelBadgeNode.image = updateBadgeImage
}
if badgeColor == nil && strongSelf.labelBadgeNode.supernode != nil {
strongSelf.labelBadgeNode.image = nil
strongSelf.labelBadgeNode.removeFromSupernode()
}
var badgeWidth = max(badgeDiameter, labelLayout.size.width + 10.0)
if case .semitransparentBadge = item.labelStyle {
badgeWidth += 2.0
}
let badgeFrame = CGRect(origin: CGPoint(x: params.width - rightInset - badgeWidth, y: floor((contentSize.height - badgeDiameter) / 2.0)), size: CGSize(width: badgeWidth, height: badgeDiameter))
strongSelf.labelBadgeNode.frame = badgeFrame
let labelFrame: CGRect
switch item.labelStyle {
case .badge:
labelFrame = CGRect(origin: CGPoint(x: params.width - rightInset - badgeWidth + (badgeWidth - labelLayout.size.width) / 2.0, y: badgeFrame.minY + 1.0), size: labelLayout.size)
case .semitransparentBadge:
labelFrame = CGRect(origin: CGPoint(x: params.width - rightInset - badgeWidth + (badgeWidth - labelLayout.size.width) / 2.0, y: badgeFrame.minY + 1.0 - UIScreenPixel + floorToScreenPixels((badgeDiameter - labelLayout.size.height) / 2.0)), size: labelLayout.size)
case .detailText, .multilineDetailText:
labelFrame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + titleSpacing), size: labelLayout.size)
default:
labelFrame = CGRect(origin: CGPoint(x: params.width - rightInset - labelLayout.size.width, y: floor((height - labelLayout.size.height) / 2.0)), size: labelLayout.size)
}
strongSelf.labelNode.frame = labelFrame
if let additionalDetailLabelInfo = additionalDetailLabelInfo {
let additionalDetailLabelNode = additionalDetailLabelInfo.1()
if strongSelf.additionalDetailLabelNode !== additionalDetailLabelNode {
strongSelf.additionalDetailLabelNode?.removeFromSupernode()
strongSelf.additionalDetailLabelNode = additionalDetailLabelNode
strongSelf.addSubnode(additionalDetailLabelNode)
}
additionalDetailLabelNode.frame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + titleSpacing), size: additionalDetailLabelInfo.0.size)
} else if let additionalDetailLabelNode = strongSelf.additionalDetailLabelNode {
strongSelf.additionalDetailLabelNode = nil
additionalDetailLabelNode.removeFromSupernode()
}
if let (badgeTextLayout, badgeTextApply) = titleBadgeTextNodeLayout {
let titleBadgeNode: ASImageNode
if let current = strongSelf.titleBadgeNode {
titleBadgeNode = current
} else {
titleBadgeNode = ASImageNode()
strongSelf.titleBadgeNode = titleBadgeNode
strongSelf.addSubnode(titleBadgeNode)
titleBadgeNode.image = generateFilledRoundedRectImage(size: CGSize(width: 16.0, height: 16.0), cornerRadius: 5.0, color: item.presentationData.theme.list.itemCheckColors.fillColor)?.stretchableImage(withLeftCapWidth: 6, topCapHeight: 6)
}
let titleBadgeTextNode = badgeTextApply()
if titleBadgeTextNode.supernode == nil {
strongSelf.addSubnode(titleBadgeTextNode)
}
let badgeSideInset: CGFloat = 5.0
let badgeVerticalInset: CGFloat = 2.0
let badgeSize = CGSize(width: badgeTextLayout.size.width + badgeSideInset * 2.0, height: badgeTextLayout.size.height + badgeVerticalInset * 2.0)
let titleBadgeFrame = CGRect(origin: CGPoint(x: titleFrame.maxX + 5.0, y: titleFrame.minY + floorToScreenPixels((titleFrame.height - badgeSize.height) * 0.5)), size: badgeSize)
let titleBadgeTextFrame = CGRect(origin: CGPoint(x: titleBadgeFrame.minX + badgeSideInset, y: titleBadgeFrame.minY + badgeVerticalInset), size: badgeTextLayout.size)
titleBadgeNode.frame = titleBadgeFrame
titleBadgeTextNode.frame = titleBadgeTextFrame
} else {
if let titleBadgeTextNode = strongSelf.titleBadgeTextNode {
strongSelf.titleBadgeTextNode = nil
titleBadgeTextNode.removeFromSupernode()
}
if let titleBadgeNode = strongSelf.titleBadgeNode {
strongSelf.titleBadgeNode = nil
titleBadgeNode.removeFromSupernode()
}
}
if let titleIcon = item.titleIcon {
if strongSelf.titleIconNode.supernode == nil {
strongSelf.addSubnode(strongSelf.titleIconNode)
}
strongSelf.titleIconNode.image = titleIcon
strongSelf.titleIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.maxX + 5.0, y: floor((layout.contentSize.height - titleIcon.size.height) / 2.0) - 1.0), size: titleIcon.size)
} else {
if strongSelf.titleIconNode.supernode != nil {
strongSelf.titleIconNode.removeFromSupernode()
}
}
if case .textWithIcon = item.labelStyle {
if let updatedLabelImage = updatedLabelImage {
strongSelf.labelImageNode.image = updatedLabelImage
}
if strongSelf.labelImageNode.supernode == nil {
strongSelf.addSubnode(strongSelf.labelImageNode)
}
if let size = strongSelf.labelImageNode.image?.size {
strongSelf.labelImageNode.frame = CGRect(origin: CGPoint(x: labelFrame.minX - size.width - 5.0, y: floor((layout.contentSize.height - size.height) / 2.0) - 1.0), size: size)
}
} else if case let .image(_, size) = item.labelStyle {
if let updatedLabelImage = updatedLabelImage {
strongSelf.labelImageNode.image = updatedLabelImage
}
if strongSelf.labelImageNode.supernode == nil {
strongSelf.addSubnode(strongSelf.labelImageNode)
}
strongSelf.labelImageNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - size.width - 30.0, y: floor((layout.contentSize.height - size.height) / 2.0)), size: size)
} else if case .color = item.labelStyle {
if let updatedLabelImage = updatedLabelImage {
strongSelf.labelImageNode.image = updatedLabelImage
}
if strongSelf.labelImageNode.supernode == nil {
strongSelf.addSubnode(strongSelf.labelImageNode)
}
if let image = strongSelf.labelImageNode.image {
strongSelf.labelImageNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 50.0, y: floor((layout.contentSize.height - image.size.height) / 2.0)), size: image.size)
}
} else if strongSelf.labelImageNode.supernode != nil {
strongSelf.labelImageNode.removeFromSupernode()
strongSelf.labelImageNode.image = nil
}
if let arrowImage = strongSelf.arrowNode.image {
let arrowRightOffset: CGFloat
switch item.disclosureStyle {
case .optionArrows:
arrowRightOffset = 18.0
case .none, .arrow:
arrowRightOffset = 7.0
}
strongSelf.arrowNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - arrowRightOffset - arrowImage.size.width, y: floorToScreenPixels((height - arrowImage.size.height) / 2.0)), size: arrowImage.size)
}
switch item.disclosureStyle {
case .none:
strongSelf.arrowNode.isHidden = true
case .arrow, .optionArrows:
strongSelf.arrowNode.isHidden = false
}
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: height + UIScreenPixel))
if let shimmeringIndex = item.shimmeringIndex {
let shimmerNode: ShimmerEffectNode
if let current = strongSelf.placeholderNode {
shimmerNode = current
} else {
shimmerNode = ShimmerEffectNode()
strongSelf.placeholderNode = shimmerNode
if strongSelf.backgroundNode.supernode != nil {
strongSelf.insertSubnode(shimmerNode, aboveSubnode: strongSelf.backgroundNode)
} else {
strongSelf.addSubnode(shimmerNode)
}
}
shimmerNode.frame = CGRect(origin: CGPoint(), size: contentSize)
if let (rect, size) = strongSelf.absoluteLocation {
shimmerNode.updateAbsoluteRect(rect, within: size)
}
var shapes: [ShimmerEffectNode.Shape] = []
let titleLineWidth: CGFloat = (shimmeringIndex % 2 == 0) ? 120.0 : 80.0
let lineDiameter: CGFloat = 8.0
let titleFrame = strongSelf.titleNode.textNode.frame
shapes.append(.roundedRectLine(startPoint: CGPoint(x: titleFrame.minX, y: titleFrame.minY + floor((titleFrame.height - lineDiameter) / 2.0)), width: titleLineWidth, diameter: lineDiameter))
shimmerNode.update(backgroundColor: item.presentationData.theme.list.itemBlocksBackgroundColor, foregroundColor: item.presentationData.theme.list.mediaPlaceholderColor, shimmeringColor: item.presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: shapes, size: contentSize)
} else if let shimmerNode = strongSelf.placeholderNode {
strongSelf.placeholderNode = nil
shimmerNode.removeFromSupernode()
}
}
})
}
}
override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
super.setHighlighted(highlighted, at: point, animated: animated)
if highlighted && (self.item?.enabled ?? false) {
self.highlightedBackgroundNode.alpha = 1.0
if self.highlightedBackgroundNode.supernode == nil {
var anchorNode: ASDisplayNode?
if self.bottomStripeNode.supernode != nil {
anchorNode = self.bottomStripeNode
} else if self.topStripeNode.supernode != nil {
anchorNode = self.topStripeNode
} else if self.backgroundNode.supernode != nil {
anchorNode = self.backgroundNode
}
if let anchorNode = anchorNode {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
} else {
self.addSubnode(self.highlightedBackgroundNode)
}
}
} else {
if self.highlightedBackgroundNode.supernode != nil {
if animated {
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
if let strongSelf = self {
if completed {
strongSelf.highlightedBackgroundNode.removeFromSupernode()
}
}
})
self.highlightedBackgroundNode.alpha = 0.0
} else {
self.highlightedBackgroundNode.removeFromSupernode()
}
}
}
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override public func animateAdded(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}
@@ -0,0 +1,531 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
public final class ItemListRevealOptionsGestureRecognizer: UIPanGestureRecognizer {
public var validatedGesture = false
public var firstLocation: CGPoint = CGPoint()
public var allowAnyDirection = false
public var lastVelocity: CGPoint = CGPoint()
override public init(target: Any?, action: Selector?) {
super.init(target: target, action: action)
if #available(iOS 13.4, *) {
self.allowedScrollTypesMask = .continuous
}
self.maximumNumberOfTouches = 1
}
override public func reset() {
super.reset()
self.validatedGesture = false
}
public func becomeCancelled() {
self.state = .cancelled
}
override public func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
let touch = touches.first!
self.firstLocation = touch.location(in: self.view)
}
override public func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
let location = touches.first!.location(in: self.view)
let translation = CGPoint(x: location.x - self.firstLocation.x, y: location.y - self.firstLocation.y)
if !self.validatedGesture {
if !self.allowAnyDirection && translation.x > 0.0 {
self.state = .failed
} else if abs(translation.y) > 4.0 && abs(translation.y) > abs(translation.x) * 2.5 {
self.state = .failed
} else if abs(translation.x) > 4.0 && abs(translation.y) * 2.5 < abs(translation.x) {
self.validatedGesture = true
}
}
if self.validatedGesture {
self.lastVelocity = self.velocity(in: self.view)
super.touchesMoved(touches, with: event)
}
}
}
open class ItemListRevealOptionsItemNode: ListViewItemNode, ASGestureRecognizerDelegate {
private var validLayout: (CGSize, CGFloat, CGFloat)?
private var leftRevealNode: ItemListRevealOptionsNode?
private var rightRevealNode: ItemListRevealOptionsNode?
private var revealOptions: (left: [ItemListRevealOption], right: [ItemListRevealOption]) = ([], [])
private var enableAnimations: Bool = true
private var initialRevealOffset: CGFloat = 0.0
public private(set) var revealOffset: CGFloat = 0.0
private var recognizer: ItemListRevealOptionsGestureRecognizer?
private var tapRecognizer: UITapGestureRecognizer?
private var hapticFeedback: HapticFeedback?
private var allowAnyDirection = false
public var isDisplayingRevealedOptions: Bool {
return !self.revealOffset.isZero
}
override open var canBeSelected: Bool {
return !self.isDisplayingRevealedOptions
}
override public init(layerBacked: Bool, dynamicBounce: Bool, rotated: Bool, seeThrough: Bool) {
super.init(layerBacked: layerBacked, dynamicBounce: dynamicBounce, rotated: rotated, seeThrough: seeThrough)
}
open var controlsContainer: ASDisplayNode {
return self
}
override open func didLoad() {
super.didLoad()
let recognizer = ItemListRevealOptionsGestureRecognizer(target: self, action: #selector(self.revealGesture(_:)))
self.recognizer = recognizer
recognizer.delegate = self.wrappedGestureRecognizerDelegate
recognizer.allowAnyDirection = self.allowAnyDirection
self.view.addGestureRecognizer(recognizer)
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.revealTapGesture(_:)))
self.tapRecognizer = tapRecognizer
tapRecognizer.delegate = self.wrappedGestureRecognizerDelegate
self.view.addGestureRecognizer(tapRecognizer)
self.view.disablesInteractiveTransitionGestureRecognizer = self.allowAnyDirection
self.view.disablesInteractiveTransitionGestureRecognizerNow = { [weak self] in
guard let strongSelf = self else {
return false
}
if !strongSelf.revealOffset.isZero {
return true
}
return false
}
}
open func setRevealOptions(_ options: (left: [ItemListRevealOption], right: [ItemListRevealOption]), enableAnimations: Bool = true) {
if self.revealOptions == options {
return
}
let previousOptions = self.revealOptions
let wasEmpty = self.revealOptions.left.isEmpty && self.revealOptions.right.isEmpty
self.revealOptions = options
self.enableAnimations = enableAnimations
let isEmpty = options.left.isEmpty && options.right.isEmpty
if options.left.isEmpty {
if let _ = self.leftRevealNode {
self.recognizer?.becomeCancelled()
self.updateRevealOffsetInternal(offset: 0.0, transition: .animated(duration: 0.3, curve: .spring))
}
} else if previousOptions.left != options.left {
/*if let _ = self.leftRevealNode {
self.revealOptionsInteractivelyClosed()
self.recognizer?.becomeCancelled()
self.updateRevealOffsetInternal(offset: 0.0, transition: .animated(duration: 0.3, curve: .spring))
}*/
}
if options.right.isEmpty {
if let _ = self.rightRevealNode {
self.recognizer?.becomeCancelled()
self.updateRevealOffsetInternal(offset: 0.0, transition: .animated(duration: 0.3, curve: .spring))
}
} else if previousOptions.right != options.right {
if let _ = self.rightRevealNode {
/*self.revealOptionsInteractivelyClosed()
self.recognizer?.becomeCancelled()
self.updateRevealOffsetInternal(offset: 0.0, transition: .animated(duration: 0.3, curve: .spring))*/
}
}
if wasEmpty != isEmpty {
self.recognizer?.isEnabled = !isEmpty
}
let allowAnyDirection = !options.left.isEmpty || !self.revealOffset.isZero
if allowAnyDirection != self.allowAnyDirection {
self.allowAnyDirection = allowAnyDirection
self.recognizer?.allowAnyDirection = allowAnyDirection
if self.isNodeLoaded {
self.view.disablesInteractiveTransitionGestureRecognizer = allowAnyDirection
}
}
}
override open func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if let recognizer = self.recognizer, gestureRecognizer == self.tapRecognizer {
return abs(self.revealOffset) > 0.0 && !recognizer.validatedGesture
} else if let recognizer = self.recognizer, gestureRecognizer == self.recognizer, recognizer.numberOfTouches == 0 {
let translation = recognizer.velocity(in: recognizer.view)
if abs(translation.y) > 4.0 && abs(translation.y) > abs(translation.x) * 2.5 {
return false
}
}
return true
}
open func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if let recognizer = self.recognizer, otherGestureRecognizer == recognizer {
return true
} else {
return false
}
}
open func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
/*if gestureRecognizer === self.recognizer && otherGestureRecognizer is InteractiveTransitionGestureRecognizer {
return true
}*/
return false
}
@objc private func revealTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.updateRevealOffsetInternal(offset: 0.0, transition: .animated(duration: 0.3, curve: .spring))
self.revealOptionsInteractivelyClosed()
}
}
@objc private func revealGesture(_ recognizer: ItemListRevealOptionsGestureRecognizer) {
guard let (size, _, _) = self.validLayout else {
return
}
switch recognizer.state {
case .began:
if let leftRevealNode = self.leftRevealNode {
let revealSize = leftRevealNode.bounds.size
let location = recognizer.location(in: self.view)
if location.x < revealSize.width {
recognizer.becomeCancelled()
} else {
self.initialRevealOffset = self.revealOffset
}
} else if let rightRevealNode = self.rightRevealNode {
let revealSize = rightRevealNode.bounds.size
let location = recognizer.location(in: self.view)
if location.x > size.width - revealSize.width {
recognizer.becomeCancelled()
} else {
self.initialRevealOffset = self.revealOffset
}
} else {
if self.revealOptions.left.isEmpty && self.revealOptions.right.isEmpty {
recognizer.becomeCancelled()
}
self.initialRevealOffset = self.revealOffset
}
case .changed:
var translation = recognizer.translation(in: self.view)
translation.x += self.initialRevealOffset
if self.revealOptions.left.isEmpty {
translation.x = min(0.0, translation.x)
}
if self.leftRevealNode == nil && CGFloat(0.0).isLess(than: translation.x) {
self.setupAndAddLeftRevealNode()
self.revealOptionsInteractivelyOpened()
} else if self.rightRevealNode == nil && translation.x.isLess(than: 0.0) {
self.setupAndAddRightRevealNode()
self.revealOptionsInteractivelyOpened()
}
self.updateRevealOffsetInternal(offset: translation.x, transition: .immediate)
if self.leftRevealNode == nil && self.rightRevealNode == nil {
self.revealOptionsInteractivelyClosed()
}
case .ended, .cancelled:
guard let recognizer = self.recognizer else {
break
}
if let leftRevealNode = self.leftRevealNode {
let velocity = recognizer.velocity(in: self.view)
let revealSize = leftRevealNode.bounds.size
var reveal = false
if abs(velocity.x) < 100.0 {
if self.initialRevealOffset.isZero && self.revealOffset > 0.0 {
reveal = true
} else if self.revealOffset > revealSize.width {
reveal = true
} else {
reveal = false
}
} else {
if velocity.x > 0.0 {
reveal = true
} else {
reveal = false
}
}
var selectedOption: ItemListRevealOption?
if reveal && leftRevealNode.isDisplayingExtendedAction() {
reveal = false
selectedOption = self.revealOptions.left.first
} else {
self.updateRevealOffsetInternal(offset: reveal ?revealSize.width : 0.0, transition: .animated(duration: 0.3, curve: .spring))
}
if let selectedOption = selectedOption {
self.revealOptionSelected(selectedOption, animated: true)
} else {
if !reveal {
self.revealOptionsInteractivelyClosed()
}
}
} else if let rightRevealNode = self.rightRevealNode {
let velocity = recognizer.velocity(in: self.view)
let revealSize = rightRevealNode.bounds.size
var reveal = false
if abs(velocity.x) < 100.0 {
if self.initialRevealOffset.isZero && self.revealOffset < 0.0 {
reveal = true
} else if self.revealOffset < -revealSize.width {
reveal = true
} else {
reveal = false
}
} else {
if velocity.x < 0.0 {
reveal = true
} else {
reveal = false
}
}
var selectedOption: ItemListRevealOption?
if reveal && rightRevealNode.isDisplayingExtendedAction() {
reveal = false
selectedOption = self.revealOptions.right.last
} else {
self.updateRevealOffsetInternal(offset: reveal ? -revealSize.width : 0.0, transition: .animated(duration: 0.3, curve: .spring))
}
if let selectedOption = selectedOption {
self.revealOptionSelected(selectedOption, animated: true)
} else {
if !reveal {
self.revealOptionsInteractivelyClosed()
}
}
}
default:
break
}
}
private func setupAndAddLeftRevealNode() {
if !self.revealOptions.left.isEmpty {
let revealNode = ItemListRevealOptionsNode(optionSelected: { [weak self] option in
self?.revealOptionSelected(option, animated: false)
}, tapticAction: { [weak self] in
self?.hapticImpact()
})
revealNode.setOptions(self.revealOptions.left, isLeft: true, enableAnimations: self.enableAnimations)
self.leftRevealNode = revealNode
if let (size, leftInset, _) = self.validLayout {
var revealSize = revealNode.measure(CGSize(width: CGFloat.greatestFiniteMagnitude, height: size.height))
revealSize.width += leftInset
revealNode.frame = CGRect(origin: CGPoint(x: min(self.revealOffset - revealSize.width, 0.0), y: 0.0), size: revealSize)
revealNode.updateRevealOffset(offset: 0.0, sideInset: leftInset, transition: .immediate)
}
self.controlsContainer.addSubnode(revealNode)
}
}
private func setupAndAddRightRevealNode() {
if !self.revealOptions.right.isEmpty {
let revealNode = ItemListRevealOptionsNode(optionSelected: { [weak self] option in
self?.revealOptionSelected(option, animated: false)
}, tapticAction: { [weak self] in
self?.hapticImpact()
})
revealNode.setOptions(self.revealOptions.right, isLeft: false, enableAnimations: self.enableAnimations)
self.rightRevealNode = revealNode
if let (size, _, rightInset) = self.validLayout {
var revealSize = revealNode.measure(CGSize(width: CGFloat.greatestFiniteMagnitude, height: size.height))
revealSize.width += rightInset
revealNode.frame = CGRect(origin: CGPoint(x: size.width + max(self.revealOffset, -revealSize.width), y: 0.0), size: revealSize)
revealNode.updateRevealOffset(offset: 0.0, sideInset: -rightInset, transition: .immediate)
}
self.controlsContainer.addSubnode(revealNode)
}
}
public func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) {
self.validLayout = (size, leftInset, rightInset)
if let leftRevealNode = self.leftRevealNode {
var revealSize = leftRevealNode.measure(CGSize(width: CGFloat.greatestFiniteMagnitude, height: size.height))
revealSize.width += leftInset
leftRevealNode.frame = CGRect(origin: CGPoint(x: min(self.revealOffset - revealSize.width, 0.0), y: 0.0), size: revealSize)
}
if let rightRevealNode = self.rightRevealNode {
var revealSize = rightRevealNode.measure(CGSize(width: CGFloat.greatestFiniteMagnitude, height: size.height))
revealSize.width += rightInset
rightRevealNode.frame = CGRect(origin: CGPoint(x: size.width + max(self.revealOffset, -revealSize.width), y: 0.0), size: revealSize)
}
}
open func updateRevealOffsetInternal(offset: CGFloat, transition: ContainedViewLayoutTransition, completion: (() -> Void)? = nil) {
self.revealOffset = offset
guard let (size, leftInset, rightInset) = self.validLayout else {
return
}
var leftRevealCompleted = true
var rightRevealCompleted = true
let intermediateCompletion = {
if leftRevealCompleted && rightRevealCompleted {
completion?()
}
}
if let leftRevealNode = self.leftRevealNode {
leftRevealCompleted = false
let revealSize = leftRevealNode.bounds.size
let revealFrame = CGRect(origin: CGPoint(x: min(self.revealOffset - revealSize.width, 0.0), y: 0.0), size: revealSize)
//let revealNodeOffset = max(-self.revealOffset, revealSize.width)
let revealNodeOffset = -self.revealOffset
leftRevealNode.updateRevealOffset(offset: revealNodeOffset, sideInset: leftInset, transition: transition)
if CGFloat(offset).isLessThanOrEqualTo(0.0) {
self.leftRevealNode = nil
transition.updateFrame(node: leftRevealNode, frame: revealFrame, completion: { [weak leftRevealNode] _ in
leftRevealNode?.removeFromSupernode()
leftRevealCompleted = true
intermediateCompletion()
})
} else {
transition.updateFrame(node: leftRevealNode, frame: revealFrame, completion: { _ in
leftRevealCompleted = true
intermediateCompletion()
})
}
}
if let rightRevealNode = self.rightRevealNode {
rightRevealCompleted = false
let revealSize = rightRevealNode.bounds.size
let revealFrame = CGRect(origin: CGPoint(x: min(size.width, size.width + self.revealOffset), y: 0.0), size: revealSize)
let revealNodeOffset = -self.revealOffset
rightRevealNode.updateRevealOffset(offset: revealNodeOffset, sideInset: -rightInset, transition: transition)
if CGFloat(0.0).isLessThanOrEqualTo(offset) {
self.rightRevealNode = nil
transition.updateFrame(node: rightRevealNode, frame: revealFrame, completion: { [weak rightRevealNode] _ in
rightRevealNode?.removeFromSupernode()
rightRevealCompleted = true
intermediateCompletion()
})
} else {
transition.updateFrame(node: rightRevealNode, frame: revealFrame, completion: { _ in
rightRevealCompleted = true
intermediateCompletion()
})
}
}
let allowAnyDirection = !self.revealOptions.left.isEmpty || !offset.isZero
if allowAnyDirection != self.allowAnyDirection {
self.allowAnyDirection = allowAnyDirection
self.recognizer?.allowAnyDirection = allowAnyDirection
self.view.disablesInteractiveTransitionGestureRecognizer = allowAnyDirection
}
self.updateRevealOffset(offset: offset, transition: transition)
}
open func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
}
open func revealOptionsInteractivelyOpened() {
}
open func revealOptionsInteractivelyClosed() {
}
open func setRevealOptionsOpened(_ value: Bool, animated: Bool) {
if value != !self.revealOffset.isZero {
if !self.revealOffset.isZero {
self.recognizer?.becomeCancelled()
}
let transition: ContainedViewLayoutTransition
if animated {
transition = .animated(duration: 0.3, curve: .spring)
} else {
transition = .immediate
}
if value {
if self.rightRevealNode == nil {
self.setupAndAddRightRevealNode()
if let rightRevealNode = self.rightRevealNode, rightRevealNode.isNodeLoaded, let _ = self.validLayout {
rightRevealNode.layout()
let revealSize = rightRevealNode.bounds.size
self.updateRevealOffsetInternal(offset: -revealSize.width, transition: transition)
}
}
} else if !self.revealOffset.isZero {
self.updateRevealOffsetInternal(offset: 0.0, transition: transition)
}
}
}
open func animateRevealOptionsFill(completion: (() -> Void)? = nil) {
if let validLayout = self.validLayout {
self.layer.allowsGroupOpacity = true
self.updateRevealOffsetInternal(offset: -validLayout.0.width - 74.0, transition: .animated(duration: 0.3, curve: .spring), completion: {
self.layer.allowsGroupOpacity = false
completion?()
})
}
}
open func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) {
}
override open var preventsTouchesToOtherItems: Bool {
return self.isDisplayingRevealedOptions
}
override open func touchesToOtherItemsPrevented() {
if self.isDisplayingRevealedOptions {
self.setRevealOptionsOpened(false, animated: true)
}
}
private func hapticImpact() {
if self.hapticFeedback == nil {
self.hapticFeedback = HapticFeedback()
}
self.hapticFeedback?.impact(.medium)
}
override open func animateFrameTransition(_ progress: CGFloat, _ currentValue: CGFloat) {
super.animateFrameTransition(progress, currentValue)
}
}
@@ -0,0 +1,721 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import SwitchNode
import AppBundle
import CheckNode
public enum ItemListExpandableSwitchItemNodeType {
case regular
case icon
}
public class ItemListExpandableSwitchItem: ListViewItem, ItemListItem {
public struct SubItem: Equatable {
public var id: AnyHashable
public var title: String
public var isSelected: Bool
public var isEnabled: Bool
public init(
id: AnyHashable,
title: String,
isSelected: Bool,
isEnabled: Bool
) {
self.id = id
self.title = title
self.isSelected = isSelected
self.isEnabled = isEnabled
}
}
let presentationData: ItemListPresentationData
let systemStyle: ItemListSystemStyle
let icon: UIImage?
let title: String
let value: Bool
let isExpanded: Bool
let subItems: [SubItem]
let type: ItemListExpandableSwitchItemNodeType
let enableInteractiveChanges: Bool
let enabled: Bool
let displayLocked: Bool
let disableLeadingInset: Bool
let maximumNumberOfLines: Int
let noCorners: Bool
public let sectionId: ItemListSectionId
let style: ItemListStyle
let updated: (Bool) -> Void
let activatedWhileDisabled: () -> Void
let selectAction: () -> Void
let subAction: (SubItem) -> Void
public let tag: ItemListItemTag?
public let selectable: Bool = true
public init(presentationData: ItemListPresentationData, systemStyle: ItemListSystemStyle = .legacy, icon: UIImage? = nil, title: String, value: Bool, isExpanded: Bool, subItems: [SubItem], type: ItemListExpandableSwitchItemNodeType = .regular, enableInteractiveChanges: Bool = true, enabled: Bool = true, displayLocked: Bool = false, disableLeadingInset: Bool = false, maximumNumberOfLines: Int = 1, noCorners: Bool = false, sectionId: ItemListSectionId, style: ItemListStyle, updated: @escaping (Bool) -> Void, activatedWhileDisabled: @escaping () -> Void = {}, selectAction: @escaping () -> Void, subAction: @escaping (SubItem) -> Void, tag: ItemListItemTag? = nil) {
self.presentationData = presentationData
self.systemStyle = systemStyle
self.icon = icon
self.title = title
self.value = value
self.isExpanded = isExpanded
self.subItems = subItems
self.type = type
self.enableInteractiveChanges = enableInteractiveChanges
self.enabled = enabled
self.displayLocked = displayLocked
self.disableLeadingInset = disableLeadingInset
self.maximumNumberOfLines = maximumNumberOfLines
self.noCorners = noCorners
self.sectionId = sectionId
self.style = style
self.updated = updated
self.activatedWhileDisabled = activatedWhileDisabled
self.selectAction = selectAction
self.subAction = subAction
self.tag = tag
}
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ItemListExpandableSwitchItemNode(type: self.type)
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply(ListViewItemUpdateAnimation.None) })
})
}
}
}
public func selected(listView: ListView) {
listView.clearHighlightAnimated(true)
self.selectAction()
}
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? ItemListExpandableSwitchItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply(animation)
})
}
}
}
}
}
}
private final class SubItemNode: HighlightTrackingButtonNode {
private let textNode: ImmediateTextNode
private var checkNode: CheckNode?
private let separatorNode: ASDisplayNode
private var theme: PresentationTheme?
private var item: ItemListExpandableSwitchItem.SubItem?
private var action: ((ItemListExpandableSwitchItem.SubItem) -> Void)?
init() {
self.textNode = ImmediateTextNode()
self.separatorNode = ASDisplayNode()
self.separatorNode.isLayerBacked = true
super.init()
self.addSubnode(self.separatorNode)
self.addSubnode(self.textNode)
self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside)
}
@objc private func pressed() {
guard let item = self.item, item.isEnabled, let action = self.action else {
return
}
action(item)
}
func update(presentationData: ItemListPresentationData, item: ItemListExpandableSwitchItem.SubItem, action: @escaping (ItemListExpandableSwitchItem.SubItem) -> Void, size: CGSize, transition: ContainedViewLayoutTransition) {
let themeUpdated = self.theme !== presentationData.theme
self.item = item
self.action = action
let leftInset: CGFloat = 60.0
let separatorRightInset: CGFloat = 16.0
if themeUpdated {
self.separatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
}
let checkNode: CheckNode
if let current = self.checkNode {
checkNode = current
if themeUpdated {
checkNode.theme = CheckNodeTheme(theme: presentationData.theme, style: .plain)
}
} else {
checkNode = CheckNode(theme: CheckNodeTheme(theme: presentationData.theme, style: .plain))
checkNode.isUserInteractionEnabled = false
self.checkNode = checkNode
self.addSubnode(checkNode)
}
let checkSize = CGSize(width: 22.0, height: 22.0)
checkNode.frame = CGRect(origin: CGPoint(x: floor((leftInset - checkSize.width) / 2.0), y: floor((size.height - checkSize.height) / 2.0)), size: checkSize)
checkNode.setSelected(item.isSelected, animated: transition.isAnimated)
transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: leftInset, y: size.height - UIScreenPixel), size: CGSize(width: size.width - leftInset - separatorRightInset, height: UIScreenPixel)))
self.textNode.attributedText = NSAttributedString(string: item.title, font: Font.regular(17.0), textColor: presentationData.theme.list.itemPrimaryTextColor)
let titleSize = self.textNode.updateLayout(CGSize(width: size.width - leftInset, height: 100.0))
self.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((size.height - titleSize.height) / 2.0)), size: titleSize)
self.alpha = item.isEnabled ? 1.0 : 0.5
}
}
public class ItemListExpandableSwitchItemNode: ListViewItemNode, ItemListItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomTopStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
private let maskNode: ASImageNode
private let iconNode: ASImageNode
private let titleNode: TextNode
private let titleValueNode: TextNode
private let expandArrowNode: ASImageNode
private var switchNode: ASDisplayNode & ItemListSwitchNodeImpl
private let switchGestureNode: ASDisplayNode
private var disabledOverlayNode: ASDisplayNode?
private var lockedIconNode: ASImageNode?
private let subItemContainer: ASDisplayNode
private var subItemNodes: [AnyHashable: SubItemNode] = [:]
private let activateArea: AccessibilityAreaNode
private var item: ItemListExpandableSwitchItem?
public var tag: ItemListItemTag? {
return self.item?.tag
}
public init(type: ItemListExpandableSwitchItemNodeType) {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.backgroundColor = .white
self.maskNode = ASImageNode()
self.maskNode.isUserInteractionEnabled = false
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomTopStripeNode = ASDisplayNode()
self.bottomTopStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.iconNode = ASImageNode()
self.iconNode.isLayerBacked = true
self.iconNode.displaysAsynchronously = false
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
switch type {
case .regular:
self.switchNode = SwitchNode()
case .icon:
self.switchNode = IconSwitchNode()
}
self.titleValueNode = TextNode()
self.titleValueNode.isUserInteractionEnabled = false
self.expandArrowNode = ASImageNode()
self.expandArrowNode.displaysAsynchronously = false
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
self.switchGestureNode = ASDisplayNode()
self.activateArea = AccessibilityAreaNode()
self.subItemContainer = ASDisplayNode()
self.subItemContainer.clipsToBounds = true
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.titleNode)
self.addSubnode(self.titleValueNode)
self.addSubnode(self.expandArrowNode)
self.addSubnode(self.switchNode)
self.addSubnode(self.switchGestureNode)
self.addSubnode(self.activateArea)
self.addSubnode(self.subItemContainer)
self.activateArea.activate = { [weak self] in
guard let strongSelf = self, let item = strongSelf.item, item.enabled else {
return false
}
let value = !strongSelf.switchNode.isOn
if item.enableInteractiveChanges {
strongSelf.switchNode.setOn(value, animated: true)
}
item.updated(value)
return true
}
}
override public func didLoad() {
super.didLoad()
(self.switchNode.view as? UISwitch)?.addTarget(self, action: #selector(self.switchValueChanged(_:)), for: .valueChanged)
self.switchGestureNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
}
func asyncLayout() -> (_ item: ItemListExpandableSwitchItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeTitleValueLayout = TextNode.asyncLayout(self.titleValueNode)
let currentItem = self.item
var currentDisabledOverlayNode = self.disabledOverlayNode
return { item, params, neighbors in
var contentSize: CGSize
var insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
let separatorRightInset: CGFloat = item.systemStyle == .glass ? 16.0 : 0.0
let itemBackgroundColor: UIColor
let itemSeparatorColor: UIColor
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize)
var updatedTheme: PresentationTheme?
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
}
var updateIcon = false
if currentItem?.icon != item.icon {
updateIcon = true
}
switch item.style {
case .plain:
itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor
contentSize = CGSize(width: params.width, height: item.systemStyle == .glass ? 52.0 : 44.0)
insets = itemListNeighborsPlainInsets(neighbors)
case .blocks:
itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor
contentSize = CGSize(width: params.width, height: item.systemStyle == .glass ? 52.0 : 44.0)
insets = itemListNeighborsGroupedInsets(neighbors, params)
}
var leftInset = 16.0 + params.leftInset
if let _ = item.icon {
leftInset += 43.0
}
if item.disableLeadingInset {
insets.top = 0.0
insets.bottom = 0.0
}
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: item.maximumNumberOfLines, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - params.rightInset - 64.0 - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let titleValue = "\(item.subItems.filter(\.isSelected).count)/\(item.subItems.count)"
let (titleValueLayout, titleValueApply) = makeTitleValueLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: titleValue, font: Font.bold(14.0), textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - params.rightInset - 64.0 - titleLayout.size.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let verticalInset: CGFloat
switch item.systemStyle {
case .glass:
verticalInset = 15.0
case .legacy:
verticalInset = 11.0
}
contentSize.height = max(contentSize.height, titleLayout.size.height + verticalInset * 2.0)
let mainContentHeight = contentSize.height
var effectiveSubItemsHeight: CGFloat = 0.0
if item.isExpanded {
effectiveSubItemsHeight = CGFloat(item.subItems.count) * (item.systemStyle == .glass ? 52.0 : 44.0)
}
contentSize.height += effectiveSubItemsHeight
if !item.enabled {
if currentDisabledOverlayNode == nil {
currentDisabledOverlayNode = ASDisplayNode()
}
} else {
currentDisabledOverlayNode = nil
}
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] animation in
if let strongSelf = self {
strongSelf.item = item
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: mainContentHeight))
strongSelf.activateArea.accessibilityLabel = item.title
strongSelf.activateArea.accessibilityValue = item.value ? item.presentationData.strings.VoiceOver_Common_On : item.presentationData.strings.VoiceOver_Common_Off
strongSelf.activateArea.accessibilityHint = item.presentationData.strings.VoiceOver_Common_SwitchHint
var accessibilityTraits = UIAccessibilityTraits()
if item.enabled {
} else {
accessibilityTraits.insert(.notEnabled)
}
strongSelf.activateArea.accessibilityTraits = accessibilityTraits
if let icon = item.icon {
if strongSelf.iconNode.supernode == nil {
strongSelf.addSubnode(strongSelf.iconNode)
}
if updateIcon {
strongSelf.iconNode.image = icon
}
let iconY = floor((mainContentHeight - icon.size.height) / 2.0)
strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: params.leftInset + floor((leftInset - params.leftInset - icon.size.width) / 2.0), y: iconY), size: icon.size)
} else if strongSelf.iconNode.supernode != nil {
strongSelf.iconNode.image = nil
strongSelf.iconNode.removeFromSupernode()
}
let transition: ContainedViewLayoutTransition = animation.transition
if let currentDisabledOverlayNode = currentDisabledOverlayNode {
if currentDisabledOverlayNode != strongSelf.disabledOverlayNode {
strongSelf.disabledOverlayNode = currentDisabledOverlayNode
strongSelf.insertSubnode(currentDisabledOverlayNode, belowSubnode: strongSelf.switchGestureNode)
currentDisabledOverlayNode.alpha = 0.0
transition.updateAlpha(node: currentDisabledOverlayNode, alpha: 1.0)
currentDisabledOverlayNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: layout.contentSize.width, height: mainContentHeight - separatorHeight))
} else {
transition.updateFrame(node: currentDisabledOverlayNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.contentSize.width, height: mainContentHeight - separatorHeight)))
}
currentDisabledOverlayNode.backgroundColor = itemBackgroundColor.withAlphaComponent(0.6)
} else if let disabledOverlayNode = strongSelf.disabledOverlayNode {
transition.updateAlpha(node: disabledOverlayNode, alpha: 0.0, completion: { [weak disabledOverlayNode] _ in
disabledOverlayNode?.removeFromSupernode()
})
strongSelf.disabledOverlayNode = nil
}
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
strongSelf.bottomTopStripeNode.backgroundColor = itemSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
strongSelf.switchNode.frameColor = item.presentationData.theme.list.itemSwitchColors.frameColor
strongSelf.switchNode.contentColor = item.presentationData.theme.list.itemSwitchColors.contentColor
strongSelf.switchNode.handleColor = item.presentationData.theme.list.itemSwitchColors.handleColor
strongSelf.switchNode.positiveContentColor = item.presentationData.theme.list.itemSwitchColors.positiveColor
strongSelf.switchNode.negativeContentColor = item.presentationData.theme.list.itemSwitchColors.negativeColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
}
let _ = titleApply()
switch item.style {
case .plain:
if strongSelf.backgroundNode.supernode != nil {
strongSelf.backgroundNode.removeFromSupernode()
}
if strongSelf.topStripeNode.supernode != nil {
strongSelf.topStripeNode.removeFromSupernode()
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0)
}
if strongSelf.bottomTopStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomTopStripeNode, at: 1)
}
if strongSelf.maskNode.supernode != nil {
strongSelf.maskNode.removeFromSupernode()
}
strongSelf.bottomTopStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: mainContentHeight - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: layout.contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight))
case .blocks:
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.bottomTopStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomTopStripeNode, at: 3)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, aboveSubnode: strongSelf.switchGestureNode)
}
let hasCorners = itemListHasRoundedBlockLayout(params) && !item.noCorners
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset
strongSelf.bottomStripeNode.isHidden = false
strongSelf.bottomTopStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
strongSelf.bottomTopStripeNode.isHidden = false
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil
let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
animation.animator.updateFrame(layer: strongSelf.backgroundNode.layer, frame: backgroundFrame, completion: nil)
animation.animator.updateFrame(layer: strongSelf.maskNode.layer, frame: backgroundFrame.insetBy(dx: params.leftInset, dy: 0.0), completion: nil)
animation.animator.updateFrame(layer: strongSelf.topStripeNode.layer, frame: CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)), completion: nil)
animation.animator.updateFrame(layer: strongSelf.bottomTopStripeNode.layer, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: mainContentHeight - separatorHeight), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)), completion: nil)
animation.animator.updateFrame(layer: strongSelf.bottomStripeNode.layer, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: layoutSize.width - bottomStripeInset - params.rightInset - separatorRightInset, height: separatorHeight)), completion: nil)
}
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floorToScreenPixels((mainContentHeight - titleLayout.size.height) / 2.0)), size: titleLayout.size)
let _ = titleValueApply()
strongSelf.titleValueNode.frame = CGRect(origin: CGPoint(x: strongSelf.titleNode.frame.maxX + 9.0, y: strongSelf.titleNode.frame.minY + floor((titleLayout.size.height - titleValueLayout.size.height) / 2.0)), size: titleValueLayout.size)
if let updatedTheme {
strongSelf.expandArrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Item List/DisclosureArrow"), color: updatedTheme.list.itemPrimaryTextColor)
}
if let image = strongSelf.expandArrowNode.image {
strongSelf.expandArrowNode.position = CGPoint(x: strongSelf.titleValueNode.frame.maxX + 9.0, y: strongSelf.titleValueNode.frame.midY)
let scaleFactor: CGFloat = 0.8
strongSelf.expandArrowNode.bounds = CGRect(origin: CGPoint(), size: CGSize(width: image.size.width * scaleFactor, height: image.size.height * scaleFactor))
transition.updateTransformRotation(node: strongSelf.expandArrowNode, angle: item.isExpanded ? CGFloat.pi * -0.5 : CGFloat.pi * 0.5)
}
if let switchView = strongSelf.switchNode.view as? UISwitch {
if strongSelf.switchNode.bounds.size.width.isZero {
switchView.sizeToFit()
}
let switchSize = switchView.bounds.size
strongSelf.switchNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - switchSize.width - 15.0, y: floor((mainContentHeight - switchSize.height) / 2.0)), size: switchSize)
strongSelf.switchGestureNode.frame = strongSelf.switchNode.frame
if switchView.isOn != item.value {
switchView.setOn(item.value, animated: animation.isAnimated)
}
switchView.isUserInteractionEnabled = item.enableInteractiveChanges
}
strongSelf.switchGestureNode.isHidden = item.enableInteractiveChanges && item.enabled
if item.displayLocked {
var updateLockedIconImage = false
if let _ = updatedTheme {
updateLockedIconImage = true
}
let lockedIconNode: ASImageNode
if let current = strongSelf.lockedIconNode {
lockedIconNode = current
} else {
updateLockedIconImage = true
lockedIconNode = ASImageNode()
strongSelf.lockedIconNode = lockedIconNode
strongSelf.insertSubnode(lockedIconNode, aboveSubnode: strongSelf.switchNode)
}
if updateLockedIconImage, let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/TextLockIcon"), color: item.presentationData.theme.list.itemSecondaryTextColor) {
lockedIconNode.image = image
}
let switchFrame = strongSelf.switchNode.frame
if let icon = lockedIconNode.image {
lockedIconNode.frame = CGRect(origin: CGPoint(x: switchFrame.minX + 10.0 + UIScreenPixel, y: switchFrame.minY + 9.0), size: icon.size)
}
} else if let lockedIconNode = strongSelf.lockedIconNode {
strongSelf.lockedIconNode = nil
lockedIconNode.removeFromSupernode()
}
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: (item.systemStyle == .glass ? 52.0 : 44.0) + UIScreenPixel + UIScreenPixel))
animation.animator.updateFrame(layer: strongSelf.subItemContainer.layer, frame: CGRect(origin: CGPoint(x: 0.0, y: mainContentHeight), size: CGSize(width: params.width, height: effectiveSubItemsHeight)), completion: nil)
var validIds: [AnyHashable] = []
let subItemSize = CGSize(width: params.width - params.leftInset - params.rightInset, height: item.systemStyle == .glass ? 52.0 : 44.0)
var nextSubItemPosition = CGPoint(x: params.leftInset, y: 0.0)
for subItem in item.subItems {
validIds.append(subItem.id)
let subItemNode: SubItemNode
var subItemNodeTransition = transition
if let current = strongSelf.subItemNodes[subItem.id] {
subItemNode = current
} else {
subItemNodeTransition = .immediate
subItemNode = SubItemNode()
strongSelf.subItemNodes[subItem.id] = subItemNode
strongSelf.subItemContainer.addSubnode(subItemNode)
}
let subItemFrame = CGRect(origin: nextSubItemPosition, size: subItemSize)
subItemNode.update(presentationData: item.presentationData, item: subItem, action: item.subAction, size: subItemSize, transition: subItemNodeTransition)
subItemNodeTransition.updateFrame(node: subItemNode, frame: subItemFrame)
nextSubItemPosition.y += subItemSize.height
}
var removeIds: [AnyHashable] = []
for (id, itemNode) in strongSelf.subItemNodes {
if !validIds.contains(id) {
removeIds.append(id)
itemNode.removeFromSupernode()
}
}
for id in removeIds {
strongSelf.subItemNodes.removeValue(forKey: id)
}
}
})
}
}
override public func accessibilityActivate() -> Bool {
guard let item = self.item else {
return false
}
if !item.enabled {
return false
}
if let switchNode = self.switchNode as? IconSwitchNode {
switchNode.isOn = !switchNode.isOn
item.updated(switchNode.isOn)
} else if let switchNode = self.switchNode as? SwitchNode {
switchNode.isOn = !switchNode.isOn
item.updated(switchNode.isOn)
}
return true
}
override public func visibleForSelection(at point: CGPoint) -> Bool {
if !self.canBeSelected {
return false
}
if point.y > self.subItemContainer.frame.minY {
return false
}
return true
}
override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
var highlighted = highlighted
if point.y > self.subItemContainer.frame.minY {
highlighted = false
}
super.setHighlighted(highlighted, at: point, animated: animated)
if highlighted {
self.highlightedBackgroundNode.alpha = 1.0
if self.highlightedBackgroundNode.supernode == nil {
var anchorNode: ASDisplayNode?
if self.bottomStripeNode.supernode != nil {
anchorNode = self.bottomStripeNode
} else if self.topStripeNode.supernode != nil {
anchorNode = self.topStripeNode
} else if self.backgroundNode.supernode != nil {
anchorNode = self.backgroundNode
}
if let anchorNode = anchorNode {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
} else {
self.addSubnode(self.highlightedBackgroundNode)
}
}
} else {
if self.highlightedBackgroundNode.supernode != nil {
if animated {
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
if let strongSelf = self {
if completed {
strongSelf.highlightedBackgroundNode.removeFromSupernode()
}
}
})
self.highlightedBackgroundNode.alpha = 0.0
} else {
self.highlightedBackgroundNode.removeFromSupernode()
}
}
}
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.allowsGroupOpacity = true
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4, completion: { [weak self] _ in
self?.layer.allowsGroupOpacity = false
})
}
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.allowsGroupOpacity = true
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
@objc private func switchValueChanged(_ switchView: UISwitch) {
if let item = self.item {
let value = switchView.isOn
item.updated(value)
}
}
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if let item = self.item, let switchView = self.switchNode.view as? UISwitch, case .ended = recognizer.state {
if item.enabled {
let value = switchView.isOn
item.updated(!value)
} else {
item.activatedWhileDisabled()
}
}
}
}
@@ -0,0 +1,468 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import TextFormat
import Markdown
public enum InfoListItemText {
case plain(String)
case markdown(String)
}
public enum InfoListItemLinkAction {
case tap(String)
}
public class InfoListItem: ListViewItem {
public let selectable: Bool = false
let presentationData: ItemListPresentationData
let systemStyle: ItemListSystemStyle
let title: String
let text: InfoListItemText
let style: ItemListStyle
let hasDecorations: Bool
let isWarning: Bool
let linkAction: ((InfoListItemLinkAction) -> Void)?
let closeAction: (() -> Void)?
public init(presentationData: ItemListPresentationData, systemStyle: ItemListSystemStyle = .legacy, title: String, text: InfoListItemText, style: ItemListStyle, hasDecorations: Bool = true, isWarning: Bool = false, linkAction: ((InfoListItemLinkAction) -> Void)? = nil, closeAction: (() -> Void)?) {
self.presentationData = presentationData
self.systemStyle = systemStyle
self.title = title
self.text = text
self.style = style
self.hasDecorations = hasDecorations
self.isWarning = isWarning
self.linkAction = linkAction
self.closeAction = closeAction
}
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = InfoItemNode()
let (layout, apply) = node.asyncLayout()(self, params, nil)
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? InfoItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, nil)
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
}
public class ItemListInfoItem: InfoListItem, ItemListItem {
public let sectionId: ItemListSectionId
public init(
presentationData: ItemListPresentationData,
systemStyle: ItemListSystemStyle = .legacy,
title: String,
text: InfoListItemText,
style: ItemListStyle,
sectionId: ItemListSectionId,
linkAction: ((InfoListItemLinkAction) -> Void)? = nil,
closeAction: (() -> Void)?
) {
self.sectionId = sectionId
super.init(presentationData: presentationData, systemStyle: systemStyle, title: title, text: text, style: style, linkAction: linkAction, closeAction: closeAction)
}
override public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = InfoItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
override public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? InfoItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
}
public class InfoItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let closeButton: HighlightableButtonNode
private let maskNode: ASImageNode
private let badgeNode: ASImageNode
private let labelNode: TextNode
private let titleNode: TextNode
private let textNode: TextNode
private var linkHighlightingNode: LinkHighlightingNode?
private let activateArea: AccessibilityAreaNode
private var item: InfoListItem?
public override var canBeSelected: Bool {
return false
}
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.backgroundColor = .white
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.badgeNode = ASImageNode()
self.badgeNode.displayWithoutProcessing = true
self.badgeNode.displaysAsynchronously = false
self.badgeNode.isLayerBacked = true
self.labelNode = TextNode()
self.labelNode.isUserInteractionEnabled = false
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.textNode = TextNode()
self.textNode.isUserInteractionEnabled = false
self.activateArea = AccessibilityAreaNode()
self.activateArea.accessibilityTraits = .staticText
self.closeButton = HighlightableButtonNode()
self.closeButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0)
self.closeButton.displaysAsynchronously = false
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.badgeNode)
self.addSubnode(self.labelNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
self.addSubnode(self.activateArea)
self.addSubnode(self.closeButton)
self.closeButton.addTarget(self, action: #selector(self.closeButtonPressed), forControlEvents: .touchUpInside)
}
public override func didLoad() {
super.didLoad()
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
recognizer.tapActionAtPoint = { [weak self] point in
if let strongSelf = self, !strongSelf.closeButton.frame.contains(point) {
return .waitForSingleTap
}
return .fail
}
recognizer.highlight = { [weak self] point in
if let strongSelf = self {
strongSelf.updateTouchesAtPoint(point)
}
}
self.view.addGestureRecognizer(recognizer)
}
func asyncLayout() -> (_ item: InfoListItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors?) -> (ListViewItemNodeLayout, () -> Void) {
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeTextLayout = TextNode.asyncLayout(self.textNode)
let currentItem = self.item
return { item, params, neighbors in
let leftInset: CGFloat = 16.0 + params.leftInset
let rightInset: CGFloat = 16.0 + params.rightInset
let titleFont = Font.medium(item.presentationData.fontSize.itemListBaseFontSize)
let smallerTextFont = Font.regular(item.presentationData.fontSize.itemListBaseLabelFontSize / 14.0 * 15.0)
let textFont = Font.regular(item.presentationData.fontSize.itemListBaseLabelFontSize / 14.0 * 16.0)
let textBoldFont = Font.semibold(item.presentationData.fontSize.itemListBaseLabelFontSize / 14.0 * 16.0)
let badgeFont = Font.regular(15.0)
let largeBadgeFont = Font.regular(24.0)
var updatedTheme: PresentationTheme?
var updatedBadgeImage: UIImage?
var updatedCloseIcon: UIImage?
let badgeDiameter: CGFloat = item.isWarning ? 30.0 : 22.0
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
updatedBadgeImage = generateStretchableFilledCircleImage(diameter: badgeDiameter, color: item.presentationData.theme.list.itemDestructiveColor)
updatedCloseIcon = PresentationResourcesItemList.itemListCloseIconImage(item.presentationData.theme)
}
let insets: UIEdgeInsets
if let neighbors = neighbors {
insets = itemListNeighborsGroupedInsets(neighbors, params)
} else {
insets = UIEdgeInsets()
}
let separatorHeight = UIScreenPixel
let separatorRightInset: CGFloat = item.systemStyle == .glass ? 16.0 : 0.0
let itemBackgroundColor: UIColor
let itemSeparatorColor: UIColor
switch item.style {
case .plain:
itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor
case .blocks:
itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor
}
let attributedText: NSAttributedString
switch item.text {
case let .plain(text):
attributedText = NSAttributedString(string: text, font: textFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor)
case let .markdown(text):
attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: item.isWarning ? smallerTextFont : textFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), bold: MarkdownAttributeSet(font: textBoldFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), link: MarkdownAttributeSet(font: textFont, textColor: item.presentationData.theme.list.itemAccentColor), linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
}))
}
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.isWarning ? "⚠️" : "!", font: item.isWarning ? largeBadgeFont : badgeFont, textColor: item.presentationData.theme.list.itemCheckColors.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 3, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - badgeDiameter - 8.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
var verticalInset: CGFloat = 0.0
if case .glass = item.systemStyle {
verticalInset = 4.0
}
let contentSize = CGSize(width: params.width, height: titleLayout.size.height + textLayout.size.height + 38.0 + verticalInset * 2.0)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.accessibilityLabel = "\(item.title)\n\(attributedText.string)"
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
strongSelf.activateArea.accessibilityLabel = strongSelf.accessibilityLabel
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
}
let _ = labelApply()
let _ = titleApply()
let _ = textApply()
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
if let neighbors = neighbors {
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners || !item.hasDecorations
}
} else if case .blocks = item.style, case .glass = item.systemStyle {
strongSelf.topStripeNode.isHidden = true
hasTopCorners = true
}
let bottomStripeInset: CGFloat
if let neighbors = neighbors {
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners || !item.hasDecorations
}
} else {
bottomStripeInset = leftInset
if !item.hasDecorations {
strongSelf.topStripeNode.isHidden = true
}
}
strongSelf.badgeNode.isHidden = item.isWarning
strongSelf.closeButton.isHidden = item.closeAction == nil
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset - params.rightInset - separatorRightInset, height: separatorHeight))
if let updateBadgeImage = updatedBadgeImage {
if strongSelf.badgeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.badgeNode, belowSubnode: strongSelf.labelNode)
}
strongSelf.badgeNode.image = updateBadgeImage
}
if let updatedCloseIcon = updatedCloseIcon {
strongSelf.closeButton.setImage(updatedCloseIcon, for: [])
}
strongSelf.badgeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset + 15.0 + (item.isWarning ? 4.0 : 0.0)), size: CGSize(width: badgeDiameter, height: badgeDiameter))
strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: strongSelf.badgeNode.frame.midX - labelLayout.size.width / 2.0, y: strongSelf.badgeNode.frame.minY + 2.0 + UIScreenPixel), size: labelLayout.size)
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: strongSelf.badgeNode.frame.maxX + 8.0, y: verticalInset + 16.0), size: titleLayout.size)
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: strongSelf.titleNode.frame.maxY + 9.0), size: textLayout.size)
strongSelf.closeButton.frame = CGRect(x: params.width - rightInset - 26.0, y: verticalInset + 10.0, width: 32.0, height: 32.0)
}
})
}
}
public override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
public override func animateAdded(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
public override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
@objc func closeButtonPressed() {
if let item = self.item {
item.closeAction?()
}
}
@objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
switch recognizer.state {
case .ended:
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
switch gesture {
case .tap:
let titleFrame = self.textNode.frame
if let item = self.item, titleFrame.contains(location) {
if let (_, attributes) = self.textNode.attributesAtPoint(CGPoint(x: location.x - titleFrame.minX, y: location.y - titleFrame.minY)) {
if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
item.linkAction?(.tap(url))
}
}
}
default:
break
}
}
default:
break
}
}
private func updateTouchesAtPoint(_ point: CGPoint?) {
if let item = self.item {
var rects: [CGRect]?
if let point = point {
let textNodeFrame = self.textNode.frame
if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) {
let possibleNames: [String] = [
TelegramTextAttributes.URL,
TelegramTextAttributes.PeerMention,
TelegramTextAttributes.PeerTextMention,
TelegramTextAttributes.BotCommand,
TelegramTextAttributes.Hashtag
]
for name in possibleNames {
if let _ = attributes[NSAttributedString.Key(rawValue: name)] {
rects = self.textNode.attributeRects(name: name, at: index)
break
}
}
}
}
if let rects = rects {
let linkHighlightingNode: LinkHighlightingNode
if let current = self.linkHighlightingNode {
linkHighlightingNode = current
} else {
linkHighlightingNode = LinkHighlightingNode(color: item.presentationData.theme.list.itemAccentColor.withAlphaComponent(0.2))
self.linkHighlightingNode = linkHighlightingNode
self.insertSubnode(linkHighlightingNode, belowSubnode: self.textNode)
}
linkHighlightingNode.frame = self.textNode.frame
linkHighlightingNode.updateRects(rects)
} else if let linkHighlightingNode = self.linkHighlightingNode {
self.linkHighlightingNode = nil
linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in
linkHighlightingNode?.removeFromSupernode()
})
}
}
}
}
@@ -0,0 +1,505 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
public enum ItemListMultilineInputItemTextLimitMode {
case characters
case bytes
}
public struct ItemListMultilineInputItemTextLimit {
public let value: Int
public let display: Bool
public let mode: ItemListMultilineInputItemTextLimitMode
public init(value: Int, display: Bool, mode: ItemListMultilineInputItemTextLimitMode = .characters) {
self.value = value
self.display = display
self.mode = mode
}
}
public struct ItemListMultilineInputInlineAction {
public let icon: UIImage
public let action: (() -> Void)?
public init(icon: UIImage, action: (() -> Void)?) {
self.icon = icon
self.action = action
}
}
public class ItemListMultilineInputItem: ListViewItem, ItemListItem {
let presentationData: ItemListPresentationData
let systemStyle: ItemListSystemStyle
let text: String
let placeholder: String
public let sectionId: ItemListSectionId
let style: ItemListStyle
let capitalization: Bool
let autocorrection: Bool
let returnKeyType: UIReturnKeyType
let action: (() -> Void)?
let textUpdated: (String) -> Void
let shouldUpdateText: (String) -> Bool
let processPaste: ((String) -> Void)?
let updatedFocus: ((Bool) -> Void)?
let maxLength: ItemListMultilineInputItemTextLimit?
let minimalHeight: CGFloat?
let inlineAction: ItemListMultilineInputInlineAction?
let noInsets: Bool
public let tag: ItemListItemTag?
public init(presentationData: ItemListPresentationData, systemStyle: ItemListSystemStyle = .legacy, text: String, placeholder: String, maxLength: ItemListMultilineInputItemTextLimit?, sectionId: ItemListSectionId, style: ItemListStyle, capitalization: Bool = true, autocorrection: Bool = true, returnKeyType: UIReturnKeyType = .default, minimalHeight: CGFloat? = nil, textUpdated: @escaping (String) -> Void, shouldUpdateText: @escaping (String) -> Bool = { _ in return true }, processPaste: ((String) -> Void)? = nil, updatedFocus: ((Bool) -> Void)? = nil, tag: ItemListItemTag? = nil, action: (() -> Void)? = nil, inlineAction: ItemListMultilineInputInlineAction? = nil, noInsets: Bool = false) {
self.presentationData = presentationData
self.systemStyle = systemStyle
self.text = text
self.placeholder = placeholder
self.maxLength = maxLength
self.sectionId = sectionId
self.style = style
self.capitalization = capitalization
self.autocorrection = autocorrection
self.returnKeyType = returnKeyType
self.minimalHeight = minimalHeight
self.textUpdated = textUpdated
self.shouldUpdateText = shouldUpdateText
self.processPaste = processPaste
self.updatedFocus = updatedFocus
self.tag = tag
self.action = action
self.inlineAction = inlineAction
self.noInsets = noInsets
}
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ItemListMultilineInputItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? ItemListMultilineInputItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
}
public class ItemListMultilineInputItemNode: ListViewItemNode, ASEditableTextNodeDelegate, ItemListItemNode, ItemListItemFocusableNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private let textClippingNode: ASDisplayNode
private let textNode: EditableTextNode
private let measureTextNode: TextNode
private let limitTextNode: TextNode
private var inlineActionButtonNode: HighlightableButtonNode?
private var item: ItemListMultilineInputItem?
private var layoutParams: ListViewItemLayoutParams?
public var tag: ItemListItemTag? {
return self.item?.tag
}
private var exceededLimit = false
public init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.textClippingNode = ASDisplayNode()
self.textClippingNode.clipsToBounds = true
self.textNode = EditableTextNode()
self.measureTextNode = TextNode()
self.limitTextNode = TextNode()
self.limitTextNode.displaysAsynchronously = false
super.init(layerBacked: false, dynamicBounce: false)
self.textClippingNode.addSubnode(self.textNode)
self.addSubnode(self.textClippingNode)
}
override public func didLoad() {
super.didLoad()
var textColor: UIColor = .black
if let item = self.item {
textColor = item.presentationData.theme.list.itemPrimaryTextColor
self.textNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(item.presentationData.fontSize.itemListBaseFontSize), NSAttributedString.Key.foregroundColor.rawValue: textColor]
} else {
self.textNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(17.0), NSAttributedString.Key.foregroundColor.rawValue: textColor]
}
self.textNode.clipsToBounds = true
self.textNode.delegate = self
self.textNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0)
}
public func asyncLayout() -> (_ item: ItemListMultilineInputItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTextLayout = TextNode.asyncLayout(self.measureTextNode)
let makeLimitTextLayout = TextNode.asyncLayout(self.limitTextNode)
let currentItem = self.item
return { item, params, neighbors in
var updatedTheme: PresentationTheme?
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
}
let itemBackgroundColor: UIColor
let itemSeparatorColor: UIColor
let leftInset = 16.0 + params.rightInset
switch item.style {
case .plain:
itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor
case .blocks:
itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor
}
var limitTextString: NSAttributedString?
var rightInset: CGFloat = params.rightInset
var exceededLimit = false
if let maxLength = item.maxLength, maxLength.display {
let textLength: Int
switch maxLength.mode {
case .characters:
textLength = item.text.count
case .bytes:
textLength = item.text.data(using: .utf8, allowLossyConversion: true)?.count ?? 0
}
let displayTextLimit = textLength > maxLength.value * 70 / 100
let remainingCount = maxLength.value - textLength
if displayTextLimit {
limitTextString = NSAttributedString(string: "\(remainingCount)", font: Font.regular(13.0), textColor: remainingCount < 0 ? item.presentationData.theme.list.itemDestructiveColor : item.presentationData.theme.list.itemSecondaryTextColor)
}
exceededLimit = remainingCount < 0
rightInset += 30.0 + 4.0
}
let (limitTextLayout, limitTextApply) = makeLimitTextLayout(TextNodeLayoutArguments(attributedString: limitTextString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0), alignment: .left, cutout: nil, insets: UIEdgeInsets()))
if limitTextLayout.size.width > 30.0 {
rightInset += 30.0
}
if let inlineAction = item.inlineAction {
rightInset += inlineAction.icon.size.width + 8.0
}
var measureText = item.text
if measureText.hasSuffix("\n") || measureText.isEmpty {
measureText += "|"
}
let attributedMeasureText = NSAttributedString(string: measureText, font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize), textColor: .black)
let attributedText = NSAttributedString(string: item.text, font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize), textColor: item.presentationData.theme.list.itemPrimaryTextColor)
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedMeasureText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - 16.0 - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
let separatorHeight = UIScreenPixel
let separatorRightInset: CGFloat = item.systemStyle == .glass ? 16.0 : 0.0
let textTopInset: CGFloat
let textBottomInset: CGFloat
switch item.systemStyle {
case .glass:
textTopInset = 15.0
textBottomInset = 15.0
case .legacy:
textTopInset = 11.0
textBottomInset = 11.0
}
var contentHeight: CGFloat = textLayout.size.height + textTopInset + textBottomInset
if let minimalHeight = item.minimalHeight {
contentHeight = max(minimalHeight, contentHeight)
}
let contentSize = CGSize(width: params.width, height: contentHeight)
let insets = item.noInsets ? UIEdgeInsets() : itemListNeighborsGroupedInsets(neighbors, params)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
let attributedPlaceholderText = NSAttributedString(string: item.placeholder, font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize), textColor: item.presentationData.theme.list.itemPlaceholderTextColor)
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.layoutParams = params
strongSelf.exceededLimit = exceededLimit
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
if strongSelf.isNodeLoaded {
strongSelf.textNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(item.presentationData.fontSize.itemListBaseFontSize), NSAttributedString.Key.foregroundColor.rawValue: item.presentationData.theme.list.itemPrimaryTextColor]
strongSelf.textNode.tintColor = item.presentationData.theme.list.itemAccentColor
}
if let inlineAction = item.inlineAction {
strongSelf.inlineActionButtonNode?.setImage(generateTintedImage(image: inlineAction.icon, color: item.presentationData.theme.list.itemAccentColor), for: .normal)
}
}
let capitalizationType: UITextAutocapitalizationType = item.capitalization ? .sentences : .none
let autocorrectionType: UITextAutocorrectionType = item.autocorrection ? .default : .no
if strongSelf.textNode.textView.autocapitalizationType != capitalizationType {
strongSelf.textNode.textView.autocapitalizationType = capitalizationType
}
if strongSelf.textNode.textView.autocorrectionType != autocorrectionType {
strongSelf.textNode.textView.autocorrectionType = autocorrectionType
}
if strongSelf.textNode.textView.returnKeyType != item.returnKeyType {
strongSelf.textNode.textView.returnKeyType = item.returnKeyType
}
let _ = textApply()
if let currentText = strongSelf.textNode.attributedText {
if currentText.string != attributedText.string || updatedTheme != nil {
strongSelf.textNode.attributedText = attributedText
}
} else {
strongSelf.textNode.attributedText = attributedText
}
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params) && !item.noInsets
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil
if !item.noInsets {
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height), size: CGSize(width: layoutSize.width - bottomStripeInset - params.rightInset - separatorRightInset, height: separatorHeight))
}
if strongSelf.textNode.attributedPlaceholderText == nil || !strongSelf.textNode.attributedPlaceholderText!.isEqual(to: attributedPlaceholderText) {
strongSelf.textNode.attributedPlaceholderText = attributedPlaceholderText
}
strongSelf.textNode.keyboardAppearance = item.presentationData.theme.rootController.keyboardColor.keyboardAppearance
if strongSelf.animationForKey("apparentHeight") == nil {
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.textClippingNode.frame = CGRect(origin: CGPoint(x: leftInset, y: textTopInset), size: CGSize(width: params.width - leftInset - params.rightInset, height: textLayout.size.height))
}
strongSelf.textNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: params.width - leftInset - 16.0 - rightInset, height: textLayout.size.height + 1.0))
let _ = limitTextApply()
strongSelf.limitTextNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 16.0 - limitTextLayout.size.width, y: layout.contentSize.height - 15.0 - limitTextLayout.size.height), size: limitTextLayout.size)
if limitTextString != nil {
if strongSelf.limitTextNode.supernode == nil {
strongSelf.addSubnode(strongSelf.limitTextNode)
}
} else if strongSelf.limitTextNode.supernode != nil {
strongSelf.limitTextNode.removeFromSupernode()
}
if let inlineAction = item.inlineAction {
let inlineActionButtonNode: HighlightableButtonNode
if let currentInlineActionButtonNode = strongSelf.inlineActionButtonNode {
inlineActionButtonNode = currentInlineActionButtonNode
} else {
inlineActionButtonNode = HighlightableButtonNode()
inlineActionButtonNode.setImage(generateTintedImage(image: inlineAction.icon, color: item.presentationData.theme.list.itemAccentColor), for: .normal)
inlineActionButtonNode.addTarget(strongSelf, action: #selector(strongSelf.inlineActionPressed), forControlEvents: .touchUpInside)
strongSelf.addSubnode(inlineActionButtonNode)
strongSelf.inlineActionButtonNode = inlineActionButtonNode
}
inlineActionButtonNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - inlineAction.icon.size.width - 11.0, y: 7.0), size: inlineAction.icon.size)
} else if let inlineActionButtonNode = strongSelf.inlineActionButtonNode {
inlineActionButtonNode.removeFromSupernode()
strongSelf.inlineActionButtonNode = nil
}
}
})
}
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
override public func animateFrameTransition(_ progress: CGFloat, _ currentValue: CGFloat) {
super.animateFrameTransition(progress, currentValue)
guard let params = self.layoutParams else {
return
}
let separatorHeight = UIScreenPixel
let insets = self.insets
let contentSize = CGSize(width: params.width, height: max(1.0, currentValue - insets.top - insets.bottom))
let leftInset = 16.0 + params.leftInset
let textTopInset: CGFloat = 11.0
let textBottomInset: CGFloat = 11.0
self.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
self.maskNode.frame = self.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
self.bottomStripeNode.frame = CGRect(origin: CGPoint(x: self.bottomStripeNode.frame.minX, y: contentSize.height), size: CGSize(width: self.bottomStripeNode.frame.size.width, height: separatorHeight))
self.textClippingNode.frame = CGRect(origin: CGPoint(x: leftInset, y: textTopInset), size: CGSize(width: max(0.0, params.width - leftInset - params.rightInset), height: max(0.0, contentSize.height - textTopInset - textBottomInset)))
}
public func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) {
self.item?.updatedFocus?(true)
}
public func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) {
self.item?.updatedFocus?(false)
}
public func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if let item = self.item {
if text.count > 1, let processPaste = item.processPaste {
processPaste(text)
return false
}
if let action = item.action, text == "\n" {
action()
return false
}
let newText = (editableTextNode.textView.text as NSString).replacingCharacters(in: range, with: text)
if !item.shouldUpdateText(newText) {
return false
}
}
return true
}
public func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) {
if let item = self.item {
if let text = self.textNode.attributedText {
let updatedText = text.string
let updatedAttributedText = NSAttributedString(string: updatedText, font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize), textColor: item.presentationData.theme.list.itemPrimaryTextColor)
if text.string != updatedAttributedText.string {
self.textNode.attributedText = updatedAttributedText
}
item.textUpdated(updatedText)
} else {
item.textUpdated("")
}
}
}
public func editableTextNodeShouldPaste(_ editableTextNode: ASEditableTextNode) -> Bool {
if let _ = self.item {
let text: String? = UIPasteboard.general.string
if let _ = text {
return true
}
}
return false
}
public func focus() {
if !self.textNode.textView.isFirstResponder {
self.textNode.textView.becomeFirstResponder()
}
}
public func selectAll() {
self.textNode.textView.selectAll(nil)
}
public func animateError() {
self.textNode.layer.addShakeAnimation()
}
public func animateErrorIfNeeded() {
if self.exceededLimit {
self.animateError()
}
}
@objc private func inlineActionPressed() {
if let action = self.item?.inlineAction?.action {
action()
}
}
}
@@ -0,0 +1,431 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import TextFormat
import AccountContext
public enum ItemListMultilineTextBaseFont {
case `default`
case monospace
}
public class ItemListMultilineTextItem: ListViewItem, ItemListItem {
let presentationData: ItemListPresentationData
let text: String
let enabledEntityTypes: EnabledEntityTypes
let font: ItemListMultilineTextBaseFont
public let sectionId: ItemListSectionId
let style: ItemListStyle
let action: (() -> Void)?
let longTapAction: (() -> Void)?
let linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)?
let tag: Any?
public let selectable: Bool
public init(presentationData: ItemListPresentationData, text: String, enabledEntityTypes: EnabledEntityTypes, font: ItemListMultilineTextBaseFont = .default, sectionId: ItemListSectionId, style: ItemListStyle, action: (() -> Void)? = nil, longTapAction: (() -> Void)? = nil, linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)? = nil, tag: Any? = nil) {
self.presentationData = presentationData
self.text = text
self.enabledEntityTypes = enabledEntityTypes
self.font = font
self.sectionId = sectionId
self.style = style
self.action = action
self.longTapAction = longTapAction
self.linkItemAction = linkItemAction
self.tag = tag
self.selectable = action != nil
}
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ItemListMultilineTextItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? ItemListMultilineTextItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
public func selected(listView: ListView){
listView.clearHighlightAnimated(true)
self.action?()
}
}
public class ItemListMultilineTextItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
private let maskNode: ASImageNode
private var linkHighlightingNode: LinkHighlightingNode?
private let textNode: TextNode
private let activateArea: AccessibilityAreaNode
private var item: ItemListMultilineTextItem?
public var tag: Any? {
return self.item?.tag
}
override public var canBeLongTapped: Bool {
return true
}
public init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.backgroundColor = .white
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.textNode = TextNode()
self.textNode.isUserInteractionEnabled = false
self.textNode.contentMode = .left
self.textNode.contentsScale = UIScreen.main.scale
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
self.activateArea = AccessibilityAreaNode()
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.textNode)
self.addSubnode(self.activateArea)
}
override public func didLoad() {
super.didLoad()
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
recognizer.tapActionAtPoint = { [weak self] point in
if let strongSelf = self, strongSelf.linkItemAtPoint(point) != nil {
return .waitForSingleTap
}
return .fail
}
recognizer.highlight = { [weak self] point in
if let strongSelf = self {
strongSelf.updateTouchesAtPoint(point)
}
}
self.view.addGestureRecognizer(recognizer)
}
public func asyncLayout() -> (_ item: ItemListMultilineTextItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTextLayout = TextNode.asyncLayout(self.textNode)
let currentItem = self.item
return { item, params, neighbors in
var updatedTheme: PresentationTheme?
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
}
let textColor: UIColor = item.presentationData.theme.list.itemPrimaryTextColor
let leftInset: CGFloat
let itemBackgroundColor: UIColor
let itemSeparatorColor: UIColor
switch item.style {
case .plain:
itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor
leftInset = 16.0 + params.leftInset
case .blocks:
itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor
leftInset = 16.0 + params.rightInset
}
let fontSize = item.presentationData.fontSize.itemListBaseFontSize
var baseFont = Font.regular(fontSize)
var linkFont = baseFont
var boldFont = Font.medium(fontSize)
var italicFont = Font.italic(fontSize)
var boldItalicFont = Font.semiboldItalic(fontSize)
let titleFixedFont = Font.monospace(fontSize)
if case .monospace = item.font {
baseFont = Font.monospace(fontSize)
linkFont = Font.monospace(fontSize)
boldFont = Font.semiboldMonospace(fontSize)
italicFont = Font.italicMonospace(fontSize)
boldItalicFont = Font.semiboldItalicMonospace(fontSize)
}
let entities = generateTextEntities(item.text, enabledTypes: item.enabledEntityTypes)
let string = stringWithAppliedEntities(item.text, entities: entities, baseColor: textColor, linkColor: item.presentationData.theme.list.itemAccentColor, baseFont: baseFont, linkFont: linkFont, boldFont: boldFont, italicFont: italicFont, boldItalicFont: boldItalicFont, fixedFont: titleFixedFont, blockQuoteFont: baseFont, message: nil)
let (titleLayout, titleApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: string, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let contentSize: CGSize
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
switch item.style {
case .plain:
contentSize = CGSize(width: params.width, height: titleLayout.size.height + 22.0)
insets = itemListNeighborsPlainInsets(neighbors)
case .blocks:
contentSize = CGSize(width: params.width, height: titleLayout.size.height + 22.0)
insets = itemListNeighborsGroupedInsets(neighbors, params)
}
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
strongSelf.activateArea.accessibilityLabel = item.text
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
}
let _ = titleApply()
switch item.style {
case .plain:
if strongSelf.backgroundNode.supernode != nil {
strongSelf.backgroundNode.removeFromSupernode()
}
if strongSelf.topStripeNode.supernode != nil {
strongSelf.topStripeNode.removeFromSupernode()
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0)
}
if strongSelf.maskNode.supernode != nil {
strongSelf.maskNode.removeFromSupernode()
}
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight))
case .blocks:
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = 16.0
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
}
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 11.0), size: titleLayout.size)
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel))
}
})
}
}
override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
super.setHighlighted(highlighted, at: point, animated: animated)
if highlighted && self.linkItemAtPoint(point) == nil {
self.highlightedBackgroundNode.alpha = 1.0
if self.highlightedBackgroundNode.supernode == nil {
var anchorNode: ASDisplayNode?
if self.bottomStripeNode.supernode != nil {
anchorNode = self.bottomStripeNode
} else if self.topStripeNode.supernode != nil {
anchorNode = self.topStripeNode
} else if self.backgroundNode.supernode != nil {
anchorNode = self.backgroundNode
}
if let anchorNode = anchorNode {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
} else {
self.addSubnode(self.highlightedBackgroundNode)
}
}
} else {
if self.highlightedBackgroundNode.supernode != nil {
if animated {
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
if let strongSelf = self {
if completed {
strongSelf.highlightedBackgroundNode.removeFromSupernode()
}
}
})
self.highlightedBackgroundNode.alpha = 0.0
} else {
self.highlightedBackgroundNode.removeFromSupernode()
}
}
}
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
@objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
switch recognizer.state {
case .ended:
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
switch gesture {
case .tap, .longTap:
if let item = self.item, let linkItem = self.linkItemAtPoint(location) {
item.linkItemAction?(gesture == .tap ? .tap : .longTap, linkItem)
}
default:
break
}
}
default:
break
}
}
private func linkItemAtPoint(_ point: CGPoint) -> TextLinkItem? {
let textNodeFrame = self.textNode.frame
if let (_, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) {
if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
return .url(url: url, concealed: false)
} else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String {
return .mention(peerName)
} else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag {
return .hashtag(hashtag.peerName, hashtag.hashtag)
} else {
return nil
}
}
return nil
}
override public func longTapped() {
self.item?.longTapAction?()
}
private func updateTouchesAtPoint(_ point: CGPoint?) {
if let item = self.item {
var rects: [CGRect]?
if let point = point {
let textNodeFrame = self.textNode.frame
if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) {
let possibleNames: [String] = [
TelegramTextAttributes.URL,
TelegramTextAttributes.PeerMention,
TelegramTextAttributes.PeerTextMention,
TelegramTextAttributes.BotCommand,
TelegramTextAttributes.Hashtag
]
for name in possibleNames {
if let _ = attributes[NSAttributedString.Key(rawValue: name)] {
rects = self.textNode.attributeRects(name: name, at: index)
break
}
}
}
}
if let rects = rects {
let linkHighlightingNode: LinkHighlightingNode
if let current = self.linkHighlightingNode {
linkHighlightingNode = current
} else {
linkHighlightingNode = LinkHighlightingNode(color: item.presentationData.theme.list.itemAccentColor.withAlphaComponent(0.2))
self.linkHighlightingNode = linkHighlightingNode
self.insertSubnode(linkHighlightingNode, belowSubnode: self.textNode)
}
linkHighlightingNode.frame = self.textNode.frame
linkHighlightingNode.updateRects(rects)
} else if let linkHighlightingNode = self.linkHighlightingNode {
self.linkHighlightingNode = nil
linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in
linkHighlightingNode?.removeFromSupernode()
})
}
}
}
}
@@ -0,0 +1,226 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
public class ItemListPlaceholderItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
let text: String
public let sectionId: ItemListSectionId
let style: ItemListStyle
public let tag: ItemListItemTag?
public init(theme: PresentationTheme, text: String, sectionId: ItemListSectionId, style: ItemListStyle, tag: ItemListItemTag? = nil) {
self.theme = theme
self.text = text
self.sectionId = sectionId
self.style = style
self.tag = tag
}
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ItemListPlaceholderItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? ItemListPlaceholderItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
public let selectable = false
}
private let textFont = Font.regular(15.0)
public class ItemListPlaceholderItemNode: ListViewItemNode, ItemListItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
public let textNode: TextNode
private var item: ItemListPlaceholderItem?
override public var canBeSelected: Bool {
return false
}
public var tag: ItemListItemTag? {
return self.item?.tag
}
public init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.backgroundColor = .white
self.maskNode = ASImageNode()
self.maskNode.isUserInteractionEnabled = false
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.textNode = TextNode()
self.textNode.isUserInteractionEnabled = false
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.textNode)
}
public func asyncLayout() -> (_ item: ItemListPlaceholderItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTextLayout = TextNode.asyncLayout(self.textNode)
let currentItem = self.item
return { item, params, neighbors in
var updatedTheme: PresentationTheme?
if currentItem?.theme !== item.theme {
updatedTheme = item.theme
}
let contentSize: CGSize
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
let itemBackgroundColor: UIColor
let itemSeparatorColor: UIColor
let leftInset = 16.0 + params.leftInset
let rightInset = 16.0 + params.rightInset
let textColor = item.theme.list.itemSecondaryTextColor
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.text, font: textFont, textColor: textColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let height: CGFloat = 42.0 + textLayout.size.height
switch item.style {
case .plain:
itemBackgroundColor = item.theme.list.plainBackgroundColor
itemSeparatorColor = item.theme.list.itemPlainSeparatorColor
contentSize = CGSize(width: params.width, height: height)
insets = itemListNeighborsPlainInsets(neighbors)
case .blocks:
itemBackgroundColor = item.theme.list.itemBlocksBackgroundColor
itemSeparatorColor = item.theme.list.itemBlocksSeparatorColor
contentSize = CGSize(width: params.width, height: height)
insets = itemListNeighborsGroupedInsets(neighbors, params)
}
return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in
if let strongSelf = self {
strongSelf.item = item
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
}
let _ = textApply()
switch item.style {
case .plain:
if strongSelf.backgroundNode.supernode != nil {
strongSelf.backgroundNode.removeFromSupernode()
}
if strongSelf.topStripeNode.supernode != nil {
strongSelf.topStripeNode.removeFromSupernode()
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0)
}
if strongSelf.maskNode.supernode != nil {
strongSelf.maskNode.removeFromSupernode()
}
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight))
case .blocks:
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight))
}
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((params.width - textLayout.size.width) / 2.0), y: floorToScreenPixels((height - textLayout.size.height) / 2.0)), size: textLayout.size)
}
})
}
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override public func animateAdded(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}
@@ -0,0 +1,379 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import ActivityIndicator
public enum ItemListSectionHeaderAccessoryTextColor {
case generic
case destructive
}
public struct ItemListSectionHeaderAccessoryText: Equatable {
public let value: String
public let color: ItemListSectionHeaderAccessoryTextColor
public let icon: UIImage?
public init(value: String, color: ItemListSectionHeaderAccessoryTextColor, icon: UIImage? = nil) {
self.value = value
self.color = color
self.icon = icon
}
}
public enum ItemListSectionHeaderActivityIndicator {
case none
case left
case right
public var hasActivity: Bool {
switch self {
case .left, .right:
return true
default:
return false
}
}
}
public class ItemListSectionHeaderItem: ListViewItem, ItemListItem {
public struct BadgeStyle: Equatable {
public var background: UIColor
public var foreground: UIColor
public init(background: UIColor, foreground: UIColor) {
self.background = background
self.foreground = foreground
}
}
let presentationData: ItemListPresentationData
let text: String
let badge: String?
let badgeStyle: BadgeStyle?
let multiline: Bool
let activityIndicator: ItemListSectionHeaderActivityIndicator
let accessoryText: ItemListSectionHeaderAccessoryText?
let actionText: String?
let action: (() -> Void)?
public let sectionId: ItemListSectionId
public let isAlwaysPlain: Bool = true
public init(presentationData: ItemListPresentationData, text: String, badge: String? = nil, badgeStyle: BadgeStyle? = nil, multiline: Bool = false, activityIndicator: ItemListSectionHeaderActivityIndicator = .none, accessoryText: ItemListSectionHeaderAccessoryText? = nil, actionText: String? = nil, action: (() -> Void)? = nil, sectionId: ItemListSectionId) {
self.presentationData = presentationData
self.text = text
self.badge = badge
self.badgeStyle = badgeStyle
self.multiline = multiline
self.activityIndicator = activityIndicator
self.accessoryText = accessoryText
self.actionText = actionText
self.action = action
self.sectionId = sectionId
}
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ItemListSectionHeaderItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
guard let nodeValue = node() as? ItemListSectionHeaderItemNode else {
assertionFailure()
return
}
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
public class ItemListSectionHeaderItemNode: ListViewItemNode {
private var item: ItemListSectionHeaderItem?
private let titleNode: TextNode
private var badgeBackgroundLayer: SimpleLayer?
private var badgeTextNode: TextNode?
private let accessoryTextNode: TextNode
private var accessoryImageNode: ASImageNode?
private var activityIndicator: ActivityIndicator?
private var actionNode: TextNode?
private var actionButtonNode: HighlightableButtonNode?
private let activateArea: AccessibilityAreaNode
public init() {
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale
self.accessoryTextNode = TextNode()
self.accessoryTextNode.isUserInteractionEnabled = false
self.accessoryTextNode.contentMode = .left
self.accessoryTextNode.contentsScale = UIScreen.main.scale
self.activateArea = AccessibilityAreaNode()
self.activateArea.accessibilityTraits = [.staticText, .header]
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.titleNode)
self.addSubnode(self.accessoryTextNode)
self.addSubnode(self.activateArea)
}
public func asyncLayout() -> (_ item: ItemListSectionHeaderItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeActionLayout = TextNode.asyncLayout(self.actionNode)
let makeBadgeTextLayout = TextNode.asyncLayout(self.badgeTextNode)
let makeAccessoryTextLayout = TextNode.asyncLayout(self.accessoryTextNode)
let previousItem = self.item
return { item, params, neighbors in
let leftInset: CGFloat = 15.0 + params.leftInset
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseHeaderFontSize)
var badgeLayoutAndApply: (TextNodeLayout, () -> TextNode)?
if let badge = item.badge {
if item.badgeStyle != nil {
let badgeFont = Font.regular(item.presentationData.fontSize.itemListBaseHeaderFontSize * 12.0 / 13.0)
badgeLayoutAndApply = makeBadgeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: badge, font: badgeFont, textColor: item.badgeStyle?.foreground ?? item.presentationData.theme.list.itemCheckColors.foregroundColor), maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0)))
} else {
let badgeFont = Font.semibold(item.presentationData.fontSize.itemListBaseHeaderFontSize * 11.0 / 13.0)
badgeLayoutAndApply = makeBadgeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: badge, font: badgeFont, textColor: item.badgeStyle?.foreground ?? item.presentationData.theme.list.itemCheckColors.foregroundColor), maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0)))
}
}
let badgeSpacing: CGFloat = 6.0
var textRightInset: CGFloat = 20.0
if let badgeLayoutAndApply {
textRightInset += badgeLayoutAndApply.0.size.width + badgeSpacing
}
var actionLayoutAndApply: (TextNodeLayout, () -> TextNode)?
if let actionText = item.actionText {
let actionLayoutAndApplyValue = makeActionLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: actionText, font: titleFont, textColor: item.presentationData.theme.list.itemAccentColor), backgroundColor: nil, maximumNumberOfLines: item.multiline ? 0 : 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
actionLayoutAndApply = actionLayoutAndApplyValue
textRightInset += actionLayoutAndApplyValue.0.size.width + 2.0
}
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.text, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: item.multiline ? 0 : 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
var accessoryTextString: NSAttributedString?
var accessoryIcon: UIImage?
if let accessoryText = item.accessoryText {
let color: UIColor
switch accessoryText.color {
case .generic:
color = item.presentationData.theme.list.sectionHeaderTextColor
case .destructive:
color = item.presentationData.theme.list.freeTextErrorColor
}
accessoryTextString = NSAttributedString(string: accessoryText.value, font: titleFont, textColor: color)
accessoryIcon = accessoryText.icon
}
let (accessoryLayout, accessoryApply) = makeAccessoryTextLayout(TextNodeLayoutArguments(attributedString: accessoryTextString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let contentSize: CGSize
var insets = UIEdgeInsets()
contentSize = CGSize(width: params.width, height: titleLayout.size.height + 13.0)
switch neighbors.top {
case .none:
insets.top += 24.0
case .otherSection:
insets.top += 28.0
default:
break
}
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
let _ = titleApply()
let _ = accessoryApply()
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
strongSelf.activateArea.accessibilityLabel = item.text
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 7.0), size: titleLayout.size)
if let (actionLayout, actionApply) = actionLayoutAndApply {
let actionButtonNode: HighlightableButtonNode
if let current = strongSelf.actionButtonNode {
actionButtonNode = current
} else {
actionButtonNode = HighlightableButtonNode()
strongSelf.actionButtonNode = actionButtonNode
actionButtonNode.hitTestSlop = UIEdgeInsets(top: -4.0, left: -4.0, bottom: -4.0, right: -4.0)
strongSelf.addSubnode(actionButtonNode)
actionButtonNode.addTarget(strongSelf, action: #selector(strongSelf.actionButtonPressed), forControlEvents: .touchUpInside)
}
let actionNode = actionApply()
if strongSelf.actionNode !== actionNode {
strongSelf.actionNode?.removeFromSupernode()
strongSelf.actionNode = actionNode
actionButtonNode.addSubnode(actionNode)
}
actionButtonNode.frame = CGRect(origin: CGPoint(x: params.width - leftInset - actionLayout.size.width, y: 7.0), size: actionLayout.size)
actionNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: actionLayout.size)
} else {
if let actionNode = strongSelf.actionNode {
strongSelf.actionNode = nil
actionNode.removeFromSupernode()
}
if let actionButtonNode = strongSelf.actionButtonNode {
strongSelf.actionButtonNode = nil
actionButtonNode.removeFromSupernode()
}
}
if let badgeLayoutAndApply {
let badgeTextNode = badgeLayoutAndApply.1()
let badgeSideInset: CGFloat = 4.0
let badgeBackgroundSize: CGSize
if item.badgeStyle != nil {
badgeBackgroundSize = CGSize(width: badgeSideInset * 2.0 + badgeLayoutAndApply.0.size.width, height: badgeLayoutAndApply.0.size.height + 3.0)
} else {
badgeBackgroundSize = CGSize(width: badgeSideInset * 2.0 + badgeLayoutAndApply.0.size.width, height: badgeLayoutAndApply.0.size.height + 3.0)
}
let badgeBackgroundFrame = CGRect(origin: CGPoint(x: strongSelf.titleNode.frame.maxX + badgeSpacing, y: strongSelf.titleNode.frame.minY - UIScreenPixel + floorToScreenPixels((strongSelf.titleNode.bounds.height - badgeBackgroundSize.height) * 0.5)), size: badgeBackgroundSize)
let badgeBackgroundLayer: SimpleLayer
if let current = strongSelf.badgeBackgroundLayer {
badgeBackgroundLayer = current
} else {
badgeBackgroundLayer = SimpleLayer()
strongSelf.badgeBackgroundLayer = badgeBackgroundLayer
strongSelf.layer.addSublayer(badgeBackgroundLayer)
}
if strongSelf.badgeTextNode !== badgeTextNode {
strongSelf.badgeTextNode?.removeFromSupernode()
strongSelf.badgeTextNode = badgeTextNode
strongSelf.addSubnode(badgeTextNode)
}
badgeBackgroundLayer.frame = badgeBackgroundFrame
badgeBackgroundLayer.backgroundColor = item.badgeStyle?.background.cgColor ?? item.presentationData.theme.list.itemCheckColors.fillColor.cgColor
badgeBackgroundLayer.cornerRadius = 5.0
badgeTextNode.frame = CGRect(origin: CGPoint(x: badgeBackgroundFrame.minX + floor((badgeBackgroundFrame.width - badgeLayoutAndApply.0.size.width) * 0.5), y: badgeBackgroundFrame.minY + 1.0 + floorToScreenPixels((badgeBackgroundFrame.height - badgeLayoutAndApply.0.size.height) * 0.5)), size: badgeLayoutAndApply.0.size)
} else {
if let badgeTextNode = strongSelf.badgeTextNode {
strongSelf.badgeTextNode = nil
badgeTextNode.removeFromSupernode()
}
if let badgeBackgroundLayer = strongSelf.badgeBackgroundLayer {
strongSelf.badgeBackgroundLayer = nil
badgeBackgroundLayer.removeFromSuperlayer()
}
}
var accessoryTextOffset: CGFloat = 0.0
if let accessoryIcon = accessoryIcon {
accessoryTextOffset += accessoryIcon.size.width + 3.0
}
strongSelf.accessoryTextNode.frame = CGRect(origin: CGPoint(x: params.width - leftInset - accessoryLayout.size.width - accessoryTextOffset, y: 7.0), size: accessoryLayout.size)
if let accessoryIcon = accessoryIcon {
let accessoryImageNode: ASImageNode
if let currentAccessoryImageNode = strongSelf.accessoryImageNode {
accessoryImageNode = currentAccessoryImageNode
} else {
accessoryImageNode = ASImageNode()
accessoryImageNode.displaysAsynchronously = false
accessoryImageNode.displayWithoutProcessing = true
strongSelf.addSubnode(accessoryImageNode)
strongSelf.accessoryImageNode = accessoryImageNode
}
accessoryImageNode.image = accessoryIcon
accessoryImageNode.frame = CGRect(origin: CGPoint(x: params.width - leftInset - accessoryIcon.size.width, y: 7.0), size: accessoryIcon.size)
} else if let accessoryImageNode = strongSelf.accessoryImageNode {
accessoryImageNode.removeFromSupernode()
strongSelf.accessoryImageNode = nil
}
if previousItem?.activityIndicator != item.activityIndicator {
if item.activityIndicator.hasActivity {
let activityIndicator: ActivityIndicator
if let currentActivityIndicator = strongSelf.activityIndicator {
activityIndicator = currentActivityIndicator
} else {
activityIndicator = ActivityIndicator(type: .custom(item.presentationData.theme.list.sectionHeaderTextColor, 18.0, 1.0, false))
strongSelf.addSubnode(activityIndicator)
strongSelf.activityIndicator = activityIndicator
}
activityIndicator.isHidden = false
if previousItem != nil {
activityIndicator.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, removeOnCompletion: false)
}
} else if let activityIndicator = strongSelf.activityIndicator {
activityIndicator.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { finished in
if finished {
activityIndicator.isHidden = true
}
})
}
}
var activityIndicatorOrigin: CGPoint?
switch item.activityIndicator {
case .left:
activityIndicatorOrigin = CGPoint(x: strongSelf.titleNode.frame.maxX + 6.0, y: 7.0 - UIScreenPixel)
case .right:
activityIndicatorOrigin = CGPoint(x: params.width - leftInset - 18.0, y: 7.0 - UIScreenPixel)
default:
break
}
if let activityIndicatorOrigin = activityIndicatorOrigin {
strongSelf.activityIndicator?.frame = CGRect(origin: activityIndicatorOrigin, size: CGSize(width: 18.0, height: 18.0))
}
}
})
}
}
@objc private func actionButtonPressed() {
self.item?.action?()
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}
@@ -0,0 +1,627 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import TextNodeWithEntities
import AccountContext
private let validIdentifierSet: CharacterSet = {
var set = CharacterSet(charactersIn: "a".unicodeScalars.first! ... "z".unicodeScalars.first!)
set.insert(charactersIn: "A".unicodeScalars.first! ... "Z".unicodeScalars.first!)
set.insert(charactersIn: "0".unicodeScalars.first! ... "9".unicodeScalars.first!)
set.insert("_")
return set
}()
public enum ItemListSingleLineInputItemType: Equatable {
case regular(capitalization: Bool, autocorrection: Bool)
case password
case email
case number
case decimal
case username
}
public enum ItemListSingleLineInputClearType: Equatable {
case none
case always
case onFocus
var hasButton: Bool {
switch self {
case .none:
return false
case .always, .onFocus:
return true
}
}
}
public enum ItemListSingleLineInputAlignment {
case `default`
case right
}
public class ItemListSingleLineInputItem: ListViewItem, ItemListItem {
let context: AccountContext?
let systemStyle: ItemListSystemStyle
let presentationData: ItemListPresentationData
let title: NSAttributedString
let text: String
let placeholder: String
let label: String?
let type: ItemListSingleLineInputItemType
let returnKeyType: UIReturnKeyType
let alignment: ItemListSingleLineInputAlignment
let spacing: CGFloat
let clearType: ItemListSingleLineInputClearType
let maxLength: Int
let enabled: Bool
let selectAllOnFocus: Bool
let secondaryStyle: Bool
public let sectionId: ItemListSectionId
let action: () -> Void
let textUpdated: (String) -> Void
let shouldUpdateText: (String) -> Bool
let processPaste: ((String) -> String)?
let updatedFocus: ((Bool) -> Void)?
let cleared: (() -> Void)?
public let tag: ItemListItemTag?
public init(context: AccountContext? = nil, presentationData: ItemListPresentationData, systemStyle: ItemListSystemStyle = .legacy, title: NSAttributedString, text: String, placeholder: String, label: String? = nil, type: ItemListSingleLineInputItemType = .regular(capitalization: true, autocorrection: true), returnKeyType: UIReturnKeyType = .`default`, alignment: ItemListSingleLineInputAlignment = .default, spacing: CGFloat = 0.0, clearType: ItemListSingleLineInputClearType = .none, maxLength: Int = 0, enabled: Bool = true, selectAllOnFocus: Bool = false, secondaryStyle: Bool = false, tag: ItemListItemTag? = nil, sectionId: ItemListSectionId, textUpdated: @escaping (String) -> Void, shouldUpdateText: @escaping (String) -> Bool = { _ in return true }, processPaste: ((String) -> String)? = nil, updatedFocus: ((Bool) -> Void)? = nil, action: @escaping () -> Void, cleared: (() -> Void)? = nil) {
self.context = context
self.presentationData = presentationData
self.systemStyle = systemStyle
self.title = title
self.text = text
self.placeholder = placeholder
self.label = label
self.type = type
self.returnKeyType = returnKeyType
self.alignment = alignment
self.spacing = spacing
self.clearType = clearType
self.maxLength = maxLength
self.enabled = enabled
self.selectAllOnFocus = selectAllOnFocus
self.secondaryStyle = secondaryStyle
self.tag = tag
self.sectionId = sectionId
self.textUpdated = textUpdated
self.shouldUpdateText = shouldUpdateText
self.processPaste = processPaste
self.updatedFocus = updatedFocus
self.action = action
self.cleared = cleared
}
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ItemListSingleLineInputItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? ItemListSingleLineInputItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
}
public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDelegate, ItemListItemNode, ItemListItemFocusableNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private let titleNode: TextNodeWithEntities
private let measureTitleSizeNode: TextNode
private let textNode: TextFieldNode
private let clearIconNode: ASImageNode
private let clearButtonNode: HighlightableButtonNode
private let labelNode: TextNode
private var item: ItemListSingleLineInputItem?
public var tag: ItemListItemTag? {
return self.item?.tag
}
public init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.titleNode = TextNodeWithEntities()
self.measureTitleSizeNode = TextNode()
self.textNode = TextFieldNode()
self.clearIconNode = ASImageNode()
self.clearIconNode.isLayerBacked = true
self.clearIconNode.displayWithoutProcessing = true
self.clearIconNode.displaysAsynchronously = false
self.clearButtonNode = HighlightableButtonNode()
self.labelNode = TextNode()
self.labelNode.isUserInteractionEnabled = false
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.titleNode.textNode)
self.addSubnode(self.textNode)
self.addSubnode(self.clearIconNode)
self.addSubnode(self.clearButtonNode)
self.addSubnode(self.textNode)
self.addSubnode(self.labelNode)
self.clearButtonNode.addTarget(self, action: #selector(self.clearButtonPressed), forControlEvents: .touchUpInside)
self.clearButtonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.clearIconNode.layer.removeAnimation(forKey: "opacity")
strongSelf.clearIconNode.alpha = 0.4
} else {
strongSelf.clearIconNode.alpha = 1.0
strongSelf.clearIconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
}
override public func didLoad() {
super.didLoad()
if let item = self.item {
self.textNode.textField.typingAttributes = [NSAttributedString.Key.font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize)]
self.textNode.textField.font = Font.regular(item.presentationData.fontSize.itemListBaseFontSize)
self.textNode.textField.textColor = item.secondaryStyle ? item.presentationData.theme.list.itemSecondaryTextColor : item.presentationData.theme.list.itemPrimaryTextColor
self.textNode.textField.keyboardAppearance = item.presentationData.theme.rootController.keyboardColor.keyboardAppearance
self.textNode.textField.tintColor = item.presentationData.theme.list.itemAccentColor
self.textNode.textField.accessibilityHint = item.placeholder
} else {
self.textNode.textField.typingAttributes = [NSAttributedString.Key.font: Font.regular(17.0)]
self.textNode.textField.font = Font.regular(17.0)
}
self.textNode.clipsToBounds = true
self.textNode.textField.delegate = self
self.textNode.textField.addTarget(self, action: #selector(self.textFieldTextChanged(_:)), for: .editingChanged)
self.textNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0)
}
public func asyncLayout() -> (_ item: ItemListSingleLineInputItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode.textNode)
let makeTitleWithEntitiesLayout = TextNodeWithEntities.asyncLayout(self.titleNode)
let makeMeasureTitleSizeLayout = TextNode.asyncLayout(self.measureTitleSizeNode)
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
let currentItem = self.item
return { item, params, neighbors in
var updatedTheme: PresentationTheme?
var updatedClearIcon: UIImage?
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
updatedClearIcon = PresentationResourcesItemList.itemListClearInputIcon(item.presentationData.theme)
}
var fontUpdated = false
if currentItem?.presentationData.fontSize != item.presentationData.fontSize {
fontUpdated = true
}
var styleUpdated = false
if currentItem?.secondaryStyle != item.secondaryStyle {
styleUpdated = true
}
let leftInset: CGFloat = 16.0 + params.leftInset
var rightInset: CGFloat = 16.0 + params.rightInset
if item.clearType.hasButton {
rightInset += 32.0
}
let titleString = NSMutableAttributedString(attributedString: item.title)
if !item.title.string.isSingleEmoji {
titleString.removeAttribute(NSAttributedString.Key.font, range: NSMakeRange(0, titleString.length))
}
titleString.addAttributes([NSAttributedString.Key.font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize)], range: NSMakeRange(0, titleString.length))
let titleArguments = TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - 32.0 - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())
let (titleLayoutAndApply) = item.context == nil ? makeTitleLayout(titleArguments) : nil
let (titleWithEntitiesLayoutAndApply) = item.context != nil ? makeTitleWithEntitiesLayout(titleArguments) : nil
let titleLayout: TextNodeLayout = (titleWithEntitiesLayoutAndApply?.0 ?? titleLayoutAndApply?.0)!
let (measureTitleLayout, measureTitleSizeApply) = makeMeasureTitleSizeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "A", font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize)), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - 32.0 - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.label ?? "", font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize), textColor: item.presentationData.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - 32.0 - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let separatorHeight = UIScreenPixel
let separatorRightInset: CGFloat = item.systemStyle == .glass ? 16.0 : 0.0
let verticalInset: CGFloat
switch item.systemStyle {
case .glass:
verticalInset = 15.0
case .legacy:
verticalInset = 11.0
}
let contentSize = CGSize(width: params.width, height: max(titleLayout.size.height, measureTitleLayout.size.height) + verticalInset * 2.0)
let insets = itemListNeighborsGroupedInsets(neighbors, params)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
let attributedPlaceholderText = NSAttributedString(string: item.placeholder, font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize), textColor: item.presentationData.theme.list.itemPlaceholderTextColor)
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
strongSelf.textNode.textField.textColor = item.secondaryStyle ? item.presentationData.theme.list.itemSecondaryTextColor : item.presentationData.theme.list.itemPrimaryTextColor
strongSelf.textNode.textField.keyboardAppearance = item.presentationData.theme.rootController.keyboardColor.keyboardAppearance
strongSelf.textNode.textField.tintColor = item.presentationData.theme.list.itemAccentColor
}
if fontUpdated {
strongSelf.textNode.textField.typingAttributes = [NSAttributedString.Key.font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize)]
}
if styleUpdated {
strongSelf.textNode.textField.textColor = item.secondaryStyle ? item.presentationData.theme.list.itemSecondaryTextColor : item.presentationData.theme.list.itemPrimaryTextColor
}
if let titleWithEntitiesApply = titleWithEntitiesLayoutAndApply?.1, let context = item.context {
let _ = titleWithEntitiesApply(
TextNodeWithEntities.Arguments(
context: context,
cache: context.animationCache,
renderer: context.animationRenderer,
placeholderColor: item.presentationData.theme.chat.inputPanel.inputTextColor.withAlphaComponent(0.12),
attemptSynchronous: false
)
)
} else if let titleApply = titleLayoutAndApply?.1 {
let _ = titleApply()
}
strongSelf.titleNode.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((layout.contentSize.height - titleLayout.size.height) / 2.0)), size: titleLayout.size)
let _ = measureTitleSizeApply()
let _ = labelApply()
let secureEntry: Bool
let capitalizationType: UITextAutocapitalizationType
let autocorrectionType: UITextAutocorrectionType
let keyboardType: UIKeyboardType
switch item.type {
case let .regular(capitalization, autocorrection):
secureEntry = false
capitalizationType = capitalization ? .sentences : .none
autocorrectionType = autocorrection ? .default : .no
keyboardType = .default
case .email:
secureEntry = false
capitalizationType = .none
autocorrectionType = .no
keyboardType = .emailAddress
case .password:
secureEntry = true
capitalizationType = .none
autocorrectionType = .no
keyboardType = .default
case .number:
secureEntry = false
capitalizationType = .none
autocorrectionType = .no
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
keyboardType = .asciiCapableNumberPad
} else {
keyboardType = .numberPad
}
case .decimal:
secureEntry = false
capitalizationType = .none
autocorrectionType = .no
keyboardType = .decimalPad
case .username:
secureEntry = false
capitalizationType = .none
autocorrectionType = .no
keyboardType = .asciiCapable
}
if strongSelf.textNode.textField.isSecureTextEntry != secureEntry {
strongSelf.textNode.textField.isSecureTextEntry = secureEntry
}
if strongSelf.textNode.textField.keyboardType != keyboardType {
strongSelf.textNode.textField.keyboardType = keyboardType
}
if strongSelf.textNode.textField.autocapitalizationType != capitalizationType {
strongSelf.textNode.textField.autocapitalizationType = capitalizationType
}
if strongSelf.textNode.textField.autocorrectionType != autocorrectionType {
strongSelf.textNode.textField.autocorrectionType = autocorrectionType
}
if strongSelf.textNode.textField.returnKeyType != item.returnKeyType {
strongSelf.textNode.textField.returnKeyType = item.returnKeyType
}
if let currentText = strongSelf.textNode.textField.text {
if currentText != item.text {
strongSelf.textNode.textField.text = item.text
}
} else {
strongSelf.textNode.textField.text = item.text
}
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset + titleLayout.size.width + item.spacing, y: 0.0), size: CGSize(width: max(1.0, params.width - (leftInset + rightInset + titleLayout.size.width + item.spacing)), height: layout.contentSize.height - 2.0))
strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: layoutSize.width - rightInset - labelLayout.size.width, y: floorToScreenPixels((layout.contentSize.height - labelLayout.size.height) / 2.0)), size: labelLayout.size)
switch item.alignment {
case .default:
strongSelf.textNode.textField.textAlignment = .natural
case .right:
strongSelf.textNode.textField.textAlignment = .right
}
if let image = updatedClearIcon {
strongSelf.clearIconNode.image = image
}
let buttonSize = CGSize(width: 38.0, height: layout.contentSize.height)
strongSelf.clearButtonNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - buttonSize.width, y: 0.0), size: buttonSize)
if let image = strongSelf.clearIconNode.image {
strongSelf.clearIconNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - buttonSize.width + floor((buttonSize.width - image.size.width) / 2.0), y: floor((layout.contentSize.height - image.size.height) / 2.0)), size: image.size)
}
strongSelf.updateClearButtonVisibility()
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - UIScreenPixel), size: CGSize(width: layoutSize.width - bottomStripeInset - params.rightInset - separatorRightInset, height: separatorHeight))
if strongSelf.textNode.textField.attributedPlaceholder == nil || !strongSelf.textNode.textField.attributedPlaceholder!.isEqual(to: attributedPlaceholderText) {
strongSelf.textNode.textField.attributedPlaceholder = attributedPlaceholderText
strongSelf.textNode.textField.accessibilityHint = attributedPlaceholderText.string
}
strongSelf.textNode.isUserInteractionEnabled = item.enabled
strongSelf.textNode.alpha = item.enabled ? 1.0 : 0.4
strongSelf.clearButtonNode.accessibilityLabel = item.presentationData.strings.VoiceOver_Editing_ClearText
}
})
}
}
private func updateClearButtonVisibility() {
guard let item = self.item else {
return
}
let isHidden: Bool
switch item.clearType {
case .none:
isHidden = true
case .always:
isHidden = item.text.isEmpty
case .onFocus:
isHidden = !self.textNode.textField.isFirstResponder || item.text.isEmpty
}
self.clearIconNode.isHidden = isHidden
self.clearButtonNode.isHidden = isHidden
self.clearButtonNode.isAccessibilityElement = isHidden
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
@objc private func textFieldTextChanged(_ textField: UITextField) {
self.textUpdated(self.textNode.textField.text ?? "")
}
@objc private func clearButtonPressed() {
self.textNode.textField.text = ""
self.textUpdated("")
self.item?.cleared?()
}
private func textUpdated(_ text: String) {
self.item?.textUpdated(text)
}
public func focus() {
if !self.textNode.textField.isFirstResponder {
self.textNode.textField.becomeFirstResponder()
}
}
public func selectAll() {
self.textNode.textField.selectAll(nil)
}
@objc public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if let item = self.item {
let newText = ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string)
if !item.shouldUpdateText(newText) {
return false
}
if item.maxLength != 0 && newText.count > item.maxLength {
self.textNode.layer.addShakeAnimation()
let hapticFeedback = HapticFeedback()
hapticFeedback.error()
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.0, execute: {
let _ = hapticFeedback
})
return false
}
}
if string.count > 1, let item = self.item, let processPaste = item.processPaste {
let result = processPaste(string)
if result != string {
var text = textField.text ?? ""
text.replaceSubrange(text.index(text.startIndex, offsetBy: range.lowerBound) ..< text.index(text.startIndex, offsetBy: range.upperBound), with: result)
textField.text = text
if case .username = item.type {
text = text.folding(options: .diacriticInsensitive, locale: .current).replacingOccurrences(of: " ", with: "_")
textField.text = text
}
if let startPosition = textField.position(from: textField.beginningOfDocument, offset: range.lowerBound + result.count) {
let selectionRange = textField.textRange(from: startPosition, to: startPosition)
DispatchQueue.main.async {
textField.selectedTextRange = selectionRange
}
}
self.textFieldTextChanged(textField)
return false
}
}
if let item = self.item, case .username = item.type {
var cleanString = string.folding(options: .diacriticInsensitive, locale: .current).replacingOccurrences(of: " ", with: "_")
let filtered = cleanString.unicodeScalars.filter { validIdentifierSet.contains($0) }
let filteredString = String(String.UnicodeScalarView(filtered))
if cleanString != filteredString {
cleanString = filteredString
self.textNode.layer.addShakeAnimation()
let hapticFeedback = HapticFeedback()
hapticFeedback.error()
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.0, execute: {
let _ = hapticFeedback
})
}
if cleanString != string {
var text = textField.text ?? ""
text.replaceSubrange(text.index(text.startIndex, offsetBy: range.lowerBound) ..< text.index(text.startIndex, offsetBy: range.upperBound), with: cleanString)
textField.text = text
if let startPosition = textField.position(from: textField.beginningOfDocument, offset: range.lowerBound + cleanString.count) {
let selectionRange = textField.textRange(from: startPosition, to: startPosition)
DispatchQueue.main.async {
textField.selectedTextRange = selectionRange
}
}
self.textFieldTextChanged(textField)
return false
}
}
return true
}
@objc public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
self.item?.action()
return false
}
@objc public func textFieldDidBeginEditing(_ textField: UITextField) {
self.item?.updatedFocus?(true)
if self.item?.selectAllOnFocus == true {
DispatchQueue.main.async {
let startPosition = self.textNode.textField.beginningOfDocument
let endPosition = self.textNode.textField.endOfDocument
self.textNode.textField.selectedTextRange = self.textNode.textField.textRange(from: startPosition, to: endPosition)
}
}
self.updateClearButtonVisibility()
}
@objc public func textFieldDidEndEditing(_ textField: UITextField) {
self.item?.updatedFocus?(false)
self.updateClearButtonVisibility()
}
public func animateError() {
self.textNode.layer.addShakeAnimation()
}
}
@@ -0,0 +1,676 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import SwitchNode
import AppBundle
import ComponentFlow
public enum ItemListSwitchItemNodeType {
case regular
case icon
}
public class ItemListSwitchItem: ListViewItem, ItemListItem {
public enum TextColor {
case primary
case accent
}
let presentationData: ItemListPresentationData
let systemStyle: ItemListSystemStyle
let icon: UIImage?
let title: String
let text: String?
let textColor: TextColor
let titleBadgeComponent: AnyComponent<Empty>?
let value: Bool
let type: ItemListSwitchItemNodeType
let enableInteractiveChanges: Bool
let enabled: Bool
let displayLocked: Bool
let disableLeadingInset: Bool
let maximumNumberOfLines: Int
let noCorners: Bool
public let sectionId: ItemListSectionId
let style: ItemListStyle
let updated: (Bool) -> Void
let activatedWhileDisabled: () -> Void
let action: (() -> Void)?
public let tag: ItemListItemTag?
public init(presentationData: ItemListPresentationData, systemStyle: ItemListSystemStyle = .legacy, icon: UIImage? = nil, title: String, text: String? = nil, textColor: TextColor = .primary, titleBadgeComponent: AnyComponent<Empty>? = nil, value: Bool, type: ItemListSwitchItemNodeType = .regular, enableInteractiveChanges: Bool = true, enabled: Bool = true, displayLocked: Bool = false, disableLeadingInset: Bool = false, maximumNumberOfLines: Int = 1, noCorners: Bool = false, sectionId: ItemListSectionId, style: ItemListStyle, updated: @escaping (Bool) -> Void, activatedWhileDisabled: @escaping () -> Void = {}, action: (() -> Void)? = nil, tag: ItemListItemTag? = nil) {
self.presentationData = presentationData
self.systemStyle = systemStyle
self.icon = icon
self.title = title
self.text = text
self.textColor = textColor
self.titleBadgeComponent = titleBadgeComponent
self.value = value
self.type = type
self.enableInteractiveChanges = enableInteractiveChanges
self.enabled = enabled
self.displayLocked = displayLocked
self.disableLeadingInset = disableLeadingInset
self.maximumNumberOfLines = maximumNumberOfLines
self.noCorners = noCorners
self.sectionId = sectionId
self.style = style
self.updated = updated
self.activatedWhileDisabled = activatedWhileDisabled
self.action = action
self.tag = tag
}
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ItemListSwitchItemNode(type: self.type)
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply(false) })
})
}
}
}
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? ItemListSwitchItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
var animated = true
if case .None = animation {
animated = false
}
apply(animated)
})
}
}
}
}
}
public var selectable: Bool {
return self.action != nil && self.enabled
}
public func selected(listView: ListView){
listView.clearHighlightAnimated(true)
if self.enabled {
self.action?()
}
}
}
protocol ItemListSwitchNodeImpl {
var frameColor: UIColor { get set }
var contentColor: UIColor { get set }
var handleColor: UIColor { get set }
var positiveContentColor: UIColor { get set }
var negativeContentColor: UIColor { get set }
var isOn: Bool { get }
func setOn(_ value: Bool, animated: Bool)
}
extension SwitchNode: ItemListSwitchNodeImpl {
var positiveContentColor: UIColor {
get {
return .white
} set(value) {
}
}
var negativeContentColor: UIColor {
get {
return .white
} set(value) {
}
}
}
extension IconSwitchNode: ItemListSwitchNodeImpl {
}
public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
private let maskNode: ASImageNode
private let iconNode: ASImageNode
private let titleNode: TextNode
private var textNode: TextNode?
private var switchNode: ASDisplayNode & ItemListSwitchNodeImpl
private let switchGestureNode: ASDisplayNode
private var disabledOverlayNode: ASDisplayNode?
private var titleBadgeComponentView: ComponentView<Empty>?
private var lockedIconNode: ASImageNode?
private let activateArea: AccessibilityAreaNode
private var item: ItemListSwitchItem?
public var tag: ItemListItemTag? {
return self.item?.tag
}
public init(type: ItemListSwitchItemNodeType) {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.backgroundColor = .white
self.maskNode = ASImageNode()
self.maskNode.isUserInteractionEnabled = false
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.iconNode = ASImageNode()
self.iconNode.isLayerBacked = true
self.iconNode.displaysAsynchronously = false
self.titleNode = TextNode()
self.titleNode.anchorPoint = CGPoint()
self.titleNode.isUserInteractionEnabled = false
switch type {
case .regular:
self.switchNode = SwitchNode()
case .icon:
self.switchNode = IconSwitchNode()
}
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
self.switchGestureNode = ASDisplayNode()
self.activateArea = AccessibilityAreaNode()
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.titleNode)
self.addSubnode(self.switchNode)
self.addSubnode(self.switchGestureNode)
self.addSubnode(self.activateArea)
self.activateArea.activate = { [weak self] in
guard let strongSelf = self, let item = strongSelf.item, item.enabled else {
return false
}
let value = !strongSelf.switchNode.isOn
if item.enableInteractiveChanges {
strongSelf.switchNode.setOn(value, animated: true)
}
item.updated(value)
return true
}
}
override public func didLoad() {
super.didLoad()
(self.switchNode.view as? UISwitch)?.addTarget(self, action: #selector(self.switchValueChanged(_:)), for: .valueChanged)
self.switchGestureNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
}
func asyncLayout() -> (_ item: ItemListSwitchItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeTextLayout = TextNode.asyncLayout(self.textNode)
let currentItem = self.item
var currentDisabledOverlayNode = self.disabledOverlayNode
return { item, params, neighbors in
var contentSize: CGSize
var insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
let separatorRightInset: CGFloat = item.systemStyle == .glass ? 16.0 : 0.0
let itemBackgroundColor: UIColor
let itemSeparatorColor: UIColor
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize)
let textFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0)
var updatedTheme: PresentationTheme?
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
}
var updatedValue = false
if currentItem?.value != item.value {
updatedValue = true
}
var updateIcon = false
if currentItem?.icon != item.icon {
updateIcon = true
}
switch item.style {
case .plain:
itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor
contentSize = CGSize(width: params.width, height: 44.0)
insets = itemListNeighborsPlainInsets(neighbors)
case .blocks:
itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor
contentSize = CGSize(width: params.width, height: 44.0)
insets = itemListNeighborsGroupedInsets(neighbors, params)
}
var topInset: CGFloat
if item.text != nil {
topInset = 9.0
} else {
topInset = 11.0
}
if case .glass = item.systemStyle {
contentSize.height = 52.0
topInset += 4.0
}
var leftInset = 16.0 + params.leftInset
if let _ = item.icon {
leftInset += 43.0
}
if item.disableLeadingInset {
insets.top = 0.0
insets.bottom = 0.0
}
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: item.maximumNumberOfLines, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - params.rightInset - 64.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
contentSize.height = max(contentSize.height, titleLayout.size.height + topInset * 2.0)
var textLayoutAndApply: (TextNodeLayout, () -> TextNode)?
if let text = item.text {
let textColor: UIColor
switch item.textColor {
case .primary:
textColor = item.presentationData.theme.list.itemSecondaryTextColor
case .accent:
textColor = item.presentationData.theme.list.itemAccentColor
}
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: text, font: textFont, textColor: textColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - params.rightInset - 84.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
contentSize.height += -1.0 + textLayout.size.height
textLayoutAndApply = (textLayout, textApply)
}
if !item.enabled {
if currentDisabledOverlayNode == nil {
currentDisabledOverlayNode = ASDisplayNode()
}
} else {
currentDisabledOverlayNode = nil
}
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] animated in
if let strongSelf = self {
let transition: ContainedViewLayoutTransition
if animated {
transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
} else {
transition = .immediate
}
strongSelf.item = item
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
strongSelf.activateArea.accessibilityLabel = item.title
strongSelf.activateArea.accessibilityValue = item.value ? item.presentationData.strings.VoiceOver_Common_On : item.presentationData.strings.VoiceOver_Common_Off
strongSelf.activateArea.accessibilityHint = item.presentationData.strings.VoiceOver_Common_SwitchHint
var accessibilityTraits = UIAccessibilityTraits()
if item.enabled {
} else {
accessibilityTraits.insert(.notEnabled)
}
strongSelf.activateArea.accessibilityTraits = accessibilityTraits
if let icon = item.icon {
var iconTransition = transition
if strongSelf.iconNode.supernode == nil {
iconTransition = .immediate
strongSelf.addSubnode(strongSelf.iconNode)
}
if updateIcon {
strongSelf.iconNode.image = icon
}
let iconY: CGFloat
if item.text == nil {
iconY = floor((layout.contentSize.height - icon.size.height) / 2.0)
} else {
iconY = max(0.0, floor(topInset + titleLayout.size.height + 1.0 - icon.size.height * 0.5))
}
iconTransition.updateFrame(node: strongSelf.iconNode, frame: CGRect(origin: CGPoint(x: params.leftInset + floor((leftInset - params.leftInset - icon.size.width) / 2.0), y: iconY), size: icon.size))
} else if strongSelf.iconNode.supernode != nil {
strongSelf.iconNode.image = nil
strongSelf.iconNode.removeFromSupernode()
}
if let currentDisabledOverlayNode = currentDisabledOverlayNode {
if currentDisabledOverlayNode != strongSelf.disabledOverlayNode {
strongSelf.disabledOverlayNode = currentDisabledOverlayNode
strongSelf.insertSubnode(currentDisabledOverlayNode, belowSubnode: strongSelf.switchGestureNode)
currentDisabledOverlayNode.alpha = 0.0
transition.updateAlpha(node: currentDisabledOverlayNode, alpha: 1.0)
currentDisabledOverlayNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: layout.contentSize.width, height: layout.contentSize.height - separatorHeight))
} else {
transition.updateFrame(node: currentDisabledOverlayNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.contentSize.width, height: layout.contentSize.height - separatorHeight)))
}
currentDisabledOverlayNode.backgroundColor = itemBackgroundColor.withAlphaComponent(0.6)
} else if let disabledOverlayNode = strongSelf.disabledOverlayNode {
transition.updateAlpha(node: disabledOverlayNode, alpha: 0.0, completion: { [weak disabledOverlayNode] _ in
disabledOverlayNode?.removeFromSupernode()
})
strongSelf.disabledOverlayNode = nil
}
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
strongSelf.switchNode.frameColor = item.presentationData.theme.list.itemSwitchColors.frameColor
strongSelf.switchNode.contentColor = item.presentationData.theme.list.itemSwitchColors.contentColor
strongSelf.switchNode.handleColor = item.presentationData.theme.list.itemSwitchColors.handleColor
strongSelf.switchNode.positiveContentColor = item.presentationData.theme.list.itemSwitchColors.positiveColor
strongSelf.switchNode.negativeContentColor = item.presentationData.theme.list.itemSwitchColors.negativeColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
}
let _ = titleApply()
switch item.style {
case .plain:
if strongSelf.backgroundNode.supernode != nil {
strongSelf.backgroundNode.removeFromSupernode()
}
if strongSelf.topStripeNode.supernode != nil {
strongSelf.topStripeNode.removeFromSupernode()
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0)
}
if strongSelf.maskNode.supernode != nil {
strongSelf.maskNode.removeFromSupernode()
}
transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight)))
case .blocks:
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, aboveSubnode: strongSelf.switchGestureNode)
}
let hasCorners = itemListHasRoundedBlockLayout(params) && !item.noCorners
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil
transition.updateFrame(node: strongSelf.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))))
transition.updateFrame(node: strongSelf.maskNode, frame: strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0))
transition.updateFrame(node: strongSelf.topStripeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)))
transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: layoutSize.width - params.rightInset - bottomStripeInset - separatorRightInset, height: separatorHeight)))
}
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: topInset), size: titleLayout.size)
transition.updatePosition(node: strongSelf.titleNode, position: titleFrame.origin)
strongSelf.titleNode.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
if let (textLayout, textApply) = textLayoutAndApply {
let textNode = textApply()
if strongSelf.textNode !== textNode {
strongSelf.textNode?.removeFromSupernode()
strongSelf.textNode = textNode
textNode.isUserInteractionEnabled = false
strongSelf.addSubnode(textNode)
if transition.isAnimated {
textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: strongSelf.titleNode.frame.maxY + 2.0), size: textLayout.size)
} else if let textNode = strongSelf.textNode {
strongSelf.textNode = nil
if transition.isAnimated {
textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak textNode] _ in
textNode?.removeFromSupernode()
})
} else {
textNode.removeFromSupernode()
}
}
if let switchView = strongSelf.switchNode.view as? UISwitch {
if strongSelf.switchNode.bounds.size.width.isZero {
switchView.sizeToFit()
}
let switchSize = switchView.bounds.size
transition.updateFrame(node: strongSelf.switchNode, frame: CGRect(origin: CGPoint(x: params.width - params.rightInset - switchSize.width - 15.0, y: floor((contentSize.height - switchSize.height) / 2.0)), size: switchSize))
strongSelf.switchGestureNode.frame = strongSelf.switchNode.frame
if switchView.isOn != item.value {
switchView.setOn(item.value, animated: animated)
}
switchView.isUserInteractionEnabled = item.enableInteractiveChanges
}
strongSelf.switchGestureNode.isHidden = item.enableInteractiveChanges && item.enabled
if item.displayLocked {
var lockedIconTransition = transition
var updateLockedIconImage = false
if let _ = updatedTheme {
updateLockedIconImage = true
}
if updatedValue {
updateLockedIconImage = true
}
let lockedIconNode: ASImageNode
if let current = strongSelf.lockedIconNode {
lockedIconNode = current
} else {
lockedIconTransition = .immediate
updateLockedIconImage = true
lockedIconNode = ASImageNode()
strongSelf.lockedIconNode = lockedIconNode
strongSelf.insertSubnode(lockedIconNode, aboveSubnode: strongSelf.switchNode)
}
if updateLockedIconImage, let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/TextLockIcon"), color: item.value ? item.presentationData.theme.list.itemSwitchColors.positiveColor : item.presentationData.theme.list.itemSecondaryTextColor) {
lockedIconNode.image = image
}
let switchFrame = strongSelf.switchNode.frame
if let icon = lockedIconNode.image {
let iconOrigin: CGPoint
switch item.systemStyle {
case .glass:
iconOrigin = CGPoint(x: item.value ? switchFrame.maxX - icon.size.width - 16.0 + UIScreenPixel : switchFrame.minX + 16.0 - UIScreenPixel, y: switchFrame.minY + 8.0)
case .legacy:
iconOrigin = CGPoint(x: item.value ? switchFrame.maxX - icon.size.width - 11.0 : switchFrame.minX + 11.0, y: switchFrame.minY + 9.0)
}
lockedIconTransition.updateFrame(node: lockedIconNode, frame: CGRect(origin: iconOrigin, size: icon.size))
}
} else if let lockedIconNode = strongSelf.lockedIconNode {
strongSelf.lockedIconNode = nil
lockedIconNode.removeFromSupernode()
}
if let component = item.titleBadgeComponent {
let componentView: ComponentView<Empty>
if let current = strongSelf.titleBadgeComponentView {
componentView = current
} else {
componentView = ComponentView<Empty>()
strongSelf.titleBadgeComponentView = componentView
}
let badgeSize = componentView.update(
transition: .immediate,
component: component,
environment: {},
containerSize: contentSize
)
if let view = componentView.view {
var titleBadgeTransition = transition
if view.superview == nil {
titleBadgeTransition = .immediate
strongSelf.view.addSubview(view)
}
titleBadgeTransition.updateFrame(view: view, frame: CGRect(origin: CGPoint(x: strongSelf.titleNode.frame.maxX + 7.0, y: floor((contentSize.height - badgeSize.height) / 2.0)), size: badgeSize))
}
} else if let componentView = strongSelf.titleBadgeComponentView {
strongSelf.titleBadgeComponentView = nil
componentView.view?.removeFromSuperview()
}
transition.updateFrame(node: strongSelf.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layoutSize.height + UIScreenPixel + UIScreenPixel)))
}
})
}
}
override public func accessibilityActivate() -> Bool {
guard let item = self.item else {
return false
}
if !item.enabled {
return false
}
if let switchNode = self.switchNode as? IconSwitchNode {
switchNode.isOn = !switchNode.isOn
item.updated(switchNode.isOn)
} else if let switchNode = self.switchNode as? SwitchNode {
switchNode.isOn = !switchNode.isOn
item.updated(switchNode.isOn)
}
return true
}
override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
super.setHighlighted(highlighted, at: point, animated: animated)
if highlighted {
self.highlightedBackgroundNode.alpha = 1.0
if self.highlightedBackgroundNode.supernode == nil {
var anchorNode: ASDisplayNode?
if self.bottomStripeNode.supernode != nil {
anchorNode = self.bottomStripeNode
} else if self.topStripeNode.supernode != nil {
anchorNode = self.topStripeNode
} else if self.backgroundNode.supernode != nil {
anchorNode = self.backgroundNode
}
if let anchorNode = anchorNode {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
} else {
self.addSubnode(self.highlightedBackgroundNode)
}
}
} else {
if self.highlightedBackgroundNode.supernode != nil {
if animated {
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
if let strongSelf = self {
if completed {
strongSelf.highlightedBackgroundNode.removeFromSupernode()
}
}
})
self.highlightedBackgroundNode.alpha = 0.0
} else {
self.highlightedBackgroundNode.removeFromSupernode()
}
}
}
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.allowsGroupOpacity = true
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4, completion: { [weak self] _ in
self?.layer.allowsGroupOpacity = false
})
}
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.allowsGroupOpacity = true
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
@objc private func switchValueChanged(_ switchView: UISwitch) {
if let item = self.item {
let value = switchView.isOn
item.updated(value)
}
}
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if let item = self.item, let switchView = self.switchNode.view as? UISwitch, case .ended = recognizer.state {
if item.enabled && !item.displayLocked {
let value = switchView.isOn
item.updated(!value)
} else {
item.activatedWhileDisabled()
}
}
}
}
@@ -0,0 +1,66 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
public final class ItemListTextEmptyStateItem: ItemListControllerEmptyStateItem {
public let text: String
public init(text: String) {
self.text = text
}
public func isEqual(to: ItemListControllerEmptyStateItem) -> Bool {
if let to = to as? ItemListTextEmptyStateItem {
return self.text == to.text
} else {
return false
}
}
public func node(current: ItemListControllerEmptyStateItemNode?) -> ItemListControllerEmptyStateItemNode {
let result: ItemListTextEmptyStateItemNode
if let current = current as? ItemListTextEmptyStateItemNode {
result = current
} else {
result = ItemListTextEmptyStateItemNode()
}
result.updateText(text: self.text)
return result
}
}
public final class ItemListTextEmptyStateItemNode: ItemListControllerEmptyStateItemNode {
private let textNode: ASTextNode
private var validLayout: (ContainerViewLayout, CGFloat)?
private var text: String?
override public init() {
self.textNode = ASTextNode()
self.textNode.isUserInteractionEnabled = false
super.init()
self.addSubnode(self.textNode)
}
public func updateText(text: String) {
if self.text != text {
self.text = text
self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: .gray, paragraphAlignment: .center)
if let validLayout = self.validLayout {
self.updateLayout(layout: validLayout.0, navigationBarHeight: validLayout.1, transition: .immediate)
}
}
}
override public func updateLayout(layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.validLayout = (layout, navigationBarHeight)
var insets = layout.insets(options: [.statusBar])
insets.top += navigationBarHeight
let textSize = self.textNode.measure(CGSize(width: layout.size.width - 40.0 - layout.safeInsets.left - layout.safeInsets.right - layout.intrinsicInsets.left - layout.intrinsicInsets.right, height: max(1.0, layout.size.height - insets.top - insets.bottom)))
self.textNode.frame = CGRect(origin: CGPoint(x: layout.safeInsets.left + layout.intrinsicInsets.left + floor((layout.size.width - layout.safeInsets.left - layout.safeInsets.right - layout.intrinsicInsets.left - layout.intrinsicInsets.right - textSize.width) / 2.0), y: insets.top + floor((layout.size.height - insets.top - insets.bottom - textSize.height) / 2.0)), size: textSize)
}
}
@@ -0,0 +1,330 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import TextFormat
import Markdown
import TextNodeWithEntities
import AccountContext
public enum ItemListTextItemText {
case plain(String)
case markdown(String)
case custom(context: AccountContext, string: NSAttributedString)
var text: String {
switch self {
case let .plain(text), let .markdown(text):
return text
case let .custom(_, string):
return string.string
}
}
}
public enum ItemListTextItemLinkAction {
case tap(String)
}
public enum ItemListTextItemTextAlignment {
case natural
case center
var textAlignment: NSTextAlignment {
switch self {
case .natural:
return .natural
case .center:
return .center
}
}
}
public enum ItemListTextItemTextSize {
case generic
case larger
}
public class ItemListTextItem: ListViewItem, ItemListItem {
let presentationData: ItemListPresentationData
let text: ItemListTextItemText
public let sectionId: ItemListSectionId
let linkAction: ((ItemListTextItemLinkAction) -> Void)?
let style: ItemListStyle
let textSize: ItemListTextItemTextSize
let textAlignment: ItemListTextItemTextAlignment
let additionalInsets: UIEdgeInsets
let additionalOuterInsets: UIEdgeInsets
public let isAlwaysPlain: Bool = true
public let tag: ItemListItemTag?
public init(presentationData: ItemListPresentationData, text: ItemListTextItemText, sectionId: ItemListSectionId, linkAction: ((ItemListTextItemLinkAction) -> Void)? = nil, style: ItemListStyle = .blocks, textSize: ItemListTextItemTextSize = .generic, textAlignment: ItemListTextItemTextAlignment = .natural, tag: ItemListItemTag? = nil, additionalInsets: UIEdgeInsets = .zero, additionalOuterInsets: UIEdgeInsets = .zero) {
self.presentationData = presentationData
self.text = text
self.sectionId = sectionId
self.linkAction = linkAction
self.style = style
self.textSize = textSize
self.textAlignment = textAlignment
self.additionalInsets = additionalInsets
self.additionalOuterInsets = additionalOuterInsets
self.tag = tag
}
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ItemListTextItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
guard let nodeValue = node() as? ItemListTextItemNode else {
assertionFailure()
return
}
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode {
private let textNode: TextNodeWithEntities
private var linkHighlightingNode: LinkHighlightingNode?
private let activateArea: AccessibilityAreaNode
private var item: ItemListTextItem?
private var chevronImage: UIImage?
public var tag: ItemListItemTag? {
return self.item?.tag
}
public init() {
self.textNode = TextNodeWithEntities()
self.textNode.textNode.isUserInteractionEnabled = false
self.textNode.textNode.contentMode = .left
self.textNode.textNode.contentsScale = UIScreen.main.scale
self.activateArea = AccessibilityAreaNode()
self.activateArea.accessibilityTraits = .staticText
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.textNode.textNode)
self.addSubnode(self.activateArea)
}
override public func didLoad() {
super.didLoad()
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
recognizer.tapActionAtPoint = { _ in
return .waitForSingleTap
}
recognizer.highlight = { [weak self] point in
if let strongSelf = self {
strongSelf.updateTouchesAtPoint(point)
}
}
self.view.addGestureRecognizer(recognizer)
}
public func asyncLayout() -> (_ item: ItemListTextItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNodeWithEntities.asyncLayout(self.textNode)
let currentChevronImage = self.chevronImage
let currentItem = self.item
return { [weak self] item, params, neighbors in
let leftInset: CGFloat = 15.0
var topInset: CGFloat = 7.0
var bottomInset: CGFloat = 7.0
var titleFont = Font.regular(item.presentationData.fontSize.itemListBaseHeaderFontSize)
var textColor: UIColor = item.presentationData.theme.list.freeTextColor
if case .larger = item.textSize {
titleFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize / 17.0 * 15.0))
textColor = item.presentationData.theme.list.itemSecondaryTextColor
}
let titleBoldFont = Font.semibold(item.presentationData.fontSize.itemListBaseHeaderFontSize)
var themeUpdated = false
var chevronImage = currentChevronImage
if currentItem?.presentationData.theme !== item.presentationData.theme {
themeUpdated = true
}
let attributedText: NSAttributedString
switch item.text {
case let .plain(text):
attributedText = NSAttributedString(string: text, font: titleFont, textColor: item.presentationData.theme.list.freeTextColor)
case let .markdown(text):
let mutableAttributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: textColor), bold: MarkdownAttributeSet(font: titleBoldFont, textColor: textColor), link: MarkdownAttributeSet(font: titleFont, textColor: item.presentationData.theme.list.itemAccentColor), linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
}), textAlignment: item.textAlignment.textAlignment).mutableCopy() as! NSMutableAttributedString
if let _ = text.range(of: ">]"), let range = mutableAttributedText.string.range(of: ">") {
if themeUpdated || currentChevronImage == nil {
switch item.textSize {
case .generic:
chevronImage = generateTintedImage(image: UIImage(bundleImageName: "Contact List/SubtitleArrow"), color: item.presentationData.theme.list.itemAccentColor)
case .larger:
chevronImage = generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: item.presentationData.theme.list.itemAccentColor)
}
}
if let chevronImage {
mutableAttributedText.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: mutableAttributedText.string))
}
}
attributedText = mutableAttributedText
case let .custom(_, string):
attributedText = string
}
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset * 2.0 - params.leftInset - params.rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: item.textAlignment.textAlignment, cutout: nil, insets: UIEdgeInsets()))
let contentSize: CGSize
var insets = itemListNeighborsGroupedInsets(neighbors, params)
insets.top += item.additionalOuterInsets.top
insets.bottom += item.additionalOuterInsets.bottom
topInset += item.additionalInsets.top
bottomInset += item.additionalInsets.bottom
contentSize = CGSize(width: params.width, height: titleLayout.size.height + topInset + bottomInset)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
return (layout, {
if let strongSelf = self {
strongSelf.item = item
strongSelf.chevronImage = chevronImage
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
strongSelf.activateArea.accessibilityLabel = attributedText.string
var textArguments: TextNodeWithEntities.Arguments?
if case let .custom(context, _) = item.text {
textArguments = TextNodeWithEntities.Arguments(
context: context,
cache: context.animationCache,
renderer: context.animationRenderer,
placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor,
attemptSynchronous: true
)
}
let _ = titleApply(textArguments)
let titleOrigin: CGFloat
switch item.textAlignment {
case .natural:
titleOrigin = leftInset + params.leftInset
case .center:
titleOrigin = floorToScreenPixels((contentSize.width - titleLayout.size.width) / 2.0)
}
strongSelf.textNode.textNode.frame = CGRect(origin: CGPoint(x: titleOrigin, y: topInset), size: titleLayout.size)
}
})
}
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
@objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
switch recognizer.state {
case .ended:
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
switch gesture {
case .tap:
let titleFrame = self.textNode.textNode.frame
if let item = self.item, titleFrame.contains(location) {
if let (_, attributes) = self.textNode.textNode.attributesAtPoint(CGPoint(x: location.x - titleFrame.minX, y: location.y - titleFrame.minY)) {
if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
item.linkAction?(.tap(url))
}
}
}
default:
break
}
}
default:
break
}
}
private func updateTouchesAtPoint(_ point: CGPoint?) {
if let item = self.item {
var rects: [CGRect]?
if let point = point {
let textNodeFrame = self.textNode.textNode.frame
if let (index, attributes) = self.textNode.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) {
let possibleNames: [String] = [
TelegramTextAttributes.URL,
TelegramTextAttributes.PeerMention,
TelegramTextAttributes.PeerTextMention,
TelegramTextAttributes.BotCommand,
TelegramTextAttributes.Hashtag
]
for name in possibleNames {
if let _ = attributes[NSAttributedString.Key(rawValue: name)] {
rects = self.textNode.textNode.attributeRects(name: name, at: index)
break
}
}
}
}
if var rects {
let linkHighlightingNode: LinkHighlightingNode
if let current = self.linkHighlightingNode {
linkHighlightingNode = current
} else {
linkHighlightingNode = LinkHighlightingNode(color: item.presentationData.theme.list.itemAccentColor.withAlphaComponent(0.2))
self.linkHighlightingNode = linkHighlightingNode
self.insertSubnode(linkHighlightingNode, belowSubnode: self.textNode.textNode)
}
if item.text.text.contains(">]"), var lastRect = rects.last {
lastRect.size.width += 8.0
rects[rects.count - 1] = lastRect
}
linkHighlightingNode.frame = self.textNode.textNode.frame
linkHighlightingNode.updateRects(rects)
} else if let linkHighlightingNode = self.linkHighlightingNode {
self.linkHighlightingNode = nil
linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in
linkHighlightingNode?.removeFromSupernode()
})
}
}
}
}
@@ -0,0 +1,479 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import TextFormat
import AccountContext
public enum ItemListTextWithLabelItemTextColor {
case primary
case accent
case highlighted
}
public final class ItemListTextWithLabelItem: ListViewItem, ItemListItem {
let presentationData: ItemListPresentationData
public let label: String
public let text: String
let style: ItemListStyle
let labelColor: ItemListTextWithLabelItemTextColor
let textColor: ItemListTextWithLabelItemTextColor
let enabledEntityTypes: EnabledEntityTypes
let multiline: Bool
let selected: Bool?
public let sectionId: ItemListSectionId
let action: (() -> Void)?
let longTapAction: (() -> Void)?
let linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)?
public let tag: Any?
public init(presentationData: ItemListPresentationData, label: String, text: String, style: ItemListStyle = .plain, labelColor: ItemListTextWithLabelItemTextColor = .primary, textColor: ItemListTextWithLabelItemTextColor = .primary, enabledEntityTypes: EnabledEntityTypes, multiline: Bool, selected: Bool? = nil, sectionId: ItemListSectionId, action: (() -> Void)?, longTapAction: (() -> Void)? = nil, linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)? = nil, tag: Any? = nil) {
self.presentationData = presentationData
self.label = label
self.text = text
self.style = style
self.labelColor = labelColor
self.textColor = textColor
self.enabledEntityTypes = enabledEntityTypes
self.multiline = multiline
self.selected = selected
self.sectionId = sectionId
self.action = action
self.longTapAction = longTapAction
self.linkItemAction = linkItemAction
self.tag = tag
}
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ItemListTextWithLabelItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply(.None) })
})
}
}
}
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? ItemListTextWithLabelItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply(animation)
})
}
}
}
}
}
public var selectable: Bool {
return self.action != nil
}
public func selected(listView: ListView) {
listView.clearHighlightAnimated(true)
self.action?()
}
}
public class ItemListTextWithLabelItemNode: ListViewItemNode {
let labelNode: TextNode
let textNode: TextNode
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
private var linkHighlightingNode: LinkHighlightingNode?
private var selectionNode: ItemListSelectableControlNode?
private let maskNode: ASImageNode
public var item: ItemListTextWithLabelItem?
override public var canBeLongTapped: Bool {
return true
}
public init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.maskNode.isUserInteractionEnabled = false
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
self.labelNode = TextNode()
self.labelNode.isUserInteractionEnabled = false
self.labelNode.contentMode = .left
self.labelNode.contentsScale = UIScreen.main.scale
self.textNode = TextNode()
self.textNode.isUserInteractionEnabled = false
self.textNode.contentMode = .left
self.textNode.contentsScale = UIScreen.main.scale
super.init(layerBacked: false, dynamicBounce: false)
self.isAccessibilityElement = true
self.addSubnode(self.labelNode)
self.addSubnode(self.textNode)
}
override public func didLoad() {
super.didLoad()
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
recognizer.tapActionAtPoint = { [weak self] point in
if let strongSelf = self, strongSelf.linkItemAtPoint(point) != nil {
return .waitForSingleTap
}
return .fail
}
recognizer.highlight = { [weak self] point in
if let strongSelf = self {
strongSelf.updateTouchesAtPoint(point)
}
}
self.view.addGestureRecognizer(recognizer)
}
public func asyncLayout() -> (_ item: ItemListTextWithLabelItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) {
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
let makeTextLayout = TextNode.asyncLayout(self.textNode)
let currentItem = self.item
let selectionNodeLayout = ItemListSelectableControlNode.asyncLayout(self.selectionNode)
return { item, params, neighbors in
var updatedTheme: PresentationTheme?
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
}
let insets = itemListNeighborsPlainInsets(neighbors)
let leftInset: CGFloat = 16.0 + params.leftInset
let rightInset: CGFloat = 8.0 + params.rightInset
let separatorHeight = UIScreenPixel
let labelFont = Font.regular(item.presentationData.fontSize.itemListBaseLabelFontSize)
let textFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize)
let textBoldFont = Font.medium(item.presentationData.fontSize.itemListBaseFontSize)
let textItalicFont = Font.italic(item.presentationData.fontSize.itemListBaseFontSize)
let textBoldItalicFont = Font.semiboldItalic(item.presentationData.fontSize.itemListBaseFontSize)
let textFixedFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize)
var leftOffset: CGFloat = 0.0
var selectionNodeWidthAndApply: (CGFloat, (CGSize, Bool) -> ItemListSelectableControlNode)?
if let selected = item.selected {
let (selectionWidth, selectionApply) = selectionNodeLayout(item.presentationData.theme.list.itemCheckColors.strokeColor, item.presentationData.theme.list.itemCheckColors.fillColor, item.presentationData.theme.list.itemCheckColors.foregroundColor, selected, .regular)
selectionNodeWidthAndApply = (selectionWidth, selectionApply)
leftOffset += selectionWidth - 8.0
}
let labelColor: UIColor
switch item.labelColor {
case .primary:
labelColor = item.presentationData.theme.list.itemPrimaryTextColor
case .accent:
labelColor = item.presentationData.theme.list.itemAccentColor
case .highlighted:
labelColor = item.presentationData.theme.list.itemHighlightedColor
}
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.label, font: labelFont, textColor: labelColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftOffset - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let entities = generateTextEntities(item.text, enabledTypes: item.enabledEntityTypes)
let baseColor: UIColor
switch item.textColor {
case .primary:
baseColor = item.presentationData.theme.list.itemPrimaryTextColor
case .accent:
baseColor = item.presentationData.theme.list.itemAccentColor
case .highlighted:
baseColor = item.presentationData.theme.list.itemHighlightedColor
}
let string = stringWithAppliedEntities(item.text, entities: entities, baseColor: baseColor, linkColor: item.presentationData.theme.list.itemAccentColor, baseFont: textFont, linkFont: textFont, boldFont: textBoldFont, italicFont: textItalicFont, boldItalicFont: textBoldItalicFont, fixedFont: textFixedFont, blockQuoteFont: textFont, message: nil)
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: string, backgroundColor: nil, maximumNumberOfLines: item.multiline ? 0 : 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftOffset - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let contentSize = CGSize(width: params.width, height: textLayout.size.height + labelLayout.size.height + 22.0)
let nodeLayout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
return (nodeLayout, { [weak self] animation in
if let strongSelf = self {
let transition: ContainedViewLayoutTransition
if animation.isAnimated {
transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
} else {
transition = .immediate
}
strongSelf.item = item
strongSelf.accessibilityLabel = item.label
strongSelf.accessibilityValue = item.text
if let _ = updatedTheme {
switch item.style {
case .plain:
strongSelf.topStripeNode.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor
strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.plainBackgroundColor
case .blocks:
strongSelf.topStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
}
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
}
let _ = labelApply()
let _ = textApply()
if let (selectionWidth, selectionApply) = selectionNodeWidthAndApply {
let selectionFrame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: selectionWidth, height: nodeLayout.contentSize.height))
let selectionNode = selectionApply(selectionFrame.size, transition.isAnimated)
if selectionNode !== strongSelf.selectionNode {
strongSelf.selectionNode?.removeFromSupernode()
strongSelf.selectionNode = selectionNode
strongSelf.addSubnode(selectionNode)
selectionNode.frame = selectionFrame
transition.animatePosition(node: selectionNode, from: CGPoint(x: -selectionFrame.size.width / 2.0, y: selectionFrame.midY))
} else {
transition.updateFrame(node: selectionNode, frame: selectionFrame)
}
} else if let selectionNode = strongSelf.selectionNode {
strongSelf.selectionNode = nil
let selectionFrame = selectionNode.frame
transition.updatePosition(node: selectionNode, position: CGPoint(x: -selectionFrame.size.width / 2.0, y: selectionFrame.midY), completion: { [weak selectionNode] _ in
selectionNode?.removeFromSupernode()
})
}
let labelFrame = CGRect(origin: CGPoint(x: leftOffset + leftInset, y: 11.0), size: labelLayout.size)
strongSelf.labelNode.frame = labelFrame
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftOffset + leftInset, y: labelFrame.maxY + 3.0), size: textLayout.size)
let leftInset: CGFloat
switch item.style {
case .plain:
leftInset = 16.0 + params.leftInset + leftOffset
if strongSelf.backgroundNode.supernode != nil {
strongSelf.backgroundNode.removeFromSupernode()
}
if strongSelf.topStripeNode.supernode != nil {
strongSelf.topStripeNode.removeFromSupernode()
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0)
}
if strongSelf.maskNode.supernode != nil {
strongSelf.maskNode.removeFromSupernode()
}
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight))
case .blocks:
leftInset = 16.0 + params.leftInset
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = 16.0 + params.leftInset
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight))
}
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: contentSize.height + UIScreenPixel + UIScreenPixel))
}
})
}
}
override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
super.setHighlighted(highlighted, at: point, animated: animated)
if highlighted && self.linkItemAtPoint(point) == nil && self.selectionNode == nil {
self.highlightedBackgroundNode.alpha = 1.0
if self.highlightedBackgroundNode.supernode == nil {
var anchorNode: ASDisplayNode?
if self.bottomStripeNode.supernode != nil {
anchorNode = self.bottomStripeNode
} else if self.topStripeNode.supernode != nil {
anchorNode = self.topStripeNode
} else if self.backgroundNode.supernode != nil {
anchorNode = self.backgroundNode
}
if let anchorNode = anchorNode {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
} else {
self.addSubnode(self.highlightedBackgroundNode)
}
}
} else {
if self.highlightedBackgroundNode.supernode != nil {
if animated {
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
if let strongSelf = self {
if completed {
strongSelf.highlightedBackgroundNode.removeFromSupernode()
}
}
})
self.highlightedBackgroundNode.alpha = 0.0
} else {
self.highlightedBackgroundNode.removeFromSupernode()
}
}
}
}
@objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
switch recognizer.state {
case .ended:
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
switch gesture {
case .tap, .longTap:
if let item = self.item, let linkItem = self.linkItemAtPoint(location) {
item.linkItemAction?(gesture == .tap ? .tap : .longTap, linkItem)
}
default:
break
}
}
default:
break
}
}
private func linkItemAtPoint(_ point: CGPoint) -> TextLinkItem? {
let textNodeFrame = self.textNode.frame
if let (_, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) {
if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
return .url(url: url, concealed: false)
} else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String {
return .mention(peerName)
} else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag {
return .hashtag(hashtag.peerName, hashtag.hashtag)
} else {
return nil
}
}
return nil
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
override public func longTapped() {
self.item?.longTapAction?()
}
private func updateTouchesAtPoint(_ point: CGPoint?) {
if let item = self.item {
var rects: [CGRect]?
if let point = point {
let textNodeFrame = self.textNode.frame
if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) {
let possibleNames: [String] = [
TelegramTextAttributes.URL,
TelegramTextAttributes.PeerMention,
TelegramTextAttributes.PeerTextMention,
TelegramTextAttributes.BotCommand,
TelegramTextAttributes.Hashtag
]
for name in possibleNames {
if let _ = attributes[NSAttributedString.Key(rawValue: name)] {
rects = self.textNode.attributeRects(name: name, at: index)
break
}
}
}
}
if let rects = rects {
let linkHighlightingNode: LinkHighlightingNode
if let current = self.linkHighlightingNode {
linkHighlightingNode = current
} else {
linkHighlightingNode = LinkHighlightingNode(color: item.presentationData.theme.list.itemAccentColor.withAlphaComponent(0.2))
self.linkHighlightingNode = linkHighlightingNode
self.insertSubnode(linkHighlightingNode, belowSubnode: self.textNode)
}
linkHighlightingNode.frame = self.textNode.frame
linkHighlightingNode.updateRects(rects)
} else if let linkHighlightingNode = self.linkHighlightingNode {
self.linkHighlightingNode = nil
linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in
linkHighlightingNode?.removeFromSupernode()
})
}
}
}
public var tag: Any? {
return self.item?.tag
}
}
@@ -0,0 +1,6 @@
import Foundation
import Display
public protocol ListViewItemWithHeader: ListViewItem {
var header: ListViewItemHeader? { get }
}