import UIKit enum RDEPUBTextPageLayoutMetrics { static let pageNumberTrailingPadding: CGFloat = 4 static let pageNumberFooterPadding: CGFloat = 8 static let pageNumberReservedHeight: CGFloat = ceil(UIFont.systemFont(ofSize: 13).lineHeight) + pageNumberFooterPadding static func contentInsets( configuration: RDEPUBReaderConfiguration, safeAreaInsets: UIEdgeInsets, isLandscapeDualPage: Bool = false, isLeadingPage: Bool = true ) -> UIEdgeInsets { let configInsets = configuration.reflowableContentInsets guard isLandscapeDualPage else { return UIEdgeInsets( top: max(configInsets.top, safeAreaInsets.top), left: max(configInsets.left, safeAreaInsets.left), bottom: max(configInsets.bottom, safeAreaInsets.bottom) + pageNumberReservedHeight, right: max(configInsets.right, safeAreaInsets.right) ) } let outerHorizontalInset = max( max(configInsets.left, configInsets.right), max(safeAreaInsets.left, safeAreaInsets.right) ) let spineInset = RDEpubReaderView.landscapeDualPageGap / 2 return UIEdgeInsets( top: max(safeAreaInsets.top, min(configInsets.top, RDEPUBSafeArea.landscapeVerticalTextMargin)), left: isLeadingPage ? outerHorizontalInset : spineInset, bottom: max(safeAreaInsets.bottom, min(configInsets.bottom, RDEPUBSafeArea.landscapeVerticalTextMargin)) + pageNumberReservedHeight, right: isLeadingPage ? spineInset : outerHorizontalInset ) } static func fixedContentInsets( configuration: RDEPUBReaderConfiguration, safeAreaInsets: UIEdgeInsets, isLandscapeDualPage: Bool, isLeadingPage: Bool ) -> UIEdgeInsets { let configInsets = configuration.fixedContentInset guard isLandscapeDualPage else { return UIEdgeInsets( top: max(configInsets.top, safeAreaInsets.top), left: max(configInsets.left, safeAreaInsets.left), bottom: max(configInsets.bottom, safeAreaInsets.bottom), right: max(configInsets.right, safeAreaInsets.right) ) } let outerHorizontalInset = max( max(configInsets.left, configInsets.right), max(safeAreaInsets.left, safeAreaInsets.right) ) let spineInset = RDEpubReaderView.landscapeDualPageGap / 2 return UIEdgeInsets( top: max(safeAreaInsets.top, min(configInsets.top, RDEPUBSafeArea.landscapeVerticalTextMargin)), left: isLeadingPage ? outerHorizontalInset : spineInset, bottom: max(safeAreaInsets.bottom, min(configInsets.bottom, RDEPUBSafeArea.landscapeVerticalTextMargin)), right: isLeadingPage ? spineInset : outerHorizontalInset ) } } final class RDEPUBReaderEnvironment { weak var controller: RDEPUBReaderController? weak var readerView: RDEpubReaderView? var displayEnvironment: any RDEPUBReaderDisplayEnvironment init( controller: RDEPUBReaderController, readerView: RDEpubReaderView, displayEnvironment: any RDEPUBReaderDisplayEnvironment ) { self.controller = controller self.readerView = readerView self.displayEnvironment = displayEnvironment } func currentLayoutContext(configuration: RDEPUBReaderConfiguration) -> RDEPUBNavigatorLayoutContext { let containerSize = readerView?.bounds.size ?? .zero let viewSize = controller?.view.bounds.size ?? containerSize let resolvedSize = containerSize == .zero ? viewSize : containerSize return RDEPUBNavigatorLayoutContext( containerSize: resolvedSize, pagesPerScreen: readerView?.pagesPerScreen ?? 1, safeAreaInsets: RDEPUBSafeArea.resolve(controller?.view.safeAreaInsets), userInterfaceIdiom: controller?.traitCollection.userInterfaceIdiom ?? .phone, reflowableContentInsets: configuration.reflowableContentInsets ) } func currentTextRenderStyle(configuration: RDEPUBReaderConfiguration) -> RDEPUBTextRenderStyle { let font = configuration.fontChoice.font(ofSize: configuration.fontSize) let lineSpacing = max(font.lineHeight * (configuration.lineHeightMultiple - 1), 4) return RDEPUBTextRenderStyle( font: font, lineSpacing: lineSpacing, textColor: configuration.theme.contentTextColor, backgroundColor: configuration.theme.contentBackgroundColor ) } func currentTextLayoutConfig( configuration: RDEPUBReaderConfiguration, pageSize: CGSize ) -> RDEPUBTextLayoutConfig { let safeAreaInsets = RDEPUBSafeArea.resolve(controller?.view.safeAreaInsets) return RDEPUBTextLayoutConfig( frameWidth: max(pageSize.width, 1), frameHeight: max(pageSize.height, 1), edgeInsets: RDEPUBTextPageLayoutMetrics.contentInsets( configuration: configuration, safeAreaInsets: safeAreaInsets, isLandscapeDualPage: readerView?.pagesPerScreen == 2 ), numberOfColumns: configuration.numberOfColumns, columnGap: configuration.columnGap, avoidOrphans: false, avoidWidows: false, avoidPageBreakInsideEnabled: true, hyphenation: true, imageMaxHeightRatio: 0.85, fallbackViewportSize: displayEnvironment.fallbackViewportSize ) } func currentPreferences( configuration: RDEPUBReaderConfiguration, pageIndex: Int? = nil ) -> RDEPUBPreferences { let safeAreaInsets = RDEPUBSafeArea.resolve(controller?.view.safeAreaInsets) let isLandscapeDualPage = readerView?.pagesPerScreen == 2 let isLeadingPage = pageIndex.map { readerView?.isLeadingPage($0) ?? true } ?? true var preferences = configuration.makePreferences(safeAreaInsets: safeAreaInsets) preferences.reflowableContentInsets = RDEPUBTextPageLayoutMetrics.contentInsets( configuration: configuration, safeAreaInsets: safeAreaInsets, isLandscapeDualPage: isLandscapeDualPage, isLeadingPage: isLeadingPage ) preferences.fixedContentInset = RDEPUBTextPageLayoutMetrics.fixedContentInsets( configuration: configuration, safeAreaInsets: safeAreaInsets, isLandscapeDualPage: isLandscapeDualPage, isLeadingPage: isLeadingPage ) return preferences } var currentBrightness: CGFloat { get { displayEnvironment.currentBrightness } set { displayEnvironment.currentBrightness = newValue } } var fallbackViewportSize: CGSize { displayEnvironment.fallbackViewportSize } }