本次提交围绕 DTCoreText 文本页的分页一致性、交互索引和高亮命中进行了集中修复。 主要改动: 1. 文本页显示改为基于整章 attributed string 的上下文布局,只对当前页 range 进行渲染,避免页面子串重新换行导致的页末断行偏差。 2. 页面布局快照与交互控制器统一改为使用 chapter-absolute 索引,修正点击、选区、菜单锚点与高亮矩形在整章上下文下的定位。 3. 修复跨章节高亮串页问题,并调整文本页高亮命中逻辑:保留 CoreText 层绘制,点击时按高亮真实 rect 精确命中,避免重复绘制和整行误判。 4. 收紧 reader 级页面缓存策略,避免预加载同时持有多份整章显示副本带来的内存放大。 5. 新增分页边界校验器、垂直对齐器和 settings-flip 自动化调试入口,用于复现与诊断页范围/显示度量不一致问题。 6. 放宽 inline attachment 的 avoid-break 处理,并补充相关分页问题调查文档与索引。
58 lines
2.2 KiB
Swift
58 lines
2.2 KiB
Swift
import UIKit
|
|
|
|
#if canImport(DTCoreText)
|
|
import DTCoreText
|
|
|
|
/// Redistributes the leftover space at the bottom of a page into the gaps
|
|
/// between lines so the last line sits flush with the content bottom edge
|
|
/// (vertical justification), keeping page character ranges untouched.
|
|
///
|
|
/// Mutating each line's `baselineOrigin` is sufficient: DTCoreText derives
|
|
/// line frames, glyph-run frames and attachment positions from it lazily,
|
|
/// so drawing, selection, highlights and hit-testing all stay consistent.
|
|
enum RDEPUBTextPageVerticalJustifier {
|
|
|
|
/// Leftover larger than this many typical line advances is kept as
|
|
/// whitespace instead of being stretched: it usually comes from a whole
|
|
/// block (image, table) pushed to the next page, and stretching would
|
|
/// make the line spacing visibly sparse.
|
|
static let maxStretchLineAdvanceRatio: CGFloat = 1.5
|
|
|
|
static func justify(
|
|
_ layoutFrame: DTCoreTextLayoutFrame,
|
|
contentHeight: CGFloat,
|
|
isChapterLastPage: Bool,
|
|
pixelScale: CGFloat
|
|
) {
|
|
guard !isChapterLastPage,
|
|
contentHeight > 0,
|
|
let lines = layoutFrame.lines as? [DTCoreTextLayoutLine],
|
|
lines.count >= 2,
|
|
let firstLine = lines.first,
|
|
let lastLine = lines.last else {
|
|
return
|
|
}
|
|
|
|
let leftover = contentHeight - lastLine.frame.maxY
|
|
guard leftover > 0.5 else { return }
|
|
|
|
let gapCount = CGFloat(lines.count - 1)
|
|
let typicalAdvance = (lastLine.baselineOrigin.y - firstLine.baselineOrigin.y) / gapCount
|
|
guard typicalAdvance > 0,
|
|
leftover <= typicalAdvance * maxStretchLineAdvanceRatio else {
|
|
return
|
|
}
|
|
|
|
let scale = max(pixelScale, 1)
|
|
for (index, line) in lines.enumerated() where index > 0 {
|
|
// Round each cumulative shift down to the pixel grid so glyphs
|
|
// stay sharp and the last line never overshoots the bottom edge.
|
|
let shift = floor(leftover * CGFloat(index) / gapCount * scale) / scale
|
|
var origin = line.baselineOrigin
|
|
origin.y += shift
|
|
line.baselineOrigin = origin
|
|
}
|
|
}
|
|
}
|
|
#endif
|