import Foundation import UIKit import Display import ComponentFlow import TelegramPresentationData import ContextUI import GlassBackgroundComponent import EdgeEffect final class BrowserNavigationBarEnvironment: Equatable { public let fraction: CGFloat public init(fraction: CGFloat) { self.fraction = fraction } public static func ==(lhs: BrowserNavigationBarEnvironment, rhs: BrowserNavigationBarEnvironment) -> Bool { if lhs.fraction != rhs.fraction { return false } return true } } final class BrowserNavigationBarComponent: Component { public class ExternalState { public fileprivate(set) var centerItemFrame: CGRect public init() { self.centerItemFrame = .zero } } let theme: PresentationTheme let topInset: CGFloat let height: CGFloat let sideInset: CGFloat let metrics: LayoutMetrics let externalState: ExternalState? let leftItems: [AnyComponentWithIdentity] let rightItems: [AnyComponentWithIdentity] let centerItem: AnyComponentWithIdentity? let collapseFraction: CGFloat let activate: () -> Void init( theme: PresentationTheme, topInset: CGFloat, height: CGFloat, sideInset: CGFloat, metrics: LayoutMetrics, externalState: ExternalState?, leftItems: [AnyComponentWithIdentity], rightItems: [AnyComponentWithIdentity], centerItem: AnyComponentWithIdentity?, collapseFraction: CGFloat, activate: @escaping () -> Void ) { self.theme = theme self.topInset = topInset self.height = height self.sideInset = sideInset self.metrics = metrics self.externalState = externalState self.leftItems = leftItems self.rightItems = rightItems self.centerItem = centerItem self.collapseFraction = collapseFraction self.activate = activate } static func ==(lhs: BrowserNavigationBarComponent, rhs: BrowserNavigationBarComponent) -> Bool { if lhs.theme !== rhs.theme { return false } if lhs.topInset != rhs.topInset { return false } if lhs.height != rhs.height { return false } if lhs.sideInset != rhs.sideInset { return false } if lhs.metrics != rhs.metrics { return false } if lhs.leftItems != rhs.leftItems { return false } if lhs.rightItems != rhs.rightItems { return false } if lhs.centerItem != rhs.centerItem { return false } if lhs.collapseFraction != rhs.collapseFraction { return false } return true } final class View: UIView { private var edgeEffectView = EdgeEffectView() private let containerView = GlassBackgroundContainerView() private var leftItemsBackground: GlassBackgroundView? private var leftItems: [AnyHashable: ComponentView] = [:] private var rightItemsBackground: GlassBackgroundView? private var rightItems: [AnyHashable: ComponentView] = [:] private var centerItems: [AnyHashable: ComponentView] = [:] private let activateButton = HighlightTrackingButton() private var component: BrowserNavigationBarComponent? private weak var state: EmptyComponentState? override init(frame: CGRect) { super.init(frame: frame) self.addSubview(self.edgeEffectView) self.addSubview(self.containerView) self.activateButton.addTarget(self, action: #selector(self.activatePressed), for: .touchUpInside) } required init?(coder: NSCoder) { preconditionFailure() } @objc private func activatePressed() { guard let component = self.component else { return } component.activate() } func update(component: BrowserNavigationBarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state var availableWidth = availableSize.width let sideInset: CGFloat = (component.metrics.isTablet ? 20.0 : 16.0) + component.sideInset let collapsedHeight: CGFloat = 54.0 let expandedHeight = component.height let contentHeight: CGFloat = expandedHeight * (1.0 - component.collapseFraction) + collapsedHeight * component.collapseFraction let size = CGSize(width: availableSize.width, height: component.topInset + contentHeight) let verticalOffset: CGFloat = component.metrics.isTablet ? -2.0 : 0.0 let itemSpacing: CGFloat = 0.0 //component.metrics.isTablet ? 26.0 : 8.0 let panelHeight: CGFloat = 44.0 var leftItemsBackground: GlassBackgroundView? var leftItemsBackgroundTransition = transition if !component.leftItems.isEmpty { if let current = self.leftItemsBackground { leftItemsBackground = current } else { leftItemsBackgroundTransition = .immediate leftItemsBackground = GlassBackgroundView() self.containerView.contentView.addSubview(leftItemsBackground!) self.leftItemsBackground = leftItemsBackground transition.animateScale(view: leftItemsBackground!, from: 0.1, to: 1.0) transition.animateAlpha(view: leftItemsBackground!, from: 0.0, to: 1.0) } } var rightItemsBackground: GlassBackgroundView? var rightItemsBackgroundTransition = transition if !component.rightItems.isEmpty { if let current = self.rightItemsBackground { rightItemsBackground = current } else { rightItemsBackgroundTransition = .immediate rightItemsBackground = GlassBackgroundView() self.containerView.contentView.addSubview(rightItemsBackground!) self.rightItemsBackground = rightItemsBackground transition.animateScale(view: rightItemsBackground!, from: 0.1, to: 1.0) transition.animateAlpha(view: rightItemsBackground!, from: 0.0, to: 1.0) } } var validLeftItemIds: Set = Set() var leftItemTransitions: [AnyHashable: (CGSize, ComponentTransition)] = [:] var leftItemsWidth: CGFloat = 0.0 for item in component.leftItems { validLeftItemIds.insert(item.id) var itemTransition = transition let itemView: ComponentView if let current = self.leftItems[item.id] { itemView = current } else { itemTransition = .immediate itemView = ComponentView() self.leftItems[item.id] = itemView } let itemSize = itemView.update( transition: itemTransition, component: item.component, environment: {}, containerSize: CGSize(width: availableWidth, height: expandedHeight) ) leftItemTransitions[item.id] = (itemSize, itemTransition) availableWidth -= itemSize.width leftItemsWidth += itemSize.width } var validRightItemIds: Set = Set() var rightItemTransitions: [AnyHashable: (CGSize, ComponentTransition)] = [:] var rightItemsWidth: CGFloat = 0.0 for item in component.rightItems { validRightItemIds.insert(item.id) var itemTransition = transition let itemView: ComponentView if let current = self.rightItems[item.id] { itemView = current } else { itemTransition = .immediate itemView = ComponentView() self.rightItems[item.id] = itemView } let itemSize = itemView.update( transition: itemTransition, component: item.component, environment: {}, containerSize: CGSize(width: availableWidth, height: expandedHeight) ) rightItemTransitions[item.id] = (itemSize, itemTransition) availableWidth -= itemSize.width rightItemsWidth += itemSize.width } var centerLeftInset = sideInset var leftItemX = 0.0 for item in component.leftItems { guard let (itemSize, itemTransition) = leftItemTransitions[item.id], let itemView = self.leftItems[item.id]?.view else { continue } let itemPosition = CGPoint(x: leftItemX + itemSize.width / 2.0, y: panelHeight * 0.5) let itemFrame = CGRect(origin: CGPoint(x: itemPosition.x - itemSize.width * 0.5, y: itemPosition.y - itemSize.height * 0.5), size: itemSize) if itemView.superview == nil { leftItemsBackground?.contentView.addSubview(itemView) transition.animateAlpha(view: itemView, from: 0.0, to: 1.0) transition.animateScale(view: itemView, from: 0.01, to: 1.0) } itemTransition.setBounds(view: itemView, bounds: CGRect(origin: .zero, size: itemFrame.size)) itemTransition.setPosition(view: itemView, position: itemFrame.center) leftItemX += itemSize.width + itemSpacing centerLeftInset += itemSize.width + itemSpacing } var centerRightInset = sideInset var rightItemX = rightItemsWidth for item in component.rightItems.reversed() { guard let (itemSize, itemTransition) = rightItemTransitions[item.id], let itemView = self.rightItems[item.id]?.view else { continue } let itemPosition = CGPoint(x: rightItemX - itemSize.width / 2.0, y: panelHeight * 0.5) let itemFrame = CGRect(origin: CGPoint(x: itemPosition.x - itemSize.width * 0.5, y: itemPosition.y - itemSize.height * 0.5), size: itemSize) if itemView.superview == nil { rightItemsBackground?.contentView.addSubview(itemView) transition.animateAlpha(view: itemView, from: 0.0, to: 1.0) transition.animateScale(view: itemView, from: 0.01, to: 1.0) } itemTransition.setBounds(view: itemView, bounds: CGRect(origin: .zero, size: itemFrame.size)) itemTransition.setPosition(view: itemView, position: itemFrame.center) itemTransition.setScale(view: itemView, scale: 1.0 - 0.35 * component.collapseFraction) itemTransition.setAlpha(view: itemView, alpha: 1.0 - component.collapseFraction) rightItemX -= itemSize.width + itemSpacing centerRightInset += itemSize.width + itemSpacing } if let leftItemsBackground { let leftItemsFrame = CGRect(origin: CGPoint(x: sideInset - (leftItemsWidth / 2.0 * 0.35 * component.collapseFraction), y: component.topInset + contentHeight / 2.0 + verticalOffset - panelHeight / 2.0), size: CGSize(width: leftItemsWidth, height: panelHeight)) leftItemsBackgroundTransition.setFrame(view: leftItemsBackground, frame: leftItemsFrame) leftItemsBackground.update(size: leftItemsFrame.size, shape: .roundedRect(cornerRadius: leftItemsFrame.height * 0.5), isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: UIColor(white: component.theme.overallDarkAppearance ? 0.0 : 1.0, alpha: 0.6)), isInteractive: true, transition: leftItemsBackgroundTransition) leftItemsBackgroundTransition.setScale(view: leftItemsBackground, scale: 1.0 - 0.999 * component.collapseFraction) leftItemsBackgroundTransition.setAlpha(view: leftItemsBackground.contentView, alpha: 1.0 - component.collapseFraction) } else if let leftItemsBackground = self.leftItemsBackground { self.leftItemsBackground = nil leftItemsBackground.removeFromSuperview() } if let rightItemsBackground { let rightItemsFrame = CGRect(origin: CGPoint(x: availableSize.width - sideInset - rightItemsWidth * (1.0 - component.collapseFraction) + (rightItemsWidth / 2.0 * 0.35 * component.collapseFraction), y: component.topInset + contentHeight / 2.0 + verticalOffset - panelHeight / 2.0), size: CGSize(width: rightItemsWidth, height: panelHeight)) rightItemsBackgroundTransition.setFrame(view: rightItemsBackground, frame: rightItemsFrame) rightItemsBackground.update(size: rightItemsFrame.size, shape: .roundedRect(cornerRadius: rightItemsFrame.height * 0.5), isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: UIColor(white: component.theme.overallDarkAppearance ? 0.0 : 1.0, alpha: 0.6)), isInteractive: true, transition: rightItemsBackgroundTransition) rightItemsBackgroundTransition.setScale(view: rightItemsBackground, scale: 1.0 - 0.999 * component.collapseFraction) rightItemsBackgroundTransition.setAlpha(view: rightItemsBackground.contentView, alpha: 1.0 - component.collapseFraction) } else if let rightItemsBackground = self.rightItemsBackground { self.rightItemsBackground = nil rightItemsBackground.removeFromSuperview() } var removeLeftItemIds: [AnyHashable] = [] for (id, item) in self.leftItems { if !validLeftItemIds.contains(id) { removeLeftItemIds.append(id) if let itemView = item.view { transition.setScale(view: itemView, scale: 0.01) transition.setAlpha(view: itemView, alpha: 0.0, completion: { _ in itemView.removeFromSuperview() }) } } } for id in removeLeftItemIds { self.leftItems.removeValue(forKey: id) } var removeRightItemIds: [AnyHashable] = [] for (id, item) in self.rightItems { if !validRightItemIds.contains(id) { removeRightItemIds.append(id) if let itemView = item.view { transition.setScale(view: itemView, scale: 0.01) transition.setAlpha(view: itemView, alpha: 0.0, completion: { _ in itemView.removeFromSuperview() }) } } } for id in removeRightItemIds { self.rightItems.removeValue(forKey: id) } let maxCenterInset = max(centerLeftInset, centerRightInset) if !component.leftItems.isEmpty || !component.rightItems.isEmpty { availableWidth -= itemSpacing * CGFloat(max(0, component.leftItems.count - 1)) + itemSpacing * CGFloat(max(0, component.rightItems.count - 1)) + 30.0 } availableWidth -= component.sideInset * 2.0 let canCenter = availableWidth > 390.0 availableWidth = min(390.0, availableWidth) let environment = BrowserNavigationBarEnvironment(fraction: component.collapseFraction) var centerX = maxCenterInset + (availableSize.width - maxCenterInset * 2.0) / 2.0 if canCenter { centerX = availableSize.width / 2.0 } else { centerX = centerLeftInset + (availableSize.width - centerLeftInset - centerRightInset) / 2.0 } var validCenterItemIds: Set = Set() if let item = component.centerItem { validCenterItemIds.insert(item.id) var itemTransition = transition let itemView: ComponentView if let current = self.centerItems[item.id] { itemView = current } else { itemTransition = .immediate itemView = ComponentView() self.centerItems[item.id] = itemView } let itemSize = itemView.update( transition: itemTransition, component: item.component, environment: { environment }, containerSize: CGSize(width: availableWidth, height: expandedHeight) ) let itemPosition = CGPoint(x: centerX, y: component.topInset + contentHeight / 2.0 + verticalOffset) let itemFrame = CGRect(origin: CGPoint(x: itemPosition.x - itemSize.width * 0.5, y: itemPosition.y - itemSize.height * 0.5), size: itemSize) if let itemView = itemView.view { if itemView.superview == nil { self.containerView.contentView.addSubview(itemView) transition.animateAlpha(view: itemView, from: 0.0, to: 1.0) } itemTransition.setBounds(view: itemView, bounds: CGRect(origin: .zero, size: itemFrame.size)) itemTransition.setPosition(view: itemView, position: itemFrame.center) itemTransition.setScale(view: itemView, scale: 1.0 - 0.25 * component.collapseFraction) } component.externalState?.centerItemFrame = itemFrame } var removeCenterItemIds: [AnyHashable] = [] for (id, item) in self.centerItems { if !validCenterItemIds.contains(id) { removeCenterItemIds.append(id) if let itemView = item.view { transition.setAlpha(view: itemView, alpha: 0.0, completion: { _ in itemView.removeFromSuperview() }) } } } for id in removeCenterItemIds { self.centerItems.removeValue(forKey: id) } if component.collapseFraction == 1.0 { if self.activateButton.superview == nil { self.addSubview(self.activateButton) } self.activateButton.frame = CGRect(origin: .zero, size: size) } else { self.activateButton.removeFromSuperview() } self.containerView.update(size: size, isDark: component.theme.overallDarkAppearance, transition: transition) transition.setFrame(view: self.containerView, frame: CGRect(origin: .zero, size: size)) let edgeEffectHeight: CGFloat = 80.0 let edgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: edgeEffectHeight)) transition.setFrame(view: self.edgeEffectView, frame: edgeEffectFrame) self.edgeEffectView.update( content: .clear, blur: true, rect: edgeEffectFrame, edge: .top, edgeSize: edgeEffectFrame.height, transition: transition ) return size } } public func makeView() -> View { return View(frame: CGRect()) } func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } final class LoadingProgressComponent: Component { let color: UIColor let height: CGFloat let value: CGFloat init( color: UIColor, height: CGFloat, value: CGFloat ) { self.color = color self.height = height self.value = value } static func ==(lhs: LoadingProgressComponent, rhs: LoadingProgressComponent) -> Bool { if lhs.color != rhs.color { return false } if lhs.height != rhs.height { return false } if lhs.value != rhs.value { return false } return true } final class View: UIView { private var lineView: UIView private var currentValue: Double = 0.0 init() { self.lineView = UIView() self.lineView.clipsToBounds = true self.lineView.layer.cornerRadius = 1.0 self.lineView.alpha = 0.0 super.init(frame: CGRect()) self.addSubview(self.lineView) } required init?(coder aDecoder: NSCoder) { preconditionFailure() } func update(component: LoadingProgressComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { self.lineView.backgroundColor = component.color let value = component.value let frame = CGRect(origin: .zero, size: CGSize(width: availableSize.width * component.value, height: component.height)) var animated = true if value < self.currentValue { if self.currentValue == 1.0 { self.lineView.frame = CGRect(origin: .zero, size: CGSize(width: 0.0, height: component.height)) } else { animated = false } } self.currentValue = value let transition: ComponentTransition if animated && value > 0.0 { transition = .spring(duration: 0.7) } else { transition = .immediate } let alphaTransition: ComponentTransition if animated { alphaTransition = .easeInOut(duration: 0.3) } else { alphaTransition = .immediate } transition.setFrame(view: self.lineView, frame: frame) let alpha: CGFloat = value < 0.01 || value > 0.99 ? 0.0 : 1.0 alphaTransition.setAlpha(view: self.lineView, alpha: alpha) return CGSize(width: availableSize.width, height: component.height) } } func makeView() -> View { return View() } func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } }