This commit is contained in:
Ali 2023-11-10 10:46:57 +04:00
parent 169fece59c
commit 857bf2190e
48 changed files with 682 additions and 207 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 463 KiB

Binary file not shown.

View File

@ -0,0 +1,63 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
load(
"@build_bazel_rules_apple//apple:resources.bzl",
"apple_resource_bundle",
"apple_resource_group",
)
load("//build-system/bazel-utils:plist_fragment.bzl",
"plist_fragment",
)
filegroup(
name = "MetalSources",
srcs = glob([
"Metal/**/*.metal",
]),
visibility = ["//visibility:public"],
)
plist_fragment(
name = "MetalEngineMetalSourcesBundleInfoPlist",
extension = "plist",
template =
"""
<key>CFBundleIdentifier</key>
<string>org.telegram.MetalEngineMetalSources</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleName</key>
<string>MetalEngine</string>
"""
)
apple_resource_bundle(
name = "MetalEngineMetalSourcesBundle",
infoplists = [
":MetalEngineMetalSourcesBundleInfoPlist",
],
resources = [
":MetalSources",
],
)
swift_library(
name = "MetalEngine",
module_name = "MetalEngine",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
data = [
":MetalEngineMetalSourcesBundle",
],
deps = [
"//submodules/Display",
"//submodules/Utils/ShelfPack",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,11 @@
#include <metal_stdlib>
using namespace metal;
vertex float4 clearVertex(const device float2* vertexArray [[ buffer(0) ]], unsigned int vid [[ vertex_id ]]) {
return float4(vertexArray[vid], 0.0, 1.0);
}
fragment half4 clearFragment(const device float4 &color [[ buffer(0) ]]) {
return half4(color);
}

View File

@ -2,15 +2,9 @@ import Foundation
import Metal
import UIKit
import IOSurface
import Display
import ShelfPack
private func alignUp(size: Int, align: Int) -> Int {
precondition(((align - 1) & align) == 0, "Align must be a power of two")
let alignmentMask = align - 1
return (size + alignmentMask) & ~alignmentMask
}
public final class Placeholder<Resolved> {
var contents: Resolved?
}
@ -110,16 +104,16 @@ public struct RenderLayerPlacement: Equatable {
public protocol RenderToLayerState: AnyObject {
var pipelineState: MTLRenderPipelineState { get }
init?(device: MTLDevice, library: MTLLibrary)
init?(device: MTLDevice)
}
public protocol ComputeState: AnyObject {
init?(device: MTLDevice, library: MTLLibrary)
init?(device: MTLDevice)
}
open class MetalSubjectLayer: SimpleLayer {
open class MetalEngineSubjectLayer: SimpleLayer {
fileprivate var internalId: Int = -1
fileprivate var surfaceAllocation: MetalContext.SurfaceAllocation?
fileprivate var surfaceAllocation: MetalEngine.SurfaceAllocation?
public override init() {
super.init()
@ -136,7 +130,7 @@ open class MetalSubjectLayer: SimpleLayer {
}
override public func setNeedsDisplay() {
if let subject = self as? MetalSubject {
if let subject = self as? MetalEngineSubject {
subject.setNeedsUpdate()
}
}
@ -171,7 +165,7 @@ public final class PooledTexture {
}
}
public func get(context: MetalSubjectContext) -> TexturePlaceholder? {
public func get(context: MetalEngineSubjectContext) -> TexturePlaceholder? {
#if DEBUG
if context.freeResourcesOnCompletion.contains(where: { $0 === self }) {
assertionFailure("Trying to get PooledTexture more than once per update cycle")
@ -193,7 +187,7 @@ public final class PooledTexture {
}
}
public final class MetalSubjectContext {
public final class MetalEngineSubjectContext {
fileprivate final class ComputeOperation {
let commands: (MTLCommandBuffer) -> Void
@ -205,13 +199,13 @@ public final class MetalSubjectContext {
fileprivate final class RenderToLayerOperation {
let spec: RenderLayerSpec
let state: RenderToLayerState
weak var layer: MetalSubjectLayer?
weak var layer: MetalEngineSubjectLayer?
let commands: (MTLRenderCommandEncoder, RenderLayerPlacement) -> Void
init(
spec: RenderLayerSpec,
state: RenderToLayerState,
layer: MetalSubjectLayer,
layer: MetalEngineSubjectLayer,
commands: @escaping (MTLRenderCommandEncoder, RenderLayerPlacement) -> Void
) {
self.spec = spec
@ -222,13 +216,13 @@ public final class MetalSubjectContext {
}
private let device: MTLDevice
private let impl: MetalContext.Impl
private let impl: MetalEngine.Impl
fileprivate var computeOperations: [ComputeOperation] = []
fileprivate var renderToLayerOperationsGroupedByState: [ObjectIdentifier: [RenderToLayerOperation]] = [:]
fileprivate var freeResourcesOnCompletion: [PooledTexture.Texture] = []
fileprivate init(device: MTLDevice, impl: MetalContext.Impl) {
fileprivate init(device: MTLDevice, impl: MetalEngine.Impl) {
self.device = device
self.impl = impl
}
@ -236,7 +230,7 @@ public final class MetalSubjectContext {
public func renderToLayer<RenderToLayerStateType: RenderToLayerState, each Resolved>(
spec: RenderLayerSpec,
state: RenderToLayerStateType.Type,
layer: MetalSubjectLayer,
layer: MetalEngineSubjectLayer,
inputs: repeat Placeholder<each Resolved>,
commands: @escaping (MTLRenderCommandEncoder, RenderLayerPlacement, repeat each Resolved) -> Void
) {
@ -245,7 +239,7 @@ public final class MetalSubjectContext {
if let current = self.impl.renderStates[stateTypeId] as? RenderToLayerStateType {
resolvedState = current
} else {
guard let value = RenderToLayerStateType(device: self.device, library: self.impl.library) else {
guard let value = RenderToLayerStateType(device: self.device) else {
assertionFailure("Could not initialize render state \(state)")
return
}
@ -278,7 +272,7 @@ public final class MetalSubjectContext {
public func renderToLayer<RenderToLayerStateType: RenderToLayerState>(
spec: RenderLayerSpec,
state: RenderToLayerStateType.Type,
layer: MetalSubjectLayer,
layer: MetalEngineSubjectLayer,
commands: @escaping (MTLRenderCommandEncoder, RenderLayerPlacement) -> Void
) {
self.renderToLayer(spec: spec, state: state, layer: layer, inputs: noInputPlaceholder, commands: { encoder, placement, _ in
@ -296,7 +290,7 @@ public final class MetalSubjectContext {
if let current = self.impl.computeStates[stateTypeId] as? ComputeStateType {
resolvedState = current
} else {
guard let value = ComputeStateType(device: self.device, library: self.impl.library) else {
guard let value = ComputeStateType(device: self.device) else {
assertionFailure("Could not initialize compute state \(state)")
return Placeholder()
}
@ -338,24 +332,24 @@ public final class MetalSubjectContext {
}
}
public final class MetalSubjectInternalData {
public final class MetalEngineSubjectInternalData {
var internalId: Int = -1
var renderSurfaceAllocation: MetalContext.SurfaceAllocation?
var renderSurfaceAllocation: MetalEngine.SurfaceAllocation?
init() {
}
}
public protocol MetalSubject: AnyObject {
var internalData: MetalSubjectInternalData? { get set }
public protocol MetalEngineSubject: AnyObject {
var internalData: MetalEngineSubjectInternalData? { get set }
func setNeedsUpdate()
func update(context: MetalSubjectContext)
func update(context: MetalEngineSubjectContext)
}
public extension MetalSubject {
public extension MetalEngineSubject {
func setNeedsUpdate() {
MetalContext.shared.impl.addSubjectNeedsUpdate(subject: self)
MetalEngine.shared.impl.addSubjectNeedsUpdate(subject: self)
}
}
@ -370,7 +364,7 @@ private final class MetalEventLayer: CAMetalLayer {
}
}
public final class MetalContext {
public final class MetalEngine {
struct SurfaceAllocation {
struct Phase {
let subRect: CGRect
@ -509,9 +503,9 @@ public final class MetalContext {
}
private final class SubjectReference {
weak var subject: MetalSubject?
weak var subject: MetalEngineSubject?
init(subject: MetalSubject) {
init(subject: MetalEngineSubject) {
self.subject = subject
}
}
@ -548,7 +542,7 @@ public final class MetalContext {
init?(device: MTLDevice) {
let mainBundle = Bundle(for: Impl.self)
guard let path = mainBundle.path(forResource: "MetalSourcesBundle", ofType: "bundle") else {
guard let path = mainBundle.path(forResource: "MetalEngineMetalSourcesBundle", ofType: "bundle") else {
return nil
}
guard let bundle = Bundle(path: path) else {
@ -624,15 +618,15 @@ public final class MetalContext {
let surfaceId = self.nextSurfaceId
self.nextSurfaceId += 1
let surfaceWidth = max(1024, alignUp(size: Int(minSize.width), align: 64))
let surfaceHeight = max(512, alignUp(size: Int(minSize.height), align: 64))
let surfaceWidth = max(1024, alignUp(Int(minSize.width), alignment: 64))
let surfaceHeight = max(512, alignUp(Int(minSize.height), alignment: 64))
let surface = Surface(id: surfaceId, device: self.device, width: surfaceWidth, height: surfaceHeight)
self.surfaces[surfaceId] = surface
return surface
}
private func refreshLayerAllocation(layer: MetalSubjectLayer, renderSpec: RenderLayerSpec) {
private func refreshLayerAllocation(layer: MetalEngineSubjectLayer, renderSpec: RenderLayerSpec) {
var previousSurfaceId: Int?
var updatedSurfaceId: Int?
@ -689,12 +683,12 @@ public final class MetalContext {
}
}
func addSubjectNeedsUpdate(subject: MetalSubject) {
let internalData: MetalSubjectInternalData
func addSubjectNeedsUpdate(subject: MetalEngineSubject) {
let internalData: MetalEngineSubjectInternalData
if let current = subject.internalData {
internalData = current
} else {
internalData = MetalSubjectInternalData()
internalData = MetalEngineSubjectInternalData()
subject.internalData = internalData
}
@ -751,7 +745,7 @@ public final class MetalContext {
return
}
let subjectContext = MetalSubjectContext(device: device, impl: self)
let subjectContext = MetalEngineSubjectContext(device: device, impl: self)
for subjectReference in self.updatedSubjects {
guard let subject = subjectReference.subject else {
@ -899,7 +893,7 @@ public final class MetalContext {
}
}
public static let shared = MetalContext()
public static let shared = MetalEngine()
fileprivate let impl: Impl

View File

@ -105,6 +105,7 @@ swift_library(
"//submodules/Components/HierarchyTrackingLayer:HierarchyTrackingLayer",
"//submodules/PeerInfoUI/CreateExternalMediaStreamScreen:CreateExternalMediaStreamScreen",
"//submodules/PhoneNumberFormat:PhoneNumberFormat",
"//submodules/TelegramUI/Components/Calls/CallScreen",
],
visibility = [
"//visibility:public",

View File

@ -135,11 +135,11 @@ public final class CallController: ViewController {
}
override public func loadDisplayNode() {
if self.call.isVideoPossible {
self.displayNode = CallControllerNode(sharedContext: self.sharedContext, account: self.account, presentationData: self.presentationData, statusBar: self.statusBar, debugInfo: self.call.debugInfo(), shouldStayHiddenUntilConnection: !self.call.isOutgoing && self.call.isIntegratedWithCallKit, easyDebugAccess: self.easyDebugAccess, call: self.call)
} else {
self.displayNode = LegacyCallControllerNode(sharedContext: self.sharedContext, account: self.account, presentationData: self.presentationData, statusBar: self.statusBar, debugInfo: self.call.debugInfo(), shouldStayHiddenUntilConnection: !self.call.isOutgoing && self.call.isIntegratedWithCallKit, easyDebugAccess: self.easyDebugAccess, call: self.call)
}
#if DEBUG
self.displayNode = CallControllerNodeV2(sharedContext: self.sharedContext, account: self.account, presentationData: self.presentationData, statusBar: self.statusBar, debugInfo: self.call.debugInfo(), shouldStayHiddenUntilConnection: !self.call.isOutgoing && self.call.isIntegratedWithCallKit, easyDebugAccess: self.easyDebugAccess, call: self.call)
#else
self.displayNode = CallControllerNode(sharedContext: self.sharedContext, account: self.account, presentationData: self.presentationData, statusBar: self.statusBar, debugInfo: self.call.debugInfo(), shouldStayHiddenUntilConnection: !self.call.isOutgoing && self.call.isIntegratedWithCallKit, easyDebugAccess: self.easyDebugAccess, call: self.call)
#endif
self.displayNodeDidLoad()
self.controllerNode.toggleMute = { [weak self] in

View File

@ -0,0 +1,91 @@
import Foundation
import AsyncDisplayKit
import Display
import TelegramCore
import Postbox
import TelegramAudio
import AccountContext
import TelegramPresentationData
import SwiftSignalKit
import CallScreen
final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeProtocol {
private let sharedContext: SharedAccountContext
private let account: Account
private let presentationData: PresentationData
private let statusBar: StatusBar
private let call: PresentationCall
private let callScreen: PrivateCallScreen
var isMuted: Bool = false
var toggleMute: (() -> Void)?
var setCurrentAudioOutput: ((AudioSessionOutput) -> Void)?
var beginAudioOuputSelection: ((Bool) -> Void)?
var acceptCall: (() -> Void)?
var endCall: (() -> Void)?
var back: (() -> Void)?
var presentCallRating: ((CallId, Bool) -> Void)?
var present: ((ViewController) -> Void)?
var callEnded: ((Bool) -> Void)?
var dismissedInteractively: (() -> Void)?
var dismissAllTooltips: (() -> Void)?
init(
sharedContext: SharedAccountContext,
account: Account,
presentationData: PresentationData,
statusBar: StatusBar,
debugInfo: Signal<(String, String), NoError>,
shouldStayHiddenUntilConnection: Bool = false,
easyDebugAccess: Bool,
call: PresentationCall
) {
self.sharedContext = sharedContext
self.account = account
self.presentationData = presentationData
self.statusBar = statusBar
self.call = call
self.callScreen = PrivateCallScreen()
super.init()
self.view.addSubview(self.callScreen)
}
func updateAudioOutputs(availableOutputs: [AudioSessionOutput], currentOutput: AudioSessionOutput?) {
}
func updateCallState(_ callState: PresentationCallState) {
if case let .terminated(id, _, reportRating) = callState.state, let callId = id {
if reportRating {
self.presentCallRating?(callId, self.call.isVideo)
}
self.callEnded?(reportRating)
}
}
func updatePeer(accountPeer: Peer, peer: Peer, hasOther: Bool) {
}
func animateIn() {
}
func animateOut(completion: @escaping () -> Void) {
}
func expandFromPipIfPossible() {
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
transition.updateFrame(view: self.callScreen, frame: CGRect(origin: CGPoint(), size: layout.size))
self.callScreen.update(size: layout.size, insets: layout.insets(options: [.statusBar]))
}
}

View File

@ -410,6 +410,7 @@ swift_library(
"//submodules/TelegramUI/Components/Settings/PeerNameColorScreen",
"//submodules/TelegramUI/Components/ContextMenuScreen",
"//submodules/TelegramUI/Components/PeerAllowedReactionsScreen",
"//submodules/MetalEngine",
] + select({
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
"//build-system:ios_sim_arm64": [],

View File

@ -10,7 +10,7 @@ load("//build-system/bazel-utils:plist_fragment.bzl",
)
filegroup(
name = "MetalSources",
name = "CallScreenMetalSources",
srcs = glob([
"Metal/**/*.metal",
]),
@ -18,7 +18,7 @@ filegroup(
)
plist_fragment(
name = "MetalSourcesBundleInfoPlist",
name = "CallScreenMetalSourcesBundleInfoPlist",
extension = "plist",
template =
"""
@ -27,20 +27,26 @@ plist_fragment(
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleName</key>
<string>AnimationCompression</string>
<string>CallScreen</string>
"""
)
apple_resource_bundle(
name = "MetalSourcesBundle",
name = "CallScreenMetalSourcesBundle",
infoplists = [
":MetalSourcesBundleInfoPlist",
":CallScreenMetalSourcesBundleInfoPlist",
],
resources = [
":MetalSources",
":CallScreenMetalSources",
],
)
filegroup(
name = "Assets",
srcs = glob(["CallScreenAssets.xcassets/**"]),
visibility = ["//visibility:public"],
)
swift_library(
name = "CallScreen",
module_name = "CallScreen",
@ -51,13 +57,14 @@ swift_library(
"-warnings-as-errors",
],
data = [
":MetalSourcesBundle",
":CallScreenMetalSourcesBundle",
":Assets",
],
deps = [
"//submodules/Display",
"//submodules/MetalEngine",
"//submodules/ComponentFlow",
"//submodules/TelegramUI/Components/AnimatedTextComponent",
"//submodules/Utils/ShelfPack",
],
visibility = [
"//visibility:public",

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ic_call_audioairpods.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ic_call_audioairpodspro.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ic_call_airpodsmax.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ic_call_audiobt.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,9 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"provides-namespace" : true
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 503 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 782 B

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "CallCancelIcon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "CallCancelIcon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ic_flip (1).pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ic_call_microphone.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ic_call_speaker.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ic_call_camera.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -21,14 +21,6 @@ struct QuadVertexOut {
float2 uv;
};
vertex float4 clearVertex(const device float2* vertexArray [[ buffer(0) ]], unsigned int vid [[ vertex_id ]]) {
return float4(vertexArray[vid], 0.0, 1.0);
}
fragment half4 clearFragment(const device float4 &color [[ buffer(0) ]]) {
return half4(color);
}
vertex QuadVertexOut callBackgroundVertex(
const device Rectangle &rect [[ buffer(0) ]],
unsigned int vid [[ vertex_id ]]

View File

@ -147,6 +147,18 @@ public final class AnimatedProperty<T: AnimationInterpolatable>: AnyAnimatedProp
self.didStartAnimation?()
}
public func animate(from: T, to: T, duration: Double, curve: AnimationCurve) {
self.value = from
self.animation = Animation(from: from, to: to, duration: duration, curve: curve)
self.animation?.start()
self.didStartAnimation?()
}
public func set(to: T) {
self.animation = nil
self.value = to
}
override public func update() {
if let animation = self.animation {
self.value = animation.update(at: CACurrentMediaTime())

View File

@ -1,4 +1,28 @@
import Foundation
import UIKit
import Display
import MetalKit
private final class BundleMarker: NSObject {
}
private var metalLibraryValue: MTLLibrary?
func metalLibrary(device: MTLDevice) -> MTLLibrary? {
if let metalLibraryValue {
return metalLibraryValue
}
let mainBundle = Bundle(for: BundleMarker.self)
guard let path = mainBundle.path(forResource: "CallScreenMetalSourcesBundle", ofType: "bundle") else {
return nil
}
guard let bundle = Bundle(path: path) else {
return nil
}
guard let library = try? device.makeDefaultLibrary(bundle: bundle) else {
return nil
}
metalLibraryValue = library
return library
}

View File

@ -1,5 +1,6 @@
import Foundation
import UIKit
import Display
final class AvatarLayer: SimpleLayer {
var image: UIImage? {

View File

@ -1,5 +1,6 @@
import Foundation
import UIKit
import Display
final class ButtonGroupView: UIView, ContentOverlayView {
enum Key: Hashable {
@ -12,6 +13,7 @@ final class ButtonGroupView: UIView, ContentOverlayView {
let overlayMaskLayer: CALayer
private var buttons: [Key: ContentOverlayButton] = [:]
var audioPressed: (() -> Void)?
var toggleVideo: (() -> Void)?
override init(frame: CGRect) {
@ -89,9 +91,15 @@ final class ButtonGroupView: UIView, ContentOverlayView {
let image: UIImage?
switch key {
case .audio:
image = UIImage(named: "Call/ButtonSpeaker")
image = UIImage(named: "Call/Speaker")
button.action = { [weak self] in
guard let self else {
return
}
self.audioPressed?()
}
case .video:
image = UIImage(named: "Call/ButtonVideo")
image = UIImage(named: "Call/Video")
button.action = { [weak self] in
guard let self else {
return
@ -99,9 +107,9 @@ final class ButtonGroupView: UIView, ContentOverlayView {
self.toggleVideo?()
}
case .mic:
image = UIImage(named: "Call/ButtonMicMuted")
image = UIImage(named: "Call/Mute")
case .close:
image = UIImage(named: "Call/ButtonEnd")
image = UIImage(named: "Call/End")
}
button.setImage(image?.withRenderingMode(.alwaysTemplate), for: .normal)

View File

@ -1,6 +1,7 @@
import Foundation
import MetalKit
import UIKit
import MetalEngine
private func shiftArray(array: [SIMD2<Float>], offset: Int) -> [SIMD2<Float>] {
var newArray = array
@ -28,13 +29,28 @@ private func hexToFloat(_ hex: Int) -> SIMD4<Float> {
return SIMD4<Float>(x: red, y: green, z: blue, w: 1.0)
}
final class CallBackgroundLayer: MetalSubjectLayer, MetalSubject {
var internalData: MetalSubjectInternalData?
private struct ColorSet: Equatable, AnimationInterpolatable {
static let animationInterpolator = AnimationInterpolator<ColorSet> { from, to, fraction in
var result: [SIMD4<Float>] = []
for i in 0 ..< min(from.colors.count, to.colors.count) {
result.append(from.colors[i] * Float(1.0 - fraction) + to.colors[i] * Float(fraction))
}
return ColorSet(colors: result)
}
var colors: [SIMD4<Float>]
}
final class CallBackgroundLayer: MetalEngineSubjectLayer, MetalEngineSubject {
var internalData: MetalEngineSubjectInternalData?
final class RenderState: RenderToLayerState {
let pipelineState: MTLRenderPipelineState
init?(device: MTLDevice, library: MTLLibrary) {
init?(device: MTLDevice) {
guard let library = metalLibrary(device: device) else {
return nil
}
guard let vertexFunction = library.makeFunction(name: "callBackgroundVertex"), let fragmentFunction = library.makeFunction(name: "callBackgroundFragment") else {
return nil
}
@ -66,11 +82,44 @@ final class CallBackgroundLayer: MetalSubjectLayer, MetalSubject {
private var displayLinkSubscription: SharedDisplayLink.Subscription?
var renderSpec: RenderLayerSpec?
var renderSpec: RenderLayerSpec? {
didSet {
if self.renderSpec != oldValue {
self.setNeedsUpdate()
}
}
}
private let colorSets: [ColorSet]
private let colorTransition: AnimatedProperty<ColorSet>
private var stateIndex: Int = 0
private let phaseAcceleration = AnimatedProperty<CGFloat>(0.0)
init(isBlur: Bool) {
self.isBlur = isBlur
self.colorSets = [
ColorSet(colors: [
hexToFloat(0x568FD6),
hexToFloat(0x626ED5),
hexToFloat(0xA667D5),
hexToFloat(0x7664DA)
]),
ColorSet(colors: [
hexToFloat(0xACBD65),
hexToFloat(0x459F8D),
hexToFloat(0x53A4D1),
hexToFloat(0x3E917A)
]),
ColorSet(colors: [
hexToFloat(0xC0508D),
hexToFloat(0xF09536),
hexToFloat(0xCE5081),
hexToFloat(0xFC7C4C)
])
]
self.colorTransition = AnimatedProperty<ColorSet>(colorSets[0])
super.init()
self.didEnterHierarchy = { [weak self] in
@ -81,8 +130,14 @@ final class CallBackgroundLayer: MetalSubjectLayer, MetalSubject {
guard let self else {
return
}
self.colorTransition.update()
self.phaseAcceleration.update()
let stepCount = 8
self.phase = (self.phase + 1.0 / 60.0).truncatingRemainder(dividingBy: Float(stepCount))
var phaseStep: CGFloat = 0.5 / 30.0
phaseStep += phaseStep * self.phaseAcceleration.value * 0.5
self.phase = (self.phase + Float(phaseStep)).truncatingRemainder(dividingBy: Float(stepCount))
self.setNeedsUpdate()
})
}
@ -95,6 +150,9 @@ final class CallBackgroundLayer: MetalSubjectLayer, MetalSubject {
}
override init(layer: Any) {
self.colorSets = []
self.colorTransition = AnimatedProperty<ColorSet>(ColorSet(colors: []))
super.init(layer: layer)
}
@ -102,7 +160,19 @@ final class CallBackgroundLayer: MetalSubjectLayer, MetalSubject {
fatalError("init(coder:) has not been implemented")
}
func update(context: MetalSubjectContext) {
func update(stateIndex: Int, animated: Bool) {
if self.stateIndex != stateIndex {
self.stateIndex = stateIndex
if animated {
self.phaseAcceleration.animate(from: 1.0, to: 0.0, duration: 2.0, curve: .easeInOut)
self.colorTransition.animate(to: self.colorSets[stateIndex % self.colorSets.count], duration: 0.3, curve: .easeInOut)
} else {
self.colorTransition.set(to: self.colorSets[stateIndex % self.colorSets.count])
}
}
}
func update(context: MetalEngineSubjectContext) {
guard let renderSpec = self.renderSpec else {
return
}
@ -127,22 +197,9 @@ final class CallBackgroundLayer: MetalSubjectLayer, MetalSubject {
}
encoder.setFragmentBytes(&positions, length: 4 * MemoryLayout<SIMD2<Float>>.size, index: 0)
var colors: [[SIMD4<Float>]] = [
[
hexToFloat(0x568FD6),
hexToFloat(0x626ED5),
hexToFloat(0xA667D5),
hexToFloat(0x7664DA)
],
[
hexToFloat(0xACBD65),
hexToFloat(0x459F8D),
hexToFloat(0x53A4D1),
hexToFloat(0x3E917A)
]
]
var colors: [SIMD4<Float>] = self.colorTransition.value.colors
encoder.setFragmentBytes(&colors[0], length: 4 * MemoryLayout<SIMD4<Float>>.size, index: 1)
encoder.setFragmentBytes(&colors, length: 4 * MemoryLayout<SIMD4<Float>>.size, index: 1)
var brightness: Float = isBlur ? 1.1 : 1.0
var saturation: Float = isBlur ? 1.2 : 1.0
encoder.setFragmentBytes(&brightness, length: 4, index: 2)

View File

@ -1,8 +1,9 @@
import Foundation
import MetalKit
import MetalEngine
final class CallBlobsLayer: MetalSubjectLayer, MetalSubject {
var internalData: MetalSubjectInternalData?
final class CallBlobsLayer: MetalEngineSubjectLayer, MetalEngineSubject {
var internalData: MetalEngineSubjectInternalData?
struct Blob {
var points: [Float]
@ -48,10 +49,10 @@ final class CallBlobsLayer: MetalSubjectLayer, MetalSubject {
let pipelineState: MTLRenderPipelineState
required init?(
device: MTLDevice,
library: MTLLibrary
) {
required init?(device: MTLDevice) {
guard let library = metalLibrary(device: device) else {
return nil
}
guard let vertexFunction = library.makeFunction(name: "callBlobVertex"), let fragmentFunction = library.makeFunction(name: "callBlobFragment") else {
return nil
}
@ -123,7 +124,7 @@ final class CallBlobsLayer: MetalSubjectLayer, MetalSubject {
fatalError("init(coder:) has not been implemented")
}
func update(context: MetalSubjectContext) {
func update(context: MetalEngineSubjectContext) {
if self.bounds.isEmpty {
return
}

View File

@ -1,5 +1,6 @@
import Foundation
import UIKit
import Display
final class ContentOverlayButton: UIButton, ContentOverlayView {
var overlayMaskLayer: CALayer {

View File

@ -1,7 +1,21 @@
import Foundation
import UIKit
import Display
import MetalEngine
final class ContentView: UIView {
private struct Params: Equatable {
var size: CGSize
var insets: UIEdgeInsets
var state: PrivateCallScreen.State
init(size: CGSize, insets: UIEdgeInsets, state: PrivateCallScreen.State) {
self.size = size
self.insets = insets
self.state = state
}
}
private let blobLayer: CallBlobsLayer
private let avatarLayer: AvatarLayer
private let titleView: TextView
@ -14,8 +28,7 @@ final class ContentView: UIView {
private var videoLayerMask: SimpleShapeLayer?
private var blurredVideoLayerMask: SimpleShapeLayer?
private var currentLayout: (size: CGSize, insets: UIEdgeInsets)?
private var phase: CGFloat = 0.0
private var params: Params?
private var isDisplayingVideo: Bool = false
private var videoDisplayFraction = AnimatedProperty<CGFloat>(0.0)
@ -54,8 +67,8 @@ final class ContentView: UIView {
guard let self else {
return
}
if let currentLayout = self.currentLayout {
self.update(size: currentLayout.size, insets: currentLayout.insets)
if let params = self.params {
self.updateInternal(params: params)
}
}
}
@ -64,41 +77,59 @@ final class ContentView: UIView {
fatalError("init(coder:) has not been implemented")
}
func update(size: CGSize, insets: UIEdgeInsets) {
self.currentLayout = (size, insets)
func update(size: CGSize, insets: UIEdgeInsets, state: PrivateCallScreen.State) {
let params = Params(size: size, insets: insets, state: state)
if self.params == params {
return
}
self.params = params
self.updateInternal(params: params)
}
private func updateInternal(params: Params) {
if self.emojiView == nil {
let emojiView = KeyEmojiView(emoji: ["🐱", "🚂", "❄️", "🎨"])
self.emojiView = emojiView
self.addSubview(emojiView)
}
if let emojiView = self.emojiView {
emojiView.frame = CGRect(origin: CGPoint(x: size.width - 12.0 - emojiView.size.width, y: insets.top + 27.0), size: emojiView.size)
emojiView.frame = CGRect(origin: CGPoint(x: params.size.width - 12.0 - emojiView.size.width, y: params.insets.top + 27.0), size: emojiView.size)
}
if self.videoInput == nil, let url = Bundle.main.url(forResource: "test2", withExtension: "mp4") {
self.videoInput = VideoInput(device: MetalContext.shared.device, url: url)
self.videoInput = VideoInput(device: MetalEngine.shared.device, url: url)
self.videoLayer.video = self.videoInput
}
self.phase += 3.0 / 60.0
self.phase = self.phase.truncatingRemainder(dividingBy: 1.0)
var avatarScale: CGFloat = 0.05 * sin(CGFloat(self.phase) * CGFloat.pi)
//self.phase += 3.0 / 60.0
//self.phase = self.phase.truncatingRemainder(dividingBy: 1.0)
var avatarScale: CGFloat = 0.05 * sin(CGFloat(0.0) * CGFloat.pi)
avatarScale *= 1.0 - self.videoDisplayFraction.value
let avatarSize: CGFloat = 136.0
let blobSize: CGFloat = 176.0
let expandedVideoRadius: CGFloat = sqrt(pow(size.width * 0.5, 2.0) + pow(size.height * 0.5, 2.0))
let expandedVideoRadius: CGFloat = sqrt(pow(params.size.width * 0.5, 2.0) + pow(params.size.height * 0.5, 2.0))
let avatarFrame = CGRect(origin: CGPoint(x: floor((size.width - avatarSize) * 0.5), y: CGFloat.animationInterpolator.interpolate(from: 222.0, to: floor((size.height - avatarSize) * 0.5), fraction: self.videoDisplayFraction.value)), size: CGSize(width: avatarSize, height: avatarSize))
let avatarFrame = CGRect(origin: CGPoint(x: floor((params.size.width - avatarSize) * 0.5), y: CGFloat.animationInterpolator.interpolate(from: 222.0, to: floor((params.size.height - avatarSize) * 0.5), fraction: self.videoDisplayFraction.value)), size: CGSize(width: avatarSize, height: avatarSize))
let titleSize = self.titleView.update(string: "Emma Walters", fontSize: CGFloat.animationInterpolator.interpolate(from: 28.0, to: 17.0, fraction: self.videoDisplayFraction.value), fontWeight: CGFloat.animationInterpolator.interpolate(from: 0.0, to: 0.25, fraction: self.videoDisplayFraction.value), constrainedWidth: size.width - 16.0 * 2.0)
let titleFrame = CGRect(origin: CGPoint(x: (size.width - titleSize.width) * 0.5, y: CGFloat.animationInterpolator.interpolate(from: avatarFrame.maxY + 39.0, to: insets.top + 17.0, fraction: self.videoDisplayFraction.value)), size: titleSize)
let titleSize = self.titleView.update(string: "Emma Walters", fontSize: CGFloat.animationInterpolator.interpolate(from: 28.0, to: 17.0, fraction: self.videoDisplayFraction.value), fontWeight: CGFloat.animationInterpolator.interpolate(from: 0.0, to: 0.25, fraction: self.videoDisplayFraction.value), constrainedWidth: params.size.width - 16.0 * 2.0)
let titleFrame = CGRect(origin: CGPoint(x: (params.size.width - titleSize.width) * 0.5, y: CGFloat.animationInterpolator.interpolate(from: avatarFrame.maxY + 39.0, to: params.insets.top + 17.0, fraction: self.videoDisplayFraction.value)), size: titleSize)
self.titleView.frame = titleFrame
let statusSize = self.statusView.update(state: .waiting(.requesting))
self.statusView.frame = CGRect(origin: CGPoint(x: (size.width - statusSize.width) * 0.5, y: titleFrame.maxY + CGFloat.animationInterpolator.interpolate(from: 4.0, to: 0.0, fraction: self.videoDisplayFraction.value)), size: statusSize)
let statusState: StatusView.State
switch params.state.lifecycleState {
case .connecting:
statusState = .waiting(.requesting)
case .ringing:
statusState = .waiting(.ringing)
case .exchangingKeys:
statusState = .waiting(.generatingKeys)
case let .active(_, signalInfo):
statusState = .active(StatusView.ActiveState(signalStrength: signalInfo.quality))
}
let statusSize = self.statusView.update(state: statusState)
self.statusView.frame = CGRect(origin: CGPoint(x: (params.size.width - statusSize.width) * 0.5, y: titleFrame.maxY + CGFloat.animationInterpolator.interpolate(from: 4.0, to: 0.0, fraction: self.videoDisplayFraction.value)), size: statusSize)
let blobFrame = CGRect(origin: CGPoint(x: floor(avatarFrame.midX - blobSize * 0.5), y: floor(avatarFrame.midY - blobSize * 0.5)), size: CGSize(width: blobSize, height: blobSize))
@ -114,7 +145,7 @@ final class ContentView: UIView {
self.blobLayer.transform = CATransform3DMakeScale(1.0 + avatarScale * 2.0, 1.0 + avatarScale * 2.0, 1.0)
let videoResolution = CGSize(width: 400.0, height: 400.0)
let videoFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size)
let videoFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: params.size)
let videoRenderingSize = CGSize(width: videoResolution.width * 2.0, height: videoResolution.height * 2.0)

View File

@ -3,6 +3,7 @@ import UIKit
import MetalKit
import MetalPerformanceShaders
import Accelerate
import MetalEngine
func imageToCVPixelBuffer(image: UIImage) -> CVPixelBuffer? {
guard let cgImage = image.cgImage, let data = cgImage.dataProvider?.data, let bytes = CFDataGetBytePtr(data), let colorSpace = cgImage.colorSpace, case .rgb = colorSpace.model, cgImage.bitsPerPixel / cgImage.bitsPerComponent == 4 else {
@ -36,10 +37,10 @@ func imageToCVPixelBuffer(image: UIImage) -> CVPixelBuffer? {
return pixelBuffer
}
final class MainVideoLayer: MetalSubjectLayer, MetalSubject {
var internalData: MetalSubjectInternalData?
final class MainVideoLayer: MetalEngineSubjectLayer, MetalEngineSubject {
var internalData: MetalEngineSubjectInternalData?
let blurredLayer: MetalSubjectLayer
let blurredLayer: MetalEngineSubjectLayer
final class BlurState: ComputeState {
let computePipelineStateYUVToRGBA: MTLComputePipelineState
@ -47,10 +48,10 @@ final class MainVideoLayer: MetalSubjectLayer, MetalSubject {
let computePipelineStateVertical: MTLComputePipelineState
let downscaleKernel: MPSImageBilinearScale
required init?(
device: MTLDevice,
library: MTLLibrary
) {
required init?(device: MTLDevice) {
guard let library = metalLibrary(device: device) else {
return nil
}
guard let functionVideoYUVToRGBA = library.makeFunction(name: "videoYUVToRGBA") else {
return nil
}
@ -79,10 +80,10 @@ final class MainVideoLayer: MetalSubjectLayer, MetalSubject {
final class RenderState: RenderToLayerState {
let pipelineState: MTLRenderPipelineState
required init?(
device: MTLDevice,
library: MTLLibrary
) {
required init?(device: MTLDevice) {
guard let library = metalLibrary(device: device) else {
return nil
}
guard let vertexFunction = library.makeFunction(name: "mainVideoVertex"), let fragmentFunction = library.makeFunction(name: "mainVideoFragment") else {
return nil
}
@ -115,13 +116,13 @@ final class MainVideoLayer: MetalSubjectLayer, MetalSubject {
private var blurredVerticalTexture: PooledTexture?
override init() {
self.blurredLayer = MetalSubjectLayer()
self.blurredLayer = MetalEngineSubjectLayer()
super.init()
}
override init(layer: Any) {
self.blurredLayer = MetalSubjectLayer()
self.blurredLayer = MetalEngineSubjectLayer()
super.init(layer: layer)
}
@ -130,7 +131,7 @@ final class MainVideoLayer: MetalSubjectLayer, MetalSubject {
fatalError("init(coder:) has not been implemented")
}
func update(context: MetalSubjectContext) {
func update(context: MetalEngineSubjectContext) {
if self.isHidden {
return
}
@ -143,16 +144,16 @@ final class MainVideoLayer: MetalSubjectLayer, MetalSubject {
let rgbaTextureSpec = TextureSpec(width: videoTextures.y.width, height: videoTextures.y.height, pixelFormat: .rgba8UnsignedNormalized)
if self.rgbaTexture == nil || self.rgbaTexture?.spec != rgbaTextureSpec {
self.rgbaTexture = MetalContext.shared.pooledTexture(spec: rgbaTextureSpec)
self.rgbaTexture = MetalEngine.shared.pooledTexture(spec: rgbaTextureSpec)
}
if self.downscaledTexture == nil {
self.downscaledTexture = MetalContext.shared.pooledTexture(spec: TextureSpec(width: 128, height: 128, pixelFormat: .rgba8UnsignedNormalized))
self.downscaledTexture = MetalEngine.shared.pooledTexture(spec: TextureSpec(width: 128, height: 128, pixelFormat: .rgba8UnsignedNormalized))
}
if self.blurredHorizontalTexture == nil {
self.blurredHorizontalTexture = MetalContext.shared.pooledTexture(spec: TextureSpec(width: 128, height: 128, pixelFormat: .rgba8UnsignedNormalized))
self.blurredHorizontalTexture = MetalEngine.shared.pooledTexture(spec: TextureSpec(width: 128, height: 128, pixelFormat: .rgba8UnsignedNormalized))
}
if self.blurredVerticalTexture == nil {
self.blurredVerticalTexture = MetalContext.shared.pooledTexture(spec: TextureSpec(width: 128, height: 128, pixelFormat: .rgba8UnsignedNormalized))
self.blurredVerticalTexture = MetalEngine.shared.pooledTexture(spec: TextureSpec(width: 128, height: 128, pixelFormat: .rgba8UnsignedNormalized))
}
guard let rgbaTexture = self.rgbaTexture?.get(context: context) else {

View File

@ -1,5 +1,6 @@
import Foundation
import UIKit
import Display
private final class AnimatedDotsLayer: SimpleLayer {
private let dotLayers: [SimpleLayer]
@ -76,8 +77,18 @@ final class StatusView: UIView {
case ringing
case generatingKeys
}
struct ActiveState {
var signalStrength: Double
init(signalStrength: Double) {
self.signalStrength = signalStrength
}
}
enum State {
case waiting(WaitingState)
case active(ActiveState)
}
private var textView: TextView
@ -110,6 +121,9 @@ final class StatusView: UIView {
case .generatingKeys:
textString = "Exchanging encryption keys"
}
case let .active(activeState):
textString = "0:00"
let _ = activeState
}
let textSize = self.textView.update(string: textString, fontSize: 16.0, fontWeight: 0.0, constrainedWidth: 200.0)
self.textView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: textSize)

View File

@ -1,5 +1,6 @@
import Foundation
import UIKit
import Display
final class ContentOverlayLayer: SimpleLayer {
private struct Params: Equatable {

View File

@ -1,5 +1,6 @@
import Foundation
import UIKit
import Display
final class MirroringLayer: SimpleLayer {
var targetLayer: SimpleLayer?

View File

@ -1,7 +1,42 @@
import Foundation
import UIKit
import Display
import MetalEngine
public final class PrivateCallScreen: UIView {
public struct State: Equatable {
public struct SignalInfo: Equatable {
public var quality: Double
public init(quality: Double) {
self.quality = quality
}
}
public enum LifecycleState: Equatable {
case connecting
case ringing
case exchangingKeys
case active(startTime: Double, signalInfo: SignalInfo)
}
public var lifecycleState: LifecycleState
public init(lifecycleState: LifecycleState) {
self.lifecycleState = lifecycleState
}
}
private struct Params: Equatable {
var size: CGSize
var insets: UIEdgeInsets
init(size: CGSize, insets: UIEdgeInsets) {
self.size = size
self.insets = insets
}
}
private let backgroundLayer: CallBackgroundLayer
private let contentOverlayLayer: ContentOverlayLayer
private let contentOverlayContainer: ContentOverlayContainer
@ -13,6 +48,18 @@ public final class PrivateCallScreen: UIView {
private let buttonGroupView: ButtonGroupView
public var state: State = State(lifecycleState: .connecting) {
didSet {
if self.state != oldValue {
if let params = self.params {
self.updateInternal(params: params, animated: true)
}
}
}
}
private var params: Params?
public override init(frame: CGRect) {
self.blurContentsLayer = SimpleLayer()
@ -44,6 +91,26 @@ public final class PrivateCallScreen: UIView {
self.contentOverlayContainer.addSubview(self.buttonGroupView)
self.buttonGroupView.audioPressed = { [weak self] in
guard let self else {
return
}
var state = self.state
switch state.lifecycleState {
case .connecting, .ringing, .exchangingKeys:
state.lifecycleState = .active(startTime: CFAbsoluteTimeGetCurrent(), signalInfo: State.SignalInfo(quality: 1.0))
case let .active(startTime, signalInfo):
if signalInfo.quality == 1.0 {
state.lifecycleState = .active(startTime: startTime, signalInfo: State.SignalInfo(quality: 0.2))
} else if signalInfo.quality == 0.2 {
state.lifecycleState = .connecting
}
}
self.state = state
}
self.buttonGroupView.toggleVideo = { [weak self] in
guard let self else {
return
@ -65,9 +132,18 @@ public final class PrivateCallScreen: UIView {
}
public func update(size: CGSize, insets: UIEdgeInsets) {
let backgroundFrame = CGRect(origin: CGPoint(), size: size)
let params = Params(size: size, insets: insets)
if self.params == params {
return
}
self.params = params
self.updateInternal(params: params, animated: false)
}
let aspect: CGFloat = size.width / size.height
private func updateInternal(params: Params, animated: Bool) {
let backgroundFrame = CGRect(origin: CGPoint(), size: params.size)
let aspect: CGFloat = params.size.width / params.size.height
let sizeNorm: CGFloat = 64.0
let renderingSize = CGSize(width: floor(sizeNorm * aspect), height: sizeNorm)
let edgeSize: Int = 2
@ -77,18 +153,36 @@ public final class PrivateCallScreen: UIView {
self.backgroundLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(renderingSize.width) + edgeSize * 2, height: Int(renderingSize.height) + edgeSize * 2))
self.backgroundLayer.frame = visualBackgroundFrame
self.contentOverlayLayer.frame = CGRect(origin: CGPoint(), size: size)
self.contentOverlayLayer.update(size: size, contentInsets: UIEdgeInsets())
let backgroundStateIndex: Int
switch self.state.lifecycleState {
case .connecting:
backgroundStateIndex = 0
case .ringing:
backgroundStateIndex = 0
case .exchangingKeys:
backgroundStateIndex = 0
case let .active(_, signalInfo):
if signalInfo.quality <= 0.2 {
backgroundStateIndex = 2
} else {
backgroundStateIndex = 1
}
}
self.backgroundLayer.update(stateIndex: backgroundStateIndex, animated: animated)
self.contentOverlayContainer.frame = CGRect(origin: CGPoint(), size: size)
self.contentOverlayLayer.frame = CGRect(origin: CGPoint(), size: params.size)
self.contentOverlayLayer.update(size: params.size, contentInsets: UIEdgeInsets())
self.contentOverlayContainer.frame = CGRect(origin: CGPoint(), size: params.size)
self.blurBackgroundLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(renderingSize.width) + edgeSize * 2, height: Int(renderingSize.height) + edgeSize * 2))
self.blurBackgroundLayer.frame = visualBackgroundFrame
self.blurBackgroundLayer.update(stateIndex: backgroundStateIndex, animated: animated)
self.buttonGroupView.frame = CGRect(origin: CGPoint(), size: size)
self.buttonGroupView.update(size: size)
self.buttonGroupView.frame = CGRect(origin: CGPoint(), size: params.size)
self.buttonGroupView.update(size: params.size)
self.contentView.frame = CGRect(origin: CGPoint(), size: size)
self.contentView.update(size: size, insets: insets)
self.contentView.frame = CGRect(origin: CGPoint(), size: params.size)
self.contentView.update(size: params.size, insets: params.insets, state: self.state)
}
}

View File

@ -1,67 +0,0 @@
import Foundation
import UIKit
public final class NullActionClass: NSObject, CAAction {
@objc public func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) {
}
}
public let nullAction = NullActionClass()
open class SimpleLayer: CALayer {
public var didEnterHierarchy: (() -> Void)?
public var didExitHierarchy: (() -> Void)?
public private(set) var isInHierarchy: Bool = false
override open func action(forKey event: String) -> CAAction? {
if event == kCAOnOrderIn {
self.isInHierarchy = true
self.didEnterHierarchy?()
} else if event == kCAOnOrderOut {
self.isInHierarchy = false
self.didExitHierarchy?()
}
return nullAction
}
override public init() {
super.init()
}
override public init(layer: Any) {
super.init(layer: layer)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
open class SimpleShapeLayer: CAShapeLayer {
public var didEnterHierarchy: (() -> Void)?
public var didExitHierarchy: (() -> Void)?
public private(set) var isInHierarchy: Bool = false
override open func action(forKey event: String) -> CAAction? {
if event == kCAOnOrderIn {
self.isInHierarchy = true
self.didEnterHierarchy?()
} else if event == kCAOnOrderOut {
self.isInHierarchy = false
self.didExitHierarchy?()
}
return nullAction
}
override public init() {
super.init()
}
override public init(layer: Any) {
super.init(layer: layer)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -41,6 +41,7 @@ import DeviceProximity
import MediaEditor
import TelegramUIDeclareEncodables
import ContextMenuScreen
import MetalEngine
#if canImport(AppCenter)
import AppCenter
@ -341,6 +342,8 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
self.window = window
self.nativeWindow = window
hostView.containerView.layer.addSublayer(MetalEngine.shared.rootLayer)
if !UIDevice.current.isBatteryMonitoringEnabled {
UIDevice.current.isBatteryMonitoringEnabled = true
}