941 lines
36 KiB
Swift
941 lines
36 KiB
Swift
//
|
|
// 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()
|
|
}
|
|
}
|