Files
ghostgram/submodules/Display/Source/ContextMenuSourceLease.swift
ichmagmaus111andClaude Opus 5.5 0622057a4f Ghostgram: open-source release
Ghostgram for iOS, rebuilt from scratch on Telegram iOS 13.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 17:57:09 +01:00

129 lines
5.1 KiB
Swift

import UIKit
import ObjectiveC
private var sourceStateKey: UInt8 = 0
private var decorationStateKey: UInt8 = 0
private final class ContextMenuSourceState {
var owners = 0
var content: ContextMenuSourceContent?
}
private final class ContextMenuDecorationState {
var owners = 0
let wasHidden: Bool
init(view: UIView) {
self.wasHidden = view.isHidden
}
}
/// Keeps separately rendered source decorations hidden until the last menu returns.
/// Overlapping menus share the same live content until the last claim ends.
public final class ContextMenuSourceLease {
private let source: UIView
private let state: ContextMenuSourceState
private let decorations: [(UIView, ContextMenuDecorationState)]
public var content: ContextMenuSourceContent? { self.state.content }
public init(source: UIView, decorations: [UIView], makeContent: (() -> ContextMenuSourceContent?)? = nil) {
assert(Thread.isMainThread)
self.source = source
let state: ContextMenuSourceState
if let current = objc_getAssociatedObject(source, &sourceStateKey) as? ContextMenuSourceState {
state = current
} else {
state = ContextMenuSourceState()
state.content = makeContent?()
objc_setAssociatedObject(source, &sourceStateKey, state, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
self.state = state
state.owners += 1
var claims: [(UIView, ContextMenuDecorationState)] = []
var seen = Set<ObjectIdentifier>()
for view in decorations where seen.insert(ObjectIdentifier(view)).inserted {
let claim: ContextMenuDecorationState
if let current = objc_getAssociatedObject(view, &decorationStateKey) as? ContextMenuDecorationState {
claim = current
} else {
claim = ContextMenuDecorationState(view: view)
objc_setAssociatedObject(view, &decorationStateKey, claim, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
claim.owners += 1
view.isHidden = true
claims.append((view, claim))
}
self.decorations = claims
}
deinit {
assert(Thread.isMainThread)
for (view, claim) in self.decorations {
claim.owners -= 1
if claim.owners == 0 {
view.isHidden = claim.wasHidden
objc_setAssociatedObject(view, &decorationStateKey, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
self.state.owners -= 1
if self.state.owners == 0 {
self.state.content?.restore()
objc_setAssociatedObject(self.source, &sourceStateKey, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}
/// Temporarily hosts the actual foreground outside its pressed button ancestor.
/// A matching live backdrop travels with it; no pixels are captured or frozen.
public final class ContextMenuSourceContent {
public let view = UIView()
private weak var source: UIView?
private weak var originalParent: UIView?
private let originalIndex: Int
private let foreground: UIView
private var restored = false
private var onRestore: (() -> Void)?
public init(source: UIView, foreground: UIView, background: UIView, container: UIView, onRestore: (() -> Void)? = nil) {
self.onRestore = onRestore
self.source = source
self.foreground = foreground
self.originalParent = foreground.superview
self.originalIndex = foreground.superview?.subviews.firstIndex(of: foreground) ?? 0
self.view.isUserInteractionEnabled = false
self.view.addSubview(background)
// Detach first so node-backed views reconcile their old node hierarchy
// before entering a plain UIView. Direct reparenting makes ASDisplayKit
// remove the view again from didMoveToSuperview.
foreground.removeFromSuperview()
self.view.addSubview(foreground)
container.addSubview(self.view)
self.updateGeometry()
}
public func updateGeometry() {
guard !self.restored, let source = self.source, let parent = self.view.superview else { return }
let origin = source.convert(CGPoint.zero, to: parent)
let x = source.convert(CGPoint(x: 1, y: 0), to: parent)
let y = source.convert(CGPoint(x: 0, y: 1), to: parent)
self.view.bounds = source.bounds
self.view.center = source.convert(CGPoint(x: source.bounds.midX, y: source.bounds.midY), to: parent)
self.view.transform = CGAffineTransform(a: x.x - origin.x, b: x.y - origin.y, c: y.x - origin.x, d: y.y - origin.y, tx: 0, ty: 0)
}
fileprivate func restore() {
guard !self.restored else { return }
self.restored = true
if let parent = self.originalParent, self.foreground.superview === self.view {
self.foreground.removeFromSuperview()
parent.insertSubview(self.foreground, at: min(self.originalIndex, parent.subviews.count))
}
self.view.removeFromSuperview()
self.onRestore?()
self.onRestore = nil
}
deinit { self.restore() }
}