import UIKit #if canImport(DTCoreText) import DTCoreText #endif /// Chapter-level shared display content and layouter /// (LONG_CHAPTER_MEMORY_OPTIMIZATION_PLAN.md P1-1 / P1-3). /// /// One entry holds the chapter-length display string (theme text color and /// dark-image adjustment injected once over the full chapter at build time) /// plus the DTCoreTextLayouter wrapping its framesetter. Page views reference /// the shared string and request per-page layout frames; they must never /// mutate the shared string — the framesetter and any live layout frame hold /// it. Page-level decoration (highlights, search, selection) is drawn by the /// overlay views instead. /// /// Main-thread only. final class RDEPUBChapterDisplayContentCache { #if canImport(DTCoreText) struct Entry { let content: NSAttributedString let layouter: DTCoreTextLayouter? fileprivate let signature: Signature } fileprivate struct Signature: Equatable { let chapterContentID: ObjectIdentifier let contentLength: Int let textColor: UIColor let backgroundColor: UIColor let darkImageAdjustmentEnabled: Bool let darkImageBlendRatio: CGFloat init(page: RDEPUBTextPage, configuration: RDEPUBReaderConfiguration) { chapterContentID = ObjectIdentifier(page.chapterContent) contentLength = page.chapterContent.length textColor = configuration.theme.contentTextColor backgroundColor = configuration.theme.contentBackgroundColor darkImageAdjustmentEnabled = configuration.darkImageAdjustmentEnabled darkImageBlendRatio = configuration.darkImageBlendRatio } } /// Current chapter plus one adjacent chapter during page-curl/scroll /// transitions across a chapter boundary. private static let capacity = 2 private var entries: [String: Entry] = [:] private var accessOrder: [String] = [] func entry(for page: RDEPUBTextPage, configuration: RDEPUBReaderConfiguration) -> Entry { dispatchPrecondition(condition: .onQueue(.main)) let signature = Signature(page: page, configuration: configuration) if let cached = entries[page.href], cached.signature == signature { touch(page.href) return cached } let entry = Self.buildEntry(page: page, configuration: configuration, signature: signature) entries[page.href] = entry touch(page.href) evictIfNeeded() RDEPUBMemoryProbe.log("displayContentBuilt href=\(page.href) length=\(entry.content.length)") return entry } func removeAll() { entries.removeAll() accessOrder.removeAll() } private func touch(_ href: String) { accessOrder.removeAll { $0 == href } accessOrder.append(href) } private func evictIfNeeded() { while accessOrder.count > Self.capacity { let evicted = accessOrder.removeFirst() entries.removeValue(forKey: evicted) } } private static func buildEntry( page: RDEPUBTextPage, configuration: RDEPUBReaderConfiguration, signature: Signature ) -> Entry { let content = NSMutableAttributedString(attributedString: page.chapterContent) let fullRange = NSRange(location: 0, length: content.length) _ = RDEPUBDarkImageAdjuster.adjustIfNeeded( content, in: fullRange, configuration: configuration ) content.addAttribute( .foregroundColor, value: configuration.theme.contentTextColor, range: fullRange ) let layouter = DTCoreTextLayouter(attributedString: content) layouter?.shouldCacheLayoutFrames = false return Entry(content: content, layouter: layouter, signature: signature) } #else func removeAll() {} #endif }