diff --git a/submodules/Display/Source/NavigationBar.swift b/submodules/Display/Source/NavigationBar.swift index c094bef0b0..8f82571cc0 100644 --- a/submodules/Display/Source/NavigationBar.swift +++ b/submodules/Display/Source/NavigationBar.swift @@ -145,6 +145,7 @@ public protocol NavigationBar: ASDisplayNode { var clippingNode: SparseNode { get } var backgroundView: UIView { get } + var customOverBackgroundContentSubview: UIView? { get set } var contentNode: NavigationBarContentNode? { get } var secondaryContentNode: ASDisplayNode? { get } var secondaryContentNodeDisplayFraction: CGFloat { get set } diff --git a/submodules/TelegramUI/Components/ChatList/ChatListTabsComponent/Sources/ChatListTabsComponent.swift b/submodules/TelegramUI/Components/ChatList/ChatListTabsComponent/Sources/ChatListTabsComponent.swift index b24bfc213c..47867c6ffd 100644 --- a/submodules/TelegramUI/Components/ChatList/ChatListTabsComponent/Sources/ChatListTabsComponent.swift +++ b/submodules/TelegramUI/Components/ChatList/ChatListTabsComponent/Sources/ChatListTabsComponent.swift @@ -88,21 +88,37 @@ public final class ChatListTabsComponent: Component { } } - public final class View: UIView { + private struct LayoutData { + var size: CGSize + var selectedItemFrame: CGRect + + init(size: CGSize, selectedItemFrame: CGRect) { + self.size = size + self.selectedItemFrame = selectedItemFrame + } + } + + public final class View: UIView, UIScrollViewDelegate { + private let lensView: LiquidLensView private let scrollView: ScrollView private let selectionView: UIImageView private var itemViews: [Tab.Id: ComponentView] = [:] + private var ignoreScrolling: Bool = false private var tabSwitchFraction: CGFloat = 0.0 + private var temporaryLiftTimer: Foundation.Timer? + + private var layoutData: LayoutData? private var component: ChatListTabsComponent? private weak var state: EmptyComponentState? override init(frame: CGRect) { + self.lensView = LiquidLensView(kind: .noContainer) self.scrollView = ScrollView() self.selectionView = UIImageView() - self.scrollView.addSubview(self.selectionView) + //self.scrollView.addSubview(self.selectionView) super.init(frame: frame) @@ -116,21 +132,58 @@ public final class ChatListTabsComponent: Component { self.scrollView.alwaysBounceVertical = false self.scrollView.scrollsToTop = false self.scrollView.clipsToBounds = false - self.addSubview(self.scrollView) + self.scrollView.delegate = self + + self.addSubview(self.lensView) + + self.lensView.contentView.addSubview(self.scrollView) } required public init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + return self.scrollView.hitTest(self.convert(point, to: self.scrollView), with: event) + } + public func updateTabSwitchFraction(fraction: CGFloat, transition: ComponentTransition) { self.tabSwitchFraction = -fraction self.state?.updated(transition: transition, isLocal: true) } + public func scrollViewDidScroll(_ scrollView: UIScrollView) { + if self.ignoreScrolling { + return + } + self.updateScrolling(transition: .immediate) + } + + private func updateScrolling(transition: ComponentTransition) { + guard let component = self.component, let layoutData = self.layoutData else { + return + } + self.lensView.update(size: layoutData.size, selectionX: -self.scrollView.contentOffset.x + layoutData.selectedItemFrame.minX, selectionWidth: layoutData.selectedItemFrame.width, isDark: component.theme.overallDarkAppearance, isLifted: self.temporaryLiftTimer != nil, transition: transition) + } + func update(component: ChatListTabsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { if self.component?.selectedTab != component.selectedTab { self.tabSwitchFraction = 0.0 + + self.temporaryLiftTimer?.invalidate() + self.temporaryLiftTimer = nil + + if !transition.animation.isImmediate { + self.temporaryLiftTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 0.25, repeats: false, block: { [weak self] timer in + guard let self else { + return + } + if self.temporaryLiftTimer === timer { + self.temporaryLiftTimer = nil + self.state?.updated(transition: .spring(duration: 0.5)) + } + }) + } } self.component = component @@ -252,12 +305,23 @@ public final class ChatListTabsComponent: Component { self.selectionView.isHidden = true } + self.layoutData = LayoutData( + size: size, + selectedItemFrame: selectedItemFrame ?? CGRect() + ) + + self.ignoreScrolling = true let contentSize = CGSize(width: contentWidth, height: size.height) transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: size)) if self.scrollView.contentSize != contentSize { self.scrollView.contentSize = contentSize } + transition.setFrame(view: self.lensView, frame: CGRect(origin: CGPoint(), size: size)) + self.ignoreScrolling = false + + self.updateScrolling(transition: transition) + return size } } diff --git a/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/LegacyGlassView.swift b/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/LegacyGlassView.swift index b4ca51611c..8040ddf289 100644 --- a/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/LegacyGlassView.swift +++ b/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/LegacyGlassView.swift @@ -4,573 +4,6 @@ import Display import ComponentFlow import MeshTransform -/// Bezier control points for displacement easing curve -private struct DisplacementBezier { - var x1: CGFloat - var y1: CGFloat - var x2: CGFloat - var y2: CGFloat -} - -/// Computes signed distance from a point to the edge of a rounded rectangle. -/// Returns negative inside, zero on edge, positive outside. -/// All values in points. -private func roundedRectSDF(x: CGFloat, y: CGFloat, width: CGFloat, height: CGFloat, cornerRadius: CGFloat) -> CGFloat { - // Center the point (SDF formula assumes center at origin) - let px = x - width / 2 - let py = y - height / 2 - - // Half extents of the box - let bx = width / 2 - let by = height / 2 - - // Standard rounded box SDF (Inigo Quilez formula) - let qx = abs(px) - bx + cornerRadius - let qy = abs(py) - by + cornerRadius - - let outsideDist = hypot(max(qx, 0), max(qy, 0)) - let insideDist = min(max(qx, qy), 0) - - return outsideDist + insideDist - cornerRadius -} - -/// Computes the gradient (outward normal) of the rounded rect SDF. -/// Returns normalized direction perpendicular to the nearest edge point. -private func roundedRectGradient(x: CGFloat, y: CGFloat, width: CGFloat, height: CGFloat, cornerRadius: CGFloat) -> (nx: CGFloat, ny: CGFloat) { - // Center the point - let px = x - width / 2 - let py = y - height / 2 - - // Half extents - let bx = width / 2 - let by = height / 2 - - // q values from SDF formula - let qx = abs(px) - bx + cornerRadius - let qy = abs(py) - by + cornerRadius - - var nx: CGFloat = 0 - var ny: CGFloat = 0 - - if qx > 0 && qy > 0 { - // Corner region - normal points radially from corner arc center - let d = hypot(qx, qy) - if d > 0 { - nx = qx / d - ny = qy / d - } - } else if qx > qy { - // Nearest point is on vertical edge (left or right) - nx = 1 - ny = 0 - } else { - // Nearest point is on horizontal edge (top or bottom) - nx = 0 - ny = 1 - } - - // Restore sign based on which side of center we're on - if px < 0 { nx = -nx } - if py < 0 { ny = -ny } - - return (nx, ny) -} - -/// Generates a displacement map image as a signed distance field from rounded rect edges. -/// - edgeDistance: The distance (in points) over which displacement is applied -/// - R channel: X displacement (127 = neutral, 0 = max left, 255 = max right) -/// - G channel: Y displacement (127 = neutral, 0 = max up, 255 = max down) -/// - B channel: Unused (always 0) -/// Displacement is maximum at the edge and fades linearly to zero at edgeDistance. -/// Actual displacement magnitude is applied when sampling the map. -private func generateDisplacementMap(size: CGSize, cornerRadius: CGFloat, edgeDistance: CGFloat, scale: CGFloat) -> CGImage? { - let width = Int(size.width * scale) - let height = Int(size.height * scale) - - // Clamp corner radius - let maxCornerRadius = min(size.width, size.height) / 2.0 - let clampedRadius = min(cornerRadius, maxCornerRadius) - - // Create bitmap context - var pixelData = [UInt8](repeating: 0, count: width * height * 4) - - for py in 0 ..< height { - for px in 0 ..< width { - // Convert pixel to point coordinates - let x = CGFloat(px) / scale - let y = CGFloat(py) / scale - - // Get signed distance (negative inside, positive outside) - let sdf = roundedRectSDF(x: x, y: y, width: size.width, height: size.height, cornerRadius: clampedRadius) - - // Get gradient (outward normal direction) - let (nx, ny) = roundedRectGradient(x: x, y: y, width: size.width, height: size.height, cornerRadius: clampedRadius) - - // Inward normal (content moves away from edge, toward center) - let inwardX = -nx - let inwardY = -ny - - // Distance from edge (positive inside the shape) - let distFromEdge = -sdf - - // Weight: 1 at edge, 0 at edgeDistance (linear falloff) - let weight = max(0, min(1, 1.0 - distFromEdge / edgeDistance)) - - // Displacement modulated by distance from edge - let displacementX = inwardX * weight - let displacementY = inwardY * weight - - // Encode in R/G: 127 = neutral, map -1..1 to 0..254 - let r = UInt8(max(0, min(255, Int(127 + displacementX * 127)))) - let g = UInt8(max(0, min(255, Int(127 + displacementY * 127)))) - - let idx = (py * width + px) * 4 - pixelData[idx + 0] = r // X displacement - pixelData[idx + 1] = g // Y displacement - pixelData[idx + 2] = 0 // Unused - pixelData[idx + 3] = 255 // A - } - } - - let colorSpace = CGColorSpaceCreateDeviceRGB() - guard let context = CGContext( - data: &pixelData, - width: width, - height: height, - bitsPerComponent: 8, - bytesPerRow: width * 4, - space: colorSpace, - bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue - ) else { - return nil - } - - return context.makeImage() -} - -/// Samples displacement from a displacement map with bilinear interpolation and bezier easing -/// - Parameters: -/// - x, y: Coordinates in the displacement map's pixel space -/// - pixels: Pointer to displacement map pixel data -/// - width, height: Displacement map dimensions -/// - bytesPerRow, bytesPerPixel: Displacement map layout -/// - bezier: Bezier control points for easing curve -/// - Returns: Displacement (dx, dy) in range -1..1 with bezier easing applied -private func sampleDisplacement( - x: CGFloat, - y: CGFloat, - pixels: UnsafePointer, - width: Int, - height: Int, - bytesPerRow: Int, - bytesPerPixel: Int, - bezier: DisplacementBezier -) -> (dx: CGFloat, dy: CGFloat) { - let clampedX = max(0, min(CGFloat(width - 1), x)) - let clampedY = max(0, min(CGFloat(height - 1), y)) - - let x0 = Int(clampedX) - let y0 = Int(clampedY) - let x1 = min(x0 + 1, width - 1) - let y1 = min(y0 + 1, height - 1) - - let fx = clampedX - CGFloat(x0) - let fy = clampedY - CGFloat(y0) - - func sample(_ sx: Int, _ sy: Int) -> (r: CGFloat, g: CGFloat) { - let offset = sy * bytesPerRow + sx * bytesPerPixel - return (CGFloat(pixels[offset + 0]), CGFloat(pixels[offset + 1])) - } - - let c00 = sample(x0, y0) - let c10 = sample(x1, y0) - let c01 = sample(x0, y1) - let c11 = sample(x1, y1) - - let r = (c00.r * (1 - fx) + c10.r * fx) * (1 - fy) + (c01.r * (1 - fx) + c11.r * fx) * fy - let g = (c00.g * (1 - fx) + c10.g * fx) * (1 - fy) + (c01.g * (1 - fx) + c11.g * fx) * fy - - // Decode: 127 = neutral, map 0..254 to -1..1 - var dx = (r - 127.0) / 127.0 - var dy = (g - 127.0) / 127.0 - - // Apply bezier easing to vector magnitude, preserving direction - let mag = hypot(dx, dy) - if mag > 0 { - let newMag = bezierPoint(bezier.x1, bezier.y1, bezier.x2, bezier.y2, mag) - let scale = newMag / mag - dx *= scale - dy *= scale - } - - return (dx, dy) -} - -/// Generates a glass mesh with corner-aware topology. -/// - 4 radial corner wedges sampled in polar space -/// - 4 edge strips aligned with the rectangle sides -/// - 1 center patch -/// Corner/edge seams share the same coordinates (but do not reuse vertices) so -/// the neighbouring faces fit perfectly without T-junctions. -private func generateGlassMeshFromDisplacementMap( - size: CGSize, - cornerRadius: CGFloat, - displacementMap: CGImage, - displacementMagnitudeU: CGFloat, - displacementMagnitudeV: CGFloat, - cornerResolution: Int, - outerEdgeDistance: CGFloat, - bezier: DisplacementBezier, - generateWireframe: Bool = false -) -> (mesh: MeshTransform, wireframe: CGPath?) { - guard let dispDataProvider = displacementMap.dataProvider, - let dispData = dispDataProvider.data, - let dispPixels = CFDataGetBytePtr(dispData) else { - return (mesh: MeshTransform(), wireframe: nil) - } - - let dispWidth = displacementMap.width - let dispHeight = displacementMap.height - let dispBytesPerRow = displacementMap.bytesPerRow - let dispBytesPerPixel = displacementMap.bitsPerPixel / 8 - - let clampedRadius = min(cornerRadius, min(size.width, size.height) / 2) - - let transform = MeshTransform() - var wireframe: CGMutablePath? - if generateWireframe { - wireframe = CGMutablePath() - } - - // Debug flags - let debugNoDisplacement = false - let debugLogCorner = false - - // Inset the mesh slightly (1 pixel) to clear the clip mask - let insetPoints = -1.0 - let usableWidth = max(1.0, size.width - insetPoints * 2) - let usableHeight = max(1.0, size.height - insetPoints * 2) - let insetUOffset = insetPoints / size.width - let insetVOffset = insetPoints / size.height - let usableUNorm = usableWidth / size.width - let usableVNorm = usableHeight / size.height - - // Helper to sample displacement and create vertex - func makeVertex(u: CGFloat, v: CGFloat, depth: CGFloat = 0) -> (vertex: MeshTransform.Vertex, point: CGPoint) { - let mappedU = insetUOffset + u * usableUNorm - let mappedV = insetVOffset + v * usableVNorm - let fromX: CGFloat - let fromY: CGFloat - - if debugNoDisplacement { - fromX = mappedU - fromY = mappedV - } else { - let (dispX, dispY) = sampleDisplacement( - x: mappedU * CGFloat(dispWidth - 1), - y: mappedV * CGFloat(dispHeight - 1), - pixels: dispPixels, - width: dispWidth, - height: dispHeight, - bytesPerRow: dispBytesPerRow, - bytesPerPixel: dispBytesPerPixel, - bezier: bezier - ) - - // Slight boost near the edge to emphasize the outer strip (rounded-corner aware) - let worldX = insetPoints + u * usableWidth - let worldY = insetPoints + v * usableHeight - let sdf = roundedRectSDF(x: worldX, y: worldY, width: size.width, height: size.height, cornerRadius: clampedRadius) - let distToEdge = max(0.0, -sdf) // distance inside the rounded rect to the edge - let edgeBand = max(0.0, outerEdgeDistance) - let edgeBoostGain: CGFloat = 0.5 // up to +50% displacement at the edge, fades inside - let edgeBoost: CGFloat - if edgeBand > 0 { - let t = max(0.0, min(1.0, (edgeBand - distToEdge) / edgeBand)) - let eased = t * t * (3 - 2 * t) // smoothstep - edgeBoost = 1.0 + eased * edgeBoostGain - } else { - edgeBoost = 1.0 - } - - fromX = max(0.0, min(1.0, mappedU + dispX * displacementMagnitudeU * edgeBoost)) - fromY = max(0.0, min(1.0, mappedV + dispY * displacementMagnitudeV * edgeBoost)) - } - - let vertex = MeshTransform.Vertex(from: CGPoint(x: fromX, y: fromY), to: MeshTransform.Point3D(x: mappedU, y: mappedV, z: depth)) - return (vertex, CGPoint(x: mappedU * size.width, y: mappedV * size.height)) - } - - var vertexIndex = 0 - var vertexPoints: [CGPoint] = [] - - func addVertex(u: CGFloat, v: CGFloat, depth: CGFloat = 0) -> Int { - let (vertex, point) = makeVertex(u: u, v: v, depth: depth) - transform.add(vertex) - vertexPoints.append(point) - let idx = vertexIndex - vertexIndex += 1 - return idx - } - - func addVertex(point: CGPoint, depth: CGFloat = 0) -> Int { - let u = point.x / size.width - let v = point.y / size.height - return addVertex(u: u, v: v, depth: depth) - } - - func addQuadFace(_ i0: Int, _ i1: Int, _ i2: Int, _ i3: Int) { - let p0 = vertexPoints[i0] - let p1 = vertexPoints[i1] - let p2 = vertexPoints[i2] - let p3 = vertexPoints[i3] - - let sdf0 = roundedRectSDF(x: p0.x, y: p0.y, width: size.width, height: size.height, cornerRadius: clampedRadius) - let sdf1 = roundedRectSDF(x: p1.x, y: p1.y, width: size.width, height: size.height, cornerRadius: clampedRadius) - let sdf2 = roundedRectSDF(x: p2.x, y: p2.y, width: size.width, height: size.height, cornerRadius: clampedRadius) - let sdf3 = roundedRectSDF(x: p3.x, y: p3.y, width: size.width, height: size.height, cornerRadius: clampedRadius) - - if sdf0 > 0 && sdf1 > 0 && sdf2 > 0 && sdf3 > 0 { - return - } - - transform.add(MeshTransform.Face(indices: (UInt32(i0), UInt32(i1), UInt32(i2), UInt32(i3)), w: (0.0, 0.0, 0.0, 0.0))) - - if let wireframe { - wireframe.move(to: p0) - wireframe.addLine(to: p1) - wireframe.addLine(to: p2) - wireframe.addLine(to: p3) - wireframe.closeSubpath() - } - } - - // Utility to build a grid of vertices from 2D points and emit quads - func buildGrid(points: [[CGPoint]]) { - guard !points.isEmpty else { return } - - var indexGrid: [[Int]] = [] - for row in points { - var rowIndices: [Int] = [] - for point in row { - rowIndices.append(addVertex(point: point)) - } - indexGrid.append(rowIndices) - } - - let numRows = indexGrid.count - 1 - let numCols = indexGrid.first!.count - 1 - for row in 0.. [CGFloat] { - guard count > 0, maxRadius > 0 else { return [0, 1] } - let bandNorm = max(0, min(1, band / maxRadius)) - - // Evenly distribute inner rings up to (1 - bandNorm), then insert the outer strip edge and 1.0. - let innerSegments = max(1, count - 1) - let innerMax = max(0, 1 - bandNorm) - - var factors: [CGFloat] = (0...innerSegments).map { i in - innerMax * CGFloat(i) / CGFloat(innerSegments) - } - - func appendUnique(_ value: CGFloat) { - if let last = factors.last, abs(last - value) < 1e-4 { return } - factors.append(value) - } - - appendUnique(innerMax) - appendUnique(1.0) - - return factors - } - - let depthFactors = depthFactorsWithOuterBand(count: radialSteps, band: outerEdgeDistance, maxRadius: clampedRadius) // 0...1 - let angularFactors = (0...angularSteps).map { CGFloat($0) / CGFloat(angularSteps) } // 0...1 - - // Edge segmentation along the long axes; even spacing - let horizontalSegments = max(2, cornerResolution / 2 + 1) - let verticalSegments = max(2, cornerResolution / 2 + 1) - - func linearPositions(count: Int, start: CGFloat, end: CGFloat) -> [CGFloat] { - return (0...count).map { i in - let t = CGFloat(i) / CGFloat(count) - return start + (end - start) * t - } - } - - // Shared tangential coordinates for strips/center - let topXPositions: [CGFloat] = linearPositions( - count: horizontalSegments, - start: clampedRadius, - end: width - clampedRadius - ) - let sideYPositions: [CGFloat] = linearPositions( - count: verticalSegments, - start: clampedRadius, - end: height - clampedRadius - ) - - // Shared depth coordinates (outer -> inner) so seams line up without T-junctions - let outerToInner = depthFactors.reversed() - let topYPositions: [CGFloat] = outerToInner.map { clampedRadius * (1 - $0) } // 0 ... radius - let bottomYPositions: [CGFloat] = depthFactors.map { height - clampedRadius + clampedRadius * $0 } // (h-r) ... h - let leftXPositions: [CGFloat] = outerToInner.map { clampedRadius * (1 - $0) } // 0 ... radius - let rightXPositions: [CGFloat] = depthFactors.map { width - clampedRadius + clampedRadius * $0 } // (w-r) ... w - - // Corner wedges in polar space with an explicit center fan to avoid zero-area quads - func buildCorner(center: CGPoint, startAngle: CGFloat, endAngle: CGFloat) { - let ringRadials = outerToInner.filter { $0 > 0 } - guard !ringRadials.isEmpty else { return } - - func formatVertex(_ idx: Int) -> String { - let p = vertexPoints[idx] - return "\(idx)=\(String(format: "(%.2f, %.2f)", p.x, p.y))" - } - - // Generate ring vertices from outer arc toward the center point - var ringIndices: [[Int]] = [] - for radial in ringRadials { - let r = clampedRadius * radial - var row: [Int] = [] - for t in angularFactors { - let angle = startAngle + (endAngle - startAngle) * t - let x = center.x + r * cos(angle) - let y = center.y + r * sin(angle) - row.append(addVertex(point: CGPoint(x: x, y: y))) - } - ringIndices.append(row) - } - - // Quad rings between concentric samples - for r in 0..<(ringIndices.count - 1) { - let outerRing = ringIndices[r] - let innerRing = ringIndices[r + 1] - for i in 0..<(outerRing.count - 1) { - addQuadFace( - outerRing[i], - outerRing[i + 1], - innerRing[i + 1], - innerRing[i] - ) - } - } - - // Final collapse: merge two wedge slices into one quad anchored at the center. - // Each quad spans a double-width wedge: center -> v0 -> v1 -> v2 (contiguous along the arc). - if let innermostRing = ringIndices.last { - let ringSegments = innermostRing.count - 1 // last point is the arc end (not wrapped) - guard ringSegments >= 2 else { return } - - if debugLogCorner { - let formatted = innermostRing.map { formatVertex($0) }.joined(separator: ", ") - print("Corner collapse ringSegments=\(ringSegments) stride=2 angularSteps=\(angularSteps)") - print("Innermost ring vertices: \(formatted)") - } - - let centerAnchor = addVertex(point: center, depth: -0.02) - let stride = 2 - - // Each quad covers two arc segments: (vi, vi+1) and (vi+1, vi+2) - var i = 0 - while i + 2 <= ringSegments { - let v0 = innermostRing[i] - let v1 = innermostRing[i + 1] - let v2 = innermostRing[i + 2] - - if debugLogCorner { - print("Quad indices: [\(centerAnchor), \(v0), \(v1), \(v2)]") - } - - addQuadFace( - centerAnchor, - v0, - v1, - v2 - ) - - i += stride - } - - // Safety: if an odd segment remains, cap it with a final quad - if i < ringSegments { - let v0 = innermostRing[ringSegments - 1] - let v1 = innermostRing[ringSegments] - let v2 = innermostRing[ringSegments] // duplicate to keep quad valid - if debugLogCorner { - print("Quad indices (odd tail): [\(centerAnchor), \(v0), \(v1), \(v2)]") - } - addQuadFace(centerAnchor, v0, v1, v2) - } - } - } - - // Edge strips - func buildStrip(xPositions: [CGFloat], yPositions: [CGFloat]) { - var points: [[CGPoint]] = [] - for y in yPositions { - let row = xPositions.map { CGPoint(x: $0, y: y) } - points.append(row) - } - buildGrid(points: points) - } - - // Top / bottom strips - buildStrip(xPositions: topXPositions, yPositions: topYPositions) - buildStrip(xPositions: topXPositions, yPositions: bottomYPositions) - - // Left / right strips - buildStrip(xPositions: leftXPositions, yPositions: sideYPositions) - buildStrip(xPositions: rightXPositions, yPositions: sideYPositions) - - // Center patch uses the same tangential sampling to meet edges cleanly - buildStrip(xPositions: topXPositions, yPositions: sideYPositions) - - // Corners (angles chosen to keep columns increasing along +x) - buildCorner( - center: CGPoint(x: clampedRadius, y: clampedRadius), - startAngle: .pi, - endAngle: 1.5 * .pi - ) - buildCorner( - center: CGPoint(x: width - clampedRadius, y: clampedRadius), - startAngle: 1.5 * .pi, - endAngle: 2 * .pi - ) - buildCorner( - center: CGPoint(x: width - clampedRadius, y: height - clampedRadius), - startAngle: .pi / 2, - endAngle: 0 - ) - buildCorner( - center: CGPoint(x: clampedRadius, y: height - clampedRadius), - startAngle: .pi, - endAngle: .pi / 2 - ) - - transform.subdivisionSteps = 0 - return (mesh: transform, wireframe: wireframe) -} - private let backdropLayerClass: NSObject? = { let name = ("CA" as NSString).appendingFormat("BackdropLayer") if let cls = NSClassFromString(name as String) as AnyObject as? NSObject { @@ -670,11 +103,18 @@ final class LegacyGlassView: UIView { if let backdropLayer = self.backdropLayer { self.layer.addSublayer(backdropLayer) backdropLayer.delegate = self.backdropLayerDelegate - invokeBackdropLayerSetScaleMethod(object: backdropLayer, scale: 1.0) - backdropLayer.rasterizationScale = 1.0 + + let blur: CGFloat + let scale: CGFloat + + blur = 2.0 + scale = 1.0 + + invokeBackdropLayerSetScaleMethod(object: backdropLayer, scale: scale) + backdropLayer.rasterizationScale = scale if let blurFilter = CALayer.blur() { - blurFilter.setValue(2.0 as NSNumber, forKey: "inputRadius") + blurFilter.setValue(blur as NSNumber, forKey: "inputRadius") backdropLayer.filters = [blurFilter] } } @@ -720,7 +160,7 @@ final class LegacyGlassView: UIView { x2: 0.5806878306878306, y2: 0.873015873015873 ) - ).mesh.value + ).mesh.makeValue() if let meshTransform { if !transition.animation.isImmediate, let previousTransform = backdropLayer.value(forKey: "meshTransform") as? NSObject { diff --git a/submodules/TelegramUI/Components/HeaderPanelContainerComponent/Sources/HeaderPanelContainerComponent.swift b/submodules/TelegramUI/Components/HeaderPanelContainerComponent/Sources/HeaderPanelContainerComponent.swift index c5d3ac344c..6d7f77a83b 100644 --- a/submodules/TelegramUI/Components/HeaderPanelContainerComponent/Sources/HeaderPanelContainerComponent.swift +++ b/submodules/TelegramUI/Components/HeaderPanelContainerComponent/Sources/HeaderPanelContainerComponent.swift @@ -64,8 +64,6 @@ public final class HeaderPanelContainerComponent: Component { override init(frame: CGRect) { super.init(frame: frame) - - self.clipsToBounds = true } required init?(coder: NSCoder) { @@ -102,7 +100,6 @@ public final class HeaderPanelContainerComponent: Component { self.backgroundContainer.contentView.addSubview(self.backgroundView) self.addSubview(self.backgroundContainer) - self.contentContainer.clipsToBounds = true self.backgroundView.contentView.addSubview(self.contentContainer) } @@ -188,6 +185,7 @@ public final class HeaderPanelContainerComponent: Component { panelView.addSubview(panelComponentView) transition.animateAlpha(view: panelView, from: 0.0, to: 1.0) panelView.separator.opacity = 0.0 + panelView.clipsToBounds = true if isAnimatingReplacement { panelView.frame = panelFrame } else { @@ -197,7 +195,12 @@ public final class HeaderPanelContainerComponent: Component { panelView.separator.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor - transition.setFrame(view: panelView, frame: panelFrame) + let isFrameUpdated = panelComponentView.frame != panelFrame + transition.setFrame(view: panelView, frame: panelFrame, completion: { [weak panelView] completed in + if let panelView, completed, isFrameUpdated { + panelView.clipsToBounds = false + } + }) panelTransition.setFrame(view: panelComponentView, frame: CGRect(origin: CGPoint(), size: panelFrame.size)) panelTransition.setFrame(layer: panelView.separator, frame: CGRect(origin: panelFrame.origin, size: CGSize(width: panelFrame.width, height: UIScreenPixel))) @@ -219,6 +222,7 @@ public final class HeaderPanelContainerComponent: Component { separator?.removeFromSuperlayer() }) if !isAnimatingReplacement { + panelView.clipsToBounds = true transition.setFrame(view: panelView, frame: CGRect(origin: panelView.frame.origin, size: CGSize(width: panelView.bounds.width, height: 0.0))) } } diff --git a/submodules/TelegramUI/Components/LiquidLens/Sources/LiquidLensView.swift b/submodules/TelegramUI/Components/LiquidLens/Sources/LiquidLensView.swift index 040181f130..182541cad6 100644 --- a/submodules/TelegramUI/Components/LiquidLens/Sources/LiquidLensView.swift +++ b/submodules/TelegramUI/Components/LiquidLens/Sources/LiquidLensView.swift @@ -58,6 +58,12 @@ private final class RestingBackgroundView: UIVisualEffectView { } public final class LiquidLensView: UIView { + public enum Kind { + case externalContainer + case builtinContainer + case noContainer + } + private struct Params: Equatable { var size: CGSize var selectionX: CGFloat @@ -89,7 +95,7 @@ public final class LiquidLensView: UIView { private let containerView: UIView private let backgroundContainer: GlassBackgroundContainerView? private let genericBackgroundContainer: UIView? - private let backgroundView: GlassBackgroundView + private let backgroundView: GlassBackgroundView? private var lensView: UIView? private let liftedContainerView: UIView public let contentView: UIView @@ -119,18 +125,23 @@ public final class LiquidLensView: UIView { return self.params?.selectionWidth } - public init(useBackgroundContainer: Bool = true) { + public init(kind: Kind) { self.containerView = UIView() - if useBackgroundContainer { + switch kind { + case .builtinContainer: self.backgroundContainer = GlassBackgroundContainerView() self.genericBackgroundContainer = nil - } else { + case .externalContainer, .noContainer: self.backgroundContainer = nil self.genericBackgroundContainer = UIView() } - self.backgroundView = GlassBackgroundView() + if case .noContainer = kind { + self.backgroundView = nil + } else { + self.backgroundView = GlassBackgroundView() + } self.contentView = UIView() self.liftedContainerView = UIView() @@ -141,13 +152,19 @@ public final class LiquidLensView: UIView { if let backgroundContainer = self.backgroundContainer { self.addSubview(backgroundContainer) - backgroundContainer.contentView.addSubview(self.backgroundView) + if let backgroundView = self.backgroundView { + backgroundContainer.contentView.addSubview(backgroundView) + backgroundView.contentView.addSubview(self.containerView) + } } else if let genericBackgroundContainer = self.genericBackgroundContainer { self.addSubview(genericBackgroundContainer) - genericBackgroundContainer.addSubview(self.backgroundView) + if let backgroundView = self.backgroundView { + genericBackgroundContainer.addSubview(backgroundView) + backgroundView.contentView.addSubview(self.containerView) + } else { + genericBackgroundContainer.addSubview(self.containerView) + } } - - self.backgroundView.contentView.addSubview(self.containerView) self.containerView.isUserInteractionEnabled = false if #available(iOS 26.0, *) { @@ -168,7 +185,10 @@ public final class LiquidLensView: UIView { } lensView.layer.zPosition = 10.0 - self.liftedContainerView.addSubview(self.restingBackgroundView) + if case .noContainer = kind { + } else { + self.liftedContainerView.addSubview(self.restingBackgroundView) + } self.containerView.addSubview(self.liftedContainerView) self.containerView.addSubview(lensView) @@ -179,8 +199,11 @@ public final class LiquidLensView: UIView { } else if let genericBackgroundContainer = self.genericBackgroundContainer { lensView.perform(NSSelectorFromString("setLiftedContainerView:"), with: genericBackgroundContainer) } - lensView.perform(NSSelectorFromString("setLiftedContentView:"), with: self.liftedContainerView) - lensView.perform(NSSelectorFromString("setOverridePunchoutView:"), with: self.contentView) + if case .noContainer = kind { + } else { + lensView.perform(NSSelectorFromString("setLiftedContentView:"), with: self.liftedContainerView) + lensView.perform(NSSelectorFromString("setOverridePunchoutView:"), with: self.contentView) + } do { let selector = NSSelectorFromString("setLiftedContentMode:") @@ -211,9 +234,16 @@ public final class LiquidLensView: UIView { lensView.setValue(UIColor(white: 0.0, alpha: 0.1), forKey: "restingBackgroundColor") } else { - let legacySelectionView = GlassBackgroundView.ContentImageView() - self.legacySelectionView = legacySelectionView - self.backgroundView.contentView.insertSubview(legacySelectionView, at: 0) + if case .noContainer = kind { + } else { + let legacySelectionView = GlassBackgroundView.ContentImageView() + self.legacySelectionView = legacySelectionView + if let backgroundView = self.backgroundView { + backgroundView.contentView.insertSubview(legacySelectionView, at: 0) + } else { + self.containerView.insertSubview(legacySelectionView, at: 0) + } + } let legacyContentMaskView = UIView() legacyContentMaskView.backgroundColor = .white @@ -346,8 +376,10 @@ public final class LiquidLensView: UIView { transition.setFrame(view: genericBackgroundContainer, frame: CGRect(origin: CGPoint(), size: params.size)) } - transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: params.size)) - self.backgroundView.update(size: params.size, cornerRadius: params.size.height * 0.5, isDark: params.isDark, tintColor: GlassBackgroundView.TintColor.init(kind: .panel, color: UIColor(white: params.isDark ? 0.0 : 1.0, alpha: 0.6)), isInteractive: true, transition: transition) + if let backgroundView = self.backgroundView { + transition.setFrame(view: backgroundView, frame: CGRect(origin: CGPoint(), size: params.size)) + backgroundView.update(size: params.size, cornerRadius: params.size.height * 0.5, isDark: params.isDark, tintColor: GlassBackgroundView.TintColor.init(kind: .panel, color: UIColor(white: params.isDark ? 0.0 : 1.0, alpha: 0.6)), isInteractive: true, transition: transition) + } if self.contentView.bounds.size != params.size { self.contentView.clipsToBounds = true @@ -369,7 +401,7 @@ public final class LiquidLensView: UIView { transition.setCornerRadius(layer: self.liftedContainerView.layer, cornerRadius: params.size.height * 0.5) } - let baseLensFrame = CGRect(origin: CGPoint(x: max(0.0, min(params.selectionX, params.size.width - params.selectionWidth)), y: 0.0), size: CGSize(width: params.selectionWidth, height: params.size.height)) + let baseLensFrame = CGRect(origin: CGPoint(x: params.selectionX, y: 0.0), size: CGSize(width: params.selectionWidth, height: params.size.height)) self.updateLens(params: LensParams(baseFrame: baseLensFrame, isLifted: params.isLifted), animated: !transition.animation.isImmediate) if let legacyContentMaskView = self.legacyContentMaskView { diff --git a/submodules/TelegramUI/Components/MeshTransform/MeshTransformApi/PublicHeaders/MeshTransformApi/MeshTransformApi.h b/submodules/TelegramUI/Components/MeshTransform/MeshTransformApi/PublicHeaders/MeshTransformApi/MeshTransformApi.h index 799020aeae..63bac352cb 100644 --- a/submodules/TelegramUI/Components/MeshTransform/MeshTransformApi/PublicHeaders/MeshTransformApi/MeshTransformApi.h +++ b/submodules/TelegramUI/Components/MeshTransform/MeshTransformApi/PublicHeaders/MeshTransformApi/MeshTransformApi.h @@ -20,5 +20,16 @@ typedef struct MeshTransformMeshVertex { MeshTransformPoint3D to; } MeshTransformMeshVertex; +@protocol MeshTransformClass + +- (id)meshTransformWithVertexCount:(NSUInteger)vertexCount + vertices:(MeshTransformMeshVertex *)vertices + faceCount:(NSUInteger)faceCount + faces:(MeshTransformMeshFace *)faces + depthNormalization:(NSString *)depthNormalization; + +@end + +extern NSString * const kCADepthNormalizationNone; #endif diff --git a/submodules/TelegramUI/Components/MeshTransform/Sources/GenerateMesh.swift b/submodules/TelegramUI/Components/MeshTransform/Sources/GenerateMesh.swift new file mode 100644 index 0000000000..f02c10429d --- /dev/null +++ b/submodules/TelegramUI/Components/MeshTransform/Sources/GenerateMesh.swift @@ -0,0 +1,628 @@ +import Foundation +import UIKit + +private func a(_ a1: CGFloat, _ a2: CGFloat) -> CGFloat +{ + return 1.0 - 3.0 * a2 + 3.0 * a1 +} + +private func b(_ a1: CGFloat, _ a2: CGFloat) -> CGFloat +{ + return 3.0 * a2 - 6.0 * a1 +} + +private func c(_ a1: CGFloat) -> CGFloat +{ + return 3.0 * a1 +} + +private func calcBezier(_ t: CGFloat, _ a1: CGFloat, _ a2: CGFloat) -> CGFloat +{ + return ((a(a1, a2)*t + b(a1, a2))*t + c(a1)) * t +} + +private func calcSlope(_ t: CGFloat, _ a1: CGFloat, _ a2: CGFloat) -> CGFloat +{ + return 3.0 * a(a1, a2) * t * t + 2.0 * b(a1, a2) * t + c(a1) +} + +private func getTForX(_ x: CGFloat, _ x1: CGFloat, _ x2: CGFloat) -> CGFloat { + var t = x + var i = 0 + while i < 4 { + let currentSlope = calcSlope(t, x1, x2) + if currentSlope == 0.0 { + return t + } else { + let currentX = calcBezier(t, x1, x2) - x + t -= currentX / currentSlope + } + + i += 1 + } + + return t +} + +private func bezierPoint(_ x1: CGFloat, _ y1: CGFloat, _ x2: CGFloat, _ y2: CGFloat, _ x: CGFloat) -> CGFloat +{ + var value = calcBezier(getTForX(x, x1, x2), y1, y2) + if value >= 0.997 { + value = 1.0 + } + return value +} + +/// Bezier control points for displacement easing curve +public struct DisplacementBezier { + var x1: CGFloat + var y1: CGFloat + var x2: CGFloat + var y2: CGFloat + + public init(x1: CGFloat, y1: CGFloat, x2: CGFloat, y2: CGFloat) { + self.x1 = x1 + self.y1 = y1 + self.x2 = x2 + self.y2 = y2 + } +} + +/// Computes signed distance from a point to the edge of a rounded rectangle. +/// Returns negative inside, zero on edge, positive outside. +/// All values in points. +public func roundedRectSDF(x: CGFloat, y: CGFloat, width: CGFloat, height: CGFloat, cornerRadius: CGFloat) -> CGFloat { + // Center the point (SDF formula assumes center at origin) + let px = x - width / 2 + let py = y - height / 2 + + // Half extents of the box + let bx = width / 2 + let by = height / 2 + + // Standard rounded box SDF (Inigo Quilez formula) + let qx = abs(px) - bx + cornerRadius + let qy = abs(py) - by + cornerRadius + + let outsideDist = hypot(max(qx, 0), max(qy, 0)) + let insideDist = min(max(qx, qy), 0) + + return outsideDist + insideDist - cornerRadius +} + +/// Computes the gradient (outward normal) of the rounded rect SDF. +/// Returns normalized direction perpendicular to the nearest edge point. +public func roundedRectGradient(x: CGFloat, y: CGFloat, width: CGFloat, height: CGFloat, cornerRadius: CGFloat) -> (nx: CGFloat, ny: CGFloat) { + // Center the point + let px = x - width / 2 + let py = y - height / 2 + + // Half extents + let bx = width / 2 + let by = height / 2 + + // q values from SDF formula + let qx = abs(px) - bx + cornerRadius + let qy = abs(py) - by + cornerRadius + + var nx: CGFloat = 0 + var ny: CGFloat = 0 + + if qx > 0 && qy > 0 { + // Corner region - normal points radially from corner arc center + let d = hypot(qx, qy) + if d > 0 { + nx = qx / d + ny = qy / d + } + } else if qx > qy { + // Nearest point is on vertical edge (left or right) + nx = 1 + ny = 0 + } else { + // Nearest point is on horizontal edge (top or bottom) + nx = 0 + ny = 1 + } + + // Restore sign based on which side of center we're on + if px < 0 { nx = -nx } + if py < 0 { ny = -ny } + + return (nx, ny) +} + +/// Generates a displacement map image as a signed distance field from rounded rect edges. +/// - edgeDistance: The distance (in points) over which displacement is applied +/// - R channel: X displacement (127 = neutral, 0 = max left, 255 = max right) +/// - G channel: Y displacement (127 = neutral, 0 = max up, 255 = max down) +/// - B channel: Unused (always 0) +/// Displacement is maximum at the edge and fades linearly to zero at edgeDistance. +/// Actual displacement magnitude is applied when sampling the map. +public func generateDisplacementMap(size: CGSize, cornerRadius: CGFloat, edgeDistance: CGFloat, scale: CGFloat) -> CGImage? { + let width = Int(size.width * scale) + let height = Int(size.height * scale) + + // Clamp corner radius + let maxCornerRadius = min(size.width, size.height) / 2.0 + let clampedRadius = min(cornerRadius, maxCornerRadius) + + // Create bitmap context + var pixelData = [UInt8](repeating: 0, count: width * height * 4) + + for py in 0 ..< height { + for px in 0 ..< width { + // Convert pixel to point coordinates + let x = CGFloat(px) / scale + let y = CGFloat(py) / scale + + // Get signed distance (negative inside, positive outside) + let sdf = roundedRectSDF(x: x, y: y, width: size.width, height: size.height, cornerRadius: clampedRadius) + + // Get gradient (outward normal direction) + let (nx, ny) = roundedRectGradient(x: x, y: y, width: size.width, height: size.height, cornerRadius: clampedRadius) + + // Inward normal (content moves away from edge, toward center) + let inwardX = -nx + let inwardY = -ny + + // Distance from edge (positive inside the shape) + let distFromEdge = -sdf + + // Weight: 1 at edge, 0 at edgeDistance (linear falloff) + let weight = max(0, min(1, 1.0 - distFromEdge / edgeDistance)) + + // Displacement modulated by distance from edge + let displacementX = inwardX * weight + let displacementY = inwardY * weight + + // Encode in R/G: 127 = neutral, map -1..1 to 0..254 + let r = UInt8(max(0, min(255, Int(127 + displacementX * 127)))) + let g = UInt8(max(0, min(255, Int(127 + displacementY * 127)))) + + let idx = (py * width + px) * 4 + pixelData[idx + 0] = r // X displacement + pixelData[idx + 1] = g // Y displacement + pixelData[idx + 2] = 0 // Unused + pixelData[idx + 3] = 255 // A + } + } + + let colorSpace = CGColorSpaceCreateDeviceRGB() + guard let context = CGContext( + data: &pixelData, + width: width, + height: height, + bitsPerComponent: 8, + bytesPerRow: width * 4, + space: colorSpace, + bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue + ) else { + return nil + } + + return context.makeImage() +} + +/// Samples displacement from a displacement map with bilinear interpolation and bezier easing +/// - Parameters: +/// - x, y: Coordinates in the displacement map's pixel space +/// - pixels: Pointer to displacement map pixel data +/// - width, height: Displacement map dimensions +/// - bytesPerRow, bytesPerPixel: Displacement map layout +/// - bezier: Bezier control points for easing curve +/// - Returns: Displacement (dx, dy) in range -1..1 with bezier easing applied +public func sampleDisplacement( + x: CGFloat, + y: CGFloat, + pixels: UnsafePointer, + width: Int, + height: Int, + bytesPerRow: Int, + bytesPerPixel: Int, + bezier: DisplacementBezier +) -> (dx: CGFloat, dy: CGFloat) { + let clampedX = max(0, min(CGFloat(width - 1), x)) + let clampedY = max(0, min(CGFloat(height - 1), y)) + + let x0 = Int(clampedX) + let y0 = Int(clampedY) + let x1 = min(x0 + 1, width - 1) + let y1 = min(y0 + 1, height - 1) + + let fx = clampedX - CGFloat(x0) + let fy = clampedY - CGFloat(y0) + + func sample(_ sx: Int, _ sy: Int) -> (r: CGFloat, g: CGFloat) { + let offset = sy * bytesPerRow + sx * bytesPerPixel + return (CGFloat(pixels[offset + 0]), CGFloat(pixels[offset + 1])) + } + + let c00 = sample(x0, y0) + let c10 = sample(x1, y0) + let c01 = sample(x0, y1) + let c11 = sample(x1, y1) + + let r = (c00.r * (1 - fx) + c10.r * fx) * (1 - fy) + (c01.r * (1 - fx) + c11.r * fx) * fy + let g = (c00.g * (1 - fx) + c10.g * fx) * (1 - fy) + (c01.g * (1 - fx) + c11.g * fx) * fy + + // Decode: 127 = neutral, map 0..254 to -1..1 + var dx = (r - 127.0) / 127.0 + var dy = (g - 127.0) / 127.0 + + // Apply bezier easing to vector magnitude, preserving direction + let mag = hypot(dx, dy) + if mag > 0 { + let newMag = bezierPoint(bezier.x1, bezier.y1, bezier.x2, bezier.y2, mag) + let scale = newMag / mag + dx *= scale + dy *= scale + } + + return (dx, dy) +} + +/// Generates a glass mesh with corner-aware topology. +/// - 4 radial corner wedges sampled in polar space +/// - 4 edge strips aligned with the rectangle sides +/// - 1 center patch +/// Corner/edge seams share the same coordinates (but do not reuse vertices) so +/// the neighbouring faces fit perfectly without T-junctions. +public func generateGlassMeshFromDisplacementMap( + size: CGSize, + cornerRadius: CGFloat, + displacementMap: CGImage, + displacementMagnitudeU: CGFloat, + displacementMagnitudeV: CGFloat, + cornerResolution: Int, + outerEdgeDistance: CGFloat, + bezier: DisplacementBezier, + generateWireframe: Bool = false +) -> (mesh: MeshTransform, wireframe: CGPath?) { + guard let dispDataProvider = displacementMap.dataProvider, + let dispData = dispDataProvider.data, + let dispPixels = CFDataGetBytePtr(dispData) else { + return (mesh: MeshTransform(), wireframe: nil) + } + + let dispWidth = displacementMap.width + let dispHeight = displacementMap.height + let dispBytesPerRow = displacementMap.bytesPerRow + let dispBytesPerPixel = displacementMap.bitsPerPixel / 8 + + let clampedRadius = min(cornerRadius, min(size.width, size.height) / 2) + + let transform = MeshTransform() + var wireframe: CGMutablePath? + if generateWireframe { + wireframe = CGMutablePath() + } + + // Debug flags + let debugNoDisplacement = false + let debugLogCorner = false + + // Inset the mesh slightly (1 pixel) to clear the clip mask + let insetPoints = -1.0 + let usableWidth = max(1.0, size.width - insetPoints * 2) + let usableHeight = max(1.0, size.height - insetPoints * 2) + let insetUOffset = insetPoints / size.width + let insetVOffset = insetPoints / size.height + let usableUNorm = usableWidth / size.width + let usableVNorm = usableHeight / size.height + + // Helper to sample displacement and create vertex + func makeVertex(u: CGFloat, v: CGFloat, depth: CGFloat = 0) -> (vertex: MeshTransform.Vertex, point: CGPoint) { + let mappedU = insetUOffset + u * usableUNorm + let mappedV = insetVOffset + v * usableVNorm + let fromX: CGFloat + let fromY: CGFloat + + if debugNoDisplacement { + fromX = mappedU + fromY = mappedV + } else { + let (dispX, dispY) = sampleDisplacement( + x: mappedU * CGFloat(dispWidth - 1), + y: mappedV * CGFloat(dispHeight - 1), + pixels: dispPixels, + width: dispWidth, + height: dispHeight, + bytesPerRow: dispBytesPerRow, + bytesPerPixel: dispBytesPerPixel, + bezier: bezier + ) + + // Slight boost near the edge to emphasize the outer strip (rounded-corner aware) + let worldX = insetPoints + u * usableWidth + let worldY = insetPoints + v * usableHeight + let sdf = roundedRectSDF(x: worldX, y: worldY, width: size.width, height: size.height, cornerRadius: clampedRadius) + let distToEdge = max(0.0, -sdf) // distance inside the rounded rect to the edge + let edgeBand = max(0.0, outerEdgeDistance) + let edgeBoostGain: CGFloat = 0.5 // up to +50% displacement at the edge, fades inside + let edgeBoost: CGFloat + if edgeBand > 0 { + let t = max(0.0, min(1.0, (edgeBand - distToEdge) / edgeBand)) + let eased = t * t * (3 - 2 * t) // smoothstep + edgeBoost = 1.0 + eased * edgeBoostGain + } else { + edgeBoost = 1.0 + } + + fromX = max(0.0, min(1.0, mappedU + dispX * displacementMagnitudeU * edgeBoost)) + fromY = max(0.0, min(1.0, mappedV + dispY * displacementMagnitudeV * edgeBoost)) + } + + let vertex = MeshTransform.Vertex(from: CGPoint(x: fromX, y: fromY), to: MeshTransform.Point3D(x: mappedU, y: mappedV, z: depth)) + return (vertex, CGPoint(x: mappedU * size.width, y: mappedV * size.height)) + } + + var vertexIndex = 0 + var vertexPoints: [CGPoint] = [] + + func addVertex(u: CGFloat, v: CGFloat, depth: CGFloat = 0) -> Int { + let (vertex, point) = makeVertex(u: u, v: v, depth: depth) + transform.add(vertex) + vertexPoints.append(point) + let idx = vertexIndex + vertexIndex += 1 + return idx + } + + func addVertex(point: CGPoint, depth: CGFloat = 0) -> Int { + let u = point.x / size.width + let v = point.y / size.height + return addVertex(u: u, v: v, depth: depth) + } + + func addQuadFace(_ i0: Int, _ i1: Int, _ i2: Int, _ i3: Int) { + let p0 = vertexPoints[i0] + let p1 = vertexPoints[i1] + let p2 = vertexPoints[i2] + let p3 = vertexPoints[i3] + + let sdf0 = roundedRectSDF(x: p0.x, y: p0.y, width: size.width, height: size.height, cornerRadius: clampedRadius) + let sdf1 = roundedRectSDF(x: p1.x, y: p1.y, width: size.width, height: size.height, cornerRadius: clampedRadius) + let sdf2 = roundedRectSDF(x: p2.x, y: p2.y, width: size.width, height: size.height, cornerRadius: clampedRadius) + let sdf3 = roundedRectSDF(x: p3.x, y: p3.y, width: size.width, height: size.height, cornerRadius: clampedRadius) + + if sdf0 > 0 && sdf1 > 0 && sdf2 > 0 && sdf3 > 0 { + return + } + + transform.add(MeshTransform.Face(indices: (UInt32(i0), UInt32(i1), UInt32(i2), UInt32(i3)), w: (0.0, 0.0, 0.0, 0.0))) + + if let wireframe { + wireframe.move(to: p0) + wireframe.addLine(to: p1) + wireframe.addLine(to: p2) + wireframe.addLine(to: p3) + wireframe.closeSubpath() + } + } + + // Utility to build a grid of vertices from 2D points and emit quads + func buildGrid(points: [[CGPoint]]) { + guard !points.isEmpty else { return } + + var indexGrid: [[Int]] = [] + for row in points { + var rowIndices: [Int] = [] + for point in row { + rowIndices.append(addVertex(point: point)) + } + indexGrid.append(rowIndices) + } + + let numRows = indexGrid.count - 1 + let numCols = indexGrid.first!.count - 1 + for row in 0.. [CGFloat] { + guard count > 0, maxRadius > 0 else { return [0, 1] } + let bandNorm = max(0, min(1, band / maxRadius)) + + // Evenly distribute inner rings up to (1 - bandNorm), then insert the outer strip edge and 1.0. + let innerSegments = max(1, count - 1) + let innerMax = max(0, 1 - bandNorm) + + var factors: [CGFloat] = (0...innerSegments).map { i in + innerMax * CGFloat(i) / CGFloat(innerSegments) + } + + func appendUnique(_ value: CGFloat) { + if let last = factors.last, abs(last - value) < 1e-4 { return } + factors.append(value) + } + + appendUnique(innerMax) + appendUnique(1.0) + + return factors + } + + let depthFactors = depthFactorsWithOuterBand(count: radialSteps, band: outerEdgeDistance, maxRadius: clampedRadius) // 0...1 + let angularFactors = (0...angularSteps).map { CGFloat($0) / CGFloat(angularSteps) } // 0...1 + + // Edge segmentation along the long axes; even spacing + let horizontalSegments = max(2, cornerResolution / 2 + 1) + let verticalSegments = max(2, cornerResolution / 2 + 1) + + func linearPositions(count: Int, start: CGFloat, end: CGFloat) -> [CGFloat] { + return (0...count).map { i in + let t = CGFloat(i) / CGFloat(count) + return start + (end - start) * t + } + } + + // Shared tangential coordinates for strips/center + let topXPositions: [CGFloat] = linearPositions( + count: horizontalSegments, + start: clampedRadius, + end: width - clampedRadius + ) + let sideYPositions: [CGFloat] = linearPositions( + count: verticalSegments, + start: clampedRadius, + end: height - clampedRadius + ) + + // Shared depth coordinates (outer -> inner) so seams line up without T-junctions + let outerToInner = depthFactors.reversed() + let topYPositions: [CGFloat] = outerToInner.map { clampedRadius * (1 - $0) } // 0 ... radius + let bottomYPositions: [CGFloat] = depthFactors.map { height - clampedRadius + clampedRadius * $0 } // (h-r) ... h + let leftXPositions: [CGFloat] = outerToInner.map { clampedRadius * (1 - $0) } // 0 ... radius + let rightXPositions: [CGFloat] = depthFactors.map { width - clampedRadius + clampedRadius * $0 } // (w-r) ... w + + // Corner wedges in polar space with an explicit center fan to avoid zero-area quads + func buildCorner(center: CGPoint, startAngle: CGFloat, endAngle: CGFloat) { + let ringRadials = outerToInner.filter { $0 > 0 } + guard !ringRadials.isEmpty else { return } + + func formatVertex(_ idx: Int) -> String { + let p = vertexPoints[idx] + return "\(idx)=\(String(format: "(%.2f, %.2f)", p.x, p.y))" + } + + // Generate ring vertices from outer arc toward the center point + var ringIndices: [[Int]] = [] + for radial in ringRadials { + let r = clampedRadius * radial + var row: [Int] = [] + for t in angularFactors { + let angle = startAngle + (endAngle - startAngle) * t + let x = center.x + r * cos(angle) + let y = center.y + r * sin(angle) + row.append(addVertex(point: CGPoint(x: x, y: y))) + } + ringIndices.append(row) + } + + // Quad rings between concentric samples + for r in 0..<(ringIndices.count - 1) { + let outerRing = ringIndices[r] + let innerRing = ringIndices[r + 1] + for i in 0..<(outerRing.count - 1) { + addQuadFace( + outerRing[i], + outerRing[i + 1], + innerRing[i + 1], + innerRing[i] + ) + } + } + + // Final collapse: merge two wedge slices into one quad anchored at the center. + // Each quad spans a double-width wedge: center -> v0 -> v1 -> v2 (contiguous along the arc). + if let innermostRing = ringIndices.last { + let ringSegments = innermostRing.count - 1 // last point is the arc end (not wrapped) + guard ringSegments >= 2 else { return } + + if debugLogCorner { + let formatted = innermostRing.map { formatVertex($0) }.joined(separator: ", ") + print("Corner collapse ringSegments=\(ringSegments) stride=2 angularSteps=\(angularSteps)") + print("Innermost ring vertices: \(formatted)") + } + + let centerAnchor = addVertex(point: center, depth: -0.02) + let stride = 2 + + // Each quad covers two arc segments: (vi, vi+1) and (vi+1, vi+2) + var i = 0 + while i + 2 <= ringSegments { + let v0 = innermostRing[i] + let v1 = innermostRing[i + 1] + let v2 = innermostRing[i + 2] + + if debugLogCorner { + print("Quad indices: [\(centerAnchor), \(v0), \(v1), \(v2)]") + } + + addQuadFace( + centerAnchor, + v0, + v1, + v2 + ) + + i += stride + } + + // Safety: if an odd segment remains, cap it with a final quad + if i < ringSegments { + let v0 = innermostRing[ringSegments - 1] + let v1 = innermostRing[ringSegments] + let v2 = innermostRing[ringSegments] // duplicate to keep quad valid + if debugLogCorner { + print("Quad indices (odd tail): [\(centerAnchor), \(v0), \(v1), \(v2)]") + } + addQuadFace(centerAnchor, v0, v1, v2) + } + } + } + + // Edge strips + func buildStrip(xPositions: [CGFloat], yPositions: [CGFloat]) { + var points: [[CGPoint]] = [] + for y in yPositions { + let row = xPositions.map { CGPoint(x: $0, y: y) } + points.append(row) + } + buildGrid(points: points) + } + + // Top / bottom strips + buildStrip(xPositions: topXPositions, yPositions: topYPositions) + buildStrip(xPositions: topXPositions, yPositions: bottomYPositions) + + // Left / right strips + buildStrip(xPositions: leftXPositions, yPositions: sideYPositions) + buildStrip(xPositions: rightXPositions, yPositions: sideYPositions) + + // Center patch uses the same tangential sampling to meet edges cleanly + buildStrip(xPositions: topXPositions, yPositions: sideYPositions) + + // Corners (angles chosen to keep columns increasing along +x) + buildCorner( + center: CGPoint(x: clampedRadius, y: clampedRadius), + startAngle: .pi, + endAngle: 1.5 * .pi + ) + buildCorner( + center: CGPoint(x: width - clampedRadius, y: clampedRadius), + startAngle: 1.5 * .pi, + endAngle: 2 * .pi + ) + buildCorner( + center: CGPoint(x: width - clampedRadius, y: height - clampedRadius), + startAngle: .pi / 2, + endAngle: 0 + ) + buildCorner( + center: CGPoint(x: clampedRadius, y: height - clampedRadius), + startAngle: .pi, + endAngle: .pi / 2 + ) + + transform.subdivisionSteps = 0 + return (mesh: transform, wireframe: wireframe) +} diff --git a/submodules/TelegramUI/Components/MeshTransform/Sources/MeshTransform.swift b/submodules/TelegramUI/Components/MeshTransform/Sources/MeshTransform.swift index c2288be2eb..55cb175299 100644 --- a/submodules/TelegramUI/Components/MeshTransform/Sources/MeshTransform.swift +++ b/submodules/TelegramUI/Components/MeshTransform/Sources/MeshTransform.swift @@ -10,6 +10,14 @@ private let transformClass: NSObject? = { return nil }() +private let immutableTransformClass: NSObject? = { + let name = ("CA" as NSString).appendingFormat("MeshTransform") + if let cls = NSClassFromString(name as String) as AnyObject as? NSObject { + return cls + } + return nil +}() + @inline(__always) private func getMethod(object: NSObject, selector: String) -> T? { guard let method = object.method(for: NSSelectorFromString(selector)) else { @@ -80,33 +88,55 @@ private func invokeTransformSetSubdivisionStepsMethod(object: NSObject, value: I } public final class MeshTransform { + public typealias Value = NSObject + public typealias Point3D = MeshTransformPoint3D public typealias Vertex = MeshTransformMeshVertex public typealias Face = MeshTransformMeshFace - public let value: NSObject? + private var vertices: ContiguousArray = [] + private var faces: ContiguousArray = [] public var subdivisionSteps: Int = -1 { didSet { - if self.subdivisionSteps != oldValue, let value { + /*if self.subdivisionSteps != oldValue, let value { invokeTransformSetSubdivisionStepsMethod(object: value, value: self.subdivisionSteps) - } + }*/ } } public init() { - self.value = invokeTransformCreateMethod() } public func add(_ vertex: Vertex) { - if let value = self.value { + /*if let value = self.value { invokeTransformAddVertexMethod(object: value, vertex: vertex) - } + }*/ + self.vertices.append(vertex) } public func add(_ face: Face) { - if let value = self.value { + /*if let value = self.value { invokeTransformAddFaceMethod(object: value, face: face) + }*/ + self.faces.append(face) + } + + public consuming func makeValue() -> Value? { + guard let transformClass else { + return nil } + let value = unsafeBitCast(transformClass, to: MeshTransformClass.self) + let result = self.vertices.withUnsafeMutableBufferPointer { vertices -> NSObject? in + return self.faces.withUnsafeMutableBufferPointer { faces -> NSObject? in + let result = value.meshTransform(withVertexCount: UInt(vertices.count), vertices: vertices.baseAddress!, faceCount: UInt(faces.count), faces: faces.baseAddress!, depthNormalization: kCADepthNormalizationNone) + return result as? NSObject + } + } + if let result { + invokeTransformSetSubdivisionStepsMethod(object: result, value: self.subdivisionSteps) + } + + return result } } diff --git a/submodules/TelegramUI/Components/NavigationBarImpl/Sources/NavigationBarImpl.swift b/submodules/TelegramUI/Components/NavigationBarImpl/Sources/NavigationBarImpl.swift index c777c212b0..a1fe774c24 100644 --- a/submodules/TelegramUI/Components/NavigationBarImpl/Sources/NavigationBarImpl.swift +++ b/submodules/TelegramUI/Components/NavigationBarImpl/Sources/NavigationBarImpl.swift @@ -548,6 +548,8 @@ public final class NavigationBarImpl: ASDisplayNode, NavigationBar { } } + public var customOverBackgroundContentSubview: UIView? + public init(presentationData: NavigationBarPresentationData) { self.presentationData = presentationData self.stripeNode = ASDisplayNode() diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift index f3918590f6..0886211e8c 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift @@ -55,17 +55,17 @@ final class UserAppearanceScreenComponent: Component { } let context: AccountContext + let overNavigationContainer: UIView init( - context: AccountContext + context: AccountContext, + overNavigationContainer: UIView ) { self.context = context + self.overNavigationContainer = overNavigationContainer } static func ==(lhs: UserAppearanceScreenComponent, rhs: UserAppearanceScreenComponent) -> Bool { - if lhs.context !== rhs.context { - return false - } return true } @@ -166,8 +166,6 @@ final class UserAppearanceScreenComponent: Component { private let actionButton = ComponentView() private let edgeEffectView: EdgeEffectView - private let backButton = PeerInfoHeaderNavigationButton() - private let tabSelector = ComponentView() enum Section: Int32 { case profile @@ -263,12 +261,6 @@ final class UserAppearanceScreenComponent: Component { self.containerView.addSubview(self.previewShadowView) self.addSubview(self.edgeEffectView) - - self.backButton.action = { [weak self] _, _ in - if let self, let controller = self.environment?.controller() as? UserAppearanceScreen { - controller.backPressed() - } - } } required init?(coder: NSCoder) { @@ -963,6 +955,8 @@ final class UserAppearanceScreenComponent: Component { self.component = component self.state = state + transition.setFrame(view: component.overNavigationContainer, frame: CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: environment.navigationHeight))) + let theme = environment.theme var animateTabChange = false @@ -1072,16 +1066,6 @@ final class UserAppearanceScreenComponent: Component { .withUpdatedProfileBackgroundEmojiId(resolvedState.backgroundFileId) ) } - - let backSize = self.backButton.update(key: .back, presentationData: component.context.sharedContext.currentPresentationData.with { $0 }, height: 44.0) - - self.backButton.updateContentsColor(backgroundColor: .clear, contentsColor: environment.theme.rootController.navigationBar.accentTextColor, canBeExpanded: true, transition: .animated(duration: 0.2, curve: .easeInOut)) - self.backButton.frame = CGRect(origin: CGPoint(x: environment.safeInsets.left + 16.0, y: environment.navigationHeight - 44.0), size: backSize) - if self.backButton.view.superview == nil { - if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar { - navigationBar.view.addSubview(self.backButton.view) - } - } var previewTransition = transition let transitionScale = (availableSize.height - 3.0) / availableSize.height @@ -1148,10 +1132,10 @@ final class UserAppearanceScreenComponent: Component { environment: {}, containerSize: CGSize(width: availableSize.width, height: 44.0) ) - let tabSelectorFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - tabSelectorSize.width) / 2.0), y: environment.statusBarHeight + floorToScreenPixels((environment.navigationHeight - environment.statusBarHeight - tabSelectorSize.height) / 2.0)), size: tabSelectorSize) + let tabSelectorFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - tabSelectorSize.width) / 2.0), y: environment.statusBarHeight + 2.0 + floorToScreenPixels((environment.navigationHeight - environment.statusBarHeight - tabSelectorSize.height) / 2.0)), size: tabSelectorSize) if let tabSelectorView = self.tabSelector.view { if tabSelectorView.superview == nil { - self.addSubview(tabSelectorView) + component.overNavigationContainer.addSubview(tabSelectorView) } transition.setFrame(view: tabSelectorView, frame: tabSelectorFrame) } @@ -1932,6 +1916,8 @@ final class UserAppearanceScreenComponent: Component { public class UserAppearanceScreen: ViewControllerComponentContainer { private let context: AccountContext + private let overNavigationContainer: UIView + private var didSetReady: Bool = false public init( @@ -1940,8 +1926,11 @@ public class UserAppearanceScreen: ViewControllerComponentContainer { ) { self.context = context + self.overNavigationContainer = SparseContainerView() + super.init(context: context, component: UserAppearanceScreenComponent( - context: context + context: context, + overNavigationContainer: self.overNavigationContainer ), navigationBarAppearance: .default, theme: .default, updatedPresentationData: updatedPresentationData) self.automaticallyControlPresentationContextLayout = false @@ -1951,7 +1940,6 @@ public class UserAppearanceScreen: ViewControllerComponentContainer { let presentationData = context.sharedContext.currentPresentationData.with { $0 } self.title = "" self.navigationItem.backBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) - self.navigationItem.leftBarButtonItem = UIBarButtonItem(customView: UIView()) self.ready.set(.never()) @@ -1969,6 +1957,10 @@ public class UserAppearanceScreen: ViewControllerComponentContainer { return componentView.attemptNavigation(complete: complete) } + + if let navigationBar = self.navigationBar { + navigationBar.view.insertSubview(self.overNavigationContainer, aboveSubview: navigationBar.backgroundView) + } } required public init(coder aDecoder: NSCoder) { diff --git a/submodules/TelegramUI/Components/TabBarComponent/Sources/TabBarComponent.swift b/submodules/TelegramUI/Components/TabBarComponent/Sources/TabBarComponent.swift index 5a03f510eb..81bed27b27 100644 --- a/submodules/TelegramUI/Components/TabBarComponent/Sources/TabBarComponent.swift +++ b/submodules/TelegramUI/Components/TabBarComponent/Sources/TabBarComponent.swift @@ -420,7 +420,7 @@ public final class TabBarComponent: Component { public override init(frame: CGRect) { self.backgroundContainer = GlassBackgroundContainerView() - self.liquidLensView = LiquidLensView(useBackgroundContainer: false) + self.liquidLensView = LiquidLensView(kind: .externalContainer) self.contextGestureContainerView = ContextControllerSourceView() self.contextGestureContainerView.isGestureEnabled = true