From bb23c6f653ac3fbd9f84cda8957da1822f41a6af Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Fri, 6 Mar 2026 18:13:48 +0100 Subject: [PATCH] Various improvements --- Telegram/BUILD | 2 +- .../ChatPresentationInterfaceState.swift | 26 +- submodules/Display/Source/UIKitUtils.swift | 4 + .../Sources/TGVideoMessageControls.m | 6 + .../Sources/TGVideoMessageTrimView.m | 6 + submodules/MtProtoKit/Sources/MTRsa.m | 6 + .../ContextControllerActionsStackNode.swift | 2 +- ...tControllerExtractedPresentationNode.swift | 4 +- .../Sources/GlassBackgroundComponent.swift | 60 +++ .../Sources/LegacyGlassView.swift | 27 +- .../Sources/TouchEffect.swift | 352 +++++++++++++++++ .../HeaderPanelContainerComponent.swift | 18 +- .../Sources/LensTransitionContainer.swift | 372 +++++++++++------- .../Chat/ChatControllerLoadDisplayNode.swift | 11 +- .../Sources/ChatControllerNode.swift | 91 ++++- .../ChatInterfaceTitlePanelNodes.swift | 12 +- .../Source/UIKitRuntimeUtils/UIKitUtils.h | 1 + .../Source/UIKitRuntimeUtils/UIKitUtils.m | 4 + 18 files changed, 839 insertions(+), 165 deletions(-) create mode 100644 submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/TouchEffect.swift diff --git a/Telegram/BUILD b/Telegram/BUILD index 7eff2fc281..15f6fe2c5f 100644 --- a/Telegram/BUILD +++ b/Telegram/BUILD @@ -1813,7 +1813,7 @@ swift_library( ios_ui_test_suite( name = "iOSAppUITestSuite", bundle_id = "org.telegram.Telegram-iOS-uitests", - minimum_os_version = "13.0", + minimum_os_version = minimum_os_version, runners = [ ":iPhone-17__26.2", ], diff --git a/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift b/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift index 9b188621af..2d7884fce7 100644 --- a/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift +++ b/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift @@ -438,11 +438,31 @@ public final class ChatPresentationInterfaceState: Equatable { } public struct PersistentPeerData: Codable, Equatable { - public var topicListPanelLocation: Bool + public enum TopicListPanelLocation: Int32 { + case top = 0 + case side = 1 + case bottom = 2 + } - public init(topicListPanelLocation: Bool) { + private enum CodingKeys: String, CodingKey { + case topicListPanelLocation + } + + public var topicListPanelLocation: TopicListPanelLocation + + public init(topicListPanelLocation: TopicListPanelLocation) { self.topicListPanelLocation = topicListPanelLocation } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.topicListPanelLocation = TopicListPanelLocation(rawValue: try container.decode(Int32.self, forKey: .topicListPanelLocation)) ?? .top + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(Int32(self.topicListPanelLocation.rawValue), forKey: .topicListPanelLocation) + } } public final class RemovePaidMessageFeeData: Equatable { @@ -660,7 +680,7 @@ public final class ChatPresentationInterfaceState: Equatable { self.alwaysShowGiftButton = false self.disallowedGifts = nil self.persistentData = PersistentPeerData( - topicListPanelLocation: false + topicListPanelLocation: .top ) self.removePaidMessageFeeData = nil self.viewForumAsMessages = false diff --git a/submodules/Display/Source/UIKitUtils.swift b/submodules/Display/Source/UIKitUtils.swift index 9c438f28ac..b6586e45bc 100644 --- a/submodules/Display/Source/UIKitUtils.swift +++ b/submodules/Display/Source/UIKitUtils.swift @@ -911,6 +911,10 @@ public extension CALayer { static func displacementMap() -> NSObject? { return makeDisplacementMapFilter() } + + static func colorMatrix() -> NSObject? { + return makeColorMatrixFilter() + } } public extension CALayer { diff --git a/submodules/LegacyComponents/Sources/TGVideoMessageControls.m b/submodules/LegacyComponents/Sources/TGVideoMessageControls.m index b6a6ed8a2e..3acabeee90 100644 --- a/submodules/LegacyComponents/Sources/TGVideoMessageControls.m +++ b/submodules/LegacyComponents/Sources/TGVideoMessageControls.m @@ -362,8 +362,11 @@ static CGRect viewFrame(UIView *view) _deleteButton = [[TGModernButton alloc] initWithFrame:CGRectMake(0.0f, (self.bounds.size.height - 45.0f) / 2.0f, 45.0f, 45.0f)]; [_deleteButton setImage:deleteImage forState:UIControlStateNormal]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" _deleteButton.adjustsImageWhenDisabled = false; _deleteButton.adjustsImageWhenHighlighted = false; +#pragma clang diagnostic pop [_deleteButton addTarget:self action:@selector(deleteButtonPressed) forControlEvents:UIControlEventTouchUpInside]; if (!_forStory) { [self addSubview:_deleteButton]; @@ -379,7 +382,10 @@ static CGRect viewFrame(UIView *view) _sendButton.alpha = 0.0f; _sendButton.exclusiveTouch = true; [_sendButton setImage:_assets.sendImage forState:UIControlStateNormal]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" _sendButton.adjustsImageWhenHighlighted = false; +#pragma clang diagnostic pop [_sendButton addTarget:self action:@selector(sendButtonPressed) forControlEvents:UIControlEventTouchUpInside]; _longPressGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(doneButtonLongPressed:)]; diff --git a/submodules/LegacyComponents/Sources/TGVideoMessageTrimView.m b/submodules/LegacyComponents/Sources/TGVideoMessageTrimView.m index 0b008e18dc..d3c22ad4b5 100644 --- a/submodules/LegacyComponents/Sources/TGVideoMessageTrimView.m +++ b/submodules/LegacyComponents/Sources/TGVideoMessageTrimView.m @@ -34,14 +34,20 @@ _leftSegmentView = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 16, height)]; _leftSegmentView.exclusiveTouch = true; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" _leftSegmentView.adjustsImageWhenHighlighted = false; +#pragma clang diagnostic pop [_leftSegmentView setBackgroundImage:TGComponentsImageNamed(@"VideoMessageLeftHandle") forState:UIControlStateNormal]; _leftSegmentView.hitTestEdgeInsets = UIEdgeInsetsMake(-5, -25, -5, -10); [self addSubview:_leftSegmentView]; _rightSegmentView = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 16, height)]; _rightSegmentView.exclusiveTouch = true; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" _rightSegmentView.adjustsImageWhenHighlighted = false; +#pragma clang diagnostic pop [_rightSegmentView setBackgroundImage:TGComponentsImageNamed(@"VideoMessageRightHandle") forState:UIControlStateNormal]; _rightSegmentView.hitTestEdgeInsets = UIEdgeInsetsMake(-5, -10, -5, -25); [self addSubview:_rightSegmentView]; diff --git a/submodules/MtProtoKit/Sources/MTRsa.m b/submodules/MtProtoKit/Sources/MTRsa.m index 92edd5bf33..c7e9707016 100644 --- a/submodules/MtProtoKit/Sources/MTRsa.m +++ b/submodules/MtProtoKit/Sources/MTRsa.m @@ -287,6 +287,8 @@ static NSData *base64_decode(NSString *str) { size_t outlen = block_size; OSStatus status = noErr; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" status = SecKeyEncrypt(keyRef, kSecPaddingNone, srcbuf + idx, @@ -294,6 +296,7 @@ static NSData *base64_decode(NSString *str) { outbuf, &outlen ); +#pragma clang diagnostic pop if (status != 0) { NSLog(@"SecKeyEncrypt fail. Error Code: %d", (int)status); ret = nil; @@ -343,6 +346,8 @@ static NSData *base64_decode(NSString *str) { size_t outlen = block_size; OSStatus status = noErr; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" status = SecKeyDecrypt(keyRef, kSecPaddingNone, srcbuf + idx, @@ -350,6 +355,7 @@ static NSData *base64_decode(NSString *str) { outbuf, &outlen ); +#pragma clang diagnostic pop if (status != 0) { NSLog(@"SecKeyEncrypt fail. Error Code: %d", (int)status); ret = nil; diff --git a/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerActionsStackNode.swift b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerActionsStackNode.swift index e568c17d0e..ddec57771f 100644 --- a/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerActionsStackNode.swift +++ b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerActionsStackNode.swift @@ -1722,7 +1722,7 @@ public final class ContextControllerActionsStackNodeImpl: ASDisplayNode, Context } transition.setPosition(view: self.contentContainer, position: CGRect(origin: CGPoint(), size: size).center) transition.setBounds(view: self.contentContainer, bounds: CGRect(origin: CGPoint(), size: size)) - self.contentContainer.update(size: size, cornerRadius: min(30.0, size.height * 0.5), transition: transition) + self.contentContainer.update(size: size, cornerRadius: min(30.0, size.height * 0.5), isDark: presentationData.theme.overallDarkAppearance, transition: transition) //let backgroundContainerFrame = CGRect(origin: CGPoint(), size: size).insetBy(dx: -self.backgroundContainerInset, dy: -self.backgroundContainerInset) diff --git a/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerExtractedPresentationNode.swift index cb5328aed8..19e231b579 100644 --- a/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerExtractedPresentationNode.swift +++ b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerExtractedPresentationNode.swift @@ -818,7 +818,9 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo if let transitionInfo = reference.transitionInfo() { if let referenceView = transitionInfo.referenceView as? ContextExtractableContainer { if #available(iOS 26.0, *) { - contextExtractableContainer = (referenceView, convertFrame(transitionInfo.referenceView.bounds.inset(by: transitionInfo.insets), from: transitionInfo.referenceView, to: self.view)) + if transitionInfo.referenceView.bounds.width == transitionInfo.referenceView.bounds.height { + contextExtractableContainer = (referenceView, convertFrame(transitionInfo.referenceView.bounds.inset(by: transitionInfo.insets), from: transitionInfo.referenceView, to: self.view)) + } } } diff --git a/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift b/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift index 14b8a8d020..68bfbc9b92 100644 --- a/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift +++ b/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift @@ -310,6 +310,8 @@ public class GlassBackgroundView: UIView { } private let legacyView: LegacyGlassView? + private let legacyHighlightContainerView: UIView? + private var glassHighlightRecognizer: GlassHighlightGestureRecognizer? private let nativeView: UIVisualEffectView? private let nativeViewClippingContext: ClippingShapeContext? @@ -339,6 +341,7 @@ public class GlassBackgroundView: UIView { public override init(frame: CGRect) { if #available(iOS 26.0, *), !GlassBackgroundView.useCustomGlassImpl { self.legacyView = nil + self.legacyHighlightContainerView = nil let glassEffect = UIGlassEffect(style: .regular) glassEffect.isInteractive = false @@ -355,6 +358,10 @@ public class GlassBackgroundView: UIView { self.shadowView = nil } else { self.legacyView = LegacyGlassView(frame: CGRect()) + let legacyHighlightContainerView = UIView() + legacyHighlightContainerView.isUserInteractionEnabled = false + legacyHighlightContainerView.clipsToBounds = true + self.legacyHighlightContainerView = legacyHighlightContainerView self.nativeView = nil self.nativeViewClippingContext = nil self.nativeParamsView = nil @@ -384,18 +391,29 @@ public class GlassBackgroundView: UIView { } if let legacyView = self.legacyView { self.addSubview(legacyView) + let glassHighlightRecognizer = GlassHighlightGestureRecognizer(target: self, action: #selector(self.onHighlightGesture(_:))) + glassHighlightRecognizer.highlightContainerView = self.legacyHighlightContainerView + self.glassHighlightRecognizer = glassHighlightRecognizer + self.addGestureRecognizer(glassHighlightRecognizer) + glassHighlightRecognizer.isEnabled = false } if let foregroundView = self.foregroundView { self.addSubview(foregroundView) foregroundView.mask = self.maskContainerView } self.addSubview(self.contentContainer) + if let legacyHighlightContainerView = self.legacyHighlightContainerView { + self.addSubview(legacyHighlightContainerView) + } } required public init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + @objc private func onHighlightGesture(_ recognizer: GlassHighlightGestureRecognizer) { + } + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if !self.isUserInteractionEnabled { return nil @@ -421,6 +439,10 @@ public class GlassBackgroundView: UIView { public func update(size: CGSize, cornerRadius: CGFloat, isDark: Bool, tintColor: TintColor, isInteractive: Bool = false, isVisible: Bool = true, transition: ComponentTransition) { let shape: Shape = .roundedRect(cornerRadius: cornerRadius) + if let glassHighlightRecognizer = self.glassHighlightRecognizer { + glassHighlightRecognizer.isEnabled = isInteractive + } + if let nativeView = self.nativeView, let nativeViewClippingContext = self.nativeViewClippingContext, (nativeView.bounds.size != size || nativeViewClippingContext.shape != shape) { nativeViewClippingContext.update(shape: shape, size: size, transition: transition) @@ -455,6 +477,16 @@ public class GlassBackgroundView: UIView { } transition.setFrame(view: legacyView, frame: CGRect(origin: CGPoint(), size: size)) transition.setAlpha(view: legacyView, alpha: isVisible ? 1.0 : 0.0) + + transition.setPosition(view: self.contentView, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5)) + transition.setBounds(view: self.contentView, bounds: CGRect(origin: CGPoint(), size: size)) + } + if let legacyHighlightContainerView = self.legacyHighlightContainerView { + transition.setFrame(view: legacyHighlightContainerView, frame: CGRect(origin: CGPoint(), size: size)) + switch shape { + case let .roundedRect(cornerRadius): + transition.setCornerRadius(layer: legacyHighlightContainerView.layer, cornerRadius: cornerRadius) + } } let shadowInset: CGFloat = 32.0 @@ -548,6 +580,9 @@ public class GlassBackgroundView: UIView { } } foregroundView.image = GlassBackgroundView.generateLegacyGlassImage(size: CGSize(width: outerCornerRadius * 2.0, height: outerCornerRadius * 2.0), inset: shadowInset, borderWidthFactor: borderWidthFactor, isDark: isDark, fillColor: fillColor) + #if DEBUG + //foregroundView.image = nil + #endif transition.setAlpha(view: foregroundView, alpha: isVisible ? 1.0 : 0.0) } else { if let nativeParamsView = self.nativeParamsView, let nativeView = self.nativeView { @@ -717,6 +752,31 @@ public final class GlassBackgroundContainerView: UIView { } for view in self.contentView.subviews.reversed() { if let result = view.hitTest(self.convert(point, to: view), with: event), result.isUserInteractionEnabled { + + #if DEBUG + func findMatrix(layer: CALayer) -> AnyObject? { + for filter in layer.filters ?? [] { + if "\(filter)".contains("vibrantColorMatrix") { + return filter as AnyObject + } + } + + for sublayer in layer.sublayers ?? [] { + if let result = findMatrix(layer: sublayer) { + return result + } + } + return nil + } + + /*if let filter = findMatrix(layer: self.layer) as? NSObject { + var matrix: [Float32] = .init(repeating: 0, count: 20) + let matrixValues = filter.value(forKey: "inputColorMatrix") as! NSValue + matrixValues.getValue(&matrix, size: 4 * 20) + assert(true) + }*/ + #endif + return result } } diff --git a/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/LegacyGlassView.swift b/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/LegacyGlassView.swift index 4a855e53dc..38790fa641 100644 --- a/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/LegacyGlassView.swift +++ b/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/LegacyGlassView.swift @@ -138,21 +138,40 @@ final class LegacyGlassView: UIView { } if previousParams?.style != style { - if let blurFilter = CALayer.blur() { + if let blurFilter = CALayer.blur(), let colorMatrixFilter = CALayer.colorMatrix() { switch style { case .clear: - blurFilter.setValue(6.0 as NSNumber, forKey: "inputRadius") + if DeviceMetrics.performance.isGraphicallyCapable { + blurFilter.setValue(2.0 as NSNumber, forKey: "inputRadius") + } else { + blurFilter.setValue(6.0 as NSNumber, forKey: "inputRadius") + } case .normal: blurFilter.setValue(2.0 as NSNumber, forKey: "inputRadius") } - backdropLayer.filters = [blurFilter] + + var matrix: [Float32] = [ + 2.6705, -1.1087999, -0.1117, 0.0, 0.049999997, + -0.3295, 1.8914, -0.111899994, 0.0, 0.049999997, + -0.3297, -1.1084, 2.8881, 0.0, 0.049999997, + 0.0, 0.0, 0.0, 1.0, 0.0 + ] + colorMatrixFilter.setValue(NSValue(bytes: &matrix, objCType: "{CAColorMatrix=ffffffffffffffffffff}"), forKey: "inputColorMatrix") + colorMatrixFilter.setValue(true as NSNumber, forKey: "inputBackdropAware") + + switch style { + case .clear: + backdropLayer.filters = [blurFilter] + case .normal: + backdropLayer.filters = [colorMatrixFilter, blurFilter] + } } } transition.setCornerRadius(layer: self.layer, cornerRadius: cornerRadius) transition.setFrame(layer: backdropLayer, frame: CGRect(origin: CGPoint(), size: size)) - if !"".isEmpty { + if DeviceMetrics.performance.isGraphicallyCapable { let size = CGSize(width: max(1.0, size.width), height: max(1.0, size.height)) let cornerRadius = min(min(size.width, size.height) * 0.5, cornerRadius) let displacementMagnitudePoints: CGFloat = 20.0 diff --git a/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/TouchEffect.swift b/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/TouchEffect.swift new file mode 100644 index 0000000000..643b4086aa --- /dev/null +++ b/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/TouchEffect.swift @@ -0,0 +1,352 @@ +import Foundation +import UIKit +import Display + +final class GlassHighlightGestureRecognizer: UIGestureRecognizer, UIGestureRecognizerDelegate { + var highlightContainerView: UIView? + + private var touchEffect: TouchEffect? + private var initialTouchLocation: CGPoint? + weak var touchEffectView: UIView? + var parameters = TouchEffect.Parameters() { + didSet { + self.touchEffect?.setParameters(self.parameters, animated: false) + } + } + + override init(target: Any?, action: Selector?) { + super.init(target: target, action: action) + + self.delegate = self + self.cancelsTouchesInView = false + self.delaysTouchesBegan = false + self.delaysTouchesEnded = false + self.requiresExclusiveTouchType = false + } + + override func canPrevent(_ preventedGestureRecognizer: UIGestureRecognizer) -> Bool { + return false + } + + override func canBePrevented(by preventingGestureRecognizer: UIGestureRecognizer) -> Bool { + return false + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + + override func reset() { + if let touchEffect = self.touchEffect { + touchEffect.setIsTracking(false) + } + + self.touchEffect = nil + self.initialTouchLocation = nil + } + + override func touchesBegan(_ touches: Set, with event: UIEvent) { + if let view = self.touchEffectView ?? self.view, let touch = touches.first { + let touchLocation = touch.location(in: view) + let touchEffect = TouchEffect(view: view, highlightContainerView: self.highlightContainerView) + touchEffect.setParameters(self.parameters, animated: false) + if let highlightContainerView = self.highlightContainerView { + touchEffect.setTouchLocation(touch.location(in: highlightContainerView), animated: false) + } + touchEffect.setStretchVector(.zero, animated: false) + self.touchEffect = touchEffect + self.initialTouchLocation = touchLocation + touchEffect.setIsTracking(true) + } + } + + override func touchesEnded(_ touches: Set, with event: UIEvent) { + if let touchEffect = self.touchEffect { + touchEffect.setIsTracking(false) + } + self.touchEffect = nil + } + + override func touchesCancelled(_ touches: Set, with event: UIEvent) { + if let touchEffect = self.touchEffect { + touchEffect.setIsTracking(false) + } + self.touchEffect = nil + } + + override func touchesMoved(_ touches: Set, with event: UIEvent) { + guard let touchEffect = self.touchEffect, + let view = self.touchEffectView ?? self.view, + let touch = touches.first, + let initialTouchLocation = self.initialTouchLocation else { + return + } + let touchLocation = touch.location(in: view) + if let highlightContainerView = self.highlightContainerView { + touchEffect.setTouchLocation(touch.location(in: highlightContainerView), animated: false) + } + touchEffect.setStretchVector( + CGPoint( + x: touchLocation.x - initialTouchLocation.x, + y: touchLocation.y - initialTouchLocation.y + ), + animated: false + ) + } +} + +final class TouchEffect { + private struct State: Equatable { + var isTracking: Bool + var stretchVector: CGPoint + var touchLocation: CGPoint? + } + + struct SpringParameters { + var mass: CGFloat + var stiffness: CGFloat + var damping: CGFloat + var initialVelocity: CGFloat + } + + struct Parameters { + var liftOn = SpringParameters( + mass: 1.36, + stiffness: 568.0, + damping: 39.7, + initialVelocity: 0.0 + ) + var liftOff = SpringParameters( + mass: 2.0, + stiffness: 460.0, + damping: 21.8, + initialVelocity: 0.0 + ) + var pressedSizeIncrease: CGFloat = 20.0 + } + + private weak var view: UIView? + private weak var highlightContainerView: UIView? + private let radialHighlightLayer: CAGradientLayer = { + let layer = CAGradientLayer() + layer.type = .radial + + let baseGradientAlpha: CGFloat = 0.5 + let numSteps = 8 + let firstStep = 1 + let firstLocation = 0.5 + let colors = (0 ..< numSteps).map { i -> UIColor in + if i < firstStep { + return UIColor(white: 1.0, alpha: 1.0) + } else { + let step: CGFloat = CGFloat(i - firstStep) / CGFloat(numSteps - firstStep - 1) + let value: CGFloat = 1.0 - bezierPoint(0.42, 0.0, 0.58, 1.0, step) + return UIColor(white: 1.0, alpha: baseGradientAlpha * value) + } + } + let locations = (0 ..< numSteps).map { i -> CGFloat in + if i < firstStep { + return 0.0 + } else { + let step: CGFloat = CGFloat(i - firstStep) / CGFloat(numSteps - firstStep - 1) + return (firstLocation + (1.0 - firstLocation) * step) + } + } + + layer.colors = colors.map(\.cgColor) + layer.locations = locations.map { $0 as NSNumber } + layer.startPoint = CGPoint(x: 0.5, y: 0.5) + layer.endPoint = CGPoint(x: 1.0, y: 1.0) + layer.opacity = 0.0 + layer.actions = [ + "position": NSNull(), + "bounds": NSNull(), + "opacity": NSNull() + ] + return layer + }() + private var state = State(isTracking: false, stretchVector: .zero, touchLocation: nil) + private var appliedState: State? + + var parameters = Parameters() + + init(view: UIView, highlightContainerView: UIView?) { + self.view = view + self.highlightContainerView = highlightContainerView + + if let highlightContainerView { + highlightContainerView.layer.addSublayer(self.radialHighlightLayer) + } + } + + deinit { + self.radialHighlightLayer.removeFromSuperlayer() + } + + private func currentTransform(for state: State, view: UIView) -> CATransform3D { + let referenceView = self.highlightContainerView ?? view + let viewWidth = max(1.0, referenceView.bounds.width) + let viewHeight = max(1.0, referenceView.bounds.height) + let aspectRatio = viewWidth / viewHeight + + let baseScaleX: CGFloat + let baseScaleY: CGFloat + if state.isTracking { + if viewWidth < viewHeight { + baseScaleY = 1.0 + self.parameters.pressedSizeIncrease / viewHeight + baseScaleX = baseScaleY + } else { + baseScaleX = 1.0 + self.parameters.pressedSizeIncrease / viewWidth + baseScaleY = baseScaleX + } + } else { + baseScaleX = 1.0 + baseScaleY = 1.0 + } + + guard state.isTracking else { + return CATransform3DScale(CATransform3DIdentity, baseScaleX, baseScaleY, 1.0) + } + + let stretchVector = state.stretchVector + let adjustedX = stretchVector.x / aspectRatio + let length = sqrt(pow(adjustedX, 2) + pow(stretchVector.y, 2)) + + guard length != 0.0 else { + return CATransform3DScale(CATransform3DIdentity, baseScaleX, baseScaleY, 1.0) + } + + let normal = CGPoint( + x: adjustedX / length, + y: stretchVector.y / length + ) + let k: CGFloat = -1.0 / ((length / viewHeight) / (5.0 * aspectRatio) + 1.0) + 1.0 + let additionalMaxScale = (viewHeight + 16.0 / aspectRatio) / viewHeight - 1.0 + let t = additionalMaxScale * k * aspectRatio + let maxOffset: CGFloat = 24.0 + + if abs(normal.x) > abs(normal.y) { + let diff = abs(normal.x) - abs(normal.y) + var transform = CATransform3DIdentity + transform.m11 = baseScaleX * (1.0 + t * diff) + transform.m22 = baseScaleY * (1.0 / (1.0 + t * diff)) + transform.m41 = normal.x * maxOffset * k + transform.m42 = normal.y * maxOffset * k + return transform + } else { + let diff = abs(normal.y) - abs(normal.x) + var transform = CATransform3DIdentity + transform.m11 = baseScaleX * (1.0 / (1.0 + t * diff)) + transform.m22 = baseScaleY * (1.0 + t * diff) + transform.m41 = normal.x * maxOffset * k + transform.m42 = normal.y * maxOffset * k + return transform + } + } + + private func currentSpringParameters(from previousState: State?, to state: State) -> SpringParameters { + guard let previousState, previousState != state else { + return state.isTracking ? self.parameters.liftOn : self.parameters.liftOff + } + if !previousState.isTracking && state.isTracking { + return self.parameters.liftOn + } else { + return self.parameters.liftOff + } + } + + private func updateRadialHighlight(animated: Bool) { + guard self.highlightContainerView != nil else { + return + } + + let baseAlpha: Float = 0.1 + let targetOpacity: Float = self.state.isTracking ? baseAlpha : 0.0 + let size = CGSize(width: 300.0, height: 300.0) + if let touchLocation = self.state.touchLocation { + self.radialHighlightLayer.bounds = CGRect(origin: CGPoint(), size: size) + self.radialHighlightLayer.position = touchLocation + } + + if animated { + let animation = CABasicAnimation(keyPath: "opacity") + animation.fromValue = self.radialHighlightLayer.presentation()?.opacity ?? self.radialHighlightLayer.opacity + self.radialHighlightLayer.opacity = targetOpacity + animation.toValue = targetOpacity + animation.duration = self.state.isTracking ? 0.12 : 0.22 + animation.timingFunction = CAMediaTimingFunction(name: self.state.isTracking ? .easeOut : .easeInEaseOut) + self.radialHighlightLayer.add(animation, forKey: "opacity") + } else { + self.radialHighlightLayer.opacity = targetOpacity + } + } + + func applyCurrentTransform(animated: Bool = true) { + guard let view = self.view else { + return + } + + let targetTransform = self.currentTransform(for: self.state, view: view) + + if !animated { + view.layer.removeAnimation(forKey: "sublayerTransform") + view.layer.sublayerTransform = targetTransform + self.updateRadialHighlight(animated: false) + self.appliedState = self.state + return + } + + let springParameters = self.currentSpringParameters(from: self.appliedState, to: self.state) + let animation = CASpringAnimation(keyPath: "sublayerTransform") + animation.fromValue = NSValue(caTransform3D: view.layer.presentation()?.sublayerTransform ?? view.layer.sublayerTransform) + animation.toValue = NSValue(caTransform3D: targetTransform) + animation.mass = springParameters.mass + animation.stiffness = springParameters.stiffness + animation.damping = springParameters.damping + animation.initialVelocity = springParameters.initialVelocity + animation.duration = animation.settlingDuration + animation.fillMode = .both + animation.isRemovedOnCompletion = false + + view.layer.sublayerTransform = targetTransform + view.layer.add(animation, forKey: "sublayerTransform") + self.updateRadialHighlight(animated: true) + self.appliedState = self.state + } + + func setParameters(_ parameters: Parameters, animated: Bool = false) { + self.parameters = parameters + self.applyCurrentTransform(animated: animated) + } + + func setIsTracking(_ value: Bool, animated: Bool = true) { + let nextState = State( + isTracking: value, + stretchVector: value ? self.state.stretchVector : .zero, + touchLocation: value ? self.state.touchLocation : self.state.touchLocation + ) + guard self.state != nextState else { + return + } + self.state = nextState + self.applyCurrentTransform(animated: animated) + } + + func setTouchLocation(_ touchLocation: CGPoint, animated: Bool = false) { + let nextState = State(isTracking: self.state.isTracking, stretchVector: self.state.stretchVector, touchLocation: touchLocation) + guard self.state != nextState else { + return + } + self.state = nextState + self.applyCurrentTransform(animated: animated) + } + + func setStretchVector(_ stretchVector: CGPoint, animated: Bool = false) { + let nextState = State(isTracking: self.state.isTracking, stretchVector: stretchVector, touchLocation: self.state.touchLocation) + guard self.state != nextState else { + return + } + self.state = nextState + self.applyCurrentTransform(animated: animated) + } +} diff --git a/submodules/TelegramUI/Components/HeaderPanelContainerComponent/Sources/HeaderPanelContainerComponent.swift b/submodules/TelegramUI/Components/HeaderPanelContainerComponent/Sources/HeaderPanelContainerComponent.swift index a009898553..94f7c0ca30 100644 --- a/submodules/TelegramUI/Components/HeaderPanelContainerComponent/Sources/HeaderPanelContainerComponent.swift +++ b/submodules/TelegramUI/Components/HeaderPanelContainerComponent/Sources/HeaderPanelContainerComponent.swift @@ -251,13 +251,27 @@ public final class HeaderPanelContainerComponent: Component { self.backgroundContainer.update(size: CGSize(width: backgroundSize.width + 32.0 * 2.0, height: backgroundSize.height + 32.0 * 2.0), isDark: component.theme.overallDarkAppearance, transition: transition) let backgroundFrame = CGRect(origin: CGPoint(x: 32.0 + sideInset, y: 32.0), size: CGSize(width: size.width - sideInset * 2.0, height: backgroundSize.height)) + + var panelCount = 0 + if component.tabs != nil { + panelCount += 1 + } + panelCount += component.panels.count + var cornerRadius: CGFloat = 0.0 + if panelCount == 1 { + cornerRadius = backgroundFrame.height * 0.5 + } else { + cornerRadius = 20.0 + } + transition.setFrame(view: self.backgroundView, frame: backgroundFrame) - self.backgroundView.update(size: backgroundFrame.size, cornerRadius: 20.0, isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: component.preferClearGlass ? .clear : .panel), isInteractive: true, transition: transition) + self.backgroundView.update(size: backgroundFrame.size, cornerRadius: cornerRadius, isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: component.preferClearGlass ? .clear : .panel), isInteractive: true, transition: transition) transition.setAlpha(view: self.backgroundContainer, alpha: (component.tabs != nil || !component.panels.isEmpty) ? 1.0 : 0.0) transition.setFrame(view: self.contentContainer, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) - self.contentContainer.layer.cornerRadius = 20.0 + + transition.setCornerRadius(layer: self.contentContainer.layer, cornerRadius: min(cornerRadius, backgroundFrame.height * 0.5)) return size } diff --git a/submodules/TelegramUI/Components/LensTransition/Sources/LensTransitionContainer.swift b/submodules/TelegramUI/Components/LensTransition/Sources/LensTransitionContainer.swift index daaa424549..564d8d235e 100644 --- a/submodules/TelegramUI/Components/LensTransition/Sources/LensTransitionContainer.swift +++ b/submodules/TelegramUI/Components/LensTransition/Sources/LensTransitionContainer.swift @@ -4,6 +4,7 @@ import Display import ComponentFlow import Display import UIKitRuntimeUtils +import GlassBackgroundComponent @inline(__always) private func getMethod(object: NSObject, selector: String) -> T? { @@ -98,13 +99,11 @@ public protocol LensTransitionContainerEffectView: UIView { } public protocol LensTransitionContainerProtocol: UIView { - var effectView: LensTransitionContainerEffectView { get } - var contentsEffectView: UIView { get } var contentsView: UIView { get } func animateIn(fromRect: CGRect, toRect: CGRect, fromCornerRadius: CGFloat, toCornerRadius: CGFloat, isDark: Bool, sourceEffectView: LensTransitionContainerEffectView) func animateOut(fromRect: CGRect, toRect: CGRect, fromCornerRadius: CGFloat, toCornerRadius: CGFloat, isDark: Bool, sourceEffectView: LensTransitionContainerEffectView) - func update(size: CGSize, cornerRadius: CGFloat, transition: ComponentTransition) + func update(size: CGSize, cornerRadius: CGFloat, isDark: Bool, transition: ComponentTransition) } @available(iOS 26.0, *) @@ -228,7 +227,7 @@ final class LensTransitionContainerImpl: UIView, LensTransitionContainerProtocol self.cancelAnimationsRecursively(layer: sdfElementLayer) } } - + private struct TransitionKeyframes { let bakedSizes: [CGSize] let bakedPositions: [CGPoint] @@ -242,7 +241,7 @@ final class LensTransitionContainerImpl: UIView, LensTransitionContainerProtocol private func makeForwardTransitionKeyframes(fromRect: CGRect, toRect: CGRect, toCornerRadius: CGFloat) -> TransitionKeyframes { let sourceMaxEdgeDistance: CGFloat = 20.0 let sourceSuckDurationFraction: CGFloat = 0.9 - let sourceFinalInsideDistance: CGFloat = -8.0 + let sourceFinalFurthestInsideDistance: CGFloat = -8.0 let sourceFinalInsideStartFraction: CGFloat = 0.65 let sourceFullInsideInset: CGFloat = max(1.0, min(fromRect.width, fromRect.height) * 0.5) let sampleCount = 30 @@ -257,7 +256,6 @@ final class LensTransitionContainerImpl: UIView, LensTransitionContainerProtocol let centerLineLength = hypot(centerLineDelta.x, centerLineDelta.y) let centerLineDirection: CGPoint = centerLineLength > 1e-6 ? CGPoint(x: centerLineDelta.x / centerLineLength, y: centerLineDelta.y / centerLineLength) : CGPoint(x: 1.0, y: 0.0) let centerLineMinDistance: CGFloat = -centerLineLength - let centerLineMaxDistance: CGFloat = 0.0 func isPointInsideRoundedRect(point: CGPoint, rectCenter: CGPoint, rectSize: CGSize, cornerRadius: CGFloat) -> Bool { let halfWidth = rectSize.width * 0.5 @@ -386,6 +384,72 @@ final class LensTransitionContainerImpl: UIView, LensTransitionContainerProtocol return CGPoint(x: rectCenter.x + nearestLocal.x, y: rectCenter.y + nearestLocal.y) } + func boundaryPointOnRoundedRectRay(rectCenter: CGPoint, rectSize: CGSize, cornerRadius: CGFloat, direction: CGPoint) -> CGPoint { + let halfWidth = rectSize.width * 0.5 + let halfHeight = rectSize.height * 0.5 + if halfWidth <= 0.0 || halfHeight <= 0.0 { + return rectCenter + } + + let radius = max(0.0, min(cornerRadius, min(halfWidth, halfHeight))) + let innerX = max(0.0, halfWidth - radius) + let innerY = max(0.0, halfHeight - radius) + + let dirLength = hypot(direction.x, direction.y) + let dir: CGPoint + if dirLength > 1e-6 { + dir = CGPoint(x: direction.x / dirLength, y: direction.y / dirLength) + } else { + dir = CGPoint(x: 1.0, y: 0.0) + } + let dx = abs(dir.x) + let dy = abs(dir.y) + + @inline(__always) + func signedPoint(_ x: CGFloat, _ y: CGFloat, _ direction: CGPoint) -> CGPoint { + let sx: CGFloat = direction.x >= 0.0 ? 1.0 : -1.0 + let sy: CGFloat = direction.y >= 0.0 ? 1.0 : -1.0 + return CGPoint(x: x * sx, y: y * sy) + } + + if dx > 1e-6 { + let tVertical = halfWidth / dx + let yAtVertical = tVertical * dy + if yAtVertical <= innerY + 1e-6 { + let local = signedPoint(halfWidth, yAtVertical, dir) + return CGPoint(x: rectCenter.x + local.x, y: rectCenter.y + local.y) + } + } + + if dy > 1e-6 { + let tHorizontal = halfHeight / dy + let xAtHorizontal = tHorizontal * dx + if xAtHorizontal <= innerX + 1e-6 { + let local = signedPoint(xAtHorizontal, halfHeight, dir) + return CGPoint(x: rectCenter.x + local.x, y: rectCenter.y + local.y) + } + } + + if radius <= 1e-6 { + let tx = dx > 1e-6 ? (halfWidth / dx) : CGFloat.greatestFiniteMagnitude + let ty = dy > 1e-6 ? (halfHeight / dy) : CGFloat.greatestFiniteMagnitude + let t = min(tx, ty) + let local = signedPoint(dx * t, dy * t, dir) + return CGPoint(x: rectCenter.x + local.x, y: rectCenter.y + local.y) + } + + let a = dx * dx + dy * dy + let b = -2.0 * (dx * innerX + dy * innerY) + let c = innerX * innerX + innerY * innerY - radius * radius + let discriminant = max(0.0, b * b - 4.0 * a * c) + let sqrtDiscriminant = sqrt(discriminant) + let t1 = (-b - sqrtDiscriminant) / (2.0 * a) + let t2 = (-b + sqrtDiscriminant) / (2.0 * a) + let tCorner = max(0.0, max(t1, t2)) + let cornerLocal = signedPoint(dx * tCorner, dy * tCorner, dir) + return CGPoint(x: rectCenter.x + cornerLocal.x, y: rectCenter.y + cornerLocal.y) + } + var bakedSizes: [CGSize] = [] var bakedPositions: [CGPoint] = [] var localPositions: [CGPoint] = [] @@ -444,14 +508,11 @@ final class LensTransitionContainerImpl: UIView, LensTransitionContainerProtocol } let lineDirection = centerLineDirection - let nearestEdgePoint = nearestBoundaryPointOnRoundedRect( - point: CGPoint( - x: blobCenter.x + lineDirection.x * 10000.0, - y: blobCenter.y + lineDirection.y * 10000.0 - ), + let nearestEdgePoint = boundaryPointOnRoundedRectRay( rectCenter: blobCenter, rectSize: scaledSize, - cornerRadius: scaledCornerRadius + cornerRadius: scaledCornerRadius, + direction: lineDirection ) let normalizedSuckProgress = max(0.0, min(1.0, t / sourceSuckDurationFraction)) @@ -462,17 +523,20 @@ final class LensTransitionContainerImpl: UIView, LensTransitionContainerProtocol let normalizedFinalInsideProgress = max(0.0, min(1.0, (t - sourceFinalInsideStartFraction) / max(0.001, 1.0 - sourceFinalInsideStartFraction))) let oneMinusFinalInsideProgress = 1.0 - normalizedFinalInsideProgress let finalInsideEase = 1.0 - oneMinusFinalInsideProgress * oneMinusFinalInsideProgress * oneMinusFinalInsideProgress - let sourceInsetDistance = baseSourceInsetDistance + (sourceFinalInsideDistance - baseSourceInsetDistance) * finalInsideEase let sourceHalfWidth = fromRect.width * 0.5 let sourceHalfHeight = fromRect.height * 0.5 let sourceHalfExtentAlongRay = abs(lineDirection.x) * sourceHalfWidth + abs(lineDirection.y) * sourceHalfHeight + let sourceFinalInsideDistance = sourceFinalFurthestInsideDistance - sourceHalfExtentAlongRay * 2.0 + let sourceInsetDistance = baseSourceInsetDistance + (sourceFinalInsideDistance - baseSourceInsetDistance) * finalInsideEase let sourceCenterDistance = sourceInsetDistance + sourceHalfExtentAlongRay let sourcePosition = CGPoint( x: nearestEdgePoint.x + lineDirection.x * sourceCenterDistance, y: nearestEdgePoint.y + lineDirection.y * sourceCenterDistance ) let centerLineDistance = (sourcePosition.x - fromCenter.x) * centerLineDirection.x + (sourcePosition.y - fromCenter.y) * centerLineDirection.y - let clampedCenterLineDistance = max(centerLineMinDistance, min(centerLineMaxDistance, centerLineDistance)) + let centerLineOutwardAllowance = max(0.0, centerLineDistance) * finalInsideEase + let centerLineUpperBound = sourceInsetDistance < 0.0 ? centerLineOutwardAllowance : 0.0 + let clampedCenterLineDistance = max(centerLineMinDistance, min(centerLineUpperBound, centerLineDistance)) var projectedSourcePosition = CGPoint( x: fromCenter.x + centerLineDirection.x * clampedCenterLineDistance, y: fromCenter.y + centerLineDirection.y * clampedCenterLineDistance @@ -481,26 +545,19 @@ final class LensTransitionContainerImpl: UIView, LensTransitionContainerProtocol x: projectedSourcePosition.x - centerLineDirection.x * sourceHalfExtentAlongRay, y: projectedSourcePosition.y - centerLineDirection.y * sourceHalfExtentAlongRay ) - let sourceNearestBoundaryPoint = nearestBoundaryPointOnRoundedRect( - point: sourceNearestPoint, + let sourceNearestBoundaryPoint = boundaryPointOnRoundedRectRay( rectCenter: blobCenter, rectSize: scaledSize, - cornerRadius: scaledCornerRadius + cornerRadius: scaledCornerRadius, + direction: lineDirection ) - let sourceNearestDistance = hypot( - sourceNearestPoint.x - sourceNearestBoundaryPoint.x, - sourceNearestPoint.y - sourceNearestBoundaryPoint.y - ) - let sourceNearestSignedDistance: CGFloat = isPointInsideRoundedRect( - point: sourceNearestPoint, - rectCenter: blobCenter, - rectSize: scaledSize, - cornerRadius: scaledCornerRadius - ) ? -sourceNearestDistance : sourceNearestDistance + let sourceNearestSignedDistance: CGFloat = + (sourceNearestPoint.x - sourceNearestBoundaryPoint.x) * lineDirection.x + + (sourceNearestPoint.y - sourceNearestBoundaryPoint.y) * lineDirection.y let centerCorrection = sourceNearestSignedDistance - sourceInsetDistance if abs(centerCorrection) > 0.01 { let correctedCenterLineDistance = centerLineDistance - centerCorrection - let clampedCorrectedCenterLineDistance = max(centerLineMinDistance, min(centerLineMaxDistance, correctedCenterLineDistance)) + let clampedCorrectedCenterLineDistance = max(centerLineMinDistance, min(centerLineUpperBound, correctedCenterLineDistance)) projectedSourcePosition = CGPoint( x: fromCenter.x + centerLineDirection.x * clampedCorrectedCenterLineDistance, y: fromCenter.y + centerLineDirection.y * clampedCorrectedCenterLineDistance @@ -551,7 +608,7 @@ final class LensTransitionContainerImpl: UIView, LensTransitionContainerProtocol let sourceMaxEdgeDistance: CGFloat = 20.0 let sourceSuckDurationFraction: CGFloat = 0.9 - let sourceFinalInsideDistance: CGFloat = -8.0 + let sourceFinalFurthestInsideDistance: CGFloat = -8.0 let sourceFinalInsideStartFraction: CGFloat = 0.65 let sourceFullInsideInset: CGFloat = max(1.0, min(fromRect.width, fromRect.height) * 0.5) let sampleCount = 30 @@ -566,7 +623,6 @@ final class LensTransitionContainerImpl: UIView, LensTransitionContainerProtocol let centerLineLength = hypot(centerLineDelta.x, centerLineDelta.y) let centerLineDirection: CGPoint = centerLineLength > 1e-6 ? CGPoint(x: centerLineDelta.x / centerLineLength, y: centerLineDelta.y / centerLineLength) : CGPoint(x: 1.0, y: 0.0) let centerLineMinDistance: CGFloat = -centerLineLength - let centerLineMaxDistance: CGFloat = 0.0 func isPointInsideRoundedRect(point: CGPoint, rectCenter: CGPoint, rectSize: CGSize, cornerRadius: CGFloat) -> Bool { let halfWidth = rectSize.width * 0.5 @@ -698,6 +754,72 @@ final class LensTransitionContainerImpl: UIView, LensTransitionContainerProtocol return CGPoint(x: rectCenter.x + nearestLocal.x, y: rectCenter.y + nearestLocal.y) } + func boundaryPointOnRoundedRectRay(rectCenter: CGPoint, rectSize: CGSize, cornerRadius: CGFloat, direction: CGPoint) -> CGPoint { + let halfWidth = rectSize.width * 0.5 + let halfHeight = rectSize.height * 0.5 + if halfWidth <= 0.0 || halfHeight <= 0.0 { + return rectCenter + } + + let radius = max(0.0, min(cornerRadius, min(halfWidth, halfHeight))) + let innerX = max(0.0, halfWidth - radius) + let innerY = max(0.0, halfHeight - radius) + + let dirLength = hypot(direction.x, direction.y) + let dir: CGPoint + if dirLength > 1e-6 { + dir = CGPoint(x: direction.x / dirLength, y: direction.y / dirLength) + } else { + dir = CGPoint(x: 1.0, y: 0.0) + } + let dx = abs(dir.x) + let dy = abs(dir.y) + + @inline(__always) + func signedPoint(_ x: CGFloat, _ y: CGFloat, _ direction: CGPoint) -> CGPoint { + let sx: CGFloat = direction.x >= 0.0 ? 1.0 : -1.0 + let sy: CGFloat = direction.y >= 0.0 ? 1.0 : -1.0 + return CGPoint(x: x * sx, y: y * sy) + } + + if dx > 1e-6 { + let tVertical = halfWidth / dx + let yAtVertical = tVertical * dy + if yAtVertical <= innerY + 1e-6 { + let local = signedPoint(halfWidth, yAtVertical, dir) + return CGPoint(x: rectCenter.x + local.x, y: rectCenter.y + local.y) + } + } + + if dy > 1e-6 { + let tHorizontal = halfHeight / dy + let xAtHorizontal = tHorizontal * dx + if xAtHorizontal <= innerX + 1e-6 { + let local = signedPoint(xAtHorizontal, halfHeight, dir) + return CGPoint(x: rectCenter.x + local.x, y: rectCenter.y + local.y) + } + } + + if radius <= 1e-6 { + let tx = dx > 1e-6 ? (halfWidth / dx) : CGFloat.greatestFiniteMagnitude + let ty = dy > 1e-6 ? (halfHeight / dy) : CGFloat.greatestFiniteMagnitude + let t = min(tx, ty) + let local = signedPoint(dx * t, dy * t, dir) + return CGPoint(x: rectCenter.x + local.x, y: rectCenter.y + local.y) + } + + let a = dx * dx + dy * dy + let b = -2.0 * (dx * innerX + dy * innerY) + let c = innerX * innerX + innerY * innerY - radius * radius + let discriminant = max(0.0, b * b - 4.0 * a * c) + let sqrtDiscriminant = sqrt(discriminant) + let t1 = (-b - sqrtDiscriminant) / (2.0 * a) + let t2 = (-b + sqrtDiscriminant) / (2.0 * a) + let tCorner = max(0.0, max(t1, t2)) + let cornerLocal = signedPoint(dx * tCorner, dy * tCorner, dir) + return CGPoint(x: rectCenter.x + cornerLocal.x, y: rectCenter.y + cornerLocal.y) + } + var bakedSizes: [CGSize] = [] var bakedPositions: [CGPoint] = [] var localPositions: [CGPoint] = [] @@ -757,14 +879,11 @@ final class LensTransitionContainerImpl: UIView, LensTransitionContainerProtocol } let lineDirection = centerLineDirection - let nearestEdgePoint = nearestBoundaryPointOnRoundedRect( - point: CGPoint( - x: blobCenter.x + lineDirection.x * 10000.0, - y: blobCenter.y + lineDirection.y * 10000.0 - ), + let nearestEdgePoint = boundaryPointOnRoundedRectRay( rectCenter: blobCenter, rectSize: scaledSize, - cornerRadius: scaledCornerRadius + cornerRadius: scaledCornerRadius, + direction: lineDirection ) let normalizedSuckProgress = max(0.0, min(1.0, t / sourceSuckDurationFraction)) @@ -775,17 +894,20 @@ final class LensTransitionContainerImpl: UIView, LensTransitionContainerProtocol let normalizedFinalInsideProgress = max(0.0, min(1.0, (t - sourceFinalInsideStartFraction) / max(0.001, 1.0 - sourceFinalInsideStartFraction))) let oneMinusFinalInsideProgress = 1.0 - normalizedFinalInsideProgress let finalInsideEase = 1.0 - oneMinusFinalInsideProgress * oneMinusFinalInsideProgress * oneMinusFinalInsideProgress - let sourceInsetDistance = baseSourceInsetDistance + (sourceFinalInsideDistance - baseSourceInsetDistance) * finalInsideEase let sourceHalfWidth = fromRect.width * 0.5 let sourceHalfHeight = fromRect.height * 0.5 let sourceHalfExtentAlongRay = abs(lineDirection.x) * sourceHalfWidth + abs(lineDirection.y) * sourceHalfHeight + let sourceFinalInsideDistance = sourceFinalFurthestInsideDistance - sourceHalfExtentAlongRay * 2.0 + let sourceInsetDistance = baseSourceInsetDistance + (sourceFinalInsideDistance - baseSourceInsetDistance) * finalInsideEase let sourceCenterDistance = sourceInsetDistance + sourceHalfExtentAlongRay let sourcePosition = CGPoint( x: nearestEdgePoint.x + lineDirection.x * sourceCenterDistance, y: nearestEdgePoint.y + lineDirection.y * sourceCenterDistance ) let centerLineDistance = (sourcePosition.x - fromCenter.x) * centerLineDirection.x + (sourcePosition.y - fromCenter.y) * centerLineDirection.y - let clampedCenterLineDistance = max(centerLineMinDistance, min(centerLineMaxDistance, centerLineDistance)) + let centerLineOutwardAllowance = max(0.0, centerLineDistance) * finalInsideEase + let centerLineUpperBound = sourceInsetDistance < 0.0 ? centerLineOutwardAllowance : 0.0 + let clampedCenterLineDistance = max(centerLineMinDistance, min(centerLineUpperBound, centerLineDistance)) var projectedSourcePosition = CGPoint( x: fromCenter.x + centerLineDirection.x * clampedCenterLineDistance, y: fromCenter.y + centerLineDirection.y * clampedCenterLineDistance @@ -794,26 +916,19 @@ final class LensTransitionContainerImpl: UIView, LensTransitionContainerProtocol x: projectedSourcePosition.x - centerLineDirection.x * sourceHalfExtentAlongRay, y: projectedSourcePosition.y - centerLineDirection.y * sourceHalfExtentAlongRay ) - let sourceNearestBoundaryPoint = nearestBoundaryPointOnRoundedRect( - point: sourceNearestPoint, + let sourceNearestBoundaryPoint = boundaryPointOnRoundedRectRay( rectCenter: blobCenter, rectSize: scaledSize, - cornerRadius: scaledCornerRadius + cornerRadius: scaledCornerRadius, + direction: lineDirection ) - let sourceNearestDistance = hypot( - sourceNearestPoint.x - sourceNearestBoundaryPoint.x, - sourceNearestPoint.y - sourceNearestBoundaryPoint.y - ) - let sourceNearestSignedDistance: CGFloat = isPointInsideRoundedRect( - point: sourceNearestPoint, - rectCenter: blobCenter, - rectSize: scaledSize, - cornerRadius: scaledCornerRadius - ) ? -sourceNearestDistance : sourceNearestDistance + let sourceNearestSignedDistance: CGFloat = + (sourceNearestPoint.x - sourceNearestBoundaryPoint.x) * lineDirection.x + + (sourceNearestPoint.y - sourceNearestBoundaryPoint.y) * lineDirection.y let centerCorrection = sourceNearestSignedDistance - sourceInsetDistance if abs(centerCorrection) > 0.01 { let correctedCenterLineDistance = centerLineDistance - centerCorrection - let clampedCorrectedCenterLineDistance = max(centerLineMinDistance, min(centerLineMaxDistance, correctedCenterLineDistance)) + let clampedCorrectedCenterLineDistance = max(centerLineMinDistance, min(centerLineUpperBound, correctedCenterLineDistance)) projectedSourcePosition = CGPoint( x: fromCenter.x + centerLineDirection.x * clampedCorrectedCenterLineDistance, y: fromCenter.y + centerLineDirection.y * clampedCorrectedCenterLineDistance @@ -996,20 +1111,13 @@ final class LensTransitionContainerImpl: UIView, LensTransitionContainerProtocol self.setIsFilterActive(isFilterActive: true) sourceEffectView.setTransitionFraction(value: 0.0, duration: 0.0) sourceEffectView.setTransitionFraction(value: 1.0, duration: 0.3) + //sourceEffectView.backgroundColor = .blue let sourceMaxEdgeDistance: CGFloat = 20.0 - let sourceEaseOutDurationFraction: CGFloat = 0.35 - let sourceCenterPullStartFraction: CGFloat - let sourceCenterPullDurationFraction: CGFloat - - let centerDistance = sqrt(pow(fromRect.midX - toRect.midX, 2.0) + pow(fromRect.midY - toRect.midY, 2.0)) - if centerDistance >= 100.0 { - sourceCenterPullStartFraction = 0.2 - sourceCenterPullDurationFraction = 0.3 - } else { - sourceCenterPullStartFraction = 0.0 - sourceCenterPullDurationFraction = 0.0 - } + let sourceSuckDurationFraction: CGFloat = 0.9 + let sourceFinalFurthestInsideDistance: CGFloat = -8.0 + let sourceFinalInsideStartFraction: CGFloat = 0.65 + let sourceFullInsideInset: CGFloat = max(1.0, min(fromRect.width, fromRect.height) * 0.5) let sampleCount = 36 let sampleEndIndex = CGFloat(sampleCount - 1) @@ -1158,24 +1266,47 @@ final class LensTransitionContainerImpl: UIView, LensTransitionContainerProtocol cornerRadius: radius, direction: centerLineDirection ) - let outwardDirection = normalized( - CGPoint( - x: sourceBoundaryPoint.x - centerLinePosition.x, - y: sourceBoundaryPoint.y - centerLinePosition.y - ) - ) - let normalizedSourceProgress = max(0.0, min(1.0, t / sourceEaseOutDurationFraction)) - let oneMinusSourceProgress = 1.0 - normalizedSourceProgress - let sourceEaseOut = 1.0 - oneMinusSourceProgress * oneMinusSourceProgress * oneMinusSourceProgress - let sourceEdgeOffset = lerp(-sourceMaxEdgeDistance, sourceMaxEdgeDistance, sourceEaseOut) - let sourceEdgePosition = CGPoint( - x: sourceBoundaryPoint.x + outwardDirection.x * sourceEdgeOffset, - y: sourceBoundaryPoint.y + outwardDirection.y * sourceEdgeOffset - ) - let normalizedCenterPullProgress = max(0.0, min(1.0, (t - sourceCenterPullStartFraction) / max(0.001, sourceCenterPullDurationFraction))) - let oneMinusCenterPull = 1.0 - normalizedCenterPullProgress + let lineDirection = centerLineDirection + let sourceHalfWidth = fromRect.width * 0.5 + let sourceHalfHeight = fromRect.height * 0.5 + let sourceHalfExtentAlongRay = abs(lineDirection.x) * sourceHalfWidth + abs(lineDirection.y) * sourceHalfHeight + let reverseT = 1.0 - t + let normalizedSuckProgress = max(0.0, min(1.0, reverseT / sourceSuckDurationFraction)) + let oneMinusSuckProgress = 1.0 - normalizedSuckProgress + let suckFraction = 1.0 - oneMinusSuckProgress * oneMinusSuckProgress * oneMinusSuckProgress + let reverseAnimatedInsetDistance = sourceMaxEdgeDistance - suckFraction * (sourceMaxEdgeDistance + sourceFullInsideInset) + let normalizedFinalInsideProgress = max(0.0, min(1.0, (reverseT - sourceFinalInsideStartFraction) / max(0.001, 1.0 - sourceFinalInsideStartFraction))) + let oneMinusFinalInsideProgress = 1.0 - normalizedFinalInsideProgress + let finalInsideEase = 1.0 - oneMinusFinalInsideProgress * oneMinusFinalInsideProgress * oneMinusFinalInsideProgress + let sourceFinalInsideDistance = sourceFinalFurthestInsideDistance - sourceHalfExtentAlongRay * 2.0 + let reverseInsetDistance = reverseAnimatedInsetDistance + (sourceFinalInsideDistance - reverseAnimatedInsetDistance) * finalInsideEase + let oneMinusCenterPull = 1.0 - t let centerPullEase = 1.0 - oneMinusCenterPull * oneMinusCenterPull * oneMinusCenterPull - sourcePositions.append(lerpPoint(sourceEdgePosition, centerLinePosition, centerPullEase)) + let sourceBoundaryDistanceFromCenter = + (sourceBoundaryPoint.x - centerLinePosition.x) * lineDirection.x + + (sourceBoundaryPoint.y - centerLinePosition.y) * lineDirection.y + let sourceCenterInsetDistance = -(sourceBoundaryDistanceFromCenter + sourceHalfExtentAlongRay) + let sourceInsetDistance = lerp(reverseInsetDistance, sourceCenterInsetDistance, centerPullEase) + let sourceCenterDistance = sourceInsetDistance + sourceHalfExtentAlongRay + var projectedSourcePosition = CGPoint( + x: sourceBoundaryPoint.x + lineDirection.x * sourceCenterDistance, + y: sourceBoundaryPoint.y + lineDirection.y * sourceCenterDistance + ) + let sourceNearestPoint = CGPoint( + x: projectedSourcePosition.x - lineDirection.x * sourceHalfExtentAlongRay, + y: projectedSourcePosition.y - lineDirection.y * sourceHalfExtentAlongRay + ) + let sourceNearestSignedDistance = + (sourceNearestPoint.x - sourceBoundaryPoint.x) * lineDirection.x + + (sourceNearestPoint.y - sourceBoundaryPoint.y) * lineDirection.y + if sourceNearestSignedDistance > sourceMaxEdgeDistance { + let centerCorrection = sourceNearestSignedDistance - sourceMaxEdgeDistance + projectedSourcePosition = CGPoint( + x: projectedSourcePosition.x - lineDirection.x * centerCorrection, + y: projectedSourcePosition.y - lineDirection.y * centerCorrection + ) + } + sourcePositions.append(projectedSourcePosition) } if let finalContainerPosition = containerPositions.last { @@ -1314,7 +1445,7 @@ final class LensTransitionContainerImpl: UIView, LensTransitionContainerProtocol ) } - public func update(size: CGSize, cornerRadius: CGFloat, transition: ComponentTransition) { + public func update(size: CGSize, cornerRadius: CGFloat, isDark: Bool, transition: ComponentTransition) { let bounds = CGRect(origin: .zero, size: size) let center = CGPoint(x: size.width * 0.5, y: size.height * 0.5) @@ -1342,24 +1473,18 @@ final class LensTransitionContainerImpl: UIView, LensTransitionContainerProtocol } private final class LensTransitionContainerFallbackImpl: UIView, LensTransitionContainerProtocol { - let effectView: LensTransitionContainerEffectView - private let containerView: UIView - let contentsEffectView: UIView - let contentsView: UIView + private let backgroundView: GlassBackgroundView - init(effectView: LensTransitionContainerEffectView) { - self.effectView = effectView - self.containerView = UIView() - self.contentsEffectView = UIView() - self.contentsView = UIView() + public var contentsView: UIView { + return self.backgroundView.contentView + } + + override init(frame: CGRect) { + self.backgroundView = GlassBackgroundView() - super.init(frame: .zero) + super.init(frame: frame) - self.addSubview(self.containerView) - self.containerView.addSubview(self.effectView) - self.containerView.addSubview(self.contentsEffectView) - self.contentsEffectView.addSubview(self.contentsView) - self.contentsView.clipsToBounds = true + self.addSubview(self.backgroundView) } required init?(coder: NSCoder) { @@ -1367,59 +1492,42 @@ private final class LensTransitionContainerFallbackImpl: UIView, LensTransitionC } func animateIn(fromRect: CGRect, toRect: CGRect, fromCornerRadius: CGFloat, toCornerRadius: CGFloat, isDark: Bool, sourceEffectView: LensTransitionContainerEffectView) { - self.update(size: fromRect.size, cornerRadius: fromCornerRadius, transition: .immediate) - UIView.animate(withDuration: 0.5, delay: 0.0, options: [.curveEaseInOut], animations: { - self.containerView.center = CGPoint(x: toRect.midX, y: toRect.midY) - self.update(size: toRect.size, cornerRadius: toCornerRadius, transition: .immediate) - }) } func animateOut(fromRect: CGRect, toRect: CGRect, fromCornerRadius: CGFloat, toCornerRadius: CGFloat, isDark: Bool, sourceEffectView: LensTransitionContainerEffectView) { - self.update(size: fromRect.size, cornerRadius: fromCornerRadius, transition: .immediate) - UIView.animate(withDuration: 0.5, delay: 0.0, options: [.curveEaseInOut], animations: { - self.containerView.center = CGPoint(x: toRect.midX, y: toRect.midY) - self.update(size: toRect.size, cornerRadius: toCornerRadius, transition: .immediate) - }) } - func update(size: CGSize, cornerRadius: CGFloat, transition: ComponentTransition) { - transition.setBounds(view: self.containerView, bounds: CGRect(origin: .zero, size: size)) - transition.setPosition(view: self.containerView, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5)) - - transition.setBounds(view: self.contentsEffectView, bounds: CGRect(origin: .zero, size: size)) - transition.setPosition(view: self.contentsEffectView, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5)) - - transition.setBounds(view: self.contentsView, bounds: CGRect(origin: .zero, size: size)) - transition.setPosition(view: self.contentsView, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5)) - transition.setCornerRadius(layer: self.contentsView.layer, cornerRadius: cornerRadius) - - transition.setBounds(view: self.effectView, bounds: CGRect(origin: .zero, size: size)) - transition.setPosition(view: self.effectView, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5)) - - self.effectView.updateCornerRadius(duration: 0.0, keyframes: [cornerRadius]) + func update(size: CGSize, cornerRadius: CGFloat, isDark: Bool, transition: ComponentTransition) { + transition.setBounds(view: self.backgroundView, bounds: CGRect(origin: .zero, size: size)) + transition.setPosition(view: self.backgroundView, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5)) + self.backgroundView.update(size: size, cornerRadius: cornerRadius, isDark: isDark, tintColor: .init(kind: .panel), transition: transition) } } public final class LensTransitionContainer: UIView { private let impl: (UIView & LensTransitionContainerProtocol) - public var effectView: LensTransitionContainerEffectView { - return self.impl.effectView - } - - public var contentsEffectView: UIView { - return self.impl.contentsEffectView + public var effectView: UIView? { + if #available(iOS 26.0, *) { + if let impl = self.impl as? LensTransitionContainerImpl { + return impl.effectView + } else { + return nil + } + } else { + return nil + } } public var contentsView: UIView { - return self.impl.contentsView + return impl.contentsView } public init(effectView: LensTransitionContainerEffectView) { if #available(iOS 26.0, *) { self.impl = LensTransitionContainerImpl(effectView: effectView) } else { - self.impl = LensTransitionContainerFallbackImpl(effectView: effectView) + self.impl = LensTransitionContainerFallbackImpl(frame: CGRect()) } super.init(frame: .zero) @@ -1443,8 +1551,8 @@ public final class LensTransitionContainer: UIView { self.impl.animateOut(fromRect: fromRect, toRect: toRect, fromCornerRadius: fromCornerRadius, toCornerRadius: toCornerRadius, isDark: isDark, sourceEffectView: sourceEffectView) } - public func update(size: CGSize, cornerRadius: CGFloat, transition: ComponentTransition) { - self.impl.update(size: size, cornerRadius: cornerRadius, transition: transition) + public func update(size: CGSize, cornerRadius: CGFloat, isDark: Bool, transition: ComponentTransition) { + self.impl.update(size: size, cornerRadius: cornerRadius, isDark: isDark, transition: transition) } } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift index c9c6c6bf17..6e84242fbf 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -662,7 +662,7 @@ extension ChatControllerImpl { guard let self else { return } - if let channel = self.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isForumOrMonoForum, self.presentationInterfaceState.persistentData.topicListPanelLocation == true, self.presentationInterfaceState.chatLocation.threadId != nil { + if let channel = self.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isForumOrMonoForum, self.presentationInterfaceState.persistentData.topicListPanelLocation == .side, self.presentationInterfaceState.chatLocation.threadId != nil { self.updateChatLocationThread(threadId: nil, animationDirection: .left) } else { if self.attemptNavigation({ [weak self] in @@ -4540,7 +4540,14 @@ extension ChatControllerImpl { } self.updateChatPresentationInterfaceState(animated: true, interactive: true, { presentationInterfaceState in var persistentData = presentationInterfaceState.persistentData - persistentData.topicListPanelLocation = !persistentData.topicListPanelLocation + switch persistentData.topicListPanelLocation { + case .top: + persistentData.topicListPanelLocation = .side + case .side: + persistentData.topicListPanelLocation = .bottom + case .bottom: + persistentData.topicListPanelLocation = .top + } return presentationInterfaceState.updatedPersistentData(persistentData) }) }, updateDisplayHistoryFilterAsList: { [weak self] displayAsList in diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 17cb88c55c..6904f6162b 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -249,6 +249,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { private var floatingTopicsPanelContainer: ChatControllerTitlePanelNodeContainer private var floatingTopicsPanel: (view: ComponentView, component: ChatFloatingTopicsPanel)? private var headerPanelsView: ComponentView? + private var footerPanelsView: ComponentView? private var topBackgroundEdgeEffectNode: WallpaperEdgeEffectNode? private var bottomBackgroundEdgeEffectNode: WallpaperEdgeEffectNode? @@ -762,6 +763,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { self.usePlainInputSeparator = false self.plainInputSeparatorAlpha = nil } + self.inputPanelBackgroundNode.isUserInteractionEnabled = false self.navigateButtons = ChatHistoryNavigationButtons(theme: self.chatPresentationInterfaceState.theme, preferClearGlass: self.chatPresentationInterfaceState.preferredGlassType == .clear, dateTimeFormat: self.chatPresentationInterfaceState.dateTimeFormat, backgroundNode: self.backgroundNode, isChatRotated: historyNodeRotated) self.navigateButtons.accessibilityElementsHidden = true @@ -1347,13 +1349,19 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { self.containerLayoutAndNavigationBarHeight = (layout, navigationBarHeight) var headerPanels: [HeaderPanelContainerComponent.Panel] = [] + var footerPanels: [HeaderPanelContainerComponent.Panel] = [] - if let headerTopicsPanel = headerTopicsPanelForChatPresentationInterfaceState(self.chatPresentationInterfaceState, context: self.context, controllerInteraction: self.controllerInteraction, interfaceInteraction: self.interfaceInteraction, force: false) { - headerPanels.append(HeaderPanelContainerComponent.Panel( + if let headerTopicsPanel = headerTopicsPanelForChatPresentationInterfaceState(self.chatPresentationInterfaceState, context: self.context, controllerInteraction: self.controllerInteraction, interfaceInteraction: self.interfaceInteraction, force: false) { + let panel = HeaderPanelContainerComponent.Panel( key: "topics", orderIndex: 0, component: headerTopicsPanel - )) + ) + if self.chatPresentationInterfaceState.persistentData.topicListPanelLocation == .top { + headerPanels.append(panel) + } else { + footerPanels.append(panel) + } } if let mediaPlayback = self.controller?.globalControlPanelsContextState?.mediaPlayback { headerPanels.append(HeaderPanelContainerComponent.Panel( @@ -2232,6 +2240,59 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } } + var containerInsets = insets + if let dismissAsOverlayLayout = self.dismissAsOverlayLayout { + if let inputNodeHeightAndOverflow = inputNodeHeightAndOverflow { + containerInsets = dismissAsOverlayLayout.insets(options: []) + containerInsets.bottom = max(inputNodeHeightAndOverflow.0 + inputNodeHeightAndOverflow.1, insets.bottom) + } else { + containerInsets = dismissAsOverlayLayout.insets(options: [.input]) + } + } + + var footerPanelsSize: CGSize? + if !footerPanels.isEmpty { + let footerPanelsView: ComponentView + var footerPanelsTransition = ComponentTransition(transition) + if let current = self.footerPanelsView { + footerPanelsView = current + } else { + footerPanelsTransition = footerPanelsTransition.withAnimation(.none) + footerPanelsView = ComponentView() + self.footerPanelsView = footerPanelsView + } + + var footerPanelsWidth = layout.size.width - layout.safeInsets.left - layout.safeInsets.right + 16.0 + if containerInsets.bottom <= 32.0 { + footerPanelsWidth -= 36.0 + } + + let footerPanelsSizeValue = footerPanelsView.update( + transition: footerPanelsTransition, + component: AnyComponent(HeaderPanelContainerComponent( + theme: self.chatPresentationInterfaceState.theme, + preferClearGlass: self.chatPresentationInterfaceState.preferredGlassType == .clear, + tabs: nil, + panels: footerPanels + )), + environment: {}, + containerSize: CGSize(width: footerPanelsWidth, height: layout.size.height) + ) + footerPanelsSize = footerPanelsSizeValue + floatingTopicsPanelInsets.bottom += footerPanelsSizeValue.height + } else if let footerPanelsView = self.footerPanelsView { + self.footerPanelsView = nil + if let footerPanelsComponentView = footerPanelsView.view { + transition.updateAlpha(layer: footerPanelsComponentView.layer, alpha: 0.0, completion: { [weak footerPanelsComponentView] _ in + footerPanelsComponentView?.removeFromSuperview() + }) + } + } + + if let footerPanelsSize { + inputPanelsHeight += 12.0 + footerPanelsSize.height + } + if self.dismissedAsOverlay { inputPanelsHeight = 0.0 } @@ -2308,16 +2369,6 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { transition.updateFrame(node: scrollContainerNode, frame: CGRect(origin: CGPoint(), size: layout.size)) } - var containerInsets = insets - if let dismissAsOverlayLayout = self.dismissAsOverlayLayout { - if let inputNodeHeightAndOverflow = inputNodeHeightAndOverflow { - containerInsets = dismissAsOverlayLayout.insets(options: []) - containerInsets.bottom = max(inputNodeHeightAndOverflow.0 + inputNodeHeightAndOverflow.1, insets.bottom) - } else { - containerInsets = dismissAsOverlayLayout.insets(options: [.input]) - } - } - let visibleAreaInset = UIEdgeInsets(top: containerInsets.top, left: 0.0, bottom: containerInsets.bottom + inputPanelsHeight + 8.0 + 8.0, right: 0.0) self.visibleAreaInset = visibleAreaInset @@ -2567,6 +2618,17 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { sidePanelTopInset += headerPanelsSize.height + 2.0 } + if let footerPanelsComponentView = self.footerPanelsView?.view, let footerPanelsSize { + let footerPanelsFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - footerPanelsSize.width) * 0.5), y: layout.size.height - (containerInsets.bottom + inputPanelsHeight + 8.0)), size: footerPanelsSize) + var footerPanelsTransition = ComponentTransition(transition) + if footerPanelsComponentView.superview == nil { + footerPanelsTransition.animateAlpha(view: footerPanelsComponentView, from: 0.0, to: 1.0) + footerPanelsTransition = footerPanelsTransition.withAnimation(.none) + self.floatingTopicsPanelContainer.view.addSubview(footerPanelsComponentView) + } + footerPanelsTransition.setFrame(view: footerPanelsComponentView, frame: footerPanelsFrame) + } + let floatingTopicsPanelContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 0.0, height: layout.size.height)) transition.updateFrame(node: self.floatingTopicsPanelContainer, frame: floatingTopicsPanelContainerFrame) if let floatingTopicsPanel = self.floatingTopicsPanel { @@ -5076,6 +5138,9 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { if let headerPanelsComponentView = self.headerPanelsView?.view as? HeaderPanelContainerComponent.View, let topicsPanelView = headerPanelsComponentView.panel(forKey: AnyHashable("topics")) as? ChatTopicsHeaderPanelComponent.View { leftIndex = topicsPanelView.topicIndex(threadId: fromLocation) rightIndex = topicsPanelView.topicIndex(threadId: toLocation) + } else if let footerPanelsComponentView = self.footerPanelsView?.view as? HeaderPanelContainerComponent.View, let topicsPanelView = footerPanelsComponentView.panel(forKey: AnyHashable("topics")) as? ChatTopicsHeaderPanelComponent.View { + leftIndex = topicsPanelView.topicIndex(threadId: fromLocation) + rightIndex = topicsPanelView.topicIndex(threadId: toLocation) } } guard let leftIndex, let rightIndex else { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift b/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift index c8602ccf47..315d8dbb5c 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift @@ -252,7 +252,7 @@ func headerTopicsPanelForChatPresentationInterfaceState(_ chatPresentationInterf } if let channel = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isMonoForum, let linkedMonoforumId = channel.linkedMonoforumId, let mainChannel = chatPresentationInterfaceState.renderedPeer?.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.hasPermission(.manageDirect), chatPresentationInterfaceState.search == nil { - let topicListDisplayModeOnTheSide = chatPresentationInterfaceState.persistentData.topicListPanelLocation + let topicListDisplayModeOnTheSide = chatPresentationInterfaceState.persistentData.topicListPanelLocation == .side if !topicListDisplayModeOnTheSide { return AnyComponent(ChatTopicsHeaderPanelComponent( context: context, @@ -282,7 +282,7 @@ func headerTopicsPanelForChatPresentationInterfaceState(_ chatPresentationInterf if !chatPresentationInterfaceState.viewForumAsMessages { return nil } - let topicListDisplayModeOnTheSide = chatPresentationInterfaceState.persistentData.topicListPanelLocation + let topicListDisplayModeOnTheSide = chatPresentationInterfaceState.persistentData.topicListPanelLocation == .side if !topicListDisplayModeOnTheSide { return AnyComponent(ChatTopicsHeaderPanelComponent( context: context, @@ -314,7 +314,7 @@ func headerTopicsPanelForChatPresentationInterfaceState(_ chatPresentationInterf return nil } } - let topicListDisplayModeOnTheSide = chatPresentationInterfaceState.persistentData.topicListPanelLocation + let topicListDisplayModeOnTheSide = chatPresentationInterfaceState.persistentData.topicListPanelLocation == .side if !topicListDisplayModeOnTheSide { return AnyComponent(ChatTopicsHeaderPanelComponent( context: context, @@ -367,7 +367,7 @@ func floatingTopicsPanelForChatPresentationInterfaceState(_ chatPresentationInte } if let channel = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isMonoForum, let linkedMonoforumId = channel.linkedMonoforumId, let mainChannel = chatPresentationInterfaceState.renderedPeer?.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.hasPermission(.manageDirect), chatPresentationInterfaceState.search == nil { - let topicListDisplayModeOnTheSide = chatPresentationInterfaceState.persistentData.topicListPanelLocation + let topicListDisplayModeOnTheSide = chatPresentationInterfaceState.persistentData.topicListPanelLocation == .side if topicListDisplayModeOnTheSide { return ChatFloatingTopicsPanel( context: context, @@ -399,7 +399,7 @@ func floatingTopicsPanelForChatPresentationInterfaceState(_ chatPresentationInte if !chatPresentationInterfaceState.viewForumAsMessages { return nil } - let topicListDisplayModeOnTheSide = chatPresentationInterfaceState.persistentData.topicListPanelLocation + let topicListDisplayModeOnTheSide = chatPresentationInterfaceState.persistentData.topicListPanelLocation == .side if topicListDisplayModeOnTheSide { return ChatFloatingTopicsPanel( context: context, @@ -433,7 +433,7 @@ func floatingTopicsPanelForChatPresentationInterfaceState(_ chatPresentationInte return nil } } - let topicListDisplayModeOnTheSide = chatPresentationInterfaceState.persistentData.topicListPanelLocation + let topicListDisplayModeOnTheSide = chatPresentationInterfaceState.persistentData.topicListPanelLocation == .side if topicListDisplayModeOnTheSide { return ChatFloatingTopicsPanel( context: context, diff --git a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIKitUtils.h b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIKitUtils.h index 01bac22f2f..66022262e5 100644 --- a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIKitUtils.h +++ b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIKitUtils.h @@ -34,6 +34,7 @@ NSObject * _Nullable makeLuminanceToAlphaFilter(); NSObject * _Nullable makeColorInvertFilter(); NSObject * _Nullable makeMonochromeFilter(); NSObject * _Nullable makeDisplacementMapFilter(); +NSObject * _Nullable makeColorMatrixFilter(); void setLayerDisableScreenshots(CALayer * _Nonnull layer, bool disableScreenshots); bool getLayerDisableScreenshots(CALayer * _Nonnull layer); diff --git a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIKitUtils.m b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIKitUtils.m index ffa8b0149f..3191bf1b3f 100644 --- a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIKitUtils.m +++ b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIKitUtils.m @@ -290,6 +290,10 @@ NSObject * _Nullable makeDisplacementMapFilter() { } } +NSObject * _Nullable makeColorMatrixFilter() { + return [(id)NSClassFromString(@"CAFilter") filterWithName:@"colorMatrix"]; +} + static const void *layerDisableScreenshotsKey = &layerDisableScreenshotsKey; void setLayerDisableScreenshots(CALayer * _Nonnull layer, bool disableScreenshots) {