// // File.swift // // // Created by Mikhail Filimonov on 08.02.2024. // import Foundation import TelegramCore import Postbox import SwiftSignalKit import CallVideoLayer import TGUIKit import MetalEngine import AppKit let colorSets_fallback = [ [ NSColor(0x568FD6), NSColor(0x626ED5), NSColor(0xA667D5), NSColor(0x7664DA) ], [ NSColor(0xACBD65), NSColor(0x459F8D), NSColor(0x53A4D1), NSColor(0x3E917A) ], [ NSColor(0xC0508D), NSColor(0xF09536), NSColor(0xCE5081), NSColor(0xFC7C4C) ] ] private class ShadowView: View { public override init() { super.init(frame: .zero) setup() } public required init(frame frameRect: NSRect) { super.init(frame: frameRect) setup() } required public init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private var gradient: CAGradientLayer { return self.layer as! CAGradientLayer } private func setup() { self.layer = CAGradientLayer() update() } private func update() { let baseGradientAlpha: CGFloat = 1.0 let numSteps = 8 let firstStep = 1 let firstLocation: CGFloat = 0 let color = NSColor.black.withAlphaComponent(0.6) self.gradient.colors = (0 ..< numSteps).map { i in if i < firstStep { return color.cgColor } else { let step: CGFloat = CGFloat(i - firstStep) / CGFloat(numSteps - firstStep - 1) let value: CGFloat = 1.0 - bezierPoint(0.42, 0.0, 0.58, 1.0, step) return color.withAlphaComponent(baseGradientAlpha * value).cgColor } } self.gradient.locations = (0 ..< numSteps).map { i -> NSNumber in if i < firstStep { return 0.0 as NSNumber } else { let step: CGFloat = CGFloat(i - firstStep) / CGFloat(numSteps - firstStep - 1) return (firstLocation + (1.0 - firstLocation) * step) as NSNumber } } } } private final class InfoHelpView : NSVisualEffectView { private var shimmer: ShimmerLayer? private let textView = TextView() override init(frame frameRect: NSRect) { super.init(frame: frameRect) self.addSubview(textView) self.textView.userInteractionEnabled = false self.textView.isSelectable = false self.wantsLayer = true self.material = .light self.state = .active self.blendingMode = .withinWindow } var string: String? { return textView.textLayout?.attributedString.string } func set(string: String, hasShimm: Bool = false) { let layout = TextViewLayout(.initialize(string: string, color: NSColor.white, font: .medium(.text)), alignment: .center) layout.measure(width: .greatestFiniteMagnitude) self.textView.update(layout) self.setFrameSize(NSMakeSize(layout.layoutSize.width + 20, layout.layoutSize.height + 10)) self.layer?.cornerRadius = frame.height / 2 if #available(macOS 10.15, *) { self.layer?.cornerCurve = .continuous } if hasShimm { let current: ShimmerLayer if let local = self.shimmer { current = local } else { current = ShimmerLayer() current.isStatic = true current.frame = self.bounds self.shimmer = current self.layer?.addSublayer(current) } current.update(backgroundColor: nil, shimmeringColor: NSColor(0xffffff, 0.3), data: nil, size: self.frame.size, imageSize: self.frame.size) current.updateAbsoluteRect(self.bounds, within: self.frame.size) } else if let shimmer = self.shimmer { performSublayerRemoval(shimmer, animated: true) } } override func layout() { super.layout() textView.center() shimmer?.frame = bounds } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } struct PeerCallVideoViewState { var incomingView: MetalCallVideoView? var outgoingView: MetalCallVideoView? var incomingInited: Bool { return self.incomingView?.videoMetrics != nil } var outgoingInited: Bool { return self.outgoingView?.videoMetrics != nil } var smallVideoSize: NSSize { if let outgoingView, let metrics = outgoingView.videoMetrics { let targetSize = NSMakeSize(190, 190) return metrics.resolution.aspectFitted(targetSize) } return NSMakeSize(190, 100) } } private enum VideoMagnify { case topLeft case topRight case bottomLeft case bottomRight } final class PeerCallScreenView : Control { private let backgroundLayer: CallBackgroundLayer? private let backgroundLayer_fallback: AnimatedGradientBackgroundView? private var photoView: PeerCallPhotoView? private let statusView = PeerCallStatusView(frame: NSMakeRect(0, 0, 300, 58)) private let settingsView = ImageButton() private var arguments: Arguments? private var state: PeerCallState? private let actions = View() private var statusTooltip: InfoHelpView? private var secretView: SecretKeyView? private var revealedKey: PeerCallRevealedSecretKeyView? private var keyoverlay: Control? private var tooltipsViews: [PeerCallTooltipStatusView] = [] private var tooltips: [PeerCallTooltipStatusView.TooltipType] = [] private var videoViewState: PeerCallVideoViewState? private var actionsViews: [PeerCallActionView] = [] private var actionsList: [PeerCallAction] = [] private weak var videoLink_large: MetalCallVideoView? private weak var videoLink_small: MetalCallVideoView? private var videoShadowView: ShadowView? private var videoBackgroundView: NSVisualEffectView? private var videoBackgroundView_color: View? private var videoMagnify: VideoMagnify = .bottomRight private var canAnimateAudioLevel = true private var processedInitialAudioLevelBump: Bool = false private var audioLevelBump: Float = 0.0 private var currentAvatarAudioScale: CGFloat = 1.0 private var targetAudioLevel: Float = 0.0 private var audioLevel: Float = 0.0 private var audioLevelUpdateSubscription: SharedDisplayLinkDriver.Link? required init(frame frameRect: NSRect) { #if arch(arm64) self.backgroundLayer = CallBackgroundLayer() self.backgroundLayer_fallback = nil #else self.backgroundLayer = nil self.backgroundLayer_fallback = AnimatedGradientBackgroundView() #endif super.init(frame: frameRect) if let backgroundLayer { self.layer?.addSublayer(backgroundLayer) self.layer?.addSublayer(backgroundLayer.blurredLayer) } else if let backgroundLayer_fallback { self.addSubview(backgroundLayer_fallback) } addSubview(actions) addSubview(statusView) settingsView.set(image: NSImage(resource: .icSettings).precomposed(.white), for: .Normal) settingsView.autohighlight = false settingsView.scaleOnClick = true settingsView.sizeToFit() addSubview(settingsView) actions.layer?.masksToBounds = false settingsView.set(handler: { [weak self] _ in self?.arguments?.openSettings() }, for: .SingleClick) updateLayout(size: self.frame.size, transition: .immediate) } override func viewDidMoveToWindow() { super.viewDidMoveToWindow() self.backgroundLayer?.isInHierarchy = window != nil } override func viewWillMove(toWindow newWindow: NSWindow?) { if let window = newWindow as? Window { var start: NSPoint? = nil var initial: NSPoint? = nil window.set(mouseHandler: { [weak self] event in guard let self, let window = self.window, let control = self.videoLink_small else { return .rejected } let startPoint = self.convert(window.mouseLocationOutsideOfEventStream, from: nil) if NSPointInRect(startPoint, control.frame) { start = startPoint initial = startPoint } else { start = nil initial = nil } return .rejected }, with: self, for: .leftMouseDown) window.set(mouseHandler: { [weak self] event in guard let self, let startPoint = start, let control = self.videoLink_small else { return .rejected } control.isMoving = true let current = self.convert(event.locationInWindow, from: nil) let difference = current - startPoint control.setFrameOrigin(control.frame.origin + difference) start = current return .rejected }, with: self, for: .leftMouseDragged) window.set(mouseHandler: { [ weak self] event in guard let self, let control = self.videoLink_small, let _ = start else { return .rejected } let current = self.convert(event.locationInWindow, from: nil) start = nil control.isMoving = false if initial == current { return .rejected } if current.x < frame.width / 2 { if current.y > frame.height / 2 { self.videoMagnify = .bottomLeft } else { self.videoMagnify = .topLeft } } else { if current.y > frame.height / 2 { self.videoMagnify = .bottomRight } else { self.videoMagnify = .topRight } } self.updateLayout(size: self.frame.size, transition: .animated(duration: 0.35, curve: .spring)) return .invoked }, with: self, for: .leftMouseUp) self.audioLevelUpdateSubscription = SharedDisplayLinkDriver.shared.add { [weak self] _ in guard let self else { return } self.attenuateAudioLevelStep() } } else { _window?.removeAllHandlers(for: self) self.audioLevelUpdateSubscription = nil } } override var mouseDownCanMoveWindow: Bool { return true } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func layout() { super.layout() self.updateLayout(size: self.frame.size, transition: .immediate) } func updateLayout(size: NSSize, transition: ContainedViewLayoutTransition) { guard let state = self.state else { return } let hideOutside = (videoLink_large != nil) ? (!state.mouseInside && state.isActive) : false transition.updateFrame(view: settingsView, frame: CGRect.init(origin: CGPoint(x: size.width - settingsView.frame.width - 5, y: 5), size: settingsView.frame.size)) transition.updateAlpha(view: settingsView, alpha: hideOutside ? 0 : 1) backgroundLayer?.frame = size.bounds backgroundLayer?.blurredLayer.frame = size.bounds backgroundLayer_fallback?.frame = size.bounds self.backgroundLayer?.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(size.width), height: Int(size.height)), edgeInset: 0) if let photoView { transition.updateFrame(view: photoView, frame: photoView.centerFrameX(y: floorToScreenPixels(size.height / 3) - 50)) photoView.updateLayout(size: photoView.frame.size, transition: transition) } if videoLink_large != nil { transition.updateFrame(view: statusView, frame: statusView.centerFrameX(y: 10)) statusView.updateLayout(size: statusView.frame.size, transition: transition) } else if let photoView { transition.updateFrame(view: statusView, frame: statusView.centerFrameX(y: photoView.frame.maxY + 32)) statusView.updateLayout(size: statusView.frame.size, transition: transition) } transition.updateAlpha(view: statusView, alpha: hideOutside ? 0 : 1) if let videoViewState { // let transition: ContainedViewLayoutTransition = transition.isAnimated ? .animated(duration: 0.2, curve: .easeOut) : .immediate if let videoLink_large = videoLink_large { let frame = largeVideoFrame(view: videoLink_large, size: size, state: videoViewState) transition.updateFrame(view: videoLink_large, frame: frame) videoLink_large.updateLayout(size: frame.size, transition: transition) } if let videoLink_small = videoLink_small, !videoLink_small.isMoving { let frame = smallVideoFrame(view: videoLink_small, size: size, videoMagnify: self.videoMagnify, state: videoViewState) transition.updateFrame(view: videoLink_small, frame: frame) videoLink_small.updateLayout(size: frame.size, transition: transition) } } if let videoShadowView { transition.updateFrame(view: videoShadowView, frame: CGRect(origin: videoShadowView.frame.origin, size: NSMakeSize(size.width, videoShadowView.frame.height))) transition.updateAlpha(view: videoShadowView, alpha: hideOutside ? 0 : 1) } if let videoBackgroundView { transition.updateFrame(view: videoBackgroundView, frame: size.bounds) } if let videoBackgroundView_color { transition.updateFrame(view: videoBackgroundView_color, frame: size.bounds) } if let statusTooltip { transition.updateFrame(view: statusTooltip, frame: statusTooltipFrame(view: statusTooltip, state: state)) transition.updateAlpha(view: statusTooltip, alpha: hideOutside ? 0 : 1) } if let secretView { transition.updateFrame(view: secretView, frame: secretKeyFrame(view: secretView, state: state, largeVideo: videoLink_large != nil)) transition.updateAlpha(view: secretView, alpha: hideOutside ? 0 : 1) secretView.updateLayout(size: secretView.frame.size, transition: transition) } if let revealedKey { transition.updateFrame(view: revealedKey, frame: revealedKeyFrame(view: revealedKey, state: state)) revealedKey.updateLayout(size: revealedKey.frame.size, transition: transition) } if let keyoverlay { transition.updateFrame(view: keyoverlay, frame: size.bounds) } transition.updateFrame(view: actions, frame: NSMakeRect(0, size.height - 70 - 40, size.width, 70)) let actions = self.actionsViews let width = actions.reduce(0, { $0 + $1.frame.width}) + 36 * CGFloat(actions.count - 1) var x = floorToScreenPixels((size.width - width) / 2) for action in actions { transition.updateFrame(view: action, frame: CGRect(origin: CGPoint(x: x, y: 0), size: action.frame.size)) transition.updateAlpha(view: action, alpha: hideOutside ? 0 : 1) x += action.frame.width + 36 } var y: CGFloat = size.height - 70 - 40 for tooltip in tooltipsViews { y -= (tooltip.frame.height + 10) transition.updateFrame(view: tooltip, frame: tooltip.centerFrameX(y: y)) transition.updateAlpha(view: tooltip, alpha: hideOutside ? 0 : 1) tooltip.reveal(animated: transition.isAnimated) } } func updateAudioLevel(_ value: Float) { if self.canAnimateAudioLevel { self.targetAudioLevel = min(1, value) } else { self.targetAudioLevel = 0.0 } if let blobView_fallback = photoView?.blobView_fallback { blobView_fallback.updateLevel(CGFloat(value)) } } private func attenuateAudioLevelStep() { self.audioLevel = self.audioLevel * 0.8 + (self.targetAudioLevel + self.audioLevelBump) * 0.2 if self.audioLevel <= 0.01 { self.audioLevel = 0.0 } self.updateAudioLevel() } private func updateAudioLevel() { if self.canAnimateAudioLevel, let photoView { let additionalAvatarScale = CGFloat(max(0.0, min(self.audioLevel, 5.0)) * 0.05) self.currentAvatarAudioScale = 1.0 + additionalAvatarScale // let blobAmplificationFactor: CGFloat = 2.0 if let blobView = photoView.blobView { blobView.blob.transform = CATransform3DMakeScale(1.0 + additionalAvatarScale * blobAmplificationFactor, 1.0 + additionalAvatarScale * blobAmplificationFactor, 1.0) } } } func updateState(_ state: PeerCallState, videoViewState: PeerCallVideoViewState, arguments: Arguments, transition: ContainedViewLayoutTransition) { self.state = state self.arguments = arguments self.videoViewState = videoViewState self.statusView.updateState(state, arguments: arguments, transition: transition) self.backgroundLayer?.update(stateIndex: state.stateIndex, isEnergySavingEnabled: false, transition: transition) self.backgroundLayer_fallback?.updateColors(colors: colorSets_fallback[state.stateIndex]) var smallVideo: MetalCallVideoView? var largeVideo: MetalCallVideoView? var largeInited: Bool var smallInited: Bool switch state.smallVideo { case .incoming: smallVideo = videoViewState.incomingView largeVideo = videoViewState.outgoingView largeInited = videoViewState.outgoingInited smallInited = videoViewState.incomingInited case .outgoing: smallVideo = videoViewState.outgoingView largeVideo = videoViewState.incomingView largeInited = videoViewState.incomingInited smallInited = videoViewState.outgoingInited } if let largeVideo = largeVideo { if largeInited { if transition.isAnimated, largeVideo.superview == nil { let frame = largeVideoFrame(view: largeVideo, size: frame.size, state: videoViewState) ContainedViewLayoutTransition.immediate.updateFrame(view: largeVideo, frame: frame) largeVideo.updateLayout(size: frame.size, transition: .immediate) largeVideo.layer?.animateAlpha(from: 0, to: 1, duration: transition.duration, timingFunction: transition.timingFunction) } addSubview(largeVideo, positioned: .below, relativeTo: self.videoLink_small ?? actions) largeVideo.layer?.cornerRadius = 0 largeVideo.userInteractionEnabled = false videoLink_large = largeVideo } else { videoLink_large = nil } } else if let view = self.videoLink_large { if view != smallVideo && view != largeVideo { performSubviewRemoval(view, animated: transition.isAnimated) } self.videoLink_large = nil } if let smallVideo = smallVideo { if smallInited { if transition.isAnimated, smallVideo.superview == nil { let frame = smallVideoFrame(view: smallVideo, size: frame.size, videoMagnify: self.videoMagnify, state: videoViewState) ContainedViewLayoutTransition.immediate.updateFrame(view: smallVideo, frame: frame) smallVideo.updateLayout(size: frame.size, transition: .immediate) smallVideo.layer?.animateAlpha(from: 0, to: 1, duration: transition.duration, timingFunction: transition.timingFunction) } addSubview(smallVideo, positioned: .below, relativeTo: actions) smallVideo.layer?.cornerRadius = 10 smallVideo.userInteractionEnabled = true videoLink_small = smallVideo } else { videoLink_small = nil } } else if let view = self.videoLink_small { if view != smallVideo && view != largeVideo { performSubviewRemoval(view, animated: transition.isAnimated) } self.videoLink_small = nil } if videoLink_large != nil { let current: ShadowView if let view = self.videoShadowView { current = view } else { current = ShadowView(frame: NSMakeRect(0, -50, frame.width, 160)) addSubview(current, positioned: .below, relativeTo: self.statusView) self.videoShadowView = current } } else if let videoShadowView { performSubviewRemoval(videoShadowView, animated: transition.isAnimated) self.videoShadowView = nil } if let videoLink_large { let current: View if let view = self.videoBackgroundView_color { current = view } else { current = View(frame: self.bounds) addSubview(current, positioned: .below, relativeTo: videoLink_large) self.videoBackgroundView_color = current } current.backgroundColor = NSColor.black.withAlphaComponent(0.7) } else if let videoBackgroundView_color { performSubviewRemoval(videoBackgroundView_color, animated: transition.isAnimated) self.videoBackgroundView_color = nil } if let videoLink_large { let current: NSVisualEffectView if let view = self.videoBackgroundView { current = view } else { current = NSVisualEffectView(frame: self.bounds) current.material = .dark current.state = .active current.blendingMode = .withinWindow current.wantsLayer = true addSubview(current, positioned: .below, relativeTo: videoLink_large) self.videoBackgroundView = current } } else if let videoBackgroundView { performSubviewRemoval(videoBackgroundView, animated: transition.isAnimated) self.videoBackgroundView = nil } if videoLink_large == nil { let current: PeerCallPhotoView if let view = self.photoView { current = view } else { current = PeerCallPhotoView(frame: NSMakeRect(0, 0, 120, 120)) if backgroundLayer_fallback != nil { addSubview(current, positioned: .above, relativeTo: backgroundLayer_fallback) } else { addSubview(current, positioned: .below, relativeTo: self.subviews.first) } self.photoView = current ContainedViewLayoutTransition.immediate.updateFrame(view: current, frame: current.centerFrameX(y: floorToScreenPixels(frame.height / 3) - 50)) if transition.isAnimated { current.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) current.layer?.animateScaleSpring(from: 0.01, to: 1, duration: 0.2, bounce: false) } } if let backgroundLayer { current.blobView?.maskLayer = backgroundLayer.blurredLayer } current.updateState(state, arguments: arguments, transition: transition) } else if let photoView { performSubviewRemoval(photoView, animated: transition.isAnimated, scale: true) self.photoView = nil } if let tooltip = state.statusTooltip { if self.statusTooltip?.string != tooltip { if let statusTooltip = self.statusTooltip { performSubviewRemoval(statusTooltip, animated: transition.isAnimated, scale: true) self.statusTooltip = nil } let current: InfoHelpView let isNew = true current = InfoHelpView(frame: .zero) self.addSubview(current) self.statusTooltip = current current.set(string: tooltip, hasShimm: true) if isNew { ContainedViewLayoutTransition.immediate.updateFrame(view: current, frame: statusTooltipFrame(view: current, state: state)) if transition.isAnimated { current.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) current.layer?.animateScaleSpring(from: 0.01, to: 1, duration: 0.2, bounce: false) } } } } else if let statusTooltip = self.statusTooltip { performSubviewRemoval(statusTooltip, animated: transition.isAnimated, scale: true) self.statusTooltip = nil } do { var tooltips:[PeerCallTooltipStatusView.TooltipType] = [] if state.externalState.isMuted, state.isActive { tooltips.append(.yourMicroOff) } if state.externalState.remoteAudioState == .muted, state.isActive { tooltips.append(.microOff(state.compactTitle)) } let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: self.tooltips, rightList: tooltips) for deleteIndex in deleteIndices.reversed() { let view = self.tooltipsViews.remove(at: deleteIndex) performSubviewRemoval(view, animated: transition.isAnimated, scale: true) } for indicesAndItem in indicesAndItems { let view = PeerCallTooltipStatusView(frame: .zero) view.set(type: indicesAndItem.1) tooltipsViews.insert(view, at: indicesAndItem.0) } for updateIndex in updateIndices { let view = self.tooltipsViews[updateIndex.0] view.set(type: updateIndex.1) } CATransaction.begin() for view in self.tooltipsViews { view.removeFromSuperview() } let modal = subviews.firstIndex(where: { $0.className.hasSuffix("ModalBackground") }) if let modal = modal { self.subviews.insert(contentsOf: self.tooltipsViews, at: modal) } else { self.subviews.append(contentsOf: self.tooltipsViews) } CATransaction.commit() self.tooltips = tooltips } do { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: self.actionsList, rightList: state.actions) var deletedViews: [Int: PeerCallActionView] = [:] for deleteIndex in deleteIndices.reversed() { let view = self.actionsViews.remove(at: deleteIndex) deletedViews[deleteIndex] = view } for indicesAndItem in indicesAndItems { let previous: PeerCallActionView? if let previousIndex = indicesAndItem.2 { previous = deletedViews[previousIndex] deletedViews.removeValue(forKey: previousIndex) } else { previous = nil } let view = previous ?? PeerCallActionView() view.setFrameOrigin(actions.focus(view.frame.size).origin) view.update(indicesAndItem.1, animated: false) actionsViews.insert(view, at: indicesAndItem.0) if transition.isAnimated, indicesAndItem.2 == nil { view.layer?.animateAlpha(from: 0, to: indicesAndItem.1.enabled ? 1.0 : 0.7, duration: 0.2) view.layer?.animateScaleSpring(from: 0.01, to: 1, duration: 0.2, bounce: false) } } for updateIndex in updateIndices { let view = self.actionsViews[updateIndex.0] view.update(updateIndex.1, animated: transition.isAnimated) } for (_, view) in deletedViews { performSubviewRemoval(view, animated: transition.isAnimated, scale: true) } CATransaction.begin() for view in self.actionsViews { view.removeFromSuperview() } self.actions.subviews.insert(contentsOf: self.actionsViews, at: 0) CATransaction.commit() self.actionsList = state.actions } if state.secretKeyViewState == .revealed, let secretView { let current: PeerCallRevealedSecretKeyView let isNew: Bool if let view = self.revealedKey { current = view isNew = false } else { if let index = self.subviews.firstIndex(of: secretView) { self.subviews.remove(at: index) self.subviews.append(secretView) } current = PeerCallRevealedSecretKeyView(frame: NSMakeRect(0, 0, 300, 300)) self.addSubview(current, positioned: .below, relativeTo: secretView) self.revealedKey = current isNew = true } current.updateState(state, arguments: arguments, transition: isNew ? .immediate : transition) if isNew { ContainedViewLayoutTransition.immediate.updateFrame(view: current, frame: revealedKeyFrame(view: current, state: state)) if transition.isAnimated { current.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) current.layer?.animateScaleSpring(from: 0.8, to: 1, duration: 0.2, bounce: false) } } if "".isEmpty { let current: Control if let view = self.keyoverlay { current = view } else { current = Control(frame: bounds) self.addSubview(current, positioned: .below, relativeTo: revealedKey) self.keyoverlay = current current.set(handler: { [weak arguments] _ in arguments?.toggleSecretKey() }, for: .Click) } ContainedViewLayoutTransition.immediate.updateFrame(view: current, frame: bounds) } } else { if let revealedKey = self.revealedKey { performSubviewRemoval(revealedKey, animated: transition.isAnimated, scale: false) self.revealedKey = nil } if let keyoverlay = self.keyoverlay { performSubviewRemoval(keyoverlay, animated: transition.isAnimated, scale: false) self.keyoverlay = nil } } if let _ = state.secretKey { let current: SecretKeyView let isNew: Bool if let view = self.secretView { current = view isNew = false } else { current = SecretKeyView(frame: NSMakeRect(0, 0, 100, 25)) if let revealedKey { self.addSubview(current, positioned: .above, relativeTo: revealedKey) } else { let modal = subviews.firstIndex(where: { $0.className.hasSuffix("ModalBackground") }) if let modal = modal { self.subviews.insert(current, at: modal) } else { self.addSubview(current) } } self.secretView = current isNew = true } current.updateState(state, arguments: arguments, transition: isNew ? .immediate : transition) if isNew { ContainedViewLayoutTransition.immediate.updateFrame(view: current, frame: secretKeyFrame(view: current, state: state, largeVideo: videoLink_large != nil)) if transition.isAnimated { current.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) current.layer?.animateScaleSpring(from: 0.01, to: 1, duration: 0.2, bounce: false) } } } else if let secretView = self.secretView { performSubviewRemoval(secretView, animated: transition.isAnimated, scale: true) self.secretView = nil } } } //RECT private extension PeerCallScreenView { func statusTooltipFrame(view: NSView, state: PeerCallState) -> NSRect { return view.centerFrameX(y: statusView.frame.maxY + 12) } func smallVideoFrame(view: MetalCallVideoView, size: NSSize, videoMagnify: VideoMagnify, state: PeerCallVideoViewState) -> NSRect { let videoSize = state.smallVideoSize let point: NSPoint switch videoMagnify { case .topLeft: point = NSMakePoint(10, 10) case .topRight: point = NSMakePoint(size.width - videoSize.width - 10, 10) case .bottomLeft: point = NSMakePoint(10, size.height - videoSize.height - 10) case .bottomRight: point = NSMakePoint(size.width - videoSize.width - 10, size.height - videoSize.height - 10) } return CGRect(origin: point, size: videoSize) } func largeVideoFrame(view: MetalCallVideoView, size: NSSize, state: PeerCallVideoViewState) -> NSRect { return size.bounds } func secretKeyFrame(view: NSView, state: PeerCallState, largeVideo: Bool) -> NSRect { if state.secretKeyViewState == .revealed { var rect = focus(NSMakeSize(200, 50)) rect.origin.y -= 30 return rect } else { var rect = focus(NSMakeSize(100, 25)) if largeVideo { rect.origin.y = 10 rect.origin.x = frame.width - rect.width - 10 - settingsView.frame.width } else { rect.origin.y = 10 } return rect } } func revealedKeyFrame(view: NSView, state: PeerCallState) -> NSRect { return view.centerFrame() } }