mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
1233 lines
51 KiB
Swift
1233 lines
51 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import AsyncDisplayKit
|
|
import Display
|
|
import TelegramPresentationData
|
|
import TextSelectionNode
|
|
import TelegramCore
|
|
import SwiftSignalKit
|
|
import AccountContext
|
|
import ReactionSelectionNode
|
|
import Markdown
|
|
|
|
public protocol ContextControllerActionsStackItemNode: ASDisplayNode {
|
|
func update(
|
|
presentationData: PresentationData,
|
|
constrainedSize: CGSize,
|
|
standardMinWidth: CGFloat,
|
|
standardMaxWidth: CGFloat,
|
|
transition: ContainedViewLayoutTransition
|
|
) -> (size: CGSize, apparentHeight: CGFloat)
|
|
|
|
func highlightGestureMoved(location: CGPoint)
|
|
func highlightGestureFinished(performAction: Bool)
|
|
}
|
|
|
|
public protocol ContextControllerActionsStackItem: AnyObject {
|
|
func node(
|
|
getController: @escaping () -> ContextControllerProtocol?,
|
|
requestDismiss: @escaping (ContextMenuActionResult) -> Void,
|
|
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void,
|
|
requestUpdateApparentHeight: @escaping (ContainedViewLayoutTransition) -> Void
|
|
) -> ContextControllerActionsStackItemNode
|
|
|
|
var tip: ContextController.Tip? { get }
|
|
var reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem])? { get }
|
|
}
|
|
|
|
protocol ContextControllerActionsListItemNode: ASDisplayNode {
|
|
func update(presentationData: PresentationData, constrainedSize: CGSize) -> (minSize: CGSize, apply: (_ size: CGSize, _ transition: ContainedViewLayoutTransition) -> Void)
|
|
|
|
func canBeHighlighted() -> Bool
|
|
func updateIsHighlighted(isHighlighted: Bool)
|
|
func performAction()
|
|
}
|
|
|
|
private final class ContextControllerActionsListActionItemNode: HighlightTrackingButtonNode, ContextControllerActionsListItemNode {
|
|
private let getController: () -> ContextControllerProtocol?
|
|
private let requestDismiss: (ContextMenuActionResult) -> Void
|
|
private let requestUpdateAction: (AnyHashable, ContextMenuActionItem) -> Void
|
|
private let item: ContextMenuActionItem
|
|
|
|
private let highlightBackgroundNode: ASDisplayNode
|
|
private let titleLabelNode: ImmediateTextNode
|
|
private let subtitleNode: ImmediateTextNode
|
|
private let iconNode: ASImageNode
|
|
|
|
private var iconDisposable: Disposable?
|
|
|
|
init(
|
|
getController: @escaping () -> ContextControllerProtocol?,
|
|
requestDismiss: @escaping (ContextMenuActionResult) -> Void,
|
|
requestUpdateAction: @escaping (AnyHashable, ContextMenuActionItem) -> Void,
|
|
item: ContextMenuActionItem
|
|
) {
|
|
self.getController = getController
|
|
self.requestDismiss = requestDismiss
|
|
self.requestUpdateAction = requestUpdateAction
|
|
self.item = item
|
|
|
|
self.highlightBackgroundNode = ASDisplayNode()
|
|
self.highlightBackgroundNode.isAccessibilityElement = false
|
|
self.highlightBackgroundNode.isUserInteractionEnabled = false
|
|
self.highlightBackgroundNode.alpha = 0.0
|
|
|
|
self.titleLabelNode = ImmediateTextNode()
|
|
self.titleLabelNode.isAccessibilityElement = false
|
|
self.titleLabelNode.displaysAsynchronously = false
|
|
self.titleLabelNode.isUserInteractionEnabled = false
|
|
|
|
self.subtitleNode = ImmediateTextNode()
|
|
self.subtitleNode.isAccessibilityElement = false
|
|
self.subtitleNode.displaysAsynchronously = false
|
|
self.subtitleNode.isUserInteractionEnabled = false
|
|
|
|
self.iconNode = ASImageNode()
|
|
self.iconNode.isAccessibilityElement = false
|
|
self.iconNode.isUserInteractionEnabled = false
|
|
|
|
super.init()
|
|
|
|
self.isAccessibilityElement = true
|
|
self.accessibilityLabel = item.text
|
|
|
|
self.addSubnode(self.highlightBackgroundNode)
|
|
self.addSubnode(self.titleLabelNode)
|
|
self.addSubnode(self.subtitleNode)
|
|
self.addSubnode(self.iconNode)
|
|
|
|
self.isEnabled = self.canBeHighlighted()
|
|
|
|
self.highligthedChanged = { [weak self] highlighted in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if highlighted {
|
|
strongSelf.highlightBackgroundNode.alpha = 1.0
|
|
} else {
|
|
strongSelf.highlightBackgroundNode.alpha = 0.0
|
|
}
|
|
}
|
|
|
|
self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside)
|
|
}
|
|
|
|
deinit {
|
|
self.iconDisposable?.dispose()
|
|
}
|
|
|
|
@objc private func pressed() {
|
|
guard let controller = self.getController() else {
|
|
return
|
|
}
|
|
|
|
self.item.action?(ContextMenuActionItem.Action(
|
|
controller: controller,
|
|
dismissWithResult: { [weak self] result in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.requestDismiss(result)
|
|
},
|
|
updateAction: { [weak self] id, updatedAction in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.requestUpdateAction(id, updatedAction)
|
|
}
|
|
))
|
|
}
|
|
|
|
func canBeHighlighted() -> Bool {
|
|
return self.item.action != nil
|
|
}
|
|
|
|
func updateIsHighlighted(isHighlighted: Bool) {
|
|
self.highlightBackgroundNode.alpha = isHighlighted ? 1.0 : 0.0
|
|
}
|
|
|
|
func performAction() {
|
|
self.pressed()
|
|
}
|
|
|
|
func update(presentationData: PresentationData, constrainedSize: CGSize) -> (minSize: CGSize, apply: (_ size: CGSize, _ transition: ContainedViewLayoutTransition) -> Void) {
|
|
let sideInset: CGFloat = 16.0
|
|
let verticalInset: CGFloat = 11.0
|
|
let titleSubtitleSpacing: CGFloat = 1.0
|
|
let iconSideInset: CGFloat = 12.0
|
|
let standardIconWidth: CGFloat = 32.0
|
|
let iconSpacing: CGFloat = 8.0
|
|
|
|
self.highlightBackgroundNode.backgroundColor = presentationData.theme.contextMenu.itemHighlightedBackgroundColor
|
|
|
|
var subtitle: String?
|
|
switch self.item.textLayout {
|
|
case .singleLine:
|
|
self.titleLabelNode.maximumNumberOfLines = 1
|
|
case .twoLinesMax:
|
|
self.titleLabelNode.maximumNumberOfLines = 2
|
|
case let .secondLineWithValue(subtitleValue):
|
|
self.titleLabelNode.maximumNumberOfLines = 1
|
|
subtitle = subtitleValue
|
|
case .multiline:
|
|
self.titleLabelNode.maximumNumberOfLines = 0
|
|
self.titleLabelNode.lineSpacing = 0.1
|
|
}
|
|
|
|
let titleFont: UIFont
|
|
let titleBoldFont: UIFont
|
|
switch self.item.textFont {
|
|
case let .custom(font):
|
|
titleFont = font
|
|
titleBoldFont = font
|
|
case .small:
|
|
let smallTextFont = Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 14.0 / 17.0))
|
|
titleFont = smallTextFont
|
|
titleBoldFont = Font.semibold(floor(presentationData.listsFontSize.baseDisplaySize * 14.0 / 17.0))
|
|
case .regular:
|
|
titleFont = Font.regular(presentationData.listsFontSize.baseDisplaySize)
|
|
titleBoldFont = Font.semibold(presentationData.listsFontSize.baseDisplaySize)
|
|
}
|
|
|
|
let subtitleFont = Font.regular(presentationData.listsFontSize.baseDisplaySize * 14.0 / 17.0)
|
|
let subtitleColor = presentationData.theme.contextMenu.secondaryColor
|
|
|
|
let titleColor: UIColor
|
|
switch self.item.textColor {
|
|
case .primary:
|
|
titleColor = presentationData.theme.contextMenu.primaryColor
|
|
case .destructive:
|
|
titleColor = presentationData.theme.contextMenu.destructiveColor
|
|
case .disabled:
|
|
titleColor = presentationData.theme.contextMenu.primaryColor.withMultipliedAlpha(0.4)
|
|
}
|
|
|
|
if self.item.parseMarkdown {
|
|
let attributedText = parseMarkdownIntoAttributedString(
|
|
self.item.text,
|
|
attributes: MarkdownAttributes(
|
|
body: MarkdownAttributeSet(font: titleFont, textColor: titleColor),
|
|
bold: MarkdownAttributeSet(font: titleBoldFont, textColor: titleColor),
|
|
link: MarkdownAttributeSet(font: titleFont, textColor: titleColor),
|
|
linkAttribute: { _ in return nil }
|
|
)
|
|
)
|
|
self.titleLabelNode.attributedText = attributedText
|
|
} else {
|
|
self.titleLabelNode.attributedText = NSAttributedString(
|
|
string: self.item.text,
|
|
font: titleFont,
|
|
textColor: titleColor)
|
|
}
|
|
|
|
self.subtitleNode.attributedText = subtitle.flatMap { subtitle in
|
|
return NSAttributedString(
|
|
string: subtitle,
|
|
font: subtitleFont,
|
|
textColor: subtitleColor
|
|
)
|
|
}
|
|
|
|
let iconSize: CGSize?
|
|
if let iconSource = self.item.iconSource {
|
|
iconSize = iconSource.size
|
|
if self.iconDisposable == nil {
|
|
self.iconDisposable = (iconSource.signal |> deliverOnMainQueue).start(next: { [weak self] image in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.iconNode.image = image
|
|
})
|
|
}
|
|
} else if let image = self.iconNode.image {
|
|
iconSize = image.size
|
|
} else {
|
|
let iconImage = self.item.icon(presentationData.theme)
|
|
self.iconNode.image = iconImage
|
|
iconSize = iconImage?.size
|
|
}
|
|
|
|
var maxTextWidth: CGFloat = constrainedSize.width
|
|
maxTextWidth -= sideInset
|
|
if let iconSize = iconSize {
|
|
maxTextWidth -= max(standardIconWidth, iconSize.width)
|
|
maxTextWidth -= iconSpacing
|
|
} else {
|
|
maxTextWidth -= sideInset
|
|
}
|
|
maxTextWidth = max(1.0, maxTextWidth)
|
|
|
|
let titleSize = self.titleLabelNode.updateLayout(CGSize(width: maxTextWidth, height: 1000.0))
|
|
let subtitleSize = self.subtitleNode.updateLayout(CGSize(width: maxTextWidth, height: 1000.0))
|
|
|
|
var minSize = CGSize()
|
|
minSize.width += sideInset
|
|
minSize.width += max(titleSize.width, subtitleSize.width)
|
|
if let iconSize = iconSize {
|
|
minSize.width += max(standardIconWidth, iconSize.width)
|
|
minSize.width += iconSideInset
|
|
minSize.width += iconSpacing
|
|
} else {
|
|
minSize.width += sideInset
|
|
}
|
|
minSize.height += verticalInset * 2.0
|
|
minSize.height += titleSize.height
|
|
if subtitle != nil {
|
|
minSize.height += titleSubtitleSpacing
|
|
minSize.height += subtitleSize.height
|
|
}
|
|
|
|
return (minSize: minSize, apply: { size, transition in
|
|
let titleFrame = CGRect(origin: CGPoint(x: sideInset, y: verticalInset), size: titleSize)
|
|
let subtitleFrame = CGRect(origin: CGPoint(x: sideInset, y: titleFrame.maxY + titleSubtitleSpacing), size: subtitleSize)
|
|
|
|
transition.updateFrame(node: self.highlightBackgroundNode, frame: CGRect(origin: CGPoint(), size: size), beginWithCurrentState: true)
|
|
transition.updateFrameAdditive(node: self.titleLabelNode, frame: titleFrame)
|
|
transition.updateFrameAdditive(node: self.subtitleNode, frame: subtitleFrame)
|
|
|
|
if let iconSize = iconSize {
|
|
let iconWidth = max(standardIconWidth, iconSize.width)
|
|
let iconFrame = CGRect(origin: CGPoint(x: size.width - iconSideInset - iconWidth + floor((iconWidth - iconSize.width) / 2.0), y: floor((size.height - iconSize.height) / 2.0)), size: iconSize)
|
|
transition.updateFrame(node: self.iconNode, frame: iconFrame, beginWithCurrentState: true)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
private final class ContextControllerActionsListSeparatorItemNode: ASDisplayNode, ContextControllerActionsListItemNode {
|
|
func canBeHighlighted() -> Bool {
|
|
return false
|
|
}
|
|
|
|
func updateIsHighlighted(isHighlighted: Bool) {
|
|
}
|
|
|
|
func performAction() {
|
|
}
|
|
|
|
override init() {
|
|
super.init()
|
|
}
|
|
|
|
func update(presentationData: PresentationData, constrainedSize: CGSize) -> (minSize: CGSize, apply: (_ size: CGSize, _ transition: ContainedViewLayoutTransition) -> Void) {
|
|
return (minSize: CGSize(width: 0.0, height: 7.0), apply: { _, _ in
|
|
self.backgroundColor = presentationData.theme.contextMenu.sectionSeparatorColor
|
|
})
|
|
}
|
|
}
|
|
|
|
private final class ContextControllerActionsListCustomItemNode: ASDisplayNode, ContextControllerActionsListItemNode {
|
|
func canBeHighlighted() -> Bool {
|
|
if let itemNode = self.itemNode {
|
|
return itemNode.canBeHighlighted()
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
func updateIsHighlighted(isHighlighted: Bool) {
|
|
if let itemNode = self.itemNode {
|
|
itemNode.updateIsHighlighted(isHighlighted: isHighlighted)
|
|
}
|
|
}
|
|
|
|
func performAction() {
|
|
if let itemNode = self.itemNode {
|
|
itemNode.performAction()
|
|
}
|
|
}
|
|
|
|
private let getController: () -> ContextControllerProtocol?
|
|
private let item: ContextMenuCustomItem
|
|
|
|
private var presentationData: PresentationData?
|
|
private var itemNode: ContextMenuCustomNode?
|
|
|
|
init(
|
|
getController: @escaping () -> ContextControllerProtocol?,
|
|
item: ContextMenuCustomItem
|
|
) {
|
|
self.getController = getController
|
|
self.item = item
|
|
|
|
super.init()
|
|
}
|
|
|
|
func update(presentationData: PresentationData, constrainedSize: CGSize) -> (minSize: CGSize, apply: (_ size: CGSize, _ transition: ContainedViewLayoutTransition) -> Void) {
|
|
if self.presentationData?.theme !== presentationData.theme {
|
|
if let itemNode = self.itemNode {
|
|
itemNode.updateTheme(presentationData: presentationData)
|
|
}
|
|
}
|
|
self.presentationData = presentationData
|
|
|
|
let itemNode: ContextMenuCustomNode
|
|
if let current = self.itemNode {
|
|
itemNode = current
|
|
} else {
|
|
itemNode = self.item.node(
|
|
presentationData: presentationData,
|
|
getController: self.getController,
|
|
actionSelected: { result in
|
|
let _ = result
|
|
}
|
|
)
|
|
self.itemNode = itemNode
|
|
self.addSubnode(itemNode)
|
|
}
|
|
|
|
let itemLayoutAndApply = itemNode.updateLayout(constrainedWidth: constrainedSize.width, constrainedHeight: constrainedSize.height)
|
|
|
|
return (minSize: itemLayoutAndApply.0, apply: { size, transition in
|
|
transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(), size: size), beginWithCurrentState: true)
|
|
itemLayoutAndApply.1(size, transition)
|
|
})
|
|
}
|
|
}
|
|
|
|
final class ContextControllerActionsListStackItem: ContextControllerActionsStackItem {
|
|
private final class Node: ASDisplayNode, ContextControllerActionsStackItemNode {
|
|
private final class Item {
|
|
let node: ContextControllerActionsListItemNode
|
|
let separatorNode: ASDisplayNode?
|
|
|
|
init(node: ContextControllerActionsListItemNode, separatorNode: ASDisplayNode?) {
|
|
self.node = node
|
|
self.separatorNode = separatorNode
|
|
}
|
|
}
|
|
|
|
private let requestUpdate: (ContainedViewLayoutTransition) -> Void
|
|
private var items: [ContextMenuItem]
|
|
private var itemNodes: [Item]
|
|
|
|
private var hapticFeedback: HapticFeedback?
|
|
private var highlightedItemNode: Item?
|
|
|
|
init(
|
|
getController: @escaping () -> ContextControllerProtocol?,
|
|
requestDismiss: @escaping (ContextMenuActionResult) -> Void,
|
|
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void,
|
|
items: [ContextMenuItem]
|
|
) {
|
|
self.requestUpdate = requestUpdate
|
|
self.items = items
|
|
|
|
var requestUpdateAction: ((AnyHashable, ContextMenuActionItem) -> Void)?
|
|
self.itemNodes = items.map { item -> Item in
|
|
switch item {
|
|
case let .action(actionItem):
|
|
return Item(
|
|
node: ContextControllerActionsListActionItemNode(
|
|
getController: getController,
|
|
requestDismiss: requestDismiss,
|
|
requestUpdateAction: { id, action in
|
|
requestUpdateAction?(id, action)
|
|
},
|
|
item: actionItem
|
|
),
|
|
separatorNode: ASDisplayNode()
|
|
)
|
|
case .separator:
|
|
return Item(
|
|
node: ContextControllerActionsListSeparatorItemNode(),
|
|
separatorNode: nil
|
|
)
|
|
case let .custom(customItem, _):
|
|
return Item(
|
|
node: ContextControllerActionsListCustomItemNode(
|
|
getController: getController,
|
|
item: customItem
|
|
),
|
|
separatorNode: ASDisplayNode()
|
|
)
|
|
}
|
|
}
|
|
|
|
super.init()
|
|
|
|
for item in self.itemNodes {
|
|
if let separatorNode = item.separatorNode {
|
|
self.addSubnode(separatorNode)
|
|
}
|
|
}
|
|
for item in self.itemNodes {
|
|
self.addSubnode(item.node)
|
|
}
|
|
|
|
requestUpdateAction = { [weak self] id, action in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
loop: for i in 0 ..< strongSelf.items.count {
|
|
switch strongSelf.items[i] {
|
|
case let .action(currentAction):
|
|
if currentAction.id == id {
|
|
let previousNode = strongSelf.itemNodes[i]
|
|
previousNode.node.removeFromSupernode()
|
|
previousNode.separatorNode?.removeFromSupernode()
|
|
|
|
let addedNode = Item(
|
|
node: ContextControllerActionsListActionItemNode(
|
|
getController: getController,
|
|
requestDismiss: requestDismiss,
|
|
requestUpdateAction: { id, action in
|
|
requestUpdateAction?(id, action)
|
|
},
|
|
item: action
|
|
),
|
|
separatorNode: ASDisplayNode()
|
|
)
|
|
strongSelf.itemNodes[i] = addedNode
|
|
if let separatorNode = addedNode.separatorNode {
|
|
strongSelf.insertSubnode(separatorNode, at: 0)
|
|
}
|
|
strongSelf.addSubnode(addedNode.node)
|
|
|
|
strongSelf.requestUpdate(.immediate)
|
|
|
|
break loop
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func update(
|
|
presentationData: PresentationData,
|
|
constrainedSize: CGSize,
|
|
standardMinWidth: CGFloat,
|
|
standardMaxWidth: CGFloat,
|
|
transition: ContainedViewLayoutTransition
|
|
) -> (size: CGSize, apparentHeight: CGFloat) {
|
|
var itemNodeLayouts: [(minSize: CGSize, apply: (_ size: CGSize, _ transition: ContainedViewLayoutTransition) -> Void)] = []
|
|
var combinedSize = CGSize()
|
|
for item in self.itemNodes {
|
|
item.separatorNode?.backgroundColor = presentationData.theme.contextMenu.itemSeparatorColor
|
|
|
|
let itemNodeLayout = item.node.update(
|
|
presentationData: presentationData,
|
|
constrainedSize: CGSize(width: standardMaxWidth, height: constrainedSize.height)
|
|
)
|
|
itemNodeLayouts.append(itemNodeLayout)
|
|
combinedSize.width = max(combinedSize.width, itemNodeLayout.minSize.width)
|
|
combinedSize.height += itemNodeLayout.minSize.height
|
|
}
|
|
combinedSize.width = max(combinedSize.width, standardMinWidth)
|
|
|
|
var nextItemOrigin = CGPoint()
|
|
for i in 0 ..< self.itemNodes.count {
|
|
let item = self.itemNodes[i]
|
|
let itemNodeLayout = itemNodeLayouts[i]
|
|
|
|
var itemTransition = transition
|
|
if item.node.frame.isEmpty {
|
|
itemTransition = .immediate
|
|
}
|
|
|
|
let itemSize = CGSize(width: combinedSize.width, height: itemNodeLayout.minSize.height)
|
|
let itemFrame = CGRect(origin: nextItemOrigin, size: itemSize)
|
|
itemTransition.updateFrame(node: item.node, frame: itemFrame, beginWithCurrentState: true)
|
|
|
|
if let separatorNode = item.separatorNode {
|
|
itemTransition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: itemFrame.minX, y: itemFrame.maxY), size: CGSize(width: itemFrame.width, height: UIScreenPixel)), beginWithCurrentState: true)
|
|
if i != self.itemNodes.count - 1 {
|
|
switch self.items[i + 1] {
|
|
case .separator:
|
|
separatorNode.isHidden = true
|
|
case .action:
|
|
separatorNode.isHidden = false
|
|
case .custom:
|
|
separatorNode.isHidden = false
|
|
}
|
|
} else {
|
|
separatorNode.isHidden = true
|
|
}
|
|
}
|
|
|
|
itemNodeLayout.apply(itemSize, itemTransition)
|
|
nextItemOrigin.y += itemSize.height
|
|
}
|
|
|
|
return (combinedSize, combinedSize.height)
|
|
}
|
|
|
|
func highlightGestureMoved(location: CGPoint) {
|
|
var highlightedItemNode: Item?
|
|
for itemNode in self.itemNodes {
|
|
if itemNode.node.frame.contains(location) {
|
|
if itemNode.node.canBeHighlighted() {
|
|
highlightedItemNode = itemNode
|
|
}
|
|
break
|
|
}
|
|
}
|
|
if self.highlightedItemNode !== highlightedItemNode {
|
|
self.highlightedItemNode?.node.updateIsHighlighted(isHighlighted: false)
|
|
highlightedItemNode?.node.updateIsHighlighted(isHighlighted: true)
|
|
|
|
self.highlightedItemNode = highlightedItemNode
|
|
if self.hapticFeedback == nil {
|
|
self.hapticFeedback = HapticFeedback()
|
|
}
|
|
self.hapticFeedback?.tap()
|
|
}
|
|
}
|
|
|
|
func highlightGestureFinished(performAction: Bool) {
|
|
if let highlightedItemNode = self.highlightedItemNode {
|
|
self.highlightedItemNode = nil
|
|
highlightedItemNode.node.updateIsHighlighted(isHighlighted: false)
|
|
if performAction {
|
|
highlightedItemNode.node.performAction()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private let items: [ContextMenuItem]
|
|
let reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem])?
|
|
let tip: ContextController.Tip?
|
|
|
|
init(
|
|
items: [ContextMenuItem],
|
|
reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem])?,
|
|
tip: ContextController.Tip?
|
|
) {
|
|
self.items = items
|
|
self.reactionItems = reactionItems
|
|
self.tip = tip
|
|
}
|
|
|
|
func node(
|
|
getController: @escaping () -> ContextControllerProtocol?,
|
|
requestDismiss: @escaping (ContextMenuActionResult) -> Void,
|
|
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void,
|
|
requestUpdateApparentHeight: @escaping (ContainedViewLayoutTransition) -> Void
|
|
) -> ContextControllerActionsStackItemNode {
|
|
return Node(
|
|
getController: getController,
|
|
requestDismiss: requestDismiss,
|
|
requestUpdate: requestUpdate,
|
|
items: self.items
|
|
)
|
|
}
|
|
}
|
|
|
|
final class ContextControllerActionsCustomStackItem: ContextControllerActionsStackItem {
|
|
private final class Node: ASDisplayNode, ContextControllerActionsStackItemNode {
|
|
private let requestUpdate: (ContainedViewLayoutTransition) -> Void
|
|
private let contentNode: ContextControllerItemsNode
|
|
|
|
init(
|
|
content: ContextControllerItemsContent,
|
|
getController: @escaping () -> ContextControllerProtocol?,
|
|
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void,
|
|
requestUpdateApparentHeight: @escaping (ContainedViewLayoutTransition) -> Void
|
|
) {
|
|
self.requestUpdate = requestUpdate
|
|
self.contentNode = content.node(requestUpdate: { transition in
|
|
requestUpdate(transition)
|
|
}, requestUpdateApparentHeight: { transition in
|
|
requestUpdateApparentHeight(transition)
|
|
})
|
|
|
|
super.init()
|
|
|
|
self.addSubnode(self.contentNode)
|
|
}
|
|
|
|
func update(
|
|
presentationData: PresentationData,
|
|
constrainedSize: CGSize,
|
|
standardMinWidth: CGFloat,
|
|
standardMaxWidth: CGFloat,
|
|
transition: ContainedViewLayoutTransition
|
|
) -> (size: CGSize, apparentHeight: CGFloat) {
|
|
let contentLayout = self.contentNode.update(
|
|
presentationData: presentationData,
|
|
constrainedWidth: constrainedSize.width,
|
|
maxHeight: constrainedSize.height,
|
|
bottomInset: 0.0,
|
|
transition: transition
|
|
)
|
|
transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(), size: contentLayout.cleanSize), beginWithCurrentState: true)
|
|
|
|
return (contentLayout.cleanSize, contentLayout.apparentHeight)
|
|
}
|
|
|
|
func highlightGestureMoved(location: CGPoint) {
|
|
}
|
|
|
|
func highlightGestureFinished(performAction: Bool) {
|
|
}
|
|
}
|
|
|
|
private let content: ContextControllerItemsContent
|
|
let reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem])?
|
|
let tip: ContextController.Tip?
|
|
|
|
init(
|
|
content: ContextControllerItemsContent,
|
|
reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem])?,
|
|
tip: ContextController.Tip?
|
|
) {
|
|
self.content = content
|
|
self.reactionItems = reactionItems
|
|
self.tip = tip
|
|
}
|
|
|
|
func node(
|
|
getController: @escaping () -> ContextControllerProtocol?,
|
|
requestDismiss: @escaping (ContextMenuActionResult) -> Void,
|
|
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void,
|
|
requestUpdateApparentHeight: @escaping (ContainedViewLayoutTransition) -> Void
|
|
) -> ContextControllerActionsStackItemNode {
|
|
return Node(
|
|
content: self.content,
|
|
getController: getController,
|
|
requestUpdate: requestUpdate,
|
|
requestUpdateApparentHeight: requestUpdateApparentHeight
|
|
)
|
|
}
|
|
}
|
|
|
|
func makeContextControllerActionsStackItem(items: ContextController.Items) -> ContextControllerActionsStackItem {
|
|
var reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem])?
|
|
if let context = items.context, !items.reactionItems.isEmpty {
|
|
reactionItems = (context, items.reactionItems)
|
|
}
|
|
switch items.content {
|
|
case let .list(listItems):
|
|
return ContextControllerActionsListStackItem(items: listItems, reactionItems: reactionItems, tip: items.tip)
|
|
case let .custom(customContent):
|
|
return ContextControllerActionsCustomStackItem(content: customContent, reactionItems: reactionItems, tip: items.tip)
|
|
}
|
|
}
|
|
|
|
final class ContextControllerActionsStackNode: ASDisplayNode {
|
|
enum Presentation {
|
|
case modal
|
|
case inline
|
|
}
|
|
|
|
final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelegate {
|
|
let backgroundNode: NavigationBackgroundNode
|
|
let parentShadowNode: ASImageNode
|
|
|
|
var requestUpdate: ((ContainedViewLayoutTransition) -> Void)?
|
|
var requestPop: (() -> Void)?
|
|
var transitionFraction: CGFloat = 0.0
|
|
|
|
private var panRecognizer: InteractiveTransitionGestureRecognizer?
|
|
|
|
var isNavigationEnabled: Bool = false {
|
|
didSet {
|
|
self.panRecognizer?.isEnabled = self.isNavigationEnabled
|
|
}
|
|
}
|
|
|
|
override init() {
|
|
self.backgroundNode = NavigationBackgroundNode(color: .clear, enableBlur: false)
|
|
self.parentShadowNode = ASImageNode()
|
|
self.parentShadowNode.image = UIImage(bundleImageName: "Components/Context Menu/Shadow")?.stretchableImage(withLeftCapWidth: 60, topCapHeight: 48)
|
|
|
|
super.init()
|
|
|
|
self.addSubnode(self.backgroundNode)
|
|
|
|
self.clipsToBounds = true
|
|
self.cornerRadius = 14.0
|
|
|
|
let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] point in
|
|
guard let strongSelf = self else {
|
|
return []
|
|
}
|
|
let _ = strongSelf
|
|
return [.right]
|
|
})
|
|
panRecognizer.delegate = self
|
|
self.view.addGestureRecognizer(panRecognizer)
|
|
self.panRecognizer = panRecognizer
|
|
}
|
|
|
|
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
return false
|
|
}
|
|
|
|
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
if let _ = otherGestureRecognizer as? InteractiveTransitionGestureRecognizer {
|
|
return false
|
|
}
|
|
if let _ = otherGestureRecognizer as? UIPanGestureRecognizer {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
@objc private func panGesture(_ recognizer: UIPanGestureRecognizer) {
|
|
switch recognizer.state {
|
|
case .began:
|
|
self.transitionFraction = 0.0
|
|
case .changed:
|
|
let distanceFactor: CGFloat = recognizer.translation(in: self.view).x / self.bounds.width
|
|
let transitionFraction = max(0.0, min(1.0, distanceFactor))
|
|
if self.transitionFraction != transitionFraction {
|
|
self.transitionFraction = transitionFraction
|
|
self.requestUpdate?(.immediate)
|
|
}
|
|
case .ended, .cancelled:
|
|
let distanceFactor: CGFloat = recognizer.translation(in: self.view).x / self.bounds.width
|
|
let transitionFraction = max(0.0, min(1.0, distanceFactor))
|
|
if transitionFraction > 0.2 {
|
|
self.transitionFraction = 0.0
|
|
self.requestPop?()
|
|
} else {
|
|
self.transitionFraction = 0.0
|
|
self.requestUpdate?(.animated(duration: 0.45, curve: .spring))
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
func update(presentationData: PresentationData, presentation: Presentation, size: CGSize, transition: ContainedViewLayoutTransition) {
|
|
switch presentation {
|
|
case .modal:
|
|
self.backgroundNode.updateColor(color: presentationData.theme.contextMenu.backgroundColor, enableBlur: false, forceKeepBlur: false, transition: transition)
|
|
self.parentShadowNode.isHidden = true
|
|
case .inline:
|
|
self.backgroundNode.updateColor(color: presentationData.theme.contextMenu.backgroundColor, enableBlur: true, forceKeepBlur: true, transition: transition)
|
|
self.parentShadowNode.isHidden = false
|
|
}
|
|
self.backgroundNode.update(size: size, transition: transition)
|
|
}
|
|
}
|
|
|
|
final class ItemContainer: ASDisplayNode {
|
|
let requestUpdate: (ContainedViewLayoutTransition) -> Void
|
|
let node: ContextControllerActionsStackItemNode
|
|
let dimNode: ASDisplayNode
|
|
let tip: ContextController.Tip?
|
|
var tipNode: InnerTextSelectionTipContainerNode?
|
|
let reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem])?
|
|
var storedScrollingState: CGFloat?
|
|
let positionLock: CGFloat?
|
|
|
|
init(
|
|
getController: @escaping () -> ContextControllerProtocol?,
|
|
requestDismiss: @escaping (ContextMenuActionResult) -> Void,
|
|
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void,
|
|
requestUpdateApparentHeight: @escaping (ContainedViewLayoutTransition) -> Void,
|
|
item: ContextControllerActionsStackItem,
|
|
tip: ContextController.Tip?,
|
|
reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem])?,
|
|
positionLock: CGFloat?
|
|
) {
|
|
self.requestUpdate = requestUpdate
|
|
self.node = item.node(
|
|
getController: getController,
|
|
requestDismiss: requestDismiss,
|
|
requestUpdate: requestUpdate,
|
|
requestUpdateApparentHeight: requestUpdateApparentHeight
|
|
)
|
|
|
|
self.dimNode = ASDisplayNode()
|
|
self.dimNode.isUserInteractionEnabled = false
|
|
self.dimNode.alpha = 0.0
|
|
|
|
self.reactionItems = reactionItems
|
|
self.positionLock = positionLock
|
|
|
|
self.tip = tip
|
|
|
|
super.init()
|
|
|
|
self.clipsToBounds = true
|
|
|
|
self.addSubnode(self.node)
|
|
self.addSubnode(self.dimNode)
|
|
}
|
|
|
|
func update(
|
|
presentationData: PresentationData,
|
|
constrainedSize: CGSize,
|
|
standardMinWidth: CGFloat,
|
|
standardMaxWidth: CGFloat,
|
|
transitionFraction: CGFloat,
|
|
transition: ContainedViewLayoutTransition
|
|
) -> (size: CGSize, apparentHeight: CGFloat) {
|
|
let (size, apparentHeight) = self.node.update(
|
|
presentationData: presentationData,
|
|
constrainedSize: constrainedSize,
|
|
standardMinWidth: standardMinWidth,
|
|
standardMaxWidth: standardMaxWidth,
|
|
transition: transition
|
|
)
|
|
|
|
let maxScaleOffset: CGFloat = 10.0
|
|
let scaleOffset: CGFloat = 0.0 * transitionFraction + maxScaleOffset * (1.0 - transitionFraction)
|
|
let scale: CGFloat = (size.width - scaleOffset) / size.width
|
|
let yOffset: CGFloat = size.height * (1.0 - scale)
|
|
let transitionOffset = (1.0 - transitionFraction) * size.width / 2.0
|
|
transition.updatePosition(node: self.node, position: CGPoint(x: size.width / 2.0 + scaleOffset / 2.0 + transitionOffset, y: size.height / 2.0 - yOffset / 2.0), beginWithCurrentState: true)
|
|
transition.updateBounds(node: self.node, bounds: CGRect(origin: CGPoint(), size: size), beginWithCurrentState: true)
|
|
transition.updateTransformScale(node: self.node, scale: scale, beginWithCurrentState: true)
|
|
|
|
return (size, apparentHeight)
|
|
}
|
|
|
|
func updateTip(presentationData: PresentationData, width: CGFloat, transition: ContainedViewLayoutTransition) -> (node: ASDisplayNode, height: CGFloat)? {
|
|
if let tip = self.tip {
|
|
var updatedTransition = transition
|
|
if self.tipNode == nil {
|
|
updatedTransition = .immediate
|
|
let tipNode = InnerTextSelectionTipContainerNode(presentationData: presentationData, tip: tip)
|
|
tipNode.isUserInteractionEnabled = false
|
|
self.tipNode = tipNode
|
|
}
|
|
|
|
if let tipNode = self.tipNode {
|
|
let size = tipNode.updateLayout(widthClass: .compact, width: width, transition: updatedTransition)
|
|
return (tipNode, size.height)
|
|
} else {
|
|
return nil
|
|
}
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func updateDimNode(presentationData: PresentationData, size: CGSize, transitionFraction: CGFloat, transition: ContainedViewLayoutTransition) {
|
|
self.dimNode.backgroundColor = presentationData.theme.contextMenu.sectionSeparatorColor
|
|
|
|
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: size), beginWithCurrentState: true)
|
|
transition.updateAlpha(node: self.dimNode, alpha: 1.0 - transitionFraction, beginWithCurrentState: true)
|
|
}
|
|
|
|
func highlightGestureMoved(location: CGPoint) {
|
|
self.node.highlightGestureMoved(location: self.view.convert(location, to: self.node.view))
|
|
}
|
|
|
|
func highlightGestureFinished(performAction: Bool) {
|
|
self.node.highlightGestureFinished(performAction: performAction)
|
|
}
|
|
}
|
|
|
|
private let getController: () -> ContextControllerProtocol?
|
|
private let requestDismiss: (ContextMenuActionResult) -> Void
|
|
private let requestUpdate: (ContainedViewLayoutTransition) -> Void
|
|
|
|
private let navigationContainer: NavigationContainer
|
|
private var itemContainers: [ItemContainer] = []
|
|
private var dismissingItemContainers: [(container: ItemContainer, isPopped: Bool)] = []
|
|
|
|
private var selectionPanGesture: UIPanGestureRecognizer?
|
|
|
|
var topReactionItems: (context: AccountContext, reactionItems: [ReactionContextItem])? {
|
|
return self.itemContainers.last?.reactionItems
|
|
}
|
|
|
|
var topPositionLock: CGFloat? {
|
|
return self.itemContainers.last?.positionLock
|
|
}
|
|
|
|
var storedScrollingState: CGFloat? {
|
|
return self.itemContainers.last?.storedScrollingState
|
|
}
|
|
|
|
init(
|
|
getController: @escaping () -> ContextControllerProtocol?,
|
|
requestDismiss: @escaping (ContextMenuActionResult) -> Void,
|
|
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void
|
|
) {
|
|
self.getController = getController
|
|
self.requestDismiss = requestDismiss
|
|
self.requestUpdate = requestUpdate
|
|
|
|
self.navigationContainer = NavigationContainer()
|
|
|
|
super.init()
|
|
|
|
self.addSubnode(self.navigationContainer.parentShadowNode)
|
|
self.addSubnode(self.navigationContainer)
|
|
|
|
self.navigationContainer.requestUpdate = { [weak self] transition in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.requestUpdate(transition)
|
|
}
|
|
|
|
self.navigationContainer.requestPop = { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.pop()
|
|
}
|
|
|
|
let selectionPanGesture = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))
|
|
self.selectionPanGesture = selectionPanGesture
|
|
self.view.addGestureRecognizer(selectionPanGesture)
|
|
selectionPanGesture.isEnabled = false
|
|
}
|
|
|
|
@objc private func panGesture(_ recognizer: UIPanGestureRecognizer) {
|
|
switch recognizer.state {
|
|
case .changed:
|
|
let location = recognizer.location(in: self.view)
|
|
self.highlightGestureMoved(location: location)
|
|
case .ended:
|
|
self.highlightGestureFinished(performAction: true)
|
|
case .cancelled:
|
|
self.highlightGestureFinished(performAction: false)
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
func replace(item: ContextControllerActionsStackItem, animated: Bool) {
|
|
for itemContainer in self.itemContainers {
|
|
if animated {
|
|
self.dismissingItemContainers.append((itemContainer, false))
|
|
} else {
|
|
itemContainer.removeFromSupernode()
|
|
}
|
|
}
|
|
self.itemContainers.removeAll()
|
|
self.navigationContainer.isNavigationEnabled = self.itemContainers.count > 1
|
|
|
|
self.push(item: item, currentScrollingState: nil, positionLock: nil, animated: animated)
|
|
}
|
|
|
|
func push(item: ContextControllerActionsStackItem, currentScrollingState: CGFloat?, positionLock: CGFloat?, animated: Bool) {
|
|
if let itemContainer = self.itemContainers.last {
|
|
itemContainer.storedScrollingState = currentScrollingState
|
|
}
|
|
let itemContainer = ItemContainer(
|
|
getController: self.getController,
|
|
requestDismiss: self.requestDismiss,
|
|
requestUpdate: self.requestUpdate,
|
|
requestUpdateApparentHeight: { [weak self] transition in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.requestUpdate(transition)
|
|
},
|
|
item: item,
|
|
tip: item.tip,
|
|
reactionItems: item.reactionItems,
|
|
positionLock: positionLock
|
|
)
|
|
self.itemContainers.append(itemContainer)
|
|
self.navigationContainer.addSubnode(itemContainer)
|
|
self.navigationContainer.isNavigationEnabled = self.itemContainers.count > 1
|
|
|
|
let transition: ContainedViewLayoutTransition
|
|
if animated {
|
|
transition = .animated(duration: self.itemContainers.count == 1 ? 0.3 : 0.45, curve: .spring)
|
|
} else {
|
|
transition = .immediate
|
|
}
|
|
self.requestUpdate(transition)
|
|
}
|
|
|
|
func clearStoredScrollingState() {
|
|
self.itemContainers.last?.storedScrollingState = nil
|
|
}
|
|
|
|
func pop() {
|
|
if self.itemContainers.count == 1 {
|
|
//dismiss
|
|
} else {
|
|
let itemContainer = self.itemContainers[self.itemContainers.count - 1]
|
|
self.itemContainers.remove(at: self.itemContainers.count - 1)
|
|
self.dismissingItemContainers.append((itemContainer, true))
|
|
}
|
|
|
|
self.navigationContainer.isNavigationEnabled = self.itemContainers.count > 1
|
|
|
|
let transition: ContainedViewLayoutTransition = .animated(duration: 0.45, curve: .spring)
|
|
self.requestUpdate(transition)
|
|
}
|
|
|
|
func update(
|
|
presentationData: PresentationData,
|
|
constrainedSize: CGSize,
|
|
presentation: Presentation,
|
|
transition: ContainedViewLayoutTransition
|
|
) -> CGSize {
|
|
let tipSpacing: CGFloat = 10.0
|
|
|
|
let animateAppearingContainers = transition.isAnimated && !self.dismissingItemContainers.isEmpty
|
|
|
|
struct ItemLayout {
|
|
var size: CGSize
|
|
var apparentHeight: CGFloat
|
|
var transitionFraction: CGFloat
|
|
var alphaTransitionFraction: CGFloat
|
|
var itemTransition: ContainedViewLayoutTransition
|
|
var animateAppearingContainer: Bool
|
|
}
|
|
|
|
var topItemSize = CGSize()
|
|
var itemLayouts: [ItemLayout] = []
|
|
for i in 0 ..< self.itemContainers.count {
|
|
let itemContainer = self.itemContainers[i]
|
|
|
|
var animateAppearingContainer = false
|
|
var itemContainerTransition = transition
|
|
if itemContainer.bounds.isEmpty {
|
|
itemContainerTransition = .immediate
|
|
animateAppearingContainer = i == self.itemContainers.count - 1 && animateAppearingContainers || self.itemContainers.count > 1
|
|
}
|
|
|
|
let itemConstrainedHeight: CGFloat = constrainedSize.height
|
|
|
|
let transitionFraction: CGFloat
|
|
let alphaTransitionFraction: CGFloat
|
|
if i == self.itemContainers.count - 1 {
|
|
transitionFraction = self.navigationContainer.transitionFraction
|
|
alphaTransitionFraction = 1.0
|
|
} else if i == self.itemContainers.count - 2 {
|
|
transitionFraction = self.navigationContainer.transitionFraction - 1.0
|
|
alphaTransitionFraction = self.navigationContainer.transitionFraction
|
|
} else {
|
|
transitionFraction = 0.0
|
|
alphaTransitionFraction = 0.0
|
|
}
|
|
|
|
let itemSize = itemContainer.update(
|
|
presentationData: presentationData,
|
|
constrainedSize: CGSize(width: constrainedSize.width, height: itemConstrainedHeight),
|
|
standardMinWidth: 220.0,
|
|
standardMaxWidth: 240.0,
|
|
transitionFraction: alphaTransitionFraction,
|
|
transition: itemContainerTransition
|
|
)
|
|
if i == self.itemContainers.count - 1 {
|
|
topItemSize = itemSize.size
|
|
}
|
|
|
|
itemLayouts.append(ItemLayout(
|
|
size: itemSize.size,
|
|
apparentHeight: itemSize.apparentHeight,
|
|
transitionFraction: transitionFraction,
|
|
alphaTransitionFraction: alphaTransitionFraction,
|
|
itemTransition: itemContainerTransition,
|
|
animateAppearingContainer: animateAppearingContainer
|
|
))
|
|
}
|
|
|
|
let topItemApparentHeight: CGFloat
|
|
let topItemWidth: CGFloat
|
|
if itemLayouts.isEmpty {
|
|
topItemApparentHeight = 0.0
|
|
topItemWidth = 0.0
|
|
} else if itemLayouts.count == 1 {
|
|
topItemApparentHeight = itemLayouts[0].apparentHeight
|
|
topItemWidth = itemLayouts[0].size.width
|
|
} else {
|
|
let lastItemLayout = itemLayouts[itemLayouts.count - 1]
|
|
let previousItemLayout = itemLayouts[itemLayouts.count - 2]
|
|
let transitionFraction = self.navigationContainer.transitionFraction
|
|
|
|
topItemApparentHeight = lastItemLayout.apparentHeight * (1.0 - transitionFraction) + previousItemLayout.apparentHeight * transitionFraction
|
|
topItemWidth = lastItemLayout.size.width * (1.0 - transitionFraction) + previousItemLayout.size.width * transitionFraction
|
|
}
|
|
|
|
let navigationContainerFrame = CGRect(origin: CGPoint(), size: CGSize(width: topItemWidth, height: max(14 * 2.0, topItemApparentHeight)))
|
|
transition.updateFrame(node: self.navigationContainer, frame: navigationContainerFrame, beginWithCurrentState: true)
|
|
self.navigationContainer.update(presentationData: presentationData, presentation: presentation, size: navigationContainerFrame.size, transition: transition)
|
|
|
|
let navigationContainerShadowFrame = navigationContainerFrame.insetBy(dx: -30.0, dy: -30.0)
|
|
transition.updateFrame(node: self.navigationContainer.parentShadowNode, frame: navigationContainerShadowFrame, beginWithCurrentState: true)
|
|
|
|
for i in 0 ..< self.itemContainers.count {
|
|
let xOffset: CGFloat
|
|
if itemLayouts[i].transitionFraction < 0.0 {
|
|
xOffset = itemLayouts[i].transitionFraction * itemLayouts[i].size.width
|
|
} else {
|
|
if i != 0 {
|
|
xOffset = itemLayouts[i].transitionFraction * itemLayouts[i - 1].size.width
|
|
} else {
|
|
xOffset = itemLayouts[i].transitionFraction * topItemWidth
|
|
}
|
|
}
|
|
let itemFrame = CGRect(origin: CGPoint(x: xOffset, y: 0.0), size: CGSize(width: itemLayouts[i].size.width, height: navigationContainerFrame.height))
|
|
|
|
itemLayouts[i].itemTransition.updateFrame(node: self.itemContainers[i], frame: itemFrame, beginWithCurrentState: true)
|
|
if itemLayouts[i].animateAppearingContainer {
|
|
transition.animatePositionAdditive(node: self.itemContainers[i], offset: CGPoint(x: itemFrame.width, y: 0.0))
|
|
}
|
|
|
|
self.itemContainers[i].updateDimNode(presentationData: presentationData, size: CGSize(width: itemLayouts[i].size.width, height: navigationContainerFrame.size.height), transitionFraction: itemLayouts[i].alphaTransitionFraction, transition: transition)
|
|
|
|
if let (tipNode, tipHeight) = self.itemContainers[i].updateTip(presentationData: presentationData, width: itemLayouts[i].size.width, transition: transition) {
|
|
var tipTransition = transition
|
|
if tipNode.supernode == nil {
|
|
tipTransition = .immediate
|
|
self.addSubnode(tipNode)
|
|
}
|
|
|
|
let tipAlpha: CGFloat = itemLayouts[i].alphaTransitionFraction
|
|
|
|
tipTransition.updateFrame(node: tipNode, frame: CGRect(origin: CGPoint(x: navigationContainerFrame.minX, y: navigationContainerFrame.maxY + tipSpacing), size: CGSize(width: itemLayouts[i].size.width, height: tipHeight)), beginWithCurrentState: true)
|
|
tipTransition.updateAlpha(node: tipNode, alpha: tipAlpha, beginWithCurrentState: true)
|
|
|
|
if i == self.itemContainers.count - 1 {
|
|
topItemSize.height += tipSpacing + tipHeight
|
|
}
|
|
}
|
|
}
|
|
|
|
for (itemContainer, isPopped) in self.dismissingItemContainers {
|
|
var position = itemContainer.position
|
|
if isPopped {
|
|
position.x = itemContainer.bounds.width / 2.0 + topItemWidth
|
|
} else {
|
|
position.x = itemContainer.bounds.width / 2.0 - topItemWidth
|
|
}
|
|
transition.updatePosition(node: itemContainer, position: position, completion: { [weak itemContainer] _ in
|
|
itemContainer?.removeFromSupernode()
|
|
})
|
|
if let tipNode = itemContainer.tipNode {
|
|
transition.updateAlpha(node: tipNode, alpha: 0.0, completion: { [weak tipNode] _ in
|
|
tipNode?.removeFromSupernode()
|
|
})
|
|
}
|
|
}
|
|
self.dismissingItemContainers.removeAll()
|
|
|
|
return CGSize(width: topItemWidth, height: topItemSize.height)
|
|
}
|
|
|
|
func highlightGestureMoved(location: CGPoint) {
|
|
if let topItemContainer = self.itemContainers.last {
|
|
topItemContainer.highlightGestureMoved(location: self.view.convert(location, to: topItemContainer.view))
|
|
}
|
|
}
|
|
|
|
func highlightGestureFinished(performAction: Bool) {
|
|
if let topItemContainer = self.itemContainers.last {
|
|
topItemContainer.highlightGestureFinished(performAction: performAction)
|
|
}
|
|
}
|
|
|
|
func updatePanSelection(isEnabled: Bool) {
|
|
if let selectionPanGesture = self.selectionPanGesture {
|
|
selectionPanGesture.isEnabled = isEnabled
|
|
}
|
|
}
|
|
|
|
func animateIn() {
|
|
for itemContainer in self.itemContainers {
|
|
if let tipNode = itemContainer.tipNode {
|
|
tipNode.animateIn()
|
|
}
|
|
}
|
|
}
|
|
}
|