import UIKit extension RDEPUBReaderController { func presentAttachmentTooltip(text: String, sourceView: UIView, sourceRect: CGRect, sourcePoint: CGPoint) { hideAttachmentTooltipIfNeeded() let overlay = RDEPUBAttachmentTooltipOverlayView(frame: view.bounds) overlay.autoresizingMask = [.flexibleWidth, .flexibleHeight] overlay.onBackgroundTap = { [weak self, weak overlay] in guard let self, let overlay else { return } self.dismissAttachmentTooltipOverlay(overlay) } let tooltip = RDEPUBAttachmentTooltipView() tooltip.alpha = 0 let horizontalPadding = max(view.safeAreaInsets.left, view.safeAreaInsets.right) + 12 tooltip.configure(text: text, maxWidth: min(view.bounds.width - horizontalPadding * 2, 320)) let anchorRect = sourceView.convert(sourceRect, to: view) let rawAnchorPoint = sourceView.convert(sourcePoint, to: view) let anchorPoint = CGPoint( x: min(max(rawAnchorPoint.x, anchorRect.minX), anchorRect.maxX), y: min(max(rawAnchorPoint.y, anchorRect.minY), anchorRect.maxY) ) let verticalSpacing: CGFloat = 6 let tooltipSize = tooltip.frame.size let idealX = anchorPoint.x - tooltipSize.width / 2 let minX = horizontalPadding let maxX = max(minX, view.bounds.width - horizontalPadding - tooltipSize.width) let originX = min(max(idealX, minX), maxX) let topSafeY = view.safeAreaInsets.top + 12 let bottomSafeY = view.bounds.height - view.safeAreaInsets.bottom - 12 let availableSpaceAbove = anchorRect.minY - topSafeY let availableSpaceBelow = bottomSafeY - anchorRect.maxY let prefersAbove = availableSpaceAbove >= tooltipSize.height + verticalSpacing || availableSpaceAbove >= availableSpaceBelow let tooltipPlacement: RDEPUBAttachmentTooltipView.ArrowPlacement = prefersAbove ? .bottom : .top let originY: CGFloat switch tooltipPlacement { case .bottom: originY = max(topSafeY, anchorRect.minY - tooltipSize.height - verticalSpacing) case .top: originY = min(bottomSafeY - tooltipSize.height, anchorRect.maxY + verticalSpacing) } let arrowTipX = min( max(anchorPoint.x - originX, tooltip.minimumArrowX), tooltipSize.width - tooltip.minimumArrowX ) tooltip.setArrowTipX(arrowTipX, placement: tooltipPlacement) tooltip.frame.origin = CGPoint(x: originX, y: originY) overlay.addSubview(tooltip) view.addSubview(overlay) UIView.animate(withDuration: 0.2) { tooltip.alpha = 1 } DispatchQueue.main.asyncAfter(deadline: .now() + 3.5) { [weak self, weak overlay] in guard let self, let overlay else { return } self.dismissAttachmentTooltipOverlay(overlay) } } private func hideAttachmentTooltipIfNeeded() { view.subviews .compactMap { $0 as? RDEPUBAttachmentTooltipOverlayView } .forEach { $0.removeFromSuperview() } } private func dismissAttachmentTooltipOverlay(_ overlay: RDEPUBAttachmentTooltipOverlayView) { UIView.animate(withDuration: 0.18, animations: { overlay.tooltipView?.alpha = 0 }, completion: { _ in overlay.removeFromSuperview() }) } }