import Foundation import UIKit import Display import ComponentFlow import MultilineTextComponent import TelegramPresentationData import GlassBackgroundComponent import LiquidLens import TabSelectionRecognizer extension CameraMode { func title(strings: PresentationStrings) -> String { switch self { case .photo: return strings.Story_Camera_Photo case .video: return strings.Story_Camera_Video case .live: return strings.Story_Camera_Live } } } private let buttonSize = CGSize(width: 55.0, height: 48.0) private let tabletButtonSize = CGSize(width: 55.0, height: 44.0) final class ModeComponent: Component { let isTablet: Bool let strings: PresentationStrings let tintColor: UIColor let availableModes: [CameraMode] let currentMode: CameraMode let updatedMode: (CameraMode) -> Void let tag: AnyObject? init( isTablet: Bool, strings: PresentationStrings, tintColor: UIColor, availableModes: [CameraMode], currentMode: CameraMode, updatedMode: @escaping (CameraMode) -> Void, tag: AnyObject? ) { self.isTablet = isTablet self.strings = strings self.tintColor = tintColor self.availableModes = availableModes self.currentMode = currentMode self.updatedMode = updatedMode self.tag = tag } static func ==(lhs: ModeComponent, rhs: ModeComponent) -> Bool { if lhs.isTablet != rhs.isTablet { return false } if lhs.strings !== rhs.strings { return false } if lhs.tintColor != rhs.tintColor { return false } if lhs.availableModes != rhs.availableModes { return false } if lhs.currentMode != rhs.currentMode { return false } return true } final class View: UIView, ComponentTaggedView { private var component: ModeComponent? private var state: EmptyComponentState? final class ItemView: HighlightTrackingButton { init() { super.init(frame: .zero) } required init(coder: NSCoder) { preconditionFailure() } func update(isTablet: Bool, value: String, selected: Bool, tintColor: UIColor) -> CGSize { let accentColor: UIColor let normalColor: UIColor if tintColor.rgb == 0xffffff { accentColor = UIColor(rgb: 0xffd300) normalColor = .white } else { accentColor = tintColor normalColor = tintColor.withAlphaComponent(0.5) } let title = NSMutableAttributedString(string: value.uppercased(), font: Font.with(size: 14.0, design: .regular, weight: .medium), textColor: selected ? accentColor : normalColor, paragraphAlignment: .center) title.addAttribute(.kern, value: -0.5 as NSNumber, range: NSMakeRange(0, title.length)) self.setAttributedTitle(title, for: .normal) self.sizeToFit() return CGSize(width: self.titleLabel?.bounds.size.width ?? 0.0, height: isTablet ? tabletButtonSize.height : buttonSize.height) } } private var backgroundView = UIView() private var backgroundContainer = GlassBackgroundContainerView() private let liquidLensView: LiquidLensView private var itemViews: [AnyHashable: ItemView] = [:] private var selectedItemViews: [AnyHashable: ItemView] = [:] private var tabSelectionRecognizer: TabSelectionRecognizer? private var selectionGestureState: (startX: CGFloat, currentX: CGFloat, itemId: AnyHashable)? public func matches(tag: Any) -> Bool { if let component = self.component, let componentTag = component.tag { let tag = tag as AnyObject if componentTag === tag { return true } } return false } init() { self.liquidLensView = LiquidLensView(kind: .externalContainer) super.init(frame: CGRect()) self.backgroundView.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.11) self.backgroundView.layer.cornerRadius = 24.0 self.layer.allowsGroupOpacity = true self.addSubview(self.backgroundView) self.backgroundView.addSubview(self.backgroundContainer) self.backgroundContainer.contentView.addSubview(self.liquidLensView) let tabSelectionRecognizer = TabSelectionRecognizer(target: self, action: #selector(self.onTabSelectionGesture(_:))) self.tabSelectionRecognizer = tabSelectionRecognizer self.liquidLensView.addGestureRecognizer(tabSelectionRecognizer) } required init?(coder aDecoder: NSCoder) { preconditionFailure() } private var animatedOut = false func animateOutToEditor(transition: ComponentTransition) { self.animatedOut = true transition.setAlpha(view: self.backgroundView, alpha: 0.0) transition.setSublayerTransform(view: self, transform: CATransform3DMakeTranslation(0.0, -buttonSize.height, 0.0)) } func animateInFromEditor(transition: ComponentTransition) { self.animatedOut = false transition.setAlpha(view: self.backgroundView, alpha: 1.0) transition.setSublayerTransform(view: self, transform: CATransform3DIdentity) } override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { return self.backgroundView.frame.contains(point) } private func item(at point: CGPoint) -> AnyHashable? { var closestItem: (AnyHashable, CGFloat)? for (id, itemView) in self.itemViews { if itemView.frame.contains(point) { return id } else { let distance = abs(point.x - itemView.center.x) if let closestItemValue = closestItem { if closestItemValue.1 > distance { closestItem = (id, distance) } } else { closestItem = (id, distance) } } } return closestItem?.0 } @objc private func onTabSelectionGesture(_ recognizer: TabSelectionRecognizer) { guard let component = self.component else { return } let location = recognizer.location(in: self.liquidLensView.contentView) switch recognizer.state { case .began: if let itemId = self.item(at: location), let itemView = self.itemViews[itemId] { let startX = itemView.frame.minX - 4.0 self.selectionGestureState = (startX, startX, itemId) self.state?.updated(transition: .spring(duration: 0.4), isLocal: true) } case .changed: if var selectionGestureState = self.selectionGestureState { selectionGestureState.currentX = selectionGestureState.startX + recognizer.translation(in: self).x if let itemId = self.item(at: location) { selectionGestureState.itemId = itemId } self.selectionGestureState = selectionGestureState self.state?.updated(transition: .immediate, isLocal: true) } case .ended, .cancelled: if let selectionGestureState = self.selectionGestureState { self.selectionGestureState = nil if case .ended = recognizer.state { guard let item = component.availableModes.first(where: { AnyHashable($0.rawValue) == selectionGestureState.itemId }) else { return } component.updatedMode(item) } self.state?.updated(transition: .spring(duration: 0.4), isLocal: true) } default: break } } func update(component: ModeComponent, availableSize: CGSize, state: EmptyComponentState, transition: ComponentTransition) -> CGSize { self.component = component self.state = state let isTablet = component.isTablet self.backgroundView.backgroundColor = component.isTablet ? .clear : UIColor(rgb: 0xffffff, alpha: 0.11) let inset: CGFloat = 23.0 let spacing: CGFloat = isTablet ? 9.0 : 40.0 var i = 0 var itemFrame = CGRect(origin: isTablet ? .zero : CGPoint(x: inset, y: 0.0), size: buttonSize) var selectedCenter = itemFrame.minX var selectedFrame = itemFrame var validKeys: Set = Set() for mode in component.availableModes.reversed() { let id = mode.rawValue validKeys.insert(id) let itemView: ItemView let selectedItemView: ItemView if let current = self.itemViews[id], let currentSelected = self.selectedItemViews[id] { itemView = current selectedItemView = currentSelected } else { itemView = ItemView() itemView.isUserInteractionEnabled = false self.itemViews[id] = itemView self.liquidLensView.contentView.addSubview(itemView) selectedItemView = ItemView() selectedItemView.isUserInteractionEnabled = false self.selectedItemViews[id] = selectedItemView self.liquidLensView.selectedContentView.addSubview(selectedItemView) } let itemSize = itemView.update(isTablet: component.isTablet, value: mode.title(strings: component.strings), selected: false, tintColor: component.tintColor) itemView.bounds = CGRect(origin: .zero, size: itemSize) let _ = selectedItemView.update(isTablet: component.isTablet, value: mode.title(strings: component.strings), selected: true, tintColor: component.tintColor) selectedItemView.bounds = CGRect(origin: .zero, size: itemSize) itemFrame = CGRect(origin: itemFrame.origin, size: itemSize) if mode == component.currentMode { selectedFrame = itemFrame } if isTablet { itemView.center = CGPoint(x: availableSize.width / 2.0, y: itemFrame.midY) selectedItemView.center = itemView.center if mode == component.currentMode { selectedCenter = itemFrame.midY } itemFrame = itemFrame.offsetBy(dx: 0.0, dy: tabletButtonSize.height + spacing) } else { itemView.center = CGPoint(x: itemFrame.midX, y: itemFrame.midY) selectedItemView.center = itemView.center if mode == component.currentMode { selectedCenter = itemFrame.midX } itemFrame = itemFrame.offsetBy(dx: itemFrame.width + spacing, dy: 0.0) } i += 1 } var removeKeys: [AnyHashable] = [] for (id, itemView) in self.itemViews { if !validKeys.contains(id) { removeKeys.append(id) transition.setAlpha(view: itemView, alpha: 0.0, completion: { _ in itemView.removeFromSuperview() }) } } for id in removeKeys { self.itemViews.removeValue(forKey: id) } let totalSize: CGSize let size: CGSize if isTablet { totalSize = CGSize(width: availableSize.width, height: tabletButtonSize.height * CGFloat(component.availableModes.count) + spacing * CGFloat(component.availableModes.count - 1)) size = CGSize(width: availableSize.width, height: availableSize.height) transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height / 2.0 - selectedCenter), size: totalSize)) } else { size = CGSize(width: availableSize.width, height: buttonSize.height) totalSize = CGSize(width: itemFrame.minX - spacing + inset, height: buttonSize.height) transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - totalSize.width) / 2.0), y: 0.0), size: totalSize)) } let containerFrame = CGRect(origin: .zero, size: self.backgroundView.frame.size) transition.setFrame(view: self.backgroundContainer, frame: containerFrame) let selectionFrame = selectedFrame.insetBy(dx: -23.0, dy: 3.0) let lensSelection: (x: CGFloat, width: CGFloat) if let selectionGestureState = self.selectionGestureState { lensSelection = (selectionGestureState.currentX, selectionFrame.width) } else { lensSelection = (selectionFrame.minX, selectionFrame.width) } transition.setFrame(view: self.liquidLensView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: containerFrame.size)) self.liquidLensView.update(size: containerFrame.size, selectionOrigin: CGPoint(x: lensSelection.x, y: 0.0), selectionSize: CGSize(width: lensSelection.width, height: selectionFrame.height), inset: 0.0, isDark: true, isLifted: self.selectionGestureState != nil, isCollapsed: false, transition: transition) self.backgroundContainer.update(size: containerFrame.size, isDark: true, transition: .immediate) return size } } 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, state: state, transition: transition) } } final class HintLabelComponent: Component { let text: String let tintColor: UIColor init( text: String, tintColor: UIColor ) { self.text = text self.tintColor = tintColor } static func ==(lhs: HintLabelComponent, rhs: HintLabelComponent) -> Bool { if lhs.text != rhs.text { return false } if lhs.tintColor != rhs.tintColor { return false } return true } final class View: UIView { private var component: HintLabelComponent? private var componentView = ComponentView() init() { super.init(frame: CGRect()) } required init?(coder aDecoder: NSCoder) { preconditionFailure() } func update(component: HintLabelComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { let previousComponent = self.component self.component = component if let previousText = previousComponent?.text, !previousText.isEmpty && previousText != component.text { if let componentView = self.componentView.view, let snapshotView = componentView.snapshotView(afterScreenUpdates: false) { snapshotView.frame = componentView.frame self.addSubview(snapshotView) snapshotView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false) snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in snapshotView?.removeFromSuperview() }) } self.componentView.view?.removeFromSuperview() self.componentView = ComponentView() } let textSize = self.componentView.update( transition: .immediate, component: AnyComponent( MultilineTextComponent( text: .plain(NSAttributedString(string: component.text.uppercased(), font: Font.with(size: 14.0, design: .camera, weight: .semibold), textColor: component.tintColor)), horizontalAlignment: .center ) ), environment: {}, containerSize: availableSize ) if let view = self.componentView.view { if view.superview == nil { view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.addSubview(view) } view.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - textSize.width) / 2.0), y: 0.0), size: textSize) } return CGSize(width: availableSize.width, height: textSize.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) } }