import Foundation import UIKit import AsyncDisplayKit import Display import TelegramCore import SwiftSignalKit import TelegramPresentationData import TelegramUIPreferences import AccountContext import ContextUI private let detailsInset: CGFloat = 17.0 private let titleInset: CGFloat = 22.0 public final class InstantPageDetailsNode: ASDisplayNode, InstantPageNode { private let context: AccountContext private let strings: PresentationStrings private let nameDisplayOrder: PresentationPersonNameOrder private let theme: InstantPageTheme public let item: InstantPageDetailsItem private let titleTile: InstantPageTile private let titleTileNode: InstantPageTileNode private let highlightedBackgroundNode: ASDisplayNode private let buttonNode: HighlightableButtonNode private let arrowNode: InstantPageDetailsArrowNode let separatorNode: ASDisplayNode public let contentNode: InstantPageContentNode private let updateExpanded: (Bool) -> Void var expanded: Bool public var previousNode: InstantPageDetailsNode? public var requestLayoutUpdate: ((Bool) -> Void)? init(context: AccountContext, sourceLocation: InstantPageSourceLocation, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, item: InstantPageDetailsItem, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, currentlyExpanded: Bool?, updateDetailsExpanded: @escaping (Bool) -> Void, getPreloadedResource: @escaping (String) -> Data?) { self.context = context self.strings = strings self.nameDisplayOrder = nameDisplayOrder self.theme = theme self.item = item self.updateExpanded = updateDetailsExpanded let frame = item.frame self.highlightedBackgroundNode = ASDisplayNode() self.highlightedBackgroundNode.isLayerBacked = true self.highlightedBackgroundNode.alpha = 0.0 self.buttonNode = HighlightableButtonNode() self.titleTile = InstantPageTile(frame: CGRect(x: 0.0, y: 0.0, width: frame.width, height: item.titleHeight)) self.titleTile.items.append(contentsOf: item.titleItems) self.titleTileNode = InstantPageTileNode(tile: self.titleTile, backgroundColor: .clear) if let expanded = currentlyExpanded { self.expanded = expanded } else { self.expanded = item.initiallyExpanded } self.arrowNode = InstantPageDetailsArrowNode(color: theme.controlColor, open: self.expanded) self.separatorNode = ASDisplayNode() self.contentNode = InstantPageContentNode(context: context, strings: strings, nameDisplayOrder: nameDisplayOrder, sourceLocation: sourceLocation, theme: theme, items: item.items, contentSize: CGSize(width: item.frame.width, height: item.frame.height - item.titleHeight), openMedia: openMedia, longPressMedia: longPressMedia, activatePinchPreview: activatePinchPreview, pinchPreviewFinished: pinchPreviewFinished, openPeer: openPeer, openUrl: openUrl, getPreloadedResource: getPreloadedResource) super.init() self.clipsToBounds = true self.addSubnode(self.contentNode) self.addSubnode(self.highlightedBackgroundNode) self.addSubnode(self.buttonNode) self.addSubnode(self.titleTileNode) self.addSubnode(self.arrowNode) self.addSubnode(self.separatorNode) self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) self.buttonNode.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { if highlighted { strongSelf.highlightedBackgroundNode.layer.removeAnimation(forKey: "opacity") strongSelf.highlightedBackgroundNode.alpha = 1.0 strongSelf.separatorNode.alpha = 0.0 if let previousSeparator = strongSelf.previousNode?.separatorNode { previousSeparator.alpha = 0.0 } } else { strongSelf.highlightedBackgroundNode.alpha = 0.0 strongSelf.highlightedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) strongSelf.separatorNode.alpha = 1.0 strongSelf.separatorNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) if let previousSeparator = strongSelf.previousNode?.separatorNode { previousSeparator.alpha = 1.0 previousSeparator.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } } } } self.contentNode.requestLayoutUpdate = { [weak self] animated in self?.requestLayoutUpdate?(animated) } self.update(strings: strings, theme: theme) } @objc func buttonPressed() { self.setExpanded(!self.expanded, animated: true) self.updateExpanded(expanded) } public func setExpanded(_ expanded: Bool, animated: Bool) { self.expanded = expanded self.arrowNode.setOpen(expanded, animated: animated) } public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { } public override func layout() { super.layout() let size = self.bounds.size let inset = detailsInset + self.item.safeInset self.titleTileNode.frame = self.titleTile.frame self.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: self.item.titleHeight + UIScreenPixel)) self.buttonNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: self.item.titleHeight)) self.arrowNode.frame = CGRect(x: inset, y: floorToScreenPixels((self.item.titleHeight - 8.0) / 2.0) + 1.0, width: 13.0, height: 8.0) self.contentNode.frame = CGRect(x: 0.0, y: self.item.titleHeight, width: size.width, height: self.item.frame.height - self.item.titleHeight) let lineSize = CGSize(width: self.frame.width - inset, height: UIScreenPixel) self.separatorNode.frame = CGRect(origin: CGPoint(x: self.item.rtl ? 0.0 : inset, y: self.item.titleHeight - lineSize.height), size: lineSize) } public func updateIsVisible(_ isVisible: Bool) { } public func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { return self.contentNode.transitionNode(media: media) } public func updateHiddenMedia(media: InstantPageMedia?) { self.contentNode.updateHiddenMedia(media: media) } public func update(strings: PresentationStrings, theme: InstantPageTheme) { self.arrowNode.color = theme.controlColor self.separatorNode.backgroundColor = theme.controlColor self.highlightedBackgroundNode.backgroundColor = theme.panelHighlightedBackgroundColor } public func updateVisibleItems(visibleBounds: CGRect, animated: Bool) { if self.bounds.height > self.item.titleHeight { self.contentNode.updateVisibleItems(visibleBounds: visibleBounds.offsetBy(dx: -self.contentNode.frame.minX, dy: -self.contentNode.frame.minY), animated: animated) } } public func textItemAtLocation(_ location: CGPoint) -> (InstantPageTextItem, CGPoint)? { if self.titleTileNode.frame.contains(location) { for case let item as InstantPageTextItem in self.item.titleItems { if item.frame.contains(location) { return (item, self.titleTileNode.frame.origin) } } } else if let (textItem, parentOffset) = self.contentNode.textItemAtLocation(location.offsetBy(dx: -self.contentNode.frame.minX, dy: -self.contentNode.frame.minY)) { return (textItem, self.contentNode.frame.origin.offsetBy(dx: parentOffset.x, dy: parentOffset.y)) } return nil } public func tapActionAtPoint(_ point: CGPoint) -> TapLongTapOrDoubleTapGestureRecognizerAction { if self.titleTileNode.frame.contains(point) { if self.item.linkSelectionRects(at: point).isEmpty { return .fail } } else if self.contentNode.frame.contains(point) { return self.contentNode.tapActionAtPoint(_: point.offsetBy(dx: -self.contentNode.frame.minX, dy: -self.contentNode.frame.minY)) } return .waitForSingleTap } public var effectiveContentSize: CGSize { return self.contentNode.effectiveContentSize } public func effectiveFrameForItem(_ item: InstantPageItem) -> CGRect { return self.contentNode.effectiveFrameForItem(item).offsetBy(dx: 0.0, dy: self.item.titleHeight) } } private final class InstantPageDetailsArrowNodeParameters: NSObject { let color: UIColor let progress: CGFloat init(color: UIColor, progress: CGFloat) { self.color = color self.progress = progress } } final class InstantPageDetailsArrowNode : ASDisplayNode { var color: UIColor { didSet { self.setNeedsDisplay() } } private(set) var open: Bool private var progress: CGFloat = 0.0 private var targetProgress: CGFloat? private var displayLink: CADisplayLink? init(color: UIColor, open: Bool) { self.color = color self.open = open self.progress = open ? 1.0 : 0.0 super.init() self.isOpaque = false self.isLayerBacked = true class DisplayLinkProxy: NSObject { weak var target: InstantPageDetailsArrowNode? init(target: InstantPageDetailsArrowNode) { self.target = target } @objc func displayLinkEvent() { self.target?.displayLinkEvent() } } self.displayLink = CADisplayLink(target: DisplayLinkProxy(target: self), selector: #selector(DisplayLinkProxy.displayLinkEvent)) self.displayLink?.isPaused = true self.displayLink?.add(to: RunLoop.main, forMode: .common) } deinit { self.displayLink?.invalidate() } func setOpen(_ open: Bool, animated: Bool) { self.open = open let openProgress: CGFloat = open ? 1.0 : 0.0 if animated { self.targetProgress = openProgress self.displayLink?.isPaused = false } else { self.progress = openProgress self.targetProgress = nil self.displayLink?.isPaused = true } } override func willEnterHierarchy() { super.willEnterHierarchy() if self.targetProgress != nil { self.displayLink?.isPaused = false } } override func didExitHierarchy() { super.didExitHierarchy() self.displayLink?.isPaused = true } private func displayLinkEvent() { if let targetProgress = self.targetProgress { let sign = CGFloat(targetProgress - self.progress > 0 ? 1 : -1) self.progress += 0.14 * sign if sign > 0 && self.progress > targetProgress { self.progress = 1.0 self.targetProgress = nil self.displayLink?.isPaused = true } else if sign < 0 && self.progress < targetProgress { self.progress = 0.0 self.targetProgress = nil self.displayLink?.isPaused = true } } self.setNeedsDisplay() } override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { return InstantPageDetailsArrowNodeParameters(color: self.color, progress: self.progress) } @objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { let context = UIGraphicsGetCurrentContext()! if let parameters = parameters as? InstantPageDetailsArrowNodeParameters { context.setStrokeColor(parameters.color.cgColor) context.setLineCap(.round) context.setLineWidth(2.0) context.move(to: CGPoint(x: 1.0, y: 1.0 + 5.0 * parameters.progress)) context.addLine(to: CGPoint(x: 6.0, y: 6.0 - 5.0 * parameters.progress)) context.addLine(to: CGPoint(x: 11.0, y: 1.0 + 5.0 * parameters.progress)) context.strokePath() } } }