Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
@@ -0,0 +1,102 @@
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 = "CameraScreenMetalResources",
srcs = glob([
"MetalResources/**/*.*",
]),
visibility = ["//visibility:public"],
)
plist_fragment(
name = "CameraScreenBundleInfoPlist",
extension = "plist",
template =
"""
<key>CFBundleIdentifier</key>
<string>org.telegram.CameraScreen</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleName</key>
<string>CameraScreen</string>
"""
)
apple_resource_bundle(
name = "CameraScreenBundle",
infoplists = [
":CameraScreenBundleInfoPlist",
],
resources = [
":CameraScreenMetalResources",
],
)
swift_library(
name = "CameraScreen",
module_name = "CameraScreen",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
data = [
":CameraScreenBundle",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/TelegramCore",
"//submodules/MetalEngine",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/ComponentFlow",
"//submodules/Components/ViewControllerComponent",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/TelegramPresentationData",
"//submodules/AccountContext",
"//submodules/AppBundle",
"//submodules/TelegramStringFormatting",
"//submodules/PresentationDataUtils",
"//submodules/LocalMediaResources",
"//submodules/Camera",
"//submodules/Components/MultilineTextComponent",
"//submodules/Components/BlurredBackgroundComponent",
"//submodules/Components/LottieAnimationComponent:LottieAnimationComponent",
"//submodules/Components/BundleIconComponent:BundleIconComponent",
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/TooltipUI",
"//submodules/TelegramUI/Components/MediaEditor",
"//submodules/Components/MetalImageView",
"//submodules/TelegramUI/Components/CameraButtonComponent",
"//submodules/Utils/VolumeButtons",
"//submodules/TelegramNotices",
"//submodules/DeviceAccess",
"//submodules/TelegramUI/Components/Utils/RoundedRectWithTailPath",
"//submodules/TelegramUI/Components/MediaAssetsContext",
"//submodules/UndoUI",
"//submodules/ContextUI",
"//submodules/AvatarNode",
"//submodules/ActivityIndicator",
"//submodules/TelegramUI/Components/Utils/AnimatableProperty",
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
"//submodules/TelegramUI/Components/GlassBarButtonComponent",
"//submodules/TelegramUI/Components/ShareWithPeersScreen",
"//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/TelegramCallsUI",
"//submodules/TelegramUI/Components/Stories/StoryContainerScreen",
"//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,253 @@
#include <metal_stdlib>
using namespace metal;
typedef struct {
packed_float2 position;
} Vertex;
struct RasterizerData
{
float4 position [[position]];
};
vertex RasterizerData cameraBlobVertex
(
constant Vertex *vertexArray[[buffer(0)]],
uint vertexID [[ vertex_id ]]
) {
RasterizerData out;
out.position = vector_float4(vertexArray[vertexID].position[0], vertexArray[vertexID].position[1], 0.0, 1.0);
return out;
}
#define BindingDistance 0.25
#define AARadius 2.0
float smin(float a, float b, float k) {
float h = clamp(0.5 + 0.5 * (a - b) / k, 0.0, 1.0);
return mix(a, b, h) - k * h * (1.0 - h);
}
float sdfRoundedRectangle(float2 uv, float2 center, float2 halfSize, float radius) {
float r = min(radius, min(halfSize.x, halfSize.y));
float2 q = abs(uv - center) - (halfSize - float2(r));
return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r;
}
float sdfCircle(float2 uv, float2 position, float radius) {
return length(uv - position) - radius;
}
float map(float2 uv, float4 primaryParameters, float2 primaryOffset, float3 secondaryParameters, float2 secondaryOffset) {
float primary = sdfRoundedRectangle(uv, primaryOffset, primaryParameters.xy, primaryParameters.w);
float secondary = sdfCircle(uv, secondaryOffset, secondaryParameters.x);
float metaballs = 1.0;
metaballs = smin(metaballs, primary, BindingDistance);
metaballs = smin(metaballs, secondary, BindingDistance);
return metaballs;
}
fragment half4 cameraBlobFragment(RasterizerData in[[stage_in]],
constant uint2 &resolution[[buffer(0)]],
constant float4 &primaryParameters[[buffer(1)]],
constant float2 &primaryOffset[[buffer(2)]],
constant float3 &primaryColor[[buffer(3)]],
constant float3 &secondaryParameters[[buffer(4)]],
constant float2 &secondaryOffset[[buffer(5)]])
{
float2 R = float2(resolution);
float2 uv;
float axis;
if (R.x > R.y) {
uv = (2.0 * in.position.xy - R) / R.y;
axis = uv.x;
} else {
uv = (2.0 * in.position.xy - R) / R.x;
axis = uv.y;
}
float t = AARadius / resolution.y;
float coverage = smoothstep(t, -t, map(uv, primaryParameters, primaryOffset,
secondaryParameters, secondaryOffset));
float bound = max(primaryParameters.x, primaryParameters.y) + 0.05;
if (abs(axis) > bound) {
float extra = min(1.0, (abs(axis) - bound) * 2.4);
coverage = mix(0.0, coverage, extra);
}
float alpha = coverage;
float3 rgb = clamp(primaryColor, 0.0, 1.0) * alpha;
return half4(half3(rgb), half(alpha));
}
struct Rectangle {
float2 origin;
float2 size;
};
constant static float2 quadVertices[6] = {
float2(0.0, 0.0),
float2(1.0, 0.0),
float2(0.0, 1.0),
float2(1.0, 0.0),
float2(0.0, 1.0),
float2(1.0, 1.0)
};
struct QuadVertexOut {
float4 position [[position]];
float2 uv;
};
kernel void videoBiPlanarToRGBA(
texture2d<half, access::read> inTextureY [[ texture(0) ]],
texture2d<half, access::read> inTextureUV [[ texture(1) ]],
texture2d<half, access::write> outTexture [[ texture(2) ]],
uint2 threadPosition [[ thread_position_in_grid ]]
) {
half y = inTextureY.read(threadPosition).r;
half2 uv = inTextureUV.read(uint2(threadPosition.x / 2, threadPosition.y / 2)).rg - half2(0.5, 0.5);
half4 color(y + 1.403 * uv.y, y - 0.344 * uv.x - 0.714 * uv.y, y + 1.770 * uv.x, 1.0);
outTexture.write(color, threadPosition);
}
kernel void videoTriPlanarToRGBA(
texture2d<half, access::read> inTextureY [[ texture(0) ]],
texture2d<half, access::read> inTextureU [[ texture(1) ]],
texture2d<half, access::read> inTextureV [[ texture(2) ]],
texture2d<half, access::write> outTexture [[ texture(3) ]],
uint2 threadPosition [[ thread_position_in_grid ]]
) {
half y = inTextureY.read(threadPosition).r;
uint2 uvPosition = uint2(threadPosition.x / 2, threadPosition.y / 2);
half2 inUV = (inTextureU.read(uvPosition).r, inTextureV.read(uvPosition).r);
half2 uv = inUV - half2(0.5, 0.5);
half4 color(y + 1.403 * uv.y, y - 0.344 * uv.x - 0.714 * uv.y, y + 1.770 * uv.x, 1.0);
outTexture.write(color, threadPosition);
}
vertex QuadVertexOut mainVideoVertex(
const device Rectangle &rect [[ buffer(0) ]],
const device uint2 &mirror [[ buffer(1) ]],
unsigned int vid [[ vertex_id ]]
) {
float2 quadVertex = quadVertices[vid];
QuadVertexOut out;
out.position = float4(rect.origin.x + quadVertex.x * rect.size.x, rect.origin.y + quadVertex.y * rect.size.y, 0.0, 1.0);
out.position.x = -1.0 + out.position.x * 2.0;
out.position.y = -1.0 + out.position.y * 2.0;
float2 uv = float2(quadVertex.x, 1.0 - quadVertex.y);
out.uv = float2(uv.y, 1.0 - uv.x);
if (mirror.x == 1) {
out.uv.x = 1.0 - out.uv.x;
}
if (mirror.y == 1) {
out.uv.y = 1.0 - out.uv.y;
}
return out;
}
half4 rgb2hsv(half4 c) {
half4 K = half4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
half4 p = mix(half4(c.bg, K.wz), half4(c.gb, K.xy), step(c.b, c.g));
half4 q = mix(half4(p.xyw, c.r), half4(c.r, p.yzx), step(p.x, c.r));
float d = q.x - min(q.w, q.y);
float e = 1.0e-10;
return half4(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x, c.a);
}
half4 hsv2rgb(half4 c) {
half4 K = half4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
half3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
return half4(c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y), c.a);
}
fragment half4 mainVideoFragment(
QuadVertexOut in [[stage_in]],
texture2d<half> texture [[ texture(0) ]],
const device float &brightness [[ buffer(0) ]],
const device float &saturation [[ buffer(1) ]],
const device float4 &overlay [[ buffer(2) ]]
) {
constexpr sampler sampler(coord::normalized, address::repeat, filter::linear);
half4 color = texture.sample(sampler, in.uv);
color = rgb2hsv(color);
color.b = clamp(color.b * brightness, 0.0, 1.0);
color.g = clamp(color.g * saturation, 0.0, 1.0);
color = hsv2rgb(color);
color.rgb += half3(overlay.rgb * overlay.a);
color.rgb = min(color.rgb, half3(1.0, 1.0, 1.0));
return half4(color.r, color.g, color.b, color.a);
}
constant int BLUR_SAMPLE_COUNT = 7;
constant float BLUR_OFFSETS[BLUR_SAMPLE_COUNT] = {
1.489585,
3.475713,
5.461880,
7.448104,
9.434408,
11.420812,
13.407332
};
constant float BLUR_WEIGHTS[BLUR_SAMPLE_COUNT] = {
0.130498886,
0.113685958,
0.0886923522,
0.0619646012,
0.0387683809,
0.0217213109,
0.0108984858
};
static void gaussianBlur(
texture2d<half, access::sample> inTexture,
texture2d<half, access::write> outTexture,
float2 offset,
uint2 gid
) {
constexpr sampler sampler(coord::normalized, address::clamp_to_edge, filter::linear);
uint2 textureDim(outTexture.get_width(), outTexture.get_height());
if(all(gid < textureDim)) {
float3 outColor(0.0);
float2 size(inTexture.get_width(), inTexture.get_height());
float2 baseTexCoord = float2(gid);
for (int i = 0; i < BLUR_SAMPLE_COUNT; i++) {
outColor += float3(inTexture.sample(sampler, (baseTexCoord + offset * BLUR_OFFSETS[i]) / size).rgb) * BLUR_WEIGHTS[i];
}
outTexture.write(half4(half3(outColor), 1.0), gid);
}
}
kernel void gaussianBlurHorizontal(
texture2d<half, access::sample> inTexture [[ texture(0) ]],
texture2d<half, access::write> outTexture [[ texture(1) ]],
uint2 gid [[ thread_position_in_grid ]]
) {
gaussianBlur(inTexture, outTexture, float2(1, 0), gid);
}
kernel void gaussianBlurVertical(
texture2d<half, access::sample> inTexture [[ texture(0) ]],
texture2d<half, access::write> outTexture [[ texture(1) ]],
uint2 gid [[ thread_position_in_grid ]]
) {
gaussianBlur(inTexture, outTexture, float2(0, 1), gid);
}
@@ -0,0 +1,177 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import Camera
final class CameraCodeFrameView: UIView {
private var cornerLayers: [SimpleShapeLayer] = []
private let cornerRadius: CGFloat = 12.0
private let focusedCornerRadius: CGFloat = 6.0
private let cornerShort: CGFloat = 16.0
private var currentSize: CGSize?
private var currentRect: CGRect?
override init(frame: CGRect) {
super.init(frame: frame)
self.isUserInteractionEnabled = false
for _ in 0..<4 {
let layer = SimpleShapeLayer()
layer.fillColor = UIColor.clear.cgColor
layer.strokeColor = UIColor.white.cgColor
layer.lineWidth = 2.0
layer.lineCap = .round
layer.lineJoin = .round
self.layer.addSublayer(layer)
self.cornerLayers.append(layer)
}
}
required init?(coder: NSCoder) {
preconditionFailure()
}
func update(size: CGSize, code: CameraCode?) {
let isFirstTime = self.currentSize == nil
self.currentSize = size
var duration: Double = 0.0
let bounds = CGRect(origin: .zero, size: size)
let rect: CGRect
if let code {
let codeRect = code.boundingBox
let side = max(codeRect.width * bounds.width, codeRect.height * bounds.height) * 0.7
let center = CGPoint(x: (1.0 - codeRect.center.y) * bounds.width, y: codeRect.center.x * bounds.height)
rect = CGSize(width: side, height: side).centered(around: center)
if !isFirstTime {
if let currentRect = self.currentRect {
if rect.center.distance(to: currentRect.center) > 40.0 || abs(rect.size.width - currentRect.size.width) > 40.0 {
duration = 0.35
} else {
duration = 0.2
}
} else {
duration = 0.4
}
}
self.currentRect = rect
} else {
rect = bounds.insetBy(dx: -2.0, dy: -2.0)
if !isFirstTime {
duration = 0.4
}
self.currentRect = nil
}
let focused = code != nil
self.applyPaths(to: self.cornerPaths(for: rect, focused: focused, rotation: 0.0), focused: focused, duration: duration)
}
private func cornerPaths(for rect: CGRect, focused: Bool, rotation: Double) -> [UIBezierPath] {
let effectiveCornerRadius = focused ? self.focusedCornerRadius : self.cornerRadius
let center = CGPoint(x: rect.midX, y: rect.midY)
let transform = CGAffineTransform(translationX: center.x, y: center.y).rotated(by: rotation).translatedBy(x: -center.x, y: -center.y)
let topLeftPath = UIBezierPath()
topLeftPath.move(to: CGPoint(x: rect.minX, y: focused ? rect.minY + self.cornerShort : rect.midY))
topLeftPath.addLine(to: CGPoint(x: rect.minX, y: rect.minY + effectiveCornerRadius))
topLeftPath.addQuadCurve(
to: CGPoint(x: rect.minX + effectiveCornerRadius, y: rect.minY),
controlPoint: CGPoint(x: rect.minX, y: rect.minY)
)
topLeftPath.addLine(to: CGPoint(x: focused ? rect.minX + self.cornerShort : rect.midX, y: rect.minY))
topLeftPath.apply(transform)
let topRightPath = UIBezierPath()
topRightPath.move(to: CGPoint(x: rect.maxX, y: focused ? rect.minY + self.cornerShort : rect.midY))
topRightPath.addLine(to: CGPoint(x: rect.maxX, y: rect.minY + effectiveCornerRadius))
topRightPath.addQuadCurve(
to: CGPoint(x: rect.maxX - effectiveCornerRadius, y: rect.minY),
controlPoint: CGPoint(x: rect.maxX, y: rect.minY)
)
topRightPath.addLine(to: CGPoint(x: focused ? rect.maxX - self.cornerShort : rect.midX, y: rect.minY))
topRightPath.apply(transform)
let bottomRightPath = UIBezierPath()
bottomRightPath.move(to: CGPoint(x: rect.maxX, y: focused ? rect.maxY - self.cornerShort : rect.midY))
bottomRightPath.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY - effectiveCornerRadius))
bottomRightPath.addQuadCurve(
to: CGPoint(x: rect.maxX - effectiveCornerRadius, y: rect.maxY),
controlPoint: CGPoint(x: rect.maxX, y: rect.maxY)
)
bottomRightPath.addLine(to: CGPoint(x: focused ? rect.maxX - self.cornerShort : rect.midX, y: rect.maxY))
bottomRightPath.apply(transform)
let bottomLeftPath = UIBezierPath()
bottomLeftPath.move(to: CGPoint(x: rect.minX, y: focused ? rect.maxY - self.cornerShort : rect.midY))
bottomLeftPath.addLine(to: CGPoint(x: rect.minX, y: rect.maxY - effectiveCornerRadius))
bottomLeftPath.addQuadCurve(
to: CGPoint(x: rect.minX + effectiveCornerRadius, y: rect.maxY),
controlPoint: CGPoint(x: rect.minX, y: rect.maxY)
)
bottomLeftPath.addLine(to: CGPoint(x: focused ? rect.minX + self.cornerShort : rect.midX, y: rect.maxY))
bottomLeftPath.apply(transform)
return [topLeftPath, topRightPath, bottomRightPath, bottomLeftPath]
}
private var animatingAppearance = false
private func applyPaths(to paths: [UIBezierPath], focused: Bool, duration: Double) {
let animatingAppearance = self.animatingAppearance
for (index, path) in paths.enumerated() {
let layer = self.cornerLayers[index]
let previousPath = layer.path
let previousAlpha = layer.opacity
let previousColor = layer.strokeColor ?? UIColor.clear.cgColor
let previousLineWidth = layer.lineWidth
if duration > 0.0 && !focused {
} else {
layer.path = path.cgPath
}
layer.opacity = focused ? 1.0 : 0.0
layer.strokeColor = focused ? UIColor(rgb: 0xffd300).cgColor : UIColor.white.cgColor
layer.lineWidth = focused ? 5.0 : 2.0
layer.shadowOffset = .zero
layer.shadowRadius = 1.0
layer.shadowColor = UIColor.black.cgColor
layer.shadowOpacity = 0.2
if duration > 0.0 && !animatingAppearance {
if focused && previousAlpha.isZero && index == 0 {
self.animatingAppearance = true
}
if focused {
var currentPath = previousPath
var duration = duration
if let presentationPath = layer.presentation()?.path {
currentPath = presentationPath
duration *= 0.5
}
layer.animate(from: currentPath, to: path.cgPath, keyPath: "path", timingFunction: duration > 0.35 ? kCAMediaTimingFunctionSpring : CAMediaTimingFunctionName.linear.rawValue, duration: duration, completion: { _ in
if focused && index == 0 {
self.animatingAppearance = false
}
})
}
layer.animateAlpha(from: CGFloat(previousAlpha), to: CGFloat(layer.opacity), duration: focused ? 0.4 : 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, completion: !focused ? { finished in
layer.path = path.cgPath
} : nil)
layer.animate(from: previousColor, to: layer.strokeColor ?? UIColor.white.cgColor, keyPath: "strokeColor", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.3, delay: 0.15)
layer.animate(from: previousLineWidth, to: layer.lineWidth, keyPath: "lineWidth", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.3)
}
}
}
}
private extension CGPoint {
func distance(to point: CGPoint) -> CGFloat {
return sqrt(pow((point.x - self.x), 2) + pow((point.y - self.y), 2))
}
}
@@ -0,0 +1,234 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import SwiftSignalKit
import TelegramCore
import MultilineTextComponent
import LottieAnimationComponent
import AvatarNode
import AccountContext
final class CameraCodeResultComponent: Component {
let context: AccountContext
let peer: EnginePeer
let pressed: (EnginePeer) -> Void
init(
context: AccountContext,
peer: EnginePeer,
pressed: @escaping (EnginePeer) -> Void
) {
self.context = context
self.peer = peer
self.pressed = pressed
}
static func ==(lhs: CameraCodeResultComponent, rhs: CameraCodeResultComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.peer != rhs.peer {
return false
}
return true
}
final class View: UIView {
private var component: CameraCodeResultComponent?
private let wrapperView = UIView()
private let backgroundView = BlurredBackgroundView(color: UIColor(rgb: 0x2a2a2a, alpha: 0.65))
private let highlightedBackgroundView = UIView()
private let contentView = UIView()
private let contentWrapperView = UIView()
private let avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 14.0))
private let title = ComponentView<Empty>()
private let subtitle = ComponentView<Empty>()
private let button = HighlightTrackingButton()
private let contentMaskView = UIView()
private let animationClippingView = UIView()
private let maskAnimation = ComponentView<Empty>()
private let maskBackgroundView = UIView()
init() {
self.animationClippingView.clipsToBounds = true
self.maskBackgroundView.backgroundColor = .white
self.maskBackgroundView.layer.cornerRadius = 12.0
self.highlightedBackgroundView.alpha = 0.0
self.highlightedBackgroundView.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.5)
self.highlightedBackgroundView.isUserInteractionEnabled = false
super.init(frame: CGRect())
self.addSubview(self.wrapperView)
self.wrapperView.mask = self.contentMaskView
self.wrapperView.addSubview(self.backgroundView)
self.wrapperView.addSubview(self.highlightedBackgroundView)
self.wrapperView.addSubview(self.contentView)
self.wrapperView.addSubview(self.button)
self.contentMaskView.addSubview(self.animationClippingView)
self.contentMaskView.addSubview(self.maskBackgroundView)
self.contentView.addSubview(self.contentWrapperView)
self.contentWrapperView.addSubview(self.avatarNode.view)
self.button.highligthedChanged = { [weak self] highlighted in
guard let self else {
return
}
if highlighted {
self.highlightedBackgroundView.layer.removeAnimation(forKey: "opacity")
self.highlightedBackgroundView.layer.opacity = 1.0
} else {
self.highlightedBackgroundView.layer.opacity = 0.0
self.highlightedBackgroundView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
}
}
self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside)
}
required init?(coder aDecoder: NSCoder) {
preconditionFailure()
}
@objc private func buttonPressed() {
if let component = self.component {
component.pressed(component.peer)
}
}
func animateIn() {
if let view = self.maskAnimation.view as? LottieAnimationComponent.View {
view.playOnce()
Queue.mainQueue().after(0.016) {
view.alpha = 1.0
}
view.layer.animatePosition(from: CGPoint(x: 0.0, y: 20.0), to: .zero, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
self.maskBackgroundView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.001, delay: 0.29, completion: { _ in
view.alpha = 0.0
})
}
let overlayLayer = SimpleLayer()
overlayLayer.frame = CGRect(origin: .zero, size: self.wrapperView.bounds.size)
overlayLayer.backgroundColor = UIColor.white.cgColor
overlayLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, delay: 0.15, removeOnCompletion: false, completion: { _ in
overlayLayer.removeFromSuperlayer()
})
self.wrapperView.layer.insertSublayer(overlayLayer, at: 2)
self.maskBackgroundView.layer.animate(from: 27.5, to: 12.0, keyPath: "cornerRadius", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.4, delay: 0.35)
self.maskBackgroundView.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: 0.0, y: 66.0)), to: NSValue(cgPoint: .zero), keyPath: "position", duration: 0.8, delay: 0.2, initialVelocity: 0.0, damping: 64.0, removeOnCompletion: true, additive: true, completion: nil)
self.maskBackgroundView.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 1.0, delay: 0.0, initialVelocity: 0.0, damping: 64.0, removeOnCompletion: true, completion: nil)
self.maskBackgroundView.layer.animateSpring(from: NSValue(cgRect: CGRect(origin: .zero, size: CGSize(width: 30.0, height: 55.0))), to: NSValue(cgRect: self.maskBackgroundView.bounds), keyPath: "bounds", duration: 0.85, delay: 0.26, initialVelocity: 0.0, damping: 90.0, removeOnCompletion: true, completion: nil)
self.contentView.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: 0.0, y: 66.0)), to: NSValue(cgPoint: .zero), keyPath: "position", duration: 0.8, delay: 0.2, initialVelocity: 0.0, damping: 64.0, removeOnCompletion: true, additive: true, completion: nil)
self.contentView.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 1.0, delay: 0.0, initialVelocity: 0.0, damping: 64.0, removeOnCompletion: true, completion: nil)
self.contentWrapperView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, delay: 0.4)
self.contentWrapperView.layer.animateScale(from: 0.2, to: 1.0, duration: 0.25, delay: 0.35)
}
func update(component: CameraCodeResultComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
self.component = component
let backgroundSize = CGSize(width: 220.0, height: 55.0)
let animationSize = self.maskAnimation.update(
transition: .immediate,
component: AnyComponent(
LottieAnimationComponent(
animation: LottieAnimationComponent.AnimationItem(
name: "UserAvatarMask",
mode: .still(position: .end),
range: (1.0, 0.0),
speed: 30.0
),
colors: ["__allcolors__": .white],
size: CGSize(width: 94.0, height: 120.0)
)
),
environment: {},
containerSize: availableSize
)
if let view = self.maskAnimation.view {
if view.superview == nil {
view.alpha = 0.0
view.transform = CGAffineTransformMakeScale(1.0, -1.0)
self.animationClippingView.addSubview(view)
}
self.animationClippingView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - animationSize.width) / 2.0), y: 29.0), size: CGSize(width: animationSize.width, height: animationSize.height - 13.0))
view.frame = CGRect(origin: .zero, size: animationSize)
}
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let avatarSize = CGSize(width: 30.0, height: 30.0)
self.avatarNode.setPeer(context: component.context, theme: presentationData.theme, peer: component.peer)
self.avatarNode.frame = CGRect(origin: CGPoint(x: 12.0, y: floorToScreenPixels((backgroundSize.height - avatarSize.height) / 2.0)), size: avatarSize)
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: component.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), font: Font.regular(17.0), textColor: .white)))
),
environment: {},
containerSize: CGSize(width: 140.0, height: availableSize.height)
)
if let view = self.title.view {
if view.superview == nil {
self.contentWrapperView.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: 54.0, y: 9.0), size: titleSize)
}
let subtitleString = NSMutableAttributedString(string: "\(presentationData.strings.Camera_OpenChat) >", font: Font.regular(15.0), textColor: UIColor(rgb: 0xffffff, alpha: 0.7))
if let range = subtitleString.string.range(of: ">"), let arrowImage = UIImage(bundleImageName: "Item List/InlineTextRightArrow") {
subtitleString.addAttribute(.attachment, value: arrowImage, range: NSRange(range, in: subtitleString.string))
subtitleString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: subtitleString.string))
}
let subtitleSize = self.subtitle.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(text: .plain(subtitleString))
),
environment: {},
containerSize: CGSize(width: 140.0, height: availableSize.height)
)
if let view = self.subtitle.view {
if view.superview == nil {
self.contentWrapperView.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: 54.0, y: 29.0), size: subtitleSize)
}
self.contentView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - backgroundSize.width) / 2.0), y: 54.0 + UIScreenPixel), size: backgroundSize)
self.contentWrapperView.frame = self.contentView.bounds
self.maskBackgroundView.frame = self.contentView.frame
self.button.frame = self.contentView.frame
self.highlightedBackgroundView.frame = self.contentView.frame
self.wrapperView.frame = CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: animationSize.height + 17.0))
self.backgroundView.frame = self.wrapperView.bounds
self.backgroundView.update(size: self.wrapperView.bounds.size, transition: .immediate)
self.contentMaskView.frame = self.wrapperView.bounds
return CGSize(width: availableSize.width, height: 120.0)
}
}
func makeView() -> View {
return View()
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,483 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import Postbox
import TelegramCore
import SwiftSignalKit
import AccountContext
import TelegramCallsUI
import TelegramPresentationData
import StoryContainerScreen
import ChatEntityKeyboardInputNode
import AvatarNode
import MultilineTextComponent
final class CameraLiveStreamComponent: Component {
let context: AccountContext
let theme: PresentationTheme
let strings: PresentationStrings
let peerId: EnginePeer.Id
let story: EngineStoryItem?
let statusBarHeight: CGFloat
let inputHeight: CGFloat
let safeInsets: UIEdgeInsets
let metrics: LayoutMetrics
let deviceMetrics: DeviceMetrics
let presentController: (ViewController, Any?) -> Void
let presentInGlobalOverlay: (ViewController, Any?) -> Void
let getController: () -> ViewController?
let didSetupMediaStream: (PresentationGroupCall) -> Void
init(
context: AccountContext,
theme: PresentationTheme,
strings: PresentationStrings,
peerId: EnginePeer.Id,
story: EngineStoryItem?,
statusBarHeight: CGFloat,
inputHeight: CGFloat,
safeInsets: UIEdgeInsets,
metrics: LayoutMetrics,
deviceMetrics: DeviceMetrics,
presentController: @escaping (ViewController, Any?) -> Void,
presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void,
getController: @escaping () -> ViewController?,
didSetupMediaStream: @escaping (PresentationGroupCall) -> Void
) {
self.context = context
self.theme = theme
self.strings = strings
self.peerId = peerId
self.story = story
self.statusBarHeight = statusBarHeight
self.inputHeight = inputHeight
self.safeInsets = safeInsets
self.metrics = metrics
self.deviceMetrics = deviceMetrics
self.presentController = presentController
self.presentInGlobalOverlay = presentInGlobalOverlay
self.getController = getController
self.didSetupMediaStream = didSetupMediaStream
}
static func ==(lhs: CameraLiveStreamComponent, rhs: CameraLiveStreamComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.peerId != rhs.peerId {
return false
}
if lhs.story != rhs.story {
return false
}
if lhs.statusBarHeight != rhs.statusBarHeight {
return false
}
if lhs.inputHeight != rhs.inputHeight {
return false
}
if lhs.safeInsets != rhs.safeInsets {
return false
}
if lhs.metrics != rhs.metrics {
return false
}
if lhs.deviceMetrics != rhs.deviceMetrics {
return false
}
return true
}
final class View: UIView {
private var liveChat: ComponentView<Empty>?
private var storyContent: SingleStoryContentContextImpl?
private var storyContentState: StoryContentContextState?
private var storyContentDisposable: Disposable?
private let externalState = StoryItemSetContainerComponent.ExternalState()
private let storyItemSharedState = StoryContentItem.SharedState()
private let inputMediaNodeDataPromise = Promise<ChatEntityKeyboardInputNode.InputData>()
private let closeFriendsPromise = Promise<[EnginePeer]>()
private var blockedPeers: BlockedPeersContext?
private var component: CameraLiveStreamComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.storyContentDisposable?.dispose()
}
var mediaStreamCall: PresentationGroupCall? {
if let view = self.liveChat?.view as? StoryItemSetContainerComponent.View {
return view.mediaStreamCall
}
return nil
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event)
if result === self {
return nil
}
return result
}
func update(component: CameraLiveStreamComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
if let story = component.story {
if self.storyContentDisposable == nil {
let storyContent = SingleStoryContentContextImpl(context: component.context, storyId: StoryId(peerId: component.peerId, id: story.id), storyItem: story, readGlobally: false)
self.storyContent = storyContent
self.storyContentDisposable = (storyContent.state
|> deliverOnMainQueue).start(next: { [weak self] state in
guard let self else {
return
}
self.storyContentState = state
self.state?.updated()
})
self.inputMediaNodeDataPromise.set(
ChatEntityKeyboardInputNode.inputData(
context: component.context,
chatPeerId: nil,
areCustomEmojiEnabled: true,
hasTrending: true,
hasSearch: true,
hideBackground: true,
maskEdge: .clip,
sendGif: nil
)
)
}
if let storyContentState = self.storyContentState, let slice = storyContentState.slice {
var mediaStreamTransition = transition
let liveChat: ComponentView<Empty>
if let current = self.liveChat {
liveChat = current
} else {
mediaStreamTransition = .immediate
liveChat = ComponentView()
self.liveChat = liveChat
}
let itemSetContainerInsets = UIEdgeInsets(top: component.statusBarHeight + 5.0, left: 0.0, bottom: 0.0, right: 0.0)
let itemSetContainerSafeInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 34.0, right: 0.0)
let _ = liveChat.update(
transition: mediaStreamTransition,
component: AnyComponent(StoryItemSetContainerComponent(
context: component.context,
externalState: self.externalState,
storyItemSharedState: self.storyItemSharedState,
availableReactions: nil,
slice: slice,
theme: defaultDarkColorPresentationTheme,
strings: component.strings,
containerInsets: itemSetContainerInsets,
safeInsets: itemSetContainerSafeInsets,
statusBarHeight: component.statusBarHeight,
inputHeight: component.inputHeight,
metrics: component.metrics,
deviceMetrics: component.deviceMetrics,
isEmbeddedInCamera: true,
isProgressPaused: false,
isAudioMuted: false,
audioMode: .off,
hideUI: false,
visibilityFraction: 1.0,
isPanning: false,
isCentral: true,
pinchState: nil,
presentController: { [weak self] c, a in
guard let self, let component = self.component else {
return
}
component.presentController(c, a)
},
presentInGlobalOverlay: { [weak self] c, a in
guard let self, let component = self.component else {
return
}
component.presentInGlobalOverlay(c, a)
},
close: {
},
navigate: { _ in
},
delete: {
},
markAsSeen: { _ in
},
reorder: {
},
createToFolder: { _, _ in
},
addToFolder: { _ in
},
controller: { [weak self] in
guard let self, let component = self.component else {
return nil
}
return component.getController()
},
toggleAmbientMode: {
},
keyboardInputData: self.inputMediaNodeDataPromise.get(),
closeFriends: self.closeFriendsPromise,
blockedPeers: self.blockedPeers,
sharedViewListsContext: StoryItemSetViewListComponent.SharedListsContext(),
stealthModeTimeout: nil,
isDismissed: false
)),
environment: {},
containerSize: availableSize
)
let liveChatFrame = CGRect(origin: CGPoint(), size: availableSize)
if let liveChatView = liveChat.view as? StoryItemSetContainerComponent.View {
if liveChatView.superview == nil {
liveChatView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
liveChat.parentState = state
self.addSubview(liveChatView)
}
mediaStreamTransition.setFrame(view: liveChatView, frame: liveChatFrame)
if let mediaStreamCall = liveChatView.mediaStreamCall {
component.didSetupMediaStream(mediaStreamCall)
}
}
}
} else {
if let liveChat = self.liveChat {
self.liveChat = nil
liveChat.view?.removeFromSuperview()
}
}
return availableSize
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
public final class StreamAsComponent: Component {
let context: AccountContext
let peerId: EnginePeer.Id
let isCustomTarget: Bool
public init(
context: AccountContext,
peerId: EnginePeer.Id,
isCustomTarget: Bool
) {
self.context = context
self.peerId = peerId
self.isCustomTarget = isCustomTarget
}
public static func ==(lhs: StreamAsComponent, rhs: StreamAsComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.peerId != rhs.peerId {
return false
}
if lhs.isCustomTarget != rhs.isCustomTarget {
return false
}
return true
}
public final class View: UIView {
private let avatarNode: AvatarNode
private let title = ComponentView<Empty>()
private let subtitle = ComponentView<Empty>()
private var arrow = UIImageView()
private var component: StreamAsComponent?
private weak var state: EmptyComponentState?
private var peer: EnginePeer?
public override init(frame: CGRect) {
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 18.0))
self.arrow.image = generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: UIColor(white: 1.0, alpha: 0.8))
super.init(frame: frame)
self.addSubnode(self.avatarNode)
self.addSubview(self.arrow)
}
public required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var scheduledAnimateIn: ComponentTransition?
func animateIn(transition: ComponentTransition) {
if self.peer == nil {
self.scheduledAnimateIn = transition
self.alpha = 0.0
return
}
self.alpha = 1.0
transition.animateAlpha(view: self.avatarNode.view, from: 0.0, to: 1.0)
transition.animateScale(view: self.avatarNode.view, from: 0.01, to: 1.0)
let offset: CGFloat = 24.0
if let titleView = self.title.view {
transition.animateAlpha(view: titleView, from: 0.0, to: 1.0)
transition.animatePosition(view: titleView, from: CGPoint(x: -titleView.bounds.width / 2.0 - offset, y: self.bounds.height / 2.0 - titleView.center.y), to: .zero, additive: true)
transition.animateScale(view: titleView, from: 0.01, to: 1.0)
}
if let subtitleView = self.subtitle.view {
transition.animateAlpha(view: subtitleView, from: 0.0, to: 1.0)
transition.animatePosition(view: subtitleView, from: CGPoint(x: -subtitleView.bounds.width / 2.0 - offset, y: self.bounds.height / 2.0 - subtitleView.center.y), to: .zero, additive: true)
transition.animateScale(view: subtitleView, from: 0.01, to: 1.0)
transition.animateAlpha(view: self.arrow, from: 0.0, to: 1.0)
transition.animatePosition(view: self.arrow, from: CGPoint(x: -subtitleView.bounds.width / 2.0 - offset - 16.0, y: self.bounds.height / 2.0 - self.arrow.center.y), to: .zero, additive: true)
transition.animateScale(view: self.arrow, from: 0.01, to: 1.0)
}
}
func animateOut(transition: ComponentTransition, completion: @escaping () -> Void) {
transition.setAlpha(view: self.avatarNode.view, alpha: 0.0, completion: { _ in
completion()
})
transition.setScale(view: self.avatarNode.view, scale: 0.01)
let offset: CGFloat = 24.0
if let titleView = self.title.view {
transition.setAlpha(view: titleView, alpha: 0.0)
transition.setPosition(view: titleView, position: titleView.center.offsetBy(dx: -titleView.bounds.width / 2.0 - offset, dy: self.bounds.height / 2.0 - titleView.center.y))
transition.setScale(view: titleView, scale: 0.01)
}
if let subtitleView = self.subtitle.view {
transition.setAlpha(view: subtitleView, alpha: 0.0)
transition.setPosition(view: subtitleView, position: subtitleView.center.offsetBy(dx: -subtitleView.bounds.width / 2.0 - offset, dy: self.bounds.height / 2.0 - subtitleView.center.y))
transition.setScale(view: subtitleView, scale: 0.01)
transition.setAlpha(view: self.arrow, alpha: 0.0)
transition.setPosition(view: self.arrow, position: self.arrow.center.offsetBy(dx: -subtitleView.bounds.width / 2.0 - offset - 16.0, dy: self.bounds.height / 2.0 - self.arrow.center.y))
transition.setScale(view: self.arrow, scale: 0.01)
}
}
public func update(component: StreamAsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
if self.peer?.id != component.peerId {
let _ = (component.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: component.peerId))
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let self, let peer else {
return
}
self.peer = peer
self.state?.updated()
if let scheduledAnimateIn = self.scheduledAnimateIn {
self.scheduledAnimateIn = nil
self.animateIn(transition: scheduledAnimateIn)
}
})
}
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 })
let avatarSize = CGSize(width: 32.0, height: 32.0)
self.avatarNode.frame = CGRect(origin: .zero, size: avatarSize)
if let peer = self.peer {
self.avatarNode.setPeer(
context: component.context,
theme: presentationData.theme,
peer: peer,
synchronousLoad: true
)
}
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: self.peer?.compactDisplayTitle ?? "", font: Font.semibold(14.0), textColor: .white, paragraphAlignment: .left))
)
),
environment: {},
containerSize: CGSize(width: availableSize.width - 38.0, height: availableSize.height)
)
let titleFrame = CGRect(origin: CGPoint(x: 42.0, y: component.isCustomTarget ? floorToScreenPixels((avatarSize.height - titleSize.height) / 2.0) : 1.0), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
self.addSubview(titleView)
}
titleView.frame = titleFrame
}
var maxWidth = titleFrame.maxX
if !component.isCustomTarget {
let subtitleSize = self.subtitle.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: presentationData.strings.Camera_LiveStream_Change, font: Font.regular(11.0), textColor: UIColor(white: 1.0, alpha: 0.8), paragraphAlignment: .left))
)
),
environment: {},
containerSize: CGSize(width: availableSize.width - 50.0, height: availableSize.height)
)
let subtitleFrame = CGRect(origin: CGPoint(x: 42.0, y: titleFrame.maxY + 2.0), size: subtitleSize)
if let subtitleView = self.subtitle.view {
if subtitleView.superview == nil {
self.addSubview(subtitleView)
}
subtitleView.frame = subtitleFrame
}
if let icon = self.arrow.image {
self.arrow.frame = CGRect(origin: CGPoint(x: subtitleFrame.maxX + 1.0, y: floorToScreenPixels(subtitleFrame.midY - icon.size.height / 2.0) + 1.0), size: icon.size).insetBy(dx: 1.0, dy: 1.0)
}
maxWidth = max(maxWidth, subtitleFrame.maxX + 16.0)
}
return CGSize(width: maxWidth, height: avatarSize.height)
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +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: "CameraScreenBundle", 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
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,312 @@
import Foundation
import UIKit
import SwiftSignalKit
import MetalKit
import MetalPerformanceShaders
import Accelerate
import MetalEngine
public final class VideoSourceOutput {
public struct MirrorDirection: OptionSet {
public var rawValue: Int32
public init(rawValue: Int32) {
self.rawValue = rawValue
}
public static let horizontal = MirrorDirection(rawValue: 1 << 0)
public static let vertical = MirrorDirection(rawValue: 1 << 1)
}
open class DataBuffer {
open var pixelBuffer: CVPixelBuffer? {
return nil
}
public init() {
}
}
public final class BiPlanarTextureLayout {
public let y: MTLTexture
public let uv: MTLTexture
public init(y: MTLTexture, uv: MTLTexture) {
self.y = y
self.uv = uv
}
}
public final class TriPlanarTextureLayout {
public let y: MTLTexture
public let u: MTLTexture
public let v: MTLTexture
public init(y: MTLTexture, u: MTLTexture, v: MTLTexture) {
self.y = y
self.u = u
self.v = v
}
}
public enum TextureLayout {
case biPlanar(BiPlanarTextureLayout)
case triPlanar(TriPlanarTextureLayout)
}
public final class NativeDataBuffer: DataBuffer {
private let pixelBufferValue: CVPixelBuffer
override public var pixelBuffer: CVPixelBuffer? {
return self.pixelBufferValue
}
public init(pixelBuffer: CVPixelBuffer) {
self.pixelBufferValue = pixelBuffer
}
}
public let resolution: CGSize
public let textureLayout: TextureLayout
public let dataBuffer: DataBuffer
public let mirrorDirection: MirrorDirection
public let sourceId: Int
public init(resolution: CGSize, textureLayout: TextureLayout, dataBuffer: DataBuffer, mirrorDirection: MirrorDirection, sourceId: Int) {
self.resolution = resolution
self.textureLayout = textureLayout
self.dataBuffer = dataBuffer
self.mirrorDirection = mirrorDirection
self.sourceId = sourceId
}
}
public protocol VideoSource: AnyObject {
typealias Output = VideoSourceOutput
var currentOutput: Output? { get }
func addOnUpdated(_ f: @escaping () -> Void) -> Disposable
}
final class CameraVideoLayer: MetalEngineSubjectLayer, MetalEngineSubject {
public var internalData: MetalEngineSubjectInternalData?
public let blurredLayer: MetalEngineSubjectLayer
final class BlurState: ComputeState {
let computePipelineStateYUVBiPlanarToRGBA: MTLComputePipelineState
let computePipelineStateYUVTriPlanarToRGBA: MTLComputePipelineState
let computePipelineStateHorizontal: MTLComputePipelineState
let computePipelineStateVertical: MTLComputePipelineState
let downscaleKernel: MPSImageBilinearScale
required init?(device: MTLDevice) {
guard let library = metalLibrary(device: device) else {
return nil
}
guard let functionVideoBiPlanarToRGBA = library.makeFunction(name: "videoBiPlanarToRGBA") else {
return nil
}
guard let computePipelineStateYUVBiPlanarToRGBA = try? device.makeComputePipelineState(function: functionVideoBiPlanarToRGBA) else {
return nil
}
self.computePipelineStateYUVBiPlanarToRGBA = computePipelineStateYUVBiPlanarToRGBA
guard let functionVideoTriPlanarToRGBA = library.makeFunction(name: "videoTriPlanarToRGBA") else {
return nil
}
guard let computePipelineStateYUVTriPlanarToRGBA = try? device.makeComputePipelineState(function: functionVideoTriPlanarToRGBA) else {
return nil
}
self.computePipelineStateYUVTriPlanarToRGBA = computePipelineStateYUVTriPlanarToRGBA
guard let gaussianBlurHorizontal = library.makeFunction(name: "gaussianBlurHorizontal"), let gaussianBlurVertical = library.makeFunction(name: "gaussianBlurVertical") else {
return nil
}
guard let computePipelineStateHorizontal = try? device.makeComputePipelineState(function: gaussianBlurHorizontal) else {
return nil
}
self.computePipelineStateHorizontal = computePipelineStateHorizontal
guard let computePipelineStateVertical = try? device.makeComputePipelineState(function: gaussianBlurVertical) else {
return nil
}
self.computePipelineStateVertical = computePipelineStateVertical
self.downscaleKernel = MPSImageBilinearScale(device: device)
}
}
final class RenderState: RenderToLayerState {
let pipelineState: MTLRenderPipelineState
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
}
let pipelineDescriptor = MTLRenderPipelineDescriptor()
pipelineDescriptor.vertexFunction = vertexFunction
pipelineDescriptor.fragmentFunction = fragmentFunction
pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
guard let pipelineState = try? device.makeRenderPipelineState(descriptor: pipelineDescriptor) else {
return nil
}
self.pipelineState = pipelineState
}
}
public var video: VideoSource.Output? {
didSet {
self.setNeedsUpdate()
}
}
public var renderSpec: RenderLayerSpec?
private var rgbaTexture: PooledTexture?
private var downscaledTexture: PooledTexture?
private var blurredHorizontalTexture: PooledTexture?
private var blurredVerticalTexture: PooledTexture?
override public init() {
self.blurredLayer = MetalEngineSubjectLayer()
super.init()
}
override public init(layer: Any) {
self.blurredLayer = MetalEngineSubjectLayer()
super.init(layer: layer)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func update(context: MetalEngineSubjectContext) {
if self.isHidden {
return
}
guard let renderSpec = self.renderSpec else {
return
}
guard let videoTextures = self.video else {
return
}
let rgbaTextureSpec = TextureSpec(width: Int(videoTextures.resolution.width), height: Int(videoTextures.resolution.height), pixelFormat: .rgba8UnsignedNormalized)
if self.rgbaTexture == nil || self.rgbaTexture?.spec != rgbaTextureSpec {
self.rgbaTexture = MetalEngine.shared.pooledTexture(spec: rgbaTextureSpec)
}
if self.downscaledTexture == nil {
self.downscaledTexture = MetalEngine.shared.pooledTexture(spec: TextureSpec(width: 256, height: 256, pixelFormat: .rgba8UnsignedNormalized))
}
if self.blurredHorizontalTexture == nil {
self.blurredHorizontalTexture = MetalEngine.shared.pooledTexture(spec: TextureSpec(width: 256, height: 256, pixelFormat: .rgba8UnsignedNormalized))
}
if self.blurredVerticalTexture == nil {
self.blurredVerticalTexture = MetalEngine.shared.pooledTexture(spec: TextureSpec(width: 256, height: 256, pixelFormat: .rgba8UnsignedNormalized))
}
guard let rgbaTexture = self.rgbaTexture?.get(context: context) else {
return
}
let _ = context.compute(state: BlurState.self, inputs: rgbaTexture.placeholer, commands: { commandBuffer, blurState, rgbaTexture in
guard let rgbaTexture else {
return
}
guard let computeEncoder = commandBuffer.makeComputeCommandEncoder() else {
return
}
let threadgroupSize = MTLSize(width: 16, height: 16, depth: 1)
let threadgroupCount = MTLSize(width: (rgbaTexture.width + threadgroupSize.width - 1) / threadgroupSize.width, height: (rgbaTexture.height + threadgroupSize.height - 1) / threadgroupSize.height, depth: 1)
switch videoTextures.textureLayout {
case let .biPlanar(biPlanar):
computeEncoder.setComputePipelineState(blurState.computePipelineStateYUVBiPlanarToRGBA)
computeEncoder.setTexture(biPlanar.y, index: 0)
computeEncoder.setTexture(biPlanar.uv, index: 1)
computeEncoder.setTexture(rgbaTexture, index: 2)
case let .triPlanar(triPlanar):
computeEncoder.setComputePipelineState(blurState.computePipelineStateYUVTriPlanarToRGBA)
computeEncoder.setTexture(triPlanar.y, index: 0)
computeEncoder.setTexture(triPlanar.u, index: 1)
computeEncoder.setTexture(triPlanar.u, index: 2)
computeEncoder.setTexture(rgbaTexture, index: 3)
}
computeEncoder.dispatchThreadgroups(threadgroupCount, threadsPerThreadgroup: threadgroupSize)
computeEncoder.endEncoding()
})
guard let downscaledTexture = self.downscaledTexture?.get(context: context), let blurredHorizontalTexture = self.blurredHorizontalTexture?.get(context: context), let blurredVerticalTexture = self.blurredVerticalTexture?.get(context: context) else {
return
}
let blurredTexture = context.compute(state: BlurState.self, inputs: rgbaTexture.placeholer, downscaledTexture.placeholer, blurredHorizontalTexture.placeholer, blurredVerticalTexture.placeholer, commands: { commandBuffer, blurState, rgbaTexture, downscaledTexture, blurredHorizontalTexture, blurredVerticalTexture -> MTLTexture? in
guard let rgbaTexture, let downscaledTexture, let blurredHorizontalTexture, let blurredVerticalTexture else {
return nil
}
blurState.downscaleKernel.encode(commandBuffer: commandBuffer, sourceTexture: rgbaTexture, destinationTexture: downscaledTexture)
do {
guard let computeEncoder = commandBuffer.makeComputeCommandEncoder() else {
return nil
}
let threadgroupSize = MTLSize(width: 16, height: 16, depth: 1)
let threadgroupCount = MTLSize(width: (downscaledTexture.width + threadgroupSize.width - 1) / threadgroupSize.width, height: (downscaledTexture.height + threadgroupSize.height - 1) / threadgroupSize.height, depth: 1)
computeEncoder.setComputePipelineState(blurState.computePipelineStateHorizontal)
computeEncoder.setTexture(downscaledTexture, index: 0)
computeEncoder.setTexture(blurredHorizontalTexture, index: 1)
computeEncoder.dispatchThreadgroups(threadgroupCount, threadsPerThreadgroup: threadgroupSize)
computeEncoder.setComputePipelineState(blurState.computePipelineStateVertical)
computeEncoder.setTexture(blurredHorizontalTexture, index: 0)
computeEncoder.setTexture(blurredVerticalTexture, index: 1)
computeEncoder.dispatchThreadgroups(threadgroupCount, threadsPerThreadgroup: threadgroupSize)
computeEncoder.endEncoding()
}
return blurredVerticalTexture
})
context.renderToLayer(spec: renderSpec, state: RenderState.self, layer: self.blurredLayer, inputs: blurredTexture, commands: { encoder, placement, blurredTexture in
guard let blurredTexture else {
return
}
let effectiveRect = placement.effectiveRect
var rect = SIMD4<Float>(Float(effectiveRect.minX), Float(effectiveRect.minY), Float(effectiveRect.width), Float(effectiveRect.height))
encoder.setVertexBytes(&rect, length: 4 * 4, index: 0)
var mirror = SIMD2<UInt32>(
videoTextures.mirrorDirection.contains(.horizontal) ? 1 : 0,
videoTextures.mirrorDirection.contains(.vertical) ? 1 : 0
)
encoder.setVertexBytes(&mirror, length: 2 * 4, index: 1)
encoder.setFragmentTexture(blurredTexture, index: 0)
var brightness: Float = 0.95
var saturation: Float = 1.3
var overlay: SIMD4<Float> = SIMD4<Float>()
encoder.setFragmentBytes(&brightness, length: 4, index: 0)
encoder.setFragmentBytes(&saturation, length: 4, index: 1)
encoder.setFragmentBytes(&overlay, length: 4 * 4, index: 2)
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6)
})
}
}
@@ -0,0 +1,294 @@
import AVFoundation
import Metal
import CoreVideo
import Display
import SwiftSignalKit
import Camera
import MetalEngine
import MediaEditor
import TelegramCore
final class CameraVideoSource: VideoSource {
private var device: MTLDevice
private var textureCache: CVMetalTextureCache?
private(set) var cameraVideoOutput: CameraVideoOutput!
public private(set) var currentOutput: Output?
private var onUpdatedListeners = Bag<() -> Void>()
public var sourceId: Int = 0
public var sizeMultiplicator: CGPoint = CGPoint(x: 1.0, y: 1.0)
public init() {
self.device = MetalEngine.shared.device
self.cameraVideoOutput = CameraVideoOutput(sink: { [weak self] buffer, mirror in
self?.push(buffer, mirror: mirror)
})
CVMetalTextureCacheCreate(nil, nil, self.device, nil, &self.textureCache)
}
public func addOnUpdated(_ f: @escaping () -> Void) -> Disposable {
let index = self.onUpdatedListeners.add(f)
return ActionDisposable { [weak self] in
DispatchQueue.main.async {
guard let self else {
return
}
self.onUpdatedListeners.remove(index)
}
}
}
private func push(_ sampleBuffer: CMSampleBuffer, mirror: Bool) {
guard let buffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
return
}
let width = CVPixelBufferGetWidth(buffer)
let height = CVPixelBufferGetHeight(buffer)
var cvMetalTextureY: CVMetalTexture?
var status = CVMetalTextureCacheCreateTextureFromImage(nil, self.textureCache!, buffer, nil, .r8Unorm, width, height, 0, &cvMetalTextureY)
guard status == kCVReturnSuccess, let yTexture = CVMetalTextureGetTexture(cvMetalTextureY!) else {
return
}
var cvMetalTextureUV: CVMetalTexture?
status = CVMetalTextureCacheCreateTextureFromImage(nil, self.textureCache!, buffer, nil, .rg8Unorm, width / 2, height / 2, 1, &cvMetalTextureUV)
guard status == kCVReturnSuccess, let uvTexture = CVMetalTextureGetTexture(cvMetalTextureUV!) else {
return
}
var resolution = CGSize(width: CGFloat(yTexture.width), height: CGFloat(yTexture.height))
resolution.width = floor(resolution.width * self.sizeMultiplicator.x)
resolution.height = floor(resolution.height * self.sizeMultiplicator.y)
self.currentOutput = Output(
resolution: resolution,
textureLayout: .biPlanar(Output.BiPlanarTextureLayout(
y: yTexture,
uv: uvTexture
)),
dataBuffer: Output.NativeDataBuffer(pixelBuffer: buffer),
mirrorDirection: mirror ? [.vertical] : [],
sourceId: self.sourceId
)
for onUpdated in self.onUpdatedListeners.copyItems() {
onUpdated()
}
}
}
private let dimensions = CGSize(width: 1080.0, height: 1920.0)
final class LiveStreamMediaSource {
private let queue = Queue()
private let pool: CVPixelBufferPool?
private(set) var mainVideoOutput: CameraVideoOutput!
private(set) var additionalVideoOutput: CameraVideoOutput!
private let composer: MediaEditorComposer
private var additionalSampleBuffer: CMSampleBuffer?
public private(set) var currentVideoOutput: CVPixelBuffer?
private var onVideoUpdatedListeners = Bag<() -> Void>()
private var values: MediaEditorValues
private var cameraPosition: Camera.Position = .back
public init() {
let width: Int32 = 720
let height: Int32 = 1280
let dimensions = CGSize(width: CGFloat(1080), height: CGFloat(1920))
let bufferOptions: [String: Any] = [
kCVPixelBufferPoolMinimumBufferCountKey as String: 3 as NSNumber
]
let pixelBufferOptions: [String: Any] = [
//kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA as NSNumber,
kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange as NSNumber,
kCVPixelBufferWidthKey as String: UInt32(width),
kCVPixelBufferHeightKey as String: UInt32(height)
]
var pool: CVPixelBufferPool?
CVPixelBufferPoolCreate(nil, bufferOptions as CFDictionary, pixelBufferOptions as CFDictionary, &pool)
self.pool = pool
self.values = MediaEditorValues(
peerId: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(0)),
originalDimensions: PixelDimensions(dimensions),
cropOffset: .zero,
cropRect: CGRect(origin: .zero, size: dimensions),
cropScale: 1.0,
cropRotation: 0.0,
cropMirroring: false,
cropOrientation: nil,
gradientColors: nil,
videoTrimRange: nil,
videoIsMuted: false,
videoIsFullHd: false,
videoIsMirrored: false,
videoVolume: nil,
additionalVideoPath: nil,
additionalVideoIsDual: true,
additionalVideoPosition: nil,
additionalVideoScale: 1.625,
additionalVideoRotation: 0.0,
additionalVideoPositionChanges: [],
additionalVideoTrimRange: nil,
additionalVideoOffset: nil,
additionalVideoVolume: nil,
collage: [],
nightTheme: false,
drawing: nil,
maskDrawing: nil,
entities: [],
toolValues: [:],
audioTrack: nil,
audioTrackTrimRange: nil,
audioTrackOffset: nil,
audioTrackVolume: nil,
audioTrackSamples: nil,
collageTrackSamples: nil,
coverImageTimestamp: nil,
coverDimensions: nil,
qualityPreset: nil
)
self.composer = MediaEditorComposer(
postbox: nil,
values: self.values,
dimensions: dimensions,
outputDimensions: CGSize(width: 720.0, height: 1280.0),
textScale: 1.0,
videoDuration: nil,
additionalVideoDuration: nil,
outputsYuvBuffers: true
)
self.mainVideoOutput = CameraVideoOutput(sink: { [weak self] buffer, mirror in
guard let self else {
return
}
self.queue.async {
self.push(mainSampleBuffer: buffer)
}
})
self.additionalVideoOutput = CameraVideoOutput(sink: { [weak self] buffer, mirror in
guard let self else {
return
}
self.queue.async {
self.additionalSampleBuffer = buffer
}
})
}
func setup(isDualCameraEnabled: Bool, dualCameraPosition: CameraScreenImpl.PIPPosition, position: Camera.Position) {
var additionalVideoPositionChanges: [VideoPositionChange] = []
if isDualCameraEnabled && position == .front {
additionalVideoPositionChanges.append(VideoPositionChange(additional: true, timestamp: CACurrentMediaTime()))
}
var values = self.values
values = values.withUpdatedAdditionalVideoPositionChanges(additionalVideoPositionChanges: additionalVideoPositionChanges)
values = values.withUpdatedAdditionalVideo(position: self.getAdditionalVideoPosition(dualCameraPosition), scale: 1.625, rotation: 0.0)
self.values = values
self.cameraPosition = position
self.composer.values = values
}
func markToggleCamera(position: Camera.Position) {
let timestamp = self.additionalSampleBuffer?.presentationTimeStamp.seconds ?? CACurrentMediaTime()
var values = self.values
var additionalVideoPositionChanges = values.additionalVideoPositionChanges
additionalVideoPositionChanges.append(VideoPositionChange(additional: position == .front, timestamp: timestamp))
values = values.withUpdatedAdditionalVideoPositionChanges(additionalVideoPositionChanges: additionalVideoPositionChanges)
self.values = values
self.cameraPosition = position
self.composer.values = self.values
}
func setDualCameraPosition(_ pipPosition: CameraScreenImpl.PIPPosition) {
let timestamp = self.additionalSampleBuffer?.presentationTimeStamp.seconds ?? CACurrentMediaTime()
var values = self.values
var additionalVideoPositionChanges = values.additionalVideoPositionChanges
additionalVideoPositionChanges.append(VideoPositionChange(additional: self.cameraPosition == .front, translationFrom: values.additionalVideoPosition ?? .zero, timestamp: timestamp))
values = values.withUpdatedAdditionalVideoPositionChanges(additionalVideoPositionChanges: additionalVideoPositionChanges)
values = values.withUpdatedAdditionalVideo(position: self.getAdditionalVideoPosition(pipPosition), scale: 1.625, rotation: 0.0)
self.values = values
self.composer.values = values
}
func getAdditionalVideoPosition(_ pipPosition: CameraScreenImpl.PIPPosition) -> CGPoint {
let topOffset = CGPoint(x: 267.0, y: 438.0)
let bottomOffset = CGPoint(x: 267.0, y: 438.0)
let position: CGPoint
switch pipPosition {
case .topLeft:
position = CGPoint(x: topOffset.x, y: topOffset.y)
case .topRight:
position = CGPoint(x: dimensions.width - topOffset.x, y: topOffset.y)
case .bottomLeft:
position = CGPoint(x: bottomOffset.x, y: dimensions.height - bottomOffset.y)
case .bottomRight:
position = CGPoint(x: dimensions.width - bottomOffset.x, y: dimensions.height - bottomOffset.y)
}
return position
}
func addOnVideoUpdated(_ f: @escaping () -> Void) -> Disposable {
let index = self.onVideoUpdatedListeners.add(f)
return ActionDisposable { [weak self] in
DispatchQueue.main.async {
guard let self else {
return
}
self.onVideoUpdatedListeners.remove(index)
}
}
}
private func push(mainSampleBuffer: CMSampleBuffer) {
let timestamp = mainSampleBuffer.presentationTimeStamp
guard let mainPixelBuffer = CMSampleBufferGetImageBuffer(mainSampleBuffer) else {
return
}
let main: MediaEditorComposer.Input = .videoBuffer(VideoPixelBuffer(pixelBuffer: mainPixelBuffer, rotation: .rotate90Degrees, timestamp: timestamp), nil, 1.0, .zero)
var additional: [MediaEditorComposer.Input?] = []
if let additionalPixelBuffer = self.additionalSampleBuffer.flatMap({ CMSampleBufferGetImageBuffer($0) }) {
additional.append(
.videoBuffer(VideoPixelBuffer(pixelBuffer: additionalPixelBuffer, rotation: .rotate90DegreesMirrored, timestamp: timestamp), nil, 1.0, .zero)
)
}
self.composer.process(
main: main,
additional: additional,
timestamp: timestamp,
pool: self.pool,
completion: { [weak self] pixelBuffer in
guard let self else {
return
}
self.queue.async {
self.currentVideoOutput = pixelBuffer
for onUpdated in self.onVideoUpdatedListeners.copyItems() {
onUpdated()
}
}
}
)
}
}
@@ -0,0 +1,329 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import Camera
import CameraButtonComponent
private func generateCollageIcon(grid: Camera.CollageGrid, crossed: Bool) -> UIImage? {
return generateImage(CGSize(width: 36.0, height: 36.0), rotatedContext: { size, context in
let bounds = CGRect(origin: .zero, size: size)
context.clear(bounds)
let lineWidth = 2.0 - UIScreenPixel
context.setLineWidth(lineWidth)
context.setStrokeColor(UIColor.white.cgColor)
let iconBounds = bounds.insetBy(dx: 11.0, dy: 9.0)
let path = UIBezierPath(roundedRect: iconBounds, cornerRadius: 3.0)
context.addPath(path.cgPath)
context.strokePath()
let rowHeight = iconBounds.height / CGFloat(grid.rows.count)
var yOffset: CGFloat = iconBounds.minY + lineWidth / 2.0
for i in 0 ..< grid.rows.count {
let row = grid.rows[i]
var xOffset: CGFloat = iconBounds.minX
let lineCount = max(0, row.columns - 1)
let colWidth = iconBounds.width / CGFloat(max(row.columns, 1))
for _ in 0 ..< lineCount {
xOffset += colWidth
context.move(to: CGPoint(x: xOffset, y: yOffset))
context.addLine(to: CGPoint(x: xOffset, y: yOffset + rowHeight))
context.strokePath()
}
yOffset += rowHeight
if i != grid.rows.count - 1 {
context.move(to: CGPoint(x: iconBounds.minX, y: yOffset - lineWidth / 2.0))
context.addLine(to: CGPoint(x: iconBounds.maxX, y: yOffset - lineWidth / 2.0))
context.strokePath()
}
}
if crossed {
context.setLineCap(.round)
let startPoint = CGPoint(x: iconBounds.minX - 3.0, y: iconBounds.minY - 2.0)
let endPoint = CGPoint(x: iconBounds.maxX + 4.0, y: iconBounds.maxY + 1.0)
context.setBlendMode(.clear)
context.move(to: startPoint.offsetBy(dx: 0.0, dy: lineWidth))
context.addLine(to: endPoint.offsetBy(dx: 0.0, dy: lineWidth))
context.strokePath()
context.setBlendMode(.normal)
context.move(to: startPoint)
context.addLine(to: endPoint)
context.strokePath()
}
})
}
final class CollageIconComponent: Component {
typealias EnvironmentType = Empty
let grid: Camera.CollageGrid
let crossed: Bool
let isSelected: Bool
let tintColor: UIColor
init(
grid: Camera.CollageGrid,
crossed: Bool,
isSelected: Bool,
tintColor: UIColor
) {
self.grid = grid
self.crossed = crossed
self.isSelected = isSelected
self.tintColor = tintColor
}
static func ==(lhs: CollageIconComponent, rhs: CollageIconComponent) -> Bool {
if lhs.grid != rhs.grid {
return false
}
if lhs.crossed != rhs.crossed {
return false
}
if lhs.isSelected != rhs.isSelected {
return false
}
if lhs.tintColor != rhs.tintColor {
return false
}
return true
}
final class View: UIView {
private let iconView = UIImageView()
private var component: CollageIconComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(self.iconView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: CollageIconComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
let previousComponent = self.component
self.component = component
self.state = state
if component.grid != previousComponent?.grid {
let image = generateCollageIcon(grid: component.grid, crossed: component.crossed)
let selectedImage = generateImage(CGSize(width: 36.0, height: 36.0), contextGenerator: { size, context in
context.clear(CGRect(origin: .zero, size: size))
context.setFillColor(UIColor.white.cgColor)
context.fillEllipse(in: CGRect(origin: .zero, size: size))
if let image, let cgImage = image.cgImage {
context.setBlendMode(.clear)
context.clip(to: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - image.size.width) / 2.0), y: floorToScreenPixels((size.height - image.size.height) / 2.0) - 1.0), size: image.size), mask: cgImage)
context.fill(CGRect(origin: .zero, size: size))
}
})?.withRenderingMode(.alwaysTemplate)
self.iconView.image = image
if self.iconView.isHighlighted {
self.iconView.isHighlighted = false
self.iconView.highlightedImage = selectedImage
self.iconView.isHighlighted = true
} else {
self.iconView.highlightedImage = selectedImage
}
}
let size = CGSize(width: 36.0, height: 36.0)
self.iconView.frame = CGRect(origin: .zero, size: size)
self.iconView.isHighlighted = component.isSelected
self.iconView.tintColor = component.tintColor
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
final class CollageIconCarouselComponent: Component {
typealias EnvironmentType = Empty
let grids: [Camera.CollageGrid]
let selected: (Camera.CollageGrid) -> Void
init(
grids: [Camera.CollageGrid],
selected: @escaping (Camera.CollageGrid) -> Void
) {
self.grids = grids
self.selected = selected
}
static func ==(lhs: CollageIconCarouselComponent, rhs: CollageIconCarouselComponent) -> Bool {
if lhs.grids != rhs.grids {
return false
}
return true
}
final class View: UIView {
private let clippingView = UIView()
private let scrollView = UIScrollView()
private var itemViews: [AnyHashable: ComponentView<Empty>] = [:]
private var component: CollageIconCarouselComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
super.init(frame: frame)
self.scrollView.contentInsetAdjustmentBehavior = .never
self.scrollView.showsVerticalScrollIndicator = false
self.scrollView.showsHorizontalScrollIndicator = false
self.addSubview(self.clippingView)
self.clippingView.addSubview(self.scrollView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: CollageIconCarouselComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let inset: CGFloat = 27.0
let spacing: CGFloat = availableSize.width > 290.0 ? 7.0 : 8.0
var contentWidth: CGFloat = inset
let buttonSize = CGSize(width: 40.0, height: 40.0)
var validIds: [AnyHashable] = []
for grid in component.grids {
validIds.append(grid)
let itemView: ComponentView<Empty>
if let current = itemViews[grid] {
itemView = current
} else {
itemView = ComponentView()
self.itemViews[grid] = itemView
}
let itemSize = itemView.update(
transition: .immediate,
component: AnyComponent(CameraButton(
content: AnyComponentWithIdentity(
id: "content",
component: AnyComponent(
CollageIconComponent(
grid: grid,
crossed: false,
isSelected: false,
tintColor: .white
)
)
),
action: { [weak self] in
if let component = self?.component {
component.selected(grid)
}
}
)),
environment: {},
containerSize: buttonSize
)
if let view = itemView.view {
if view.superview == nil {
self.scrollView.addSubview(view)
view.layer.shadowOffset = CGSize(width: 0.0, height: 0.0)
view.layer.shadowRadius = 3.0
view.layer.shadowColor = UIColor.black.cgColor
view.layer.shadowOpacity = 0.25
view.layer.rasterizationScale = UIScreenScale
view.layer.shouldRasterize = true
}
view.frame = CGRect(origin: CGPoint(x: contentWidth, y: 0.0), size: itemSize)
}
contentWidth += itemSize.width + spacing
}
let contentSize = CGSize(width: contentWidth, height: buttonSize.height)
if self.scrollView.contentSize != contentSize {
self.scrollView.contentSize = contentSize
}
self.scrollView.frame = CGRect(origin: .zero, size: availableSize)
self.clippingView.frame = CGRect(origin: .zero, size: availableSize)
if self.clippingView.mask == nil {
if let maskImage = generateGradientImage(size: CGSize(width: 42.0, height: 10.0), colors: [UIColor.clear, UIColor.black, UIColor.black, UIColor.clear], locations: [0.0, 0.2, 0.8, 1.0], direction: .horizontal) {
let maskView = UIImageView(image: maskImage.stretchableImage(withLeftCapWidth: 13, topCapHeight: 0))
self.clippingView.mask = maskView
}
}
self.clippingView.mask?.frame = CGRect(origin: .zero, size: availableSize)
var removeIds: [AnyHashable] = []
for (id, itemView) in self.itemViews {
if !validIds.contains(id) {
removeIds.append(id)
itemView.view?.removeFromSuperview()
}
}
for id in removeIds {
self.itemViews.removeValue(forKey: id)
}
return availableSize
}
func animateIn() {
guard self.frame.width > 0.0 else {
return
}
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
for (_, itemView) in self.itemViews {
itemView.view?.layer.animatePosition(from: CGPoint(x: self.frame.width, y: 0.0), to: .zero, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
}
}
func animateOut(completion: @escaping () -> Void) {
guard self.frame.width > 0.0 else {
return
}
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
completion()
})
for (_, itemView) in self.itemViews {
itemView.view?.layer.animatePosition(from: .zero, to: CGPoint(x: self.frame.width + self.scrollView.contentOffset.x, y: 0.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true)
}
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,90 @@
import Foundation
import UIKit
import Display
import ComponentFlow
final class DualIconComponent: Component {
typealias EnvironmentType = Empty
let isSelected: Bool
let tintColor: UIColor
init(
isSelected: Bool,
tintColor: UIColor
) {
self.isSelected = isSelected
self.tintColor = tintColor
}
static func ==(lhs: DualIconComponent, rhs: DualIconComponent) -> Bool {
if lhs.isSelected != rhs.isSelected {
return false
}
if lhs.tintColor != rhs.tintColor {
return false
}
return true
}
final class View: UIView {
private let iconView = UIImageView()
private var component: DualIconComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
super.init(frame: frame)
let image = generateImage(CGSize(width: 36.0, height: 36.0), rotatedContext: { size, context in
context.clear(CGRect(origin: .zero, size: size))
if let image = UIImage(bundleImageName: "Camera/DualIcon"), let cgImage = image.cgImage {
context.draw(cgImage, in: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - image.size.width) / 2.0), y: floorToScreenPixels((size.height - image.size.height) / 2.0) - 1.0), size: image.size))
}
})?.withRenderingMode(.alwaysTemplate)
let selectedImage = generateImage(CGSize(width: 36.0, height: 36.0), rotatedContext: { size, context in
context.clear(CGRect(origin: .zero, size: size))
context.setFillColor(UIColor.white.cgColor)
context.fillEllipse(in: CGRect(origin: .zero, size: size))
if let image = UIImage(bundleImageName: "Camera/DualIcon"), let cgImage = image.cgImage {
context.setBlendMode(.clear)
context.clip(to: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - image.size.width) / 2.0), y: floorToScreenPixels((size.height - image.size.height) / 2.0) - 1.0), size: image.size), mask: cgImage)
context.fill(CGRect(origin: .zero, size: size))
}
})?.withRenderingMode(.alwaysTemplate)
self.iconView.image = image
self.iconView.highlightedImage = selectedImage
self.addSubview(self.iconView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: DualIconComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let size = CGSize(width: 36.0, height: 36.0)
self.iconView.frame = CGRect(origin: .zero, size: size)
self.iconView.isHighlighted = component.isSelected
self.iconView.tintColor = component.tintColor
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,466 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import RoundedRectWithTailPath
private final class FlashColorComponent: Component {
let tint: CameraState.FlashTint?
let isSelected: Bool
let action: () -> Void
init(
tint: CameraState.FlashTint?,
isSelected: Bool,
action: @escaping () -> Void
) {
self.tint = tint
self.isSelected = isSelected
self.action = action
}
static func == (lhs: FlashColorComponent, rhs: FlashColorComponent) -> Bool {
return lhs.tint == rhs.tint && lhs.isSelected == rhs.isSelected
}
final class View: UIButton {
private var component: FlashColorComponent?
private var contentView: UIView
private let circleLayer: SimpleShapeLayer
private var ringLayer: CALayer?
private var iconLayer: CALayer?
private var currentIsHighlighted: Bool = false {
didSet {
if self.currentIsHighlighted != oldValue {
self.contentView.alpha = self.currentIsHighlighted ? 0.6 : 1.0
}
}
}
override init(frame: CGRect) {
self.contentView = UIView(frame: CGRect(origin: .zero, size: frame.size))
self.contentView.isUserInteractionEnabled = false
self.circleLayer = SimpleShapeLayer()
super.init(frame: frame)
self.addSubview(self.contentView)
self.contentView.layer.addSublayer(self.circleLayer)
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func pressed() {
self.component?.action()
}
override public func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
self.currentIsHighlighted = true
return super.beginTracking(touch, with: event)
}
override public func endTracking(_ touch: UITouch?, with event: UIEvent?) {
self.currentIsHighlighted = false
super.endTracking(touch, with: event)
}
override public func cancelTracking(with event: UIEvent?) {
self.currentIsHighlighted = false
super.cancelTracking(with: event)
}
func update(component: FlashColorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
let contentSize = CGSize(width: 30.0, height: 30.0)
self.contentView.frame = CGRect(origin: .zero, size: contentSize)
let bounds = CGRect(origin: .zero, size: contentSize)
self.layer.allowsGroupOpacity = true
self.contentView.layer.allowsGroupOpacity = true
self.circleLayer.frame = bounds
if self.ringLayer == nil {
let ringLayer = SimpleLayer()
ringLayer.backgroundColor = UIColor.clear.cgColor
ringLayer.cornerRadius = contentSize.width / 2.0
ringLayer.borderWidth = 1.0 + UIScreenPixel
ringLayer.frame = CGRect(origin: .zero, size: contentSize)
self.contentView.layer.insertSublayer(ringLayer, at: 0)
self.ringLayer = ringLayer
}
if component.isSelected {
transition.setShapeLayerPath(layer: self.circleLayer, path: CGPath(ellipseIn: bounds.insetBy(dx: 3.0 - UIScreenPixel, dy: 3.0 - UIScreenPixel), transform: nil))
} else {
transition.setShapeLayerPath(layer: self.circleLayer, path: CGPath(ellipseIn: bounds, transform: nil))
}
if let color = component.tint?.color {
self.circleLayer.fillColor = color.cgColor
self.ringLayer?.borderColor = color.cgColor
} else {
if self.iconLayer == nil {
let iconLayer = SimpleLayer()
iconLayer.contents = UIImage(bundleImageName: "Camera/FlashOffIcon")?.cgImage
iconLayer.contentsGravity = .resizeAspect
iconLayer.frame = bounds.insetBy(dx: -4.0, dy: -4.0)
self.contentView.layer.addSublayer(iconLayer)
self.iconLayer = iconLayer
}
self.circleLayer.fillColor = UIColor(rgb: 0xffffff, alpha: 0.1).cgColor
self.ringLayer?.borderColor = UIColor.clear.cgColor
}
return contentSize
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
final class FlashTintControlComponent: Component {
let position: CGPoint
let tint: CameraState.FlashTint
let size: CGFloat
let update: (CameraState.FlashTint?) -> Void
let updateSize: (CGFloat) -> Void
let dismiss: () -> Void
init(
position: CGPoint,
tint: CameraState.FlashTint,
size: CGFloat,
update: @escaping (CameraState.FlashTint?) -> Void,
updateSize: @escaping (CGFloat) -> Void,
dismiss: @escaping () -> Void
) {
self.position = position
self.tint = tint
self.size = size
self.update = update
self.updateSize = updateSize
self.dismiss = dismiss
}
static func == (lhs: FlashTintControlComponent, rhs: FlashTintControlComponent) -> Bool {
return lhs.position == rhs.position && lhs.tint == rhs.tint && lhs.size == rhs.size
}
final class View: UIButton {
private var component: FlashTintControlComponent?
private let dismissView = UIView()
private let containerView = UIView()
private let effectView: UIVisualEffectView
private let maskLayer = CAShapeLayer()
private let swatches = ComponentView<Empty>()
private let sliderView: SliderView
override init(frame: CGRect) {
self.effectView = UIVisualEffectView(effect: UIBlurEffect(style: .dark))
var sizeUpdateImpl: ((CGFloat) -> Void)?
self.sliderView = SliderView(minValue: 0.0, maxValue: 1.0, value: 1.0, valueChanged: { value , _ in
sizeUpdateImpl?(value)
})
super.init(frame: frame)
self.containerView.layer.anchorPoint = CGPoint(x: 0.8, y: 0.0)
self.addSubview(self.dismissView)
self.addSubview(self.containerView)
self.containerView.addSubview(self.effectView)
self.containerView.addSubview(self.sliderView)
self.dismissView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dismissTapped)))
sizeUpdateImpl = { [weak self] size in
if let self, let component {
component.updateSize(size)
}
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func dismissTapped() {
self.component?.dismiss()
}
func update(component: FlashTintControlComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let isFirstTime = self.component == nil
self.component = component
let size = CGSize(width: 184.0, height: 92.0)
self.sliderView.frame = CGRect(origin: CGPoint(x: 8.0, y: size.height - 38.0), size: CGSize(width: size.width - 16.0, height: 30.0))
if isFirstTime {
self.sliderView.value = component.size
self.maskLayer.path = generateRoundedRectWithTailPath(rectSize: size, cornerRadius: 10.0, tailSize: CGSize(width: 18, height: 7.0), tailRadius: 1.0, tailPosition: 0.8, transformTail: false).cgPath
self.maskLayer.frame = CGRect(origin: .zero, size: CGSize(width: size.width, height: size.height + 7.0))
self.effectView.layer.mask = self.maskLayer
}
let swatchesSize = self.swatches.update(
transition: transition,
component: AnyComponent(
HStack(
[
AnyComponentWithIdentity(
id: "off",
component: AnyComponent(
FlashColorComponent(
tint: nil,
isSelected: false,
action: {
component.update(nil)
component.dismiss()
}
)
)
),
AnyComponentWithIdentity(
id: "white",
component: AnyComponent(
FlashColorComponent(
tint: .white,
isSelected: component.tint == .white,
action: {
component.update(.white)
}
)
)
),
AnyComponentWithIdentity(
id: "yellow",
component: AnyComponent(
FlashColorComponent(
tint: .yellow,
isSelected: component.tint == .yellow,
action: {
component.update(.yellow)
}
)
)
),
AnyComponentWithIdentity(
id: "blue",
component: AnyComponent(
FlashColorComponent(
tint: .blue,
isSelected: component.tint == .blue,
action: {
component.update(.blue)
}
)
)
)
],
spacing: 16.0
)
),
environment: {},
containerSize: availableSize
)
if let view = self.swatches.view {
if view.superview == nil {
self.containerView.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - swatchesSize.width) / 2.0), y: 8.0), size: swatchesSize)
}
self.dismissView.frame = CGRect(origin: .zero, size: availableSize)
self.containerView.bounds = CGRect(origin: .zero, size: size)
self.containerView.center = component.position
self.effectView.frame = CGRect(origin: CGPoint(x: 0.0, y: -7.0), size: CGSize(width: size.width, height: size.height + 7.0))
if isFirstTime {
self.containerView.layer.animateScale(from: 0.0, to: 1.0, duration: 0.35, timingFunction: kCAMediaTimingFunctionSpring)
self.containerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
return availableSize
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if !self.containerView.frame.contains(point) {
return self.dismissView
}
return super.hitTest(point, with: event)
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private final class SliderView: UIView {
private let foregroundView: UIView
private let knobView: UIImageView
let minValue: CGFloat
let maxValue: CGFloat
var value: CGFloat = 1.0 {
didSet {
self.updateValue()
}
}
private let valueChanged: (CGFloat, Bool) -> Void
private let hapticFeedback = HapticFeedback()
init(minValue: CGFloat, maxValue: CGFloat, value: CGFloat, valueChanged: @escaping (CGFloat, Bool) -> Void) {
self.minValue = minValue
self.maxValue = maxValue
self.value = value
self.valueChanged = valueChanged
self.foregroundView = UIView()
self.foregroundView.backgroundColor = UIColor(rgb: 0x8b8b8a)
self.knobView = UIImageView(image: generateFilledCircleImage(diameter: 30.0, color: .white))
super.init(frame: .zero)
self.backgroundColor = UIColor(rgb: 0x3e3e3e)
self.clipsToBounds = true
self.layer.cornerRadius = 15.0
self.foregroundView.isUserInteractionEnabled = false
self.knobView.isUserInteractionEnabled = false
self.addSubview(self.foregroundView)
self.addSubview(self.knobView)
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))
self.addGestureRecognizer(panGestureRecognizer)
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
self.addGestureRecognizer(tapGestureRecognizer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func updateValue(transition: ComponentTransition = .immediate) {
let width = self.frame.width
let range = self.maxValue - self.minValue
let value = (self.value - self.minValue) / range
transition.setFrame(view: self.foregroundView, frame: CGRect(origin: CGPoint(), size: CGSize(width: 15.0 + value * (width - 30.0), height: 30.0)))
transition.setFrame(view: self.knobView, frame: CGRect(origin: CGPoint(x: (width - 30.0) * value, y: 0.0), size: CGSize(width: 30.0, height: 30.0)))
}
@objc private func panGesture(_ gestureRecognizer: UIPanGestureRecognizer) {
let range = self.maxValue - self.minValue
switch gestureRecognizer.state {
case .began:
break
case .changed:
let previousValue = self.value
let translation: CGFloat = gestureRecognizer.translation(in: gestureRecognizer.view).x
let delta = translation / self.bounds.width * range
self.value = max(self.minValue, min(self.maxValue, self.value + delta))
gestureRecognizer.setTranslation(CGPoint(), in: gestureRecognizer.view)
if self.value == 1.0 && previousValue != 1.0 {
self.hapticFeedback.impact(.soft)
} else if self.value == 0.0 && previousValue != 0.0 {
self.hapticFeedback.impact(.soft)
}
if abs(previousValue - self.value) >= 0.001 {
self.valueChanged(self.value, false)
}
case .ended:
let translation: CGFloat = gestureRecognizer.translation(in: gestureRecognizer.view).x
let delta = translation / self.bounds.width * range
self.value = max(self.minValue, min(self.maxValue, self.value + delta))
self.valueChanged(self.value, true)
default:
break
}
}
@objc private func tapGesture(_ gestureRecognizer: UITapGestureRecognizer) {
let range = self.maxValue - self.minValue
let location = gestureRecognizer.location(in: gestureRecognizer.view)
self.value = max(self.minValue, min(self.maxValue, self.minValue + location.x / self.bounds.width * range))
self.valueChanged(self.value, true)
}
func canBeHighlighted() -> Bool {
return false
}
func updateIsHighlighted(isHighlighted: Bool) {
}
func performAction() {
}
}
final class CameraFrontFlashOverlayController: ViewController {
class Node: ASDisplayNode {
init(color: UIColor) {
super.init()
self.backgroundColor = color
}
}
private let color: UIColor
init(color: UIColor) {
self.color = color
super.init(navigationBarPresentationData: nil)
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadDisplayNode() {
self.displayNode = Node(color: self.color)
self.displayNodeDidLoad()
}
func dismissAnimated() {
self.displayNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
self.dismiss()
})
}
}
@@ -0,0 +1,22 @@
import Foundation
import UIKit
final class FocusCrosshairsView: UIView {
private let indicatorView: UIImageView
override init(frame: CGRect) {
self.indicatorView = UIImageView()
super.init(frame: frame)
self.addSubview(self.indicatorView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(pointOfInterest: CGPoint) {
}
}
@@ -0,0 +1,355 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import MultilineTextComponent
import TelegramPresentationData
import GlassBackgroundComponent
extension CameraMode {
func title(strings: PresentationStrings) -> String {
switch self {
case .photo:
return strings.Story_Camera_Photo
case .video:
return strings.Story_Camera_Video
case .live:
return strings.Story_Camera_Live
}
}
}
private let buttonSize = CGSize(width: 55.0, height: 48.0)
private let tabletButtonSize = CGSize(width: 55.0, height: 44.0)
final class ModeComponent: Component {
let isTablet: Bool
let strings: PresentationStrings
let tintColor: UIColor
let availableModes: [CameraMode]
let currentMode: CameraMode
let updatedMode: (CameraMode) -> Void
let tag: AnyObject?
init(
isTablet: Bool,
strings: PresentationStrings,
tintColor: UIColor,
availableModes: [CameraMode],
currentMode: CameraMode,
updatedMode: @escaping (CameraMode) -> Void,
tag: AnyObject?
) {
self.isTablet = isTablet
self.strings = strings
self.tintColor = tintColor
self.availableModes = availableModes
self.currentMode = currentMode
self.updatedMode = updatedMode
self.tag = tag
}
static func ==(lhs: ModeComponent, rhs: ModeComponent) -> Bool {
if lhs.isTablet != rhs.isTablet {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.tintColor != rhs.tintColor {
return false
}
if lhs.availableModes != rhs.availableModes {
return false
}
if lhs.currentMode != rhs.currentMode {
return false
}
return true
}
final class View: UIView, ComponentTaggedView {
private var component: ModeComponent?
final class ItemView: HighlightTrackingButton {
var pressed: () -> Void = {
}
init() {
super.init(frame: .zero)
self.isExclusiveTouch = true
self.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside)
}
required init(coder: NSCoder) {
preconditionFailure()
}
@objc func buttonPressed() {
self.pressed()
}
func update(isTablet: Bool, value: String, selected: Bool, tintColor: UIColor) -> CGSize {
let accentColor: UIColor
let normalColor: UIColor
if tintColor.rgb == 0xffffff {
accentColor = UIColor(rgb: 0xffd300)
normalColor = .white
} else {
accentColor = tintColor
normalColor = tintColor.withAlphaComponent(0.5)
}
let title = NSMutableAttributedString(string: value.uppercased(), font: Font.with(size: 14.0, design: .regular, weight: .medium), textColor: selected ? accentColor : normalColor, paragraphAlignment: .center)
title.addAttribute(.kern, value: -0.5 as NSNumber, range: NSMakeRange(0, title.length))
self.setAttributedTitle(title, for: .normal)
self.sizeToFit()
return CGSize(width: self.titleLabel?.bounds.size.width ?? 0.0, height: isTablet ? tabletButtonSize.height : buttonSize.height)
}
}
private var backgroundView = UIView()
private var glassContainerView = GlassBackgroundContainerView()
private var selectionView = GlassBackgroundView()
private var itemViews: [Int32: ItemView] = [:]
public func matches(tag: Any) -> Bool {
if let component = self.component, let componentTag = component.tag {
let tag = tag as AnyObject
if componentTag === tag {
return true
}
}
return false
}
init() {
super.init(frame: CGRect())
self.backgroundView.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.11)
self.backgroundView.layer.cornerRadius = 24.0
self.layer.allowsGroupOpacity = true
self.addSubview(self.backgroundView)
self.backgroundView.addSubview(self.glassContainerView)
self.glassContainerView.contentView.addSubview(self.selectionView)
}
required init?(coder aDecoder: NSCoder) {
preconditionFailure()
}
private var animatedOut = false
func animateOutToEditor(transition: ComponentTransition) {
self.animatedOut = true
transition.setAlpha(view: self.backgroundView, alpha: 0.0)
transition.setSublayerTransform(view: self, transform: CATransform3DMakeTranslation(0.0, -buttonSize.height, 0.0))
}
func animateInFromEditor(transition: ComponentTransition) {
self.animatedOut = false
transition.setAlpha(view: self.backgroundView, alpha: 1.0)
transition.setSublayerTransform(view: self, transform: CATransform3DIdentity)
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return self.backgroundView.frame.contains(point)
}
func update(component: ModeComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
self.component = component
let isTablet = component.isTablet
let updatedMode = component.updatedMode
self.glassContainerView.isHidden = component.isTablet
self.backgroundView.backgroundColor = component.isTablet ? .clear : UIColor(rgb: 0xffffff, alpha: 0.11)
let inset: CGFloat = 23.0
let spacing: CGFloat = isTablet ? 9.0 : 40.0
var i = 0
var itemFrame = CGRect(origin: isTablet ? .zero : CGPoint(x: inset, y: 0.0), size: buttonSize)
var selectedCenter = itemFrame.minX
var selectedFrame = itemFrame
var validKeys: Set<Int32> = Set()
for mode in component.availableModes.reversed() {
let id = mode.rawValue
validKeys.insert(id)
let itemView: ItemView
if let current = self.itemViews[id] {
itemView = current
} else {
itemView = ItemView()
self.backgroundView.addSubview(itemView)
self.itemViews[id] = itemView
}
itemView.pressed = {
updatedMode(mode)
}
let itemSize = itemView.update(isTablet: component.isTablet, value: mode.title(strings: component.strings), selected: mode == component.currentMode, tintColor: component.tintColor)
itemView.bounds = CGRect(origin: .zero, size: itemSize)
itemFrame = CGRect(origin: itemFrame.origin, size: itemSize)
if mode == component.currentMode {
selectedFrame = itemFrame
}
if isTablet {
itemView.center = CGPoint(x: availableSize.width / 2.0, y: itemFrame.midY)
if mode == component.currentMode {
selectedCenter = itemFrame.midY
}
itemFrame = itemFrame.offsetBy(dx: 0.0, dy: tabletButtonSize.height + spacing)
} else {
itemView.center = CGPoint(x: itemFrame.midX, y: itemFrame.midY)
if mode == component.currentMode {
selectedCenter = itemFrame.midX
}
itemFrame = itemFrame.offsetBy(dx: itemFrame.width + spacing, dy: 0.0)
}
i += 1
}
var removeKeys: [Int32] = []
for (id, itemView) in self.itemViews {
if !validKeys.contains(id) {
removeKeys.append(id)
transition.setAlpha(view: itemView, alpha: 0.0, completion: { _ in
itemView.removeFromSuperview()
})
}
}
for id in removeKeys {
self.itemViews.removeValue(forKey: id)
}
let totalSize: CGSize
let size: CGSize
if isTablet {
totalSize = CGSize(width: availableSize.width, height: tabletButtonSize.height * CGFloat(component.availableModes.count) + spacing * CGFloat(component.availableModes.count - 1))
size = CGSize(width: availableSize.width, height: availableSize.height)
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height / 2.0 - selectedCenter), size: totalSize))
} else {
size = CGSize(width: availableSize.width, height: buttonSize.height)
totalSize = CGSize(width: itemFrame.minX - spacing + inset, height: buttonSize.height)
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - totalSize.width) / 2.0), y: 0.0), size: totalSize))
}
let containerFrame = CGRect(origin: .zero, size: self.backgroundView.frame.size)
transition.setFrame(view: self.glassContainerView, frame: containerFrame)
let selectionFrame = selectedFrame.insetBy(dx: -20.0, dy: 3.0)
self.glassContainerView.update(size: containerFrame.size, isDark: true, transition: .immediate)
self.selectionView.update(size: selectionFrame.size, cornerRadius: selectionFrame.height * 0.5, isDark: true, tintColor: .init(kind: .custom, color: UIColor(rgb: 0xffffff, alpha: 0.16)), transition: transition)
transition.setFrame(view: self.selectionView, frame: selectionFrame)
return size
}
}
func makeView() -> View {
return View()
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
final class HintLabelComponent: Component {
let text: String
let tintColor: UIColor
init(
text: String,
tintColor: UIColor
) {
self.text = text
self.tintColor = tintColor
}
static func ==(lhs: HintLabelComponent, rhs: HintLabelComponent) -> Bool {
if lhs.text != rhs.text {
return false
}
if lhs.tintColor != rhs.tintColor {
return false
}
return true
}
final class View: UIView {
private var component: HintLabelComponent?
private var componentView = ComponentView<Empty>()
init() {
super.init(frame: CGRect())
}
required init?(coder aDecoder: NSCoder) {
preconditionFailure()
}
func update(component: HintLabelComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
let previousComponent = self.component
self.component = component
if let previousText = previousComponent?.text, !previousText.isEmpty && previousText != component.text {
if let componentView = self.componentView.view, let snapshotView = componentView.snapshotView(afterScreenUpdates: false) {
snapshotView.frame = componentView.frame
self.addSubview(snapshotView)
snapshotView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
}
self.componentView.view?.removeFromSuperview()
self.componentView = ComponentView<Empty>()
}
let textSize = self.componentView.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: component.text.uppercased(), font: Font.with(size: 14.0, design: .camera, weight: .semibold), textColor: component.tintColor)),
horizontalAlignment: .center
)
),
environment: {},
containerSize: availableSize
)
if let view = self.componentView.view {
if view.superview == nil {
view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - textSize.width) / 2.0), y: 0.0), size: textSize)
}
return CGSize(width: availableSize.width, height: textSize.height)
}
}
func makeView() -> View {
return View()
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
@@ -0,0 +1,222 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import SwiftSignalKit
import TelegramCore
import AccountContext
import BundleIconComponent
import MultilineTextComponent
import ButtonComponent
import LottieComponent
final class PlaceholderComponent: Component {
typealias EnvironmentType = Empty
enum Mode {
case request
case denied
}
let context: AccountContext
let mode: Mode
let action: () -> Void
init(
context: AccountContext,
mode: Mode,
action: @escaping () -> Void
) {
self.context = context
self.mode = mode
self.action = action
}
static func ==(lhs: PlaceholderComponent, rhs: PlaceholderComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.mode != rhs.mode {
return false
}
return true
}
public final class View: UIView {
private let animation = ComponentView<Empty>()
private let title = ComponentView<Empty>()
private let text = ComponentView<Empty>()
private let button = ComponentView<Empty>()
private var component: PlaceholderComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor(rgb: 0x1c1c1e)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: PlaceholderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let sideInset: CGFloat = 36.0
let animationHeight: CGFloat = 120.0
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let title = presentationData.strings.Story_Camera_AccessPlaceholderTitle
let text = presentationData.strings.Story_Camera_AccessPlaceholderText
let buttonTitle = presentationData.strings.Story_Camera_AccessOpenSettings
let animationSize = self.animation.update(
transition: .immediate,
component: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(name: "Photos")
)),
environment: {},
containerSize: CGSize(width: animationHeight, height: animationHeight)
)
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: title, font: Font.semibold(17.0), textColor: UIColor.white)),
horizontalAlignment: .center,
maximumNumberOfLines: 0
)
),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 3.0, height: availableSize.height)
)
let textSize = self.text.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: text, font: Font.regular(15.0), textColor: UIColor(rgb: 0x98989f))),
horizontalAlignment: .center,
maximumNumberOfLines: 0
)
),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height)
)
let buttonSize = self.button.update(
transition: .immediate,
component: AnyComponent(
ButtonComponent(
background: ButtonComponent.Background(
color: UIColor(rgb: 0x0088ff),
foreground: .white,
pressedColor: UIColor(rgb: 0x0088ff, alpha: 0.55)
),
content: AnyComponentWithIdentity(
id: buttonTitle,
component: AnyComponent(ButtonTextContentComponent(
text: buttonTitle,
badge: 0,
textColor: .white,
badgeBackground: .clear,
badgeForeground: .clear
))
),
isEnabled: true,
displaysProgress: false,
action: { [weak self] in
if let self {
self.component?.action()
}
}
)
),
environment: {},
containerSize: CGSize(width: 240.0, height: 50.0)
)
let titleSpacing: CGFloat = 12.0
let textSpacing: CGFloat = 14.0
let buttonSpacing: CGFloat = 18.0
let totalHeight = animationSize.height + titleSpacing + titleSize.height + textSpacing + textSize.height + buttonSpacing + buttonSize.height
var originY = floorToScreenPixels((availableSize.height - totalHeight) / 2.0)
let animationFrame = CGRect(
origin: CGPoint(
x: floorToScreenPixels((availableSize.width - animationSize.width) / 2.0),
y: originY
),
size: animationSize
)
if let view = self.animation.view as? LottieComponent.View {
if view.superview == nil {
self.addSubview(view)
Queue.mainQueue().justDispatch {
view.playOnce()
}
}
view.frame = animationFrame
}
originY += animationSize.height + titleSpacing
let titleFrame = CGRect(
origin: CGPoint(
x: floorToScreenPixels((availableSize.width - titleSize.width) / 2.0),
y: originY
),
size: titleSize
)
if let view = self.title.view {
if view.superview == nil {
self.addSubview(view)
}
view.frame = titleFrame
}
originY += titleSize.height + textSpacing
let textFrame = CGRect(
origin: CGPoint(
x: floorToScreenPixels((availableSize.width - textSize.width) / 2.0),
y: originY
),
size: textSize
)
if let view = self.text.view {
if view.superview == nil {
self.addSubview(view)
}
view.frame = textFrame
}
originY += textSize.height + buttonSpacing
let buttonFrame = CGRect(
origin: CGPoint(
x: floorToScreenPixels((availableSize.width - buttonSize.width) / 2.0),
y: originY
),
size: buttonSize
)
if let view = self.button.view {
if view.superview == nil {
self.addSubview(view)
}
view.frame = buttonFrame
}
return availableSize
}
}
func makeView() -> View {
return View()
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,379 @@
import Foundation
import Metal
import MetalKit
import ComponentFlow
import Display
import MetalImageView
import AnimatableProperty
private class ShutterBlobLayer: MetalImageLayer {
override public init() {
super.init()
self.renderer.imageUpdated = { [weak self] image in
self?.contents = image
}
}
override public init(layer: Any) {
super.init()
if let layer = layer as? ShutterBlobLayer {
self.contents = layer.contents
}
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
final class ShutterBlobView: UIView {
enum BlobState {
case generic
case video
case transientToLock
case lock
case transientToFlip
case stopVideo
case live
var primarySize: CGSize {
switch self {
case .generic, .video, .transientToFlip:
return CGSize(width: 0.63, height: 0.63)
case .live:
return CGSize(width: 3.4, height: 0.55)
case .transientToLock, .lock, .stopVideo:
return CGSize(width: 0.275, height: 0.275)
}
}
func primaryColor(tintColor: UIColor) -> CGRect {
var color: UIColor
switch self {
case .generic:
if tintColor.rgb == 0x000000 {
color = UIColor(rgb: 0x000000)
} else {
color = UIColor(rgb: 0xffffff)
}
case .live:
color = UIColor(rgb: 0xfa325a)
default:
color = UIColor(rgb: 0xff0b18)
}
var r: CGFloat = 0.0
var g: CGFloat = 0.0
var b: CGFloat = 0.0
if color.getRed(&r, green: &g, blue: &b, alpha: nil) {
return CGRect(x: r, y: g, width: b, height: 1.0)
}
return CGRect(x: 0, y: 0, width: 0, height: 1.0)
}
var primaryCornerRadius: CGFloat {
switch self {
case .generic, .video, .transientToFlip:
return 0.63
case .live:
return 0.55
case .transientToLock, .lock, .stopVideo:
return 0.185
}
}
var secondarySize: CGFloat {
switch self {
case .generic, .video, .transientToFlip, .transientToLock:
return 0.335
case .lock:
return 0.5
case .stopVideo, .live:
return 0.0
}
}
var secondaryRedness: CGFloat {
switch self {
case .generic, .lock, .transientToLock, .transientToFlip, .live:
return 0.0
default:
return 1.0
}
}
}
private let commandQueue: MTLCommandQueue
private let drawPassthroughPipelineState: MTLRenderPipelineState
private var displayLink: SharedDisplayLinkDriver.Link?
private var primaryWidth = AnimatableProperty<CGFloat>(value: 0.63)
private var primaryHeight = AnimatableProperty<CGFloat>(value: 0.63)
private var primaryOffsetX = AnimatableProperty<CGFloat>(value: 0.0)
private var primaryOffsetY = AnimatableProperty<CGFloat>(value: 0.0)
private var primaryColor = AnimatableProperty<CGRect>(value: CGRect(x: 1.0, y: 1.0, width: 1.0, height: 1.0))
private var primaryCornerRadius = AnimatableProperty<CGFloat>(value: 0.63)
private var secondarySize = AnimatableProperty<CGFloat>(value: 0.34)
private var secondaryOffsetX = AnimatableProperty<CGFloat>(value: 0.0)
private var secondaryOffsetY = AnimatableProperty<CGFloat>(value: 0.0)
private var secondaryRedness = AnimatableProperty<CGFloat>(value: 0.0)
private(set) var state: BlobState = .generic
static override var layerClass: AnyClass {
return ShutterBlobLayer.self
}
public init?(test: Bool) {
guard let device = MTLCreateSystemDefaultDevice() else {
return nil
}
guard let library = metalLibrary(device: device) else {
return nil
}
guard let commandQueue = device.makeCommandQueue() else {
return nil
}
self.commandQueue = commandQueue
guard let loadedVertexProgram = library.makeFunction(name: "cameraBlobVertex") else {
return nil
}
guard let loadedFragmentProgram = library.makeFunction(name: "cameraBlobFragment") else {
return nil
}
let pipelineStateDescriptor = MTLRenderPipelineDescriptor()
pipelineStateDescriptor.vertexFunction = loadedVertexProgram
pipelineStateDescriptor.fragmentFunction = loadedFragmentProgram
pipelineStateDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
pipelineStateDescriptor.colorAttachments[0].isBlendingEnabled = true
pipelineStateDescriptor.colorAttachments[0].sourceRGBBlendFactor = .one
pipelineStateDescriptor.colorAttachments[0].destinationRGBBlendFactor = .oneMinusSourceAlpha
pipelineStateDescriptor.colorAttachments[0].rgbBlendOperation = .add
pipelineStateDescriptor.colorAttachments[0].sourceAlphaBlendFactor = .one
pipelineStateDescriptor.colorAttachments[0].destinationAlphaBlendFactor = .oneMinusSourceAlpha
pipelineStateDescriptor.colorAttachments[0].alphaBlendOperation = .add
// pipelineStateDescriptor.colorAttachments[0].isBlendingEnabled = true
// pipelineStateDescriptor.colorAttachments[0].rgbBlendOperation = .add
// pipelineStateDescriptor.colorAttachments[0].alphaBlendOperation = .add
// pipelineStateDescriptor.colorAttachments[0].sourceRGBBlendFactor = .sourceAlpha
// pipelineStateDescriptor.colorAttachments[0].sourceAlphaBlendFactor = .sourceAlpha
// pipelineStateDescriptor.colorAttachments[0].destinationRGBBlendFactor = .oneMinusSourceAlpha
// pipelineStateDescriptor.colorAttachments[0].destinationAlphaBlendFactor = .oneMinusSourceAlpha
self.drawPassthroughPipelineState = try! device.makeRenderPipelineState(descriptor: pipelineStateDescriptor)
super.init(frame: CGRect())
(self.layer as! ShutterBlobLayer).renderer.device = device
self.isOpaque = false
self.backgroundColor = .clear
self.displayLink = SharedDisplayLinkDriver.shared.add { [weak self] _ in
self?.tick()
}
self.displayLink?.isPaused = true
}
required public init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.displayLink?.invalidate()
}
func updateState(_ state: BlobState, tintColor: UIColor, transition: ComponentTransition = .immediate) {
guard self.state != state else {
return
}
self.state = state
self.primaryWidth.update(value: state.primarySize.width, transition: transition)
self.primaryHeight.update(value: state.primarySize.height, transition: transition)
self.primaryColor.update(value: state.primaryColor(tintColor: tintColor), transition: transition)
self.primaryCornerRadius.update(value: state.primaryCornerRadius, transition: transition)
self.secondarySize.update(value: state.secondarySize, transition: transition)
self.secondaryRedness.update(value: state.secondaryRedness, transition: transition)
self.tick()
}
func updatePrimaryOffsetX(_ offset: CGFloat, transition: ComponentTransition = .immediate) {
guard self.frame.height > 0.0 else {
return
}
let mappedOffset = offset / self.frame.height * 2.0
self.primaryOffsetX.update(value: mappedOffset, transition: transition)
self.tick()
}
func updatePrimaryOffsetY(_ offset: CGFloat, transition: ComponentTransition = .immediate) {
guard self.frame.height > 0.0 else {
return
}
let mappedOffset = offset / self.frame.width * 2.0
self.primaryOffsetY.update(value: mappedOffset, transition: transition)
self.tick()
}
func updateSecondaryOffsetX(_ offset: CGFloat, transition: ComponentTransition = .immediate) {
guard self.frame.height > 0.0 else {
return
}
let mappedOffset = offset / self.frame.height * 2.0
self.secondaryOffsetX.update(value: mappedOffset, transition: transition)
self.tick()
}
func updateSecondaryOffsetY(_ offset: CGFloat, transition: ComponentTransition = .immediate) {
guard self.frame.height > 0.0 else {
return
}
let mappedOffset = offset / self.frame.width * 2.0
self.secondaryOffsetY.update(value: mappedOffset, transition: transition)
self.tick()
}
private func updateAnimations() {
let properties = [
self.primaryWidth,
self.primaryHeight,
self.primaryOffsetX,
self.primaryOffsetY,
self.primaryCornerRadius,
self.secondarySize,
self.secondaryOffsetX,
self.secondaryOffsetY,
self.secondaryRedness
]
let timestamp = CACurrentMediaTime()
var hasAnimations = false
for property in properties {
if property.tick(timestamp: timestamp) {
hasAnimations = true
}
}
if self.primaryColor.tick(timestamp: timestamp) {
hasAnimations = true
}
self.displayLink?.isPaused = !hasAnimations
}
private func tick() {
self.updateAnimations()
self.draw()
}
override func layoutSubviews() {
super.layoutSubviews()
self.tick()
}
private func getNextDrawable(layer: MetalImageLayer, drawableSize: CGSize) -> MetalImageLayer.Drawable? {
layer.renderer.drawableSize = drawableSize
return layer.renderer.nextDrawable()
}
func draw() {
guard let layer = self.layer as? MetalImageLayer else {
return
}
self.updateAnimations()
let drawableSize = CGSize(width: self.bounds.width * UIScreen.main.scale, height: self.bounds.height * UIScreen.main.scale)
guard let drawable = self.getNextDrawable(layer: layer, drawableSize: drawableSize) else {
return
}
let renderPassDescriptor = MTLRenderPassDescriptor()
renderPassDescriptor.colorAttachments[0].texture = drawable.texture
renderPassDescriptor.colorAttachments[0].loadAction = .clear
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 0)
guard let commandBuffer = self.commandQueue.makeCommandBuffer() else {
return
}
guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else {
return
}
renderEncoder.setViewport(MTLViewport(originX: 0.0, originY: 0.0, width: drawableSize.width, height: drawableSize.height, znear: -1.0, zfar: 1.0))
renderEncoder.setRenderPipelineState(self.drawPassthroughPipelineState)
var vertices: [Float] = [
1, -1,
-1, -1,
-1, 1,
1, -1,
-1, 1,
1, 1
]
renderEncoder.setVertexBytes(&vertices, length: 4 * vertices.count, index: 0)
var resolution = simd_uint2(UInt32(drawableSize.width), UInt32(drawableSize.height))
renderEncoder.setFragmentBytes(&resolution, length: MemoryLayout<simd_uint2>.size * 2, index: 0)
var primaryParameters = simd_float4(
Float(self.primaryWidth.presentationValue),
Float(self.primaryHeight.presentationValue),
Float(0.0),
Float(self.primaryCornerRadius.presentationValue)
)
renderEncoder.setFragmentBytes(&primaryParameters, length: MemoryLayout<simd_float3>.size, index: 1)
var primaryOffset = simd_float2(
Float(self.primaryOffsetX.presentationValue),
Float(self.primaryOffsetY.presentationValue)
)
renderEncoder.setFragmentBytes(&primaryOffset, length: MemoryLayout<simd_float2>.size, index: 2)
var primaryColor = simd_float3(Float(self.primaryColor.presentationValue.minX), Float(self.primaryColor.presentationValue.minY), Float(self.primaryColor.presentationValue.width))
renderEncoder.setFragmentBytes(&primaryColor, length: MemoryLayout<simd_float3>.stride, index: 3)
var secondaryParameters = simd_float2(
Float(self.secondarySize.presentationValue),
Float(self.secondaryRedness.presentationValue)
)
renderEncoder.setFragmentBytes(&secondaryParameters, length: MemoryLayout<simd_float4>.size, index: 4)
var secondaryOffset = simd_float2(
Float(self.secondaryOffsetX.presentationValue),
Float(self.secondaryOffsetY.presentationValue)
)
renderEncoder.setFragmentBytes(&secondaryOffset, length: MemoryLayout<simd_float2>.size, index: 5)
renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6, instanceCount: 1)
renderEncoder.endEncoding()
var storedDrawable: MetalImageLayer.Drawable? = drawable
commandBuffer.addCompletedHandler { _ in
DispatchQueue.main.async {
autoreleasepool {
storedDrawable?.present(completion: {})
storedDrawable = nil
}
}
}
commandBuffer.commit()
}
}
@@ -0,0 +1,172 @@
import Foundation
import UIKit
import Display
import ComponentFlow
final class ZoomComponent: Component {
let availableValues: [Float]
let value: Float
let tag: AnyObject?
init(
availableValues: [Float],
value: Float,
tag: AnyObject?
) {
self.availableValues = availableValues
self.value = value
self.tag = tag
}
static func ==(lhs: ZoomComponent, rhs: ZoomComponent) -> Bool {
if lhs.availableValues != rhs.availableValues {
return false
}
if lhs.value != rhs.value {
return false
}
return true
}
final class View: UIView, UIGestureRecognizerDelegate, ComponentTaggedView {
final class ItemView: HighlightTrackingButton {
init() {
super.init(frame: .zero)
self.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.3)
if #available(iOS 13.0, *) {
self.layer.cornerCurve = .circular
}
self.layer.cornerRadius = 18.5
}
required init(coder: NSCoder) {
preconditionFailure()
}
func update(value: String, selected: Bool) {
self.setAttributedTitle(NSAttributedString(string: value, font: Font.with(size: 13.0, design: .round, weight: .semibold), textColor: selected ? UIColor(rgb: 0xffd300) : .white, paragraphAlignment: .center), for: .normal)
}
}
private let backgroundView: BlurredBackgroundView
private var itemViews: [ItemView] = []
private var component: ZoomComponent?
public func matches(tag: Any) -> Bool {
if let component = self.component, let componentTag = component.tag {
let tag = tag as AnyObject
if componentTag === tag {
return true
}
}
return false
}
init() {
self.backgroundView = BlurredBackgroundView(color: UIColor(rgb: 0x222222, alpha: 0.3))
self.backgroundView.clipsToBounds = true
self.backgroundView.layer.cornerRadius = 43.0 / 2.0
super.init(frame: CGRect())
self.layer.allowsGroupOpacity = true
self.addSubview(self.backgroundView)
let pressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handlePress(_:)))
pressGestureRecognizer.minimumPressDuration = 0.01
pressGestureRecognizer.delegate = self
self.addGestureRecognizer(pressGestureRecognizer)
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))
panGestureRecognizer.delegate = self
self.addGestureRecognizer(panGestureRecognizer)
}
required init?(coder aDecoder: NSCoder) {
preconditionFailure()
}
@objc func handlePress(_ gestureRecognizer: UILongPressGestureRecognizer) {
}
@objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
}
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
func animateIn() {
self.backgroundView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
}
func animateOut() {
self.backgroundView.alpha = 0.0
self.backgroundView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
}
func update(component: ZoomComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
self.component = component
let sideInset: CGFloat = 3.0
let spacing: CGFloat = 3.0
let buttonSize = CGSize(width: 37.0, height: 37.0)
let size: CGSize = CGSize(width: buttonSize.width * CGFloat(component.availableValues.count) + spacing * CGFloat(component.availableValues.count - 1) + sideInset * 2.0, height: 43.0)
var i = 0
var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: 3.0), size: buttonSize)
for value in component.availableValues {
let itemView: ItemView
if self.itemViews.count == i {
itemView = ItemView()
self.addSubview(itemView)
self.itemViews.append(itemView)
} else {
itemView = self.itemViews[i]
}
let text: String
if value > 0.5 {
if value == 1.0 {
text = "1×"
} else {
text = "\(Int(value))"
}
} else {
text = String(format: "%0.1f", value)
}
itemView.update(value: text, selected: value == 1.0)
itemView.bounds = CGRect(origin: .zero, size: itemFrame.size)
itemView.center = CGPoint(x: itemFrame.midX, y: itemFrame.midY)
if value == 1.0 {
itemView.transform = CGAffineTransformIdentity
} else {
itemView.transform = CGAffineTransform(scaleX: 0.7, y: 0.7)
}
i += 1
itemFrame = itemFrame.offsetBy(dx: buttonSize.width + spacing, dy: 0.0)
}
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: .zero, size: size))
self.backgroundView.update(size: size, transition: transition.containedViewLayoutTransition)
return size
}
}
func makeView() -> View {
return View()
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}