import SwiftUI import UIKit // MARK: - Environment Key: Glass Active /// When false, TelegramGlassUIView removes its CABackdropLayer from the /// layer tree, stopping real-time blur (zero GPU cost). When true, the /// backdrop layer is re-inserted and blur resumes. /// /// Usage: set `.environment(\.telegramGlassActive, false)` on views whose /// glass effects should be frozen (e.g. hidden tabs in a ZStack pager). private struct TelegramGlassActiveKey: EnvironmentKey { static let defaultValue: Bool = true } extension EnvironmentValues { var telegramGlassActive: Bool { get { self[TelegramGlassActiveKey.self] } set { self[TelegramGlassActiveKey.self] = newValue } } } // MARK: - Telegram Glass (CABackdropLayer + CAFilter + Mesh + Foreground Image) // // Exact port of Telegram iOS LegacyGlassView + GlassBackgroundView foreground. // iOS < 26: CABackdropLayer with gaussianBlur + colorMatrix + mesh displacement // + generated foreground image with shadows, gradient border, blend modes. // iOS 26+: native UIGlassEffect(style: .regular). /// SwiftUI wrapper for Telegram-style glass background. /// Capsule shape with proper corner radius. struct TelegramGlassCapsule: UIViewRepresentable { func makeUIView(context: Context) -> TelegramGlassUIView { let view = TelegramGlassUIView(frame: .zero) view.backgroundColor = .clear return view } func updateUIView(_ uiView: TelegramGlassUIView, context: Context) { uiView.isFrozen = !context.environment.telegramGlassActive uiView.updateGlass() } } /// SwiftUI wrapper for Telegram-style glass circle. struct TelegramGlassCircle: UIViewRepresentable { func makeUIView(context: Context) -> TelegramGlassUIView { let view = TelegramGlassUIView(frame: .zero) view.backgroundColor = .clear view.isCircle = true return view } func updateUIView(_ uiView: TelegramGlassUIView, context: Context) { uiView.isFrozen = !context.environment.telegramGlassActive uiView.updateGlass() } } /// SwiftUI wrapper for Telegram-style glass with custom corner radius. struct TelegramGlassRoundedRect: UIViewRepresentable { let cornerRadius: CGFloat func makeUIView(context: Context) -> TelegramGlassUIView { let view = TelegramGlassUIView(frame: .zero) view.backgroundColor = .clear view.fixedCornerRadius = cornerRadius return view } func updateUIView(_ uiView: TelegramGlassUIView, context: Context) { uiView.isFrozen = !context.environment.telegramGlassActive uiView.fixedCornerRadius = cornerRadius uiView.applyCornerRadius() uiView.updateGlass() } } // MARK: - UIKit Implementation final class TelegramGlassUIView: UIView { var isCircle = false /// When set, overrides auto-calculated corner radius (height/2 for capsule, min/2 for circle). var fixedCornerRadius: CGFloat? /// When true, the CABackdropLayer is removed from the layer tree, /// stopping real-time blur capture. Set to true for views inside /// hidden tabs to eliminate GPU work from invisible glass effects. var isFrozen: Bool = false { didSet { guard isFrozen != oldValue else { return } if isFrozen { backdropLayer?.removeFromSuperlayer() } else if let backdrop = backdropLayer, backdrop.superlayer == nil { clippingContainer.insertSublayer(backdrop, at: 0) } } } // Layers private var backdropLayer: CALayer? private let clippingContainer = CALayer() // Foreground + Shadow image views (legacy glass) private var foregroundImageView: UIImageView? private var shadowImageView: UIImageView? // Track params for image regeneration private var lastImageCornerRadius: CGFloat = -1 private var lastImageIsDark: Bool? // iOS 26+ native glass private var nativeGlassView: UIVisualEffectView? private static let shadowInset: CGFloat = 32.0 override init(frame: CGRect) { super.init(frame: frame) clipsToBounds = false // CRITICAL: disable user interaction so glass background never intercepts // touches from SwiftUI Buttons that use this view as .background. isUserInteractionEnabled = false if #available(iOS 26.0, *) { setupNativeGlass() } else { setupLegacyGlass() } } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // MARK: - iOS 26+ (native UIGlassEffect) @available(iOS 26.0, *) private func setupNativeGlass() { let effect = UIGlassEffect(style: .regular) effect.isInteractive = false effect.tintColor = UIColor { traits in traits.userInterfaceStyle == .dark ? UIColor(white: 1.0, alpha: 0.025) : UIColor(white: 0.0, alpha: 0.025) } let glassView = UIVisualEffectView(effect: effect) glassView.clipsToBounds = true glassView.layer.cornerCurve = .continuous // Set initial corner radius — will be updated in layoutSubviews. glassView.layer.cornerRadius = 22 glassView.isUserInteractionEnabled = false addSubview(glassView) nativeGlassView = glassView } // MARK: - iOS < 26 (CABackdropLayer + ColorMatrix + Mesh — Telegram parity) private func setupLegacyGlass() { // Shadow image view — positioned behind glass with negative inset let shadow = UIImageView() shadow.isUserInteractionEnabled = false addSubview(shadow) shadowImageView = shadow // Clipping container — holds backdrop, clips to pill shape. // Telegram uses .circular cornerCurve for LegacyGlassView. clippingContainer.masksToBounds = true clippingContainer.cornerCurve = .circular layer.addSublayer(clippingContainer) // 1. CABackdropLayer — blurs + tones content behind this view if let backdrop = BackdropLayerHelper.createBackdropLayer() { backdrop.delegate = BackdropLayerDelegate.shared backdrop.rasterizationScale = 1.0 BackdropLayerHelper.setScale(backdrop, scale: 1.0) // Blur + Color Matrix filters (Telegram .normal style) if let blurFilter = CALayer.blurFilter(), let colorMatrixFilter = CALayer.colorMatrixFilter() { blurFilter.setValue(2.0 as NSNumber, forKey: "inputRadius") 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") backdrop.filters = [colorMatrixFilter, blurFilter] } clippingContainer.addSublayer(backdrop) self.backdropLayer = backdrop } // 2. Foreground image — generated overlay with fill, shadows, gradient border let fg = UIImageView() fg.isUserInteractionEnabled = false addSubview(fg) foregroundImageView = fg } // MARK: - Layout func updateGlass() { setNeedsLayout() } /// Directly applies current fixedCornerRadius to all layers without waiting for layout. /// Call when cornerRadius changes but bounds may not — ensures immediate visual update. func applyCornerRadius() { let bounds = bounds guard bounds.width > 0, bounds.height > 0 else { return } let radius: CGFloat if let fixed = fixedCornerRadius { radius = min(fixed, bounds.height / 2) } else if isCircle { radius = min(bounds.width, bounds.height) / 2 } else { radius = bounds.height / 2 } if #available(iOS 26.0, *), let glassView = nativeGlassView { glassView.layer.cornerRadius = radius } else { clippingContainer.cornerRadius = radius } } override func layoutSubviews() { super.layoutSubviews() let bounds = bounds guard bounds.width > 0, bounds.height > 0 else { return } let cornerRadius: CGFloat if let fixed = fixedCornerRadius { cornerRadius = min(fixed, bounds.height / 2) } else if isCircle { cornerRadius = min(bounds.width, bounds.height) / 2 } else { cornerRadius = bounds.height / 2 } if #available(iOS 26.0, *), let glassView = nativeGlassView { glassView.frame = bounds glassView.layer.cornerRadius = cornerRadius return } // Legacy layout clippingContainer.frame = bounds clippingContainer.cornerRadius = cornerRadius backdropLayer?.frame = bounds // Shadow image view — extends beyond bounds let si = Self.shadowInset shadowImageView?.frame = bounds.insetBy(dx: -si, dy: -si) // Foreground image view — same as shadow (image includes inset) foregroundImageView?.frame = bounds.insetBy(dx: -si, dy: -si) // Apply mesh displacement (iOS 17+, capable devices) if #available(iOS 17.0, *), DeviceCapability.isGraphicallyCapable { let size = CGSize(width: max(1.0, bounds.width), height: max(1.0, bounds.height)) let cr = min(min(size.width, size.height) * 0.5, cornerRadius) let displacementMagnitudePoints: CGFloat = 20.0 let displacementMagnitudeU = displacementMagnitudePoints / size.width let displacementMagnitudeV = displacementMagnitudePoints / size.height let mesh = generateGlassMesh( size: size, cornerRadius: cr, edgeDistance: min(12.0, cr), displacementMagnitudeU: displacementMagnitudeU, displacementMagnitudeV: displacementMagnitudeV, cornerResolution: 12, outerEdgeDistance: 2.0, bezier: DisplacementBezier( x1: 0.816137566137566, y1: 0.20502645502645533, x2: 0.5806878306878306, y2: 0.873015873015873 ) ) if let meshValue = mesh.makeValue() { backdropLayer?.setValue(meshValue, forKey: "meshTransform") } } // Regenerate foreground/shadow images if needed regenerateGlassImages(cornerRadius: cornerRadius) } // MARK: - Image Generation private func regenerateGlassImages(cornerRadius: CGFloat) { let isDark = traitCollection.userInterfaceStyle == .dark // Skip if params unchanged if cornerRadius == lastImageCornerRadius && isDark == lastImageIsDark { return } lastImageCornerRadius = cornerRadius lastImageIsDark = isDark let inset = Self.shadowInset // Fill color (Telegram parity) let fillColor: UIColor if isDark { fillColor = UIColor(white: 1.0, alpha: 1.0).mixedWith(.black, alpha: 1.0 - 0.11).withAlphaComponent(0.85) } else { fillColor = UIColor(white: 1.0, alpha: 0.7) } // Foreground image let fgImage = GlassImageGeneration.generateLegacyGlassImage( size: CGSize(width: cornerRadius * 2, height: cornerRadius * 2), inset: inset, borderWidthFactor: 1.0, isDark: isDark, fillColor: fillColor ) foregroundImageView?.image = fgImage // Shadow image shadowImageView?.image = GlassImageGeneration.generateShadowImage(cornerRadius: cornerRadius) } // MARK: - Adaptive Colors for Legacy Glass override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) guard traitCollection.userInterfaceStyle != previousTraitCollection?.userInterfaceStyle else { return } // Force image regeneration on theme change lastImageIsDark = nil setNeedsLayout() } override func didMoveToWindow() { super.didMoveToWindow() // Resolve images once view is in a window and has valid traitCollection if nativeGlassView == nil { lastImageIsDark = nil setNeedsLayout() } } // MARK: - Shadow (static helper — kept for external callers) /// Call from parent to add Telegram-exact shadow. /// Telegram: blur 40, color black 4%, offset (0,1), inset 32pt. static func makeShadowImage(cornerRadius: CGFloat) -> UIImage? { return GlassImageGeneration.generateShadowImage(cornerRadius: cornerRadius) } }