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,77 @@
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 = "StoryPeerListMetalResources",
srcs = glob([
"MetalResources/**/*.*",
]),
visibility = ["//visibility:public"],
)
plist_fragment(
name = "StoryPeerListBundleInfoPlist",
extension = "plist",
template =
"""
<key>CFBundleIdentifier</key>
<string>org.telegram.StoryPeerList</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleName</key>
<string>StoryPeerList</string>
"""
)
apple_resource_bundle(
name = "StoryPeerListBundle",
infoplists = [
":StoryPeerListBundleInfoPlist",
],
resources = [
":StoryPeerListMetalResources",
],
)
swift_library(
name = "StoryPeerListComponent",
module_name = "StoryPeerListComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
data = [
":StoryPeerListBundle",
],
deps = [
"//submodules/Display",
"//submodules/MetalEngine",
"//submodules/ComponentFlow",
"//submodules/AppBundle",
"//submodules/Components/BundleIconComponent",
"//submodules/AccountContext",
"//submodules/TelegramCore",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramPresentationData",
"//submodules/AvatarNode",
"//submodules/ContextUI",
"//submodules/TelegramUI/Components/Stories/StoryContainerScreen",
"//submodules/Components/MultilineTextComponent",
"//submodules/TelegramUI/Components/EmojiStatusComponent",
"//submodules/Components/HierarchyTrackingLayer",
"//submodules/TelegramUI/Components/ChatListTitleView",
"//submodules/TelegramUI/Components/Utils/AnimatableProperty",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,109 @@
#include <metal_stdlib>
using namespace metal;
struct QuadVertexOut {
float4 position [[position]];
float2 uv;
};
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)
};
vertex QuadVertexOut cameraBlobVertex(
constant float4 &rect [[ buffer(0) ]],
uint vid [[ vertex_id ]]
) {
float2 quadVertex = quadVertices[vid];
QuadVertexOut out;
out.position = float4(rect.x + quadVertex.x * rect.z, rect.y + quadVertex.y * rect.w, 0.0, 1.0);
out.position.x = -1.0 + out.position.x * 2.0;
out.position.y = -1.0 + out.position.y * 2.0;
out.uv = quadVertex;
return out;
}
#define BindingDistance 0.45
#define AARadius 2.0
#define PersistenceFactor 1.1
float smin(float a, float b, float k) {
float h = clamp(0.5 + 0.5 * (a - b) / k, 0.0, 1.0);
h = pow(h, 0.6);
return mix(a, b, h) - k * h * (1.0 - h) * 1.0;
}
float sdfCircle(float2 uv, float2 position, float radius) {
return length(uv - position) - radius;
}
float dynamicBinding(float2 primaryPos, float2 secondaryPos) {
float distance = length(primaryPos - secondaryPos);
return BindingDistance * (1.0 + PersistenceFactor * smoothstep(0.0, 2.0, distance));
}
float map(float2 uv, float2 primaryParameters, float2 primaryOffset, float2 secondaryParameters, float2 secondaryOffset) {
float primary = sdfCircle(uv, primaryOffset, primaryParameters.x);
float secondary = sdfCircle(uv, secondaryOffset, secondaryParameters.x);
float bindDist = dynamicBinding(primaryOffset, secondaryOffset);
float metaballs = 1.0;
metaballs = smin(metaballs, primary, bindDist);
metaballs = smin(metaballs, secondary, bindDist);
return metaballs;
}
fragment half4 cameraBlobFragment(
QuadVertexOut in [[stage_in]],
constant float2 &primaryParameters [[buffer(0)]],
constant float2 &primaryOffset [[buffer(1)]],
constant float2 &secondaryParameters [[buffer(2)]],
constant float2 &secondaryOffset [[buffer(3)]],
constant float2 &resolution [[buffer(4)]]
) {
float aspectRatio = resolution.x / resolution.y;
float2 uv = in.uv * 2.0 - 1.0;
uv.x *= aspectRatio;
float t = AARadius / resolution.y;
float c = smoothstep(t, -t, map(uv, primaryParameters, primaryOffset, secondaryParameters, secondaryOffset));
if (primaryParameters.y > 0) {
float innerHoleRadius = primaryParameters.y;
float hole = smoothstep(-t, t, length(uv - primaryOffset) - innerHoleRadius);
float primaryInfluence = smoothstep(t, -t, sdfCircle(uv, primaryOffset, primaryParameters.x * 1.2));
c *= mix(1.0, hole, primaryInfluence);
} else if (primaryParameters.y < 0) {
float cutRadius = abs(primaryParameters.y);
float2 primaryFeatheredOffset = primaryOffset;
primaryFeatheredOffset.x *= 1.131;
float distFromCenter = length(uv - primaryFeatheredOffset);
float gradientWidth = 0.21;
float featheredEdge = smoothstep(cutRadius - gradientWidth, cutRadius + gradientWidth, distFromCenter);
float primaryInfluence = smoothstep(t, -t, sdfCircle(uv, primaryOffset, primaryParameters.x * 1.2));
float horizontalFade = 1.0;
float rightEdgePosition = 0.94 * aspectRatio;
if (uv.x > rightEdgePosition) {
horizontalFade = 1.0 - smoothstep(0.0, 0.22, uv.x - rightEdgePosition);
}
c *= mix(1.0, featheredEdge, primaryInfluence) * horizontalFade;
}
return half4(0.0, 0.0, 0.0, c);
}
@@ -0,0 +1,426 @@
import Foundation
import Display
import Metal
import MetalKit
import MetalEngine
import ComponentFlow
import TelegramPresentationData
import AnimatableProperty
private var metalLibraryValue: MTLLibrary?
func metalLibrary(device: MTLDevice) -> MTLLibrary? {
if let metalLibraryValue {
return metalLibraryValue
}
let mainBundle = Bundle(for: StoryBlobLayer.self)
guard let path = mainBundle.path(forResource: "StoryPeerListBundle", 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
}
final class StoryBlobLayer: MetalEngineSubjectLayer, MetalEngineSubject {
var internalData: MetalEngineSubjectInternalData?
private 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: "cameraBlobVertex"),
let fragmentFunction = library.makeFunction(name: "cameraBlobFragment") else {
return nil
}
let pipelineDescriptor = MTLRenderPipelineDescriptor()
pipelineDescriptor.vertexFunction = vertexFunction
pipelineDescriptor.fragmentFunction = fragmentFunction
pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
pipelineDescriptor.colorAttachments[0].isBlendingEnabled = true
pipelineDescriptor.colorAttachments[0].rgbBlendOperation = .add
pipelineDescriptor.colorAttachments[0].alphaBlendOperation = .add
pipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = .sourceAlpha
pipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = .sourceAlpha
pipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = .oneMinusSourceAlpha
pipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = .oneMinusSourceAlpha
guard let pipelineState = try? device.makeRenderPipelineState(descriptor: pipelineDescriptor) else {
return nil
}
self.pipelineState = pipelineState
}
}
private var primarySize = AnimatableProperty<CGFloat>(value: 1.0)
private var primaryHoleSize = AnimatableProperty<CGFloat>(value: 0.0)
private var primaryOffsetX = AnimatableProperty<CGFloat>(value: 45.0)
private var primaryOffsetY = AnimatableProperty<CGFloat>(value: 0.0)
private var secondarySize = AnimatableProperty<CGFloat>(value: 0.85)
private var secondaryOffsetX = AnimatableProperty<CGFloat>(value: 45.0)
private var secondaryOffsetY = AnimatableProperty<CGFloat>(value: 0.0)
private var displayLinkSubscription: SharedDisplayLinkDriver.Link?
private var hasActiveAnimations: Bool = false
override init() {
super.init()
self.isOpaque = false
self.didEnterHierarchy = { [weak self] in
guard let self else {
return
}
self.displayLinkSubscription = SharedDisplayLinkDriver.shared.add { [weak self] _ in
guard let self else {
return
}
self.updateAnimations()
if self.hasActiveAnimations {
self.setNeedsUpdate()
}
}
}
self.didExitHierarchy = { [weak self] in
guard let self else {
return
}
self.displayLinkSubscription = nil
}
}
override init(layer: Any) {
super.init(layer: layer)
if let layer = layer as? StoryBlobLayer {
self.primarySize = layer.primarySize
self.primaryHoleSize = layer.primaryHoleSize
self.primaryOffsetX = layer.primaryOffsetX
self.primaryOffsetY = layer.primaryOffsetY
self.secondarySize = layer.secondarySize
self.secondaryOffsetX = layer.secondaryOffsetX
self.secondaryOffsetY = layer.secondaryOffsetY
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func updatePrimarySize(_ size: CGFloat, transition: ComponentTransition = .immediate) {
guard self.bounds.height > 0.0 else {
return
}
let mappedSize = size / self.bounds.height
self.primarySize.update(value: mappedSize, transition: transition)
self.setNeedsUpdate()
}
func updatePrimaryHoleSize(_ size: CGFloat, transition: ComponentTransition = .immediate) {
guard self.bounds.height > 0.0 else {
return
}
let mappedSize = size / self.bounds.height
self.primaryHoleSize.update(value: mappedSize, transition: transition)
self.setNeedsUpdate()
}
func updatePrimaryOffsetX(_ offset: CGFloat, transition: ComponentTransition = .immediate) {
guard self.bounds.height > 0.0 else {
return
}
let mappedOffset = offset / self.bounds.height * 2.0
self.primaryOffsetX.update(value: mappedOffset, transition: transition)
self.setNeedsUpdate()
}
func updatePrimaryOffsetY(_ offset: CGFloat, transition: ComponentTransition = .immediate) {
guard self.bounds.width > 0.0 else {
return
}
let mappedOffset = offset / self.bounds.width * 2.0
self.primaryOffsetY.update(value: mappedOffset, transition: transition)
self.setNeedsUpdate()
}
func updateSecondarySize(_ size: CGFloat, transition: ComponentTransition = .immediate) {
guard self.bounds.height > 0.0 else {
return
}
let mappedSize = size / self.bounds.height
self.secondarySize.update(value: mappedSize, transition: transition)
self.setNeedsUpdate()
}
func updateSecondaryOffsetX(_ offset: CGFloat, transition: ComponentTransition = .immediate) {
guard self.bounds.height > 0.0 else {
return
}
let mappedOffset = offset / self.bounds.height * 2.0
self.secondaryOffsetX.update(value: mappedOffset, transition: transition)
self.setNeedsUpdate()
}
func updateSecondaryOffsetY(_ offset: CGFloat, transition: ComponentTransition = .immediate) {
guard self.bounds.width > 0.0 else {
return
}
let mappedOffset = offset / self.bounds.width * 2.0
self.secondaryOffsetY.update(value: mappedOffset, transition: transition)
self.setNeedsUpdate()
}
private func updateAnimations() {
let properties = [
self.primarySize,
self.primaryHoleSize,
self.primaryOffsetX,
self.primaryOffsetY,
self.secondarySize,
self.secondaryOffsetX,
self.secondaryOffsetY,
]
let timestamp = CACurrentMediaTime()
var hasAnimations = false
for property in properties {
if property.tick(timestamp: timestamp) {
hasAnimations = true
}
}
self.hasActiveAnimations = hasAnimations
}
func update(context: MetalEngineSubjectContext) {
if self.bounds.isEmpty {
return
}
let drawableSize = CGSize(width: self.bounds.width * UIScreen.main.scale, height: self.bounds.height * UIScreen.main.scale)
context.renderToLayer(spec: RenderLayerSpec(size: RenderSize(width: Int(drawableSize.width), height: Int(drawableSize.height))), state: RenderState.self, layer: self, commands: { encoder, placement in
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 primaryParameters = simd_float2(
Float(self.primarySize.presentationValue),
Float(self.primaryHoleSize.presentationValue)
)
encoder.setFragmentBytes(&primaryParameters, length: MemoryLayout<simd_float2>.size, index: 0)
var primaryOffset = simd_float2(
Float(self.primaryOffsetX.presentationValue),
Float(self.primaryOffsetY.presentationValue)
)
encoder.setFragmentBytes(&primaryOffset, length: MemoryLayout<simd_float2>.size, index: 1)
var secondaryParameters = simd_float2(
Float(self.secondarySize.presentationValue),
Float(0)
)
encoder.setFragmentBytes(&secondaryParameters, length: MemoryLayout<simd_float2>.size, index: 2)
var secondaryOffset = simd_float2(
Float(self.secondaryOffsetX.presentationValue),
Float(self.secondaryOffsetY.presentationValue)
)
encoder.setFragmentBytes(&secondaryOffset, length: MemoryLayout<simd_float2>.size, index: 3)
var resolution = simd_float2(
Float(drawableSize.width),
Float(drawableSize.height)
)
encoder.setFragmentBytes(&resolution, length: MemoryLayout<simd_float2>.size, index: 4)
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6)
})
}
}
final class StoryComposeLayer: CALayer {
private let theme: PresentationTheme?
private let strings: PresentationStrings?
private let blobLayer = SimpleLayer()
private let maskLayer = StoryBlobLayer()
private let backgroundLayer = SimpleGradientLayer()
private let foregroundLayer = SimpleGradientLayer()
private let iconLayer = SimpleLayer()
private let labelLayer = SimpleLayer()
init(theme: PresentationTheme, strings: PresentationStrings) {
self.theme = theme
self.strings = strings
super.init()
self.addSublayer(self.blobLayer)
self.blobLayer.mask = self.maskLayer
self.blobLayer.masksToBounds = true
self.backgroundLayer.type = .axial
self.backgroundLayer.startPoint = CGPoint(x: 0.5, y: 0.0)
self.backgroundLayer.endPoint = CGPoint(x: 0.5, y: 1.0)
self.blobLayer.addSublayer(self.backgroundLayer)
self.foregroundLayer.type = .axial
self.foregroundLayer.startPoint = CGPoint(x: 0.5, y: 0.5)
self.foregroundLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
self.blobLayer.addSublayer(self.foregroundLayer)
self.iconLayer.contents = generateAddIcon(color: theme.list.itemCheckColors.foregroundColor)?.cgImage
self.iconLayer.opacity = 0.0
self.blobLayer.addSublayer(self.iconLayer)
if let image = generateAddLabel(strings: strings, color: theme.list.itemPrimaryTextColor) {
self.labelLayer.contents = image.cgImage
self.labelLayer.bounds = CGRect(origin: .zero, size: image.size)
self.addSublayer(self.labelLayer)
self.labelLayer.opacity = 0.0
}
if let blurEffect = CALayer.blur() {
self.iconLayer.filters = [blurEffect]
blurEffect.setValue(8.0 as NSNumber, forKey: "inputRadius")
self.labelLayer.filters = [blurEffect]
blurEffect.setValue(8.0 as NSNumber, forKey: "inputRadius")
}
}
override init(layer: Any) {
if let layer = layer as? StoryComposeLayer {
self.theme = layer.theme
self.strings = layer.strings
} else {
self.theme = nil
self.strings = nil
}
super.init(layer: layer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func updateOffset(_ offset: CGFloat, baseSize: CGFloat, colors: [CGColor], transition: ComponentTransition = .immediate) {
self.backgroundLayer.colors = colors
self.foregroundLayer.locations = [0.0, 0.68, 1.0]
if let theme = self.theme {
let fillColor = theme.list.itemCheckColors.fillColor
self.foregroundLayer.colors = [
fillColor.cgColor,
fillColor.withAlphaComponent(0.0).cgColor,
fillColor.withAlphaComponent(0.0).cgColor
]
}
var holeSize = baseSize > 52.0 ? 58.0 : -60.0
var primaryBaseScale = 1.0
var secondaryBaseScale = 0.68
if holeSize < 0.0 {
if offset > 48.0 {
holeSize -= min(14.0, (max(0.0, (offset - 48.0)) / 20.0) * 14.0)
}
primaryBaseScale = 0.96 + min(1.0, max(0.0, (offset - 20.0)) / 10.0) * 0.04
secondaryBaseScale = 0.62
}
var secondaryScale = secondaryBaseScale
if offset < 20.0 {
secondaryScale += (offset / 20.0) * 0.2
} else {
secondaryScale = min(1.0, max(secondaryBaseScale + 0.2, offset / 75.0))
}
self.maskLayer.updatePrimaryOffsetX(85.0, transition: transition)
self.maskLayer.updatePrimarySize(baseSize * primaryBaseScale, transition: transition)
self.maskLayer.updateSecondaryOffsetX(85.0 - offset, transition: transition)
self.maskLayer.updatePrimaryHoleSize(holeSize, transition: transition)
self.maskLayer.updateSecondarySize(55.0 * secondaryScale, transition: transition)
if holeSize < 0.0 {
transition.setAlpha(layer: self.blobLayer, alpha: min(1.0, max(0.0, offset / 10.0)))
}
let layerX = self.bounds.width - offset + 5.0
transition.setPosition(layer: self.iconLayer, position: CGPoint(x: layerX, y: self.bounds.height / 2.0))
transition.setPosition(layer: self.labelLayer, position: CGPoint(x: layerX, y: self.bounds.height + 13.0 - UIScreenPixel))
let iconOffset = max(0.0, offset - 35.0)
let alpha = max(0.0, min(1.0, iconOffset / 15.0))
let blurRadius = 8.0 - min(8.0, (iconOffset / 20.0) * 8.0)
for layer in [self.iconLayer, self.labelLayer] {
layer.setValue(blurRadius as NSNumber, forKeyPath: "filters.gaussianBlur.inputRadius")
transition.setAlpha(layer: layer, alpha: alpha)
}
}
override func layoutSublayers() {
super.layoutSublayers()
self.blobLayer.frame = self.bounds
self.maskLayer.frame = self.bounds
self.backgroundLayer.frame = self.bounds
self.foregroundLayer.frame = self.bounds
self.iconLayer.bounds = CGRect(origin: .zero, size: CGSize(width: 30.0, height: 30.0))
}
}
private func generateAddIcon(color: UIColor) -> UIImage? {
return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in
context.clear(CGRect(origin: .zero, size: size))
context.setStrokeColor(color.cgColor)
context.setLineWidth(3.0)
context.setLineCap(.round)
context.move(to: CGPoint(x: 15.0, y: 5.5))
context.addLine(to: CGPoint(x: 15.0, y: 24.5))
context.strokePath()
context.move(to: CGPoint(x: 5.5, y: 15.0))
context.addLine(to: CGPoint(x: 24.5, y: 15.0))
context.strokePath()
})
}
private func generateAddLabel(strings: PresentationStrings, color: UIColor) -> UIImage? {
let titleString = NSAttributedString(string: strings.StoryFeed_AddStory, font: Font.regular(11.0), textColor: color, paragraphAlignment: .center)
var textRect = titleString.boundingRect(with: CGSize(width: 200.0, height: 20.0), options: .usesLineFragmentOrigin, context: nil)
textRect.size.width = ceil(textRect.size.width)
textRect.size.height = ceil(textRect.size.height)
return generateImage(textRect.size, rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
UIGraphicsPushContext(context)
titleString.draw(in: textRect)
UIGraphicsPopContext()
})
}
@@ -0,0 +1,225 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import HierarchyTrackingLayer
public final class TitleActivityIndicatorComponent: Component {
let color: UIColor
public init(
color: UIColor
) {
self.color = color
}
public static func ==(lhs: TitleActivityIndicatorComponent, rhs: TitleActivityIndicatorComponent) -> Bool {
if lhs.color != rhs.color {
return false
}
return true
}
public final class View: UIView {
private let shapeLayer: SimpleShapeLayer
private let hierarchyTrackingLayer: HierarchyTrackingLayer
private var component: TitleActivityIndicatorComponent?
private var animator: ConstantDisplayLinkAnimator?
private var animationPhase: CGFloat = 0.0
public override init(frame: CGRect) {
self.shapeLayer = SimpleShapeLayer()
self.hierarchyTrackingLayer = HierarchyTrackingLayer()
super.init(frame: frame)
self.layer.addSublayer(self.hierarchyTrackingLayer)
self.hierarchyTrackingLayer.didExitHierarchy = { [weak self] in
guard let self else {
return
}
self.refreshAnimation()
}
self.hierarchyTrackingLayer.didEnterHierarchy = { [weak self] in
guard let self else {
return
}
self.refreshAnimation()
}
self.layer.addSublayer(self.shapeLayer)
self.shapeLayer.lineCap = .round
self.shapeLayer.lineWidth = 1.5
self.shapeLayer.fillColor = nil
}
required public init?(coder: NSCoder) {
preconditionFailure()
}
deinit {
}
private func refreshAnimation() {
if self.hierarchyTrackingLayer.isInHierarchy {
if self.animator == nil {
let animationStartTime = CACurrentMediaTime()
self.animator = ConstantDisplayLinkAnimator(update: { [weak self] in
guard let self else {
return
}
let duration: Double = 0.5
self.animationPhase = (CACurrentMediaTime() - animationStartTime).truncatingRemainder(dividingBy: duration) / duration
self.updateAnimation()
})
self.animator?.isPaused = false
}
} else {
if let animator = self.animator {
self.animator = nil
animator.invalidate()
}
}
}
private func updateAnimation() {
let size = self.shapeLayer.bounds
let path = CGMutablePath()
let radius = size.height * 0.5
enum Segment {
case line(start: CGPoint, end: CGPoint)
case halfCircle(center: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat)
func length(radius: CGFloat) -> CGFloat {
switch self {
case let .line(start, end):
return abs(start.x - end.x)
case let .halfCircle(_, radius, startAngle, endAngle):
return (endAngle - startAngle) * radius
}
}
func addPath(into path: CGMutablePath, fromFraction: CGFloat, toFraction: CGFloat) {
switch self {
case let .line(start, end):
if fromFraction != 0.0 {
path.move(to: CGPoint(
x: start.x * (1.0 - fromFraction) + end.x * fromFraction,
y: start.y * (1.0 - fromFraction) + end.y * fromFraction
))
}
path.addLine(to: CGPoint(
x: start.x * (1.0 - toFraction) + end.x * toFraction,
y: start.y * (1.0 - toFraction) + end.y * toFraction
))
case let .halfCircle(center, radius, startAngle, endAngle):
path.addArc(center: center, radius: radius, startAngle: startAngle + fromFraction * (endAngle - startAngle), endAngle: startAngle + toFraction * (endAngle - startAngle), clockwise: false)
}
}
}
let segments: [Segment] = [
.halfCircle(center: CGPoint(x: size.width - radius, y: radius), radius: radius, startAngle: -CGFloat.pi * 0.5, endAngle: CGFloat.pi * 0.5),
.line(start: CGPoint(x: size.width - radius, y: size.height), end: CGPoint(x: radius, y: size.height)),
.halfCircle(center: CGPoint(x: radius, y: radius), radius: radius, startAngle: CGFloat.pi * 0.5, endAngle: CGFloat.pi * 1.5),
.line(start: CGPoint(x: radius, y: 0.0), end: CGPoint(x: size.width - radius, y: 0.0)),
]
var totalLength: CGFloat = 0.0
for segment in segments {
totalLength += segment.length(radius: radius)
}
let startOffset: CGFloat = self.animationPhase
let endOffset: CGFloat = startOffset + 0.8
var startLength = startOffset * totalLength
var startSegment: (Int, CGFloat)?
while startSegment == nil {
for i in 0 ..< segments.count {
let segment = segments[i]
let segmentLength = segment.length(radius: radius)
if segmentLength <= startLength {
startLength -= segmentLength
} else {
let subOffset = startLength
startSegment = (i, subOffset)
break
}
}
}
var isFirst = true
var pathLength = (endOffset - startOffset) * totalLength
if let (startIndex, startOffset) = startSegment {
var index = startIndex
while pathLength > 0.0 {
let segment = segments[index]
let segmentOffset: CGFloat = isFirst ? startOffset : 0.0
let segmentLength = segment.length(radius: radius)
let segmentSubLength = segmentLength - segmentOffset
if segmentSubLength > 0.0 {
//remainingLength <= segmentRemainingLength -> take remainingLength
//remainingLength > segmentRemainingLength -> take segmentRemainingLength
let pathPart: CGFloat
if pathLength <= segmentSubLength {
pathPart = pathLength
} else {
pathPart = segmentSubLength
}
pathLength -= pathPart
segment.addPath(into: path, fromFraction: segmentOffset / segmentLength, toFraction: (segmentOffset + pathPart) / segmentLength)
}
index = (index + 1) % segments.count
isFirst = false
}
}
/*for segment in segments {
segment.addPath(into: path, fromFraction: 0.0, toFraction: 1.0)
}*/
if let currentPath = self.shapeLayer.path {
if currentPath != path {
self.shapeLayer.path = path
}
} else {
self.shapeLayer.path = path
}
}
func update(component: TitleActivityIndicatorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let isFirstTime = self.component == nil
self.component = component
let _ = isFirstTime
transition.setFrame(layer: self.shapeLayer, frame: CGRect(origin: CGPoint(), size: availableSize))
transition.setShapeLayerPath(layer: self.shapeLayer, path: UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: availableSize), cornerRadius: availableSize.height * 0.5).cgPath)
self.shapeLayer.strokeColor = component.color.cgColor
self.refreshAnimation()
self.updateAnimation()
return availableSize
}
}
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)
}
}