|
@@ -29,7 +29,6 @@ class ChineseLineBreaker(
|
|
|
private fun breakLines() {
|
|
private fun breakLines() {
|
|
|
if (clusters.isEmpty()) return
|
|
if (clusters.isEmpty()) return
|
|
|
var lineWidth = 0f
|
|
var lineWidth = 0f
|
|
|
- var previousWidth = 0f
|
|
|
|
|
var textLength = 0
|
|
var textLength = 0
|
|
|
clusters.forEachIndexed { index, cluster ->
|
|
clusters.forEachIndexed { index, cluster ->
|
|
|
val currentWidth = widthsPx[index]
|
|
val currentWidth = widthsPx[index]
|
|
@@ -40,6 +39,18 @@ class ChineseLineBreaker(
|
|
|
var hungLine = false
|
|
var hungLine = false
|
|
|
val currentWidthLimit = if (widths.isEmpty()) firstWidthLimit else widthLimit
|
|
val currentWidthLimit = if (widths.isEmpty()) firstWidthLimit else widthLimit
|
|
|
if (lineWidth > currentWidthLimit) {
|
|
if (lineWidth > currentWidthLimit) {
|
|
|
|
|
+ val lineStart = clusterStarts.last()
|
|
|
|
|
+ fun carryFrom(candidate: Int) {
|
|
|
|
|
+ val start = safeBreakStart(candidate, lineStart)
|
|
|
|
|
+ val carriedRange = start..index
|
|
|
|
|
+ carriedWidth = carriedRange.sumOf { widthsPx[it].toDouble() }.toFloat()
|
|
|
|
|
+ carriedCharacters = carriedRange.sumOf { clusters[it].length }
|
|
|
|
|
+ carriedClusters = carriedRange.count()
|
|
|
|
|
+ addStart(
|
|
|
|
|
+ character = textLength - carriedCharacters + cluster.length,
|
|
|
|
|
+ cluster = start,
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
// 旧 ZhLayout 把行尾标点的处置分成两类:可压缩的窄标点回退到更早的合法边界
|
|
// 旧 ZhLayout 把行尾标点的处置分成两类:可压缩的窄标点回退到更早的合法边界
|
|
|
// (BREAK_MORE_CHAR),全角标点则直接悬挂在本行右边界之外(CPS_1/2/3:
|
|
// (BREAK_MORE_CHAR),全角标点则直接悬挂在本行右边界之外(CPS_1/2/3:
|
|
|
// `offset = 0f`,行宽允许超过 width)。下面三个判定与旧版逐条对应。
|
|
// `offset = 0f`,行宽允许超过 width)。下面三个判定与旧版逐条对应。
|
|
@@ -72,19 +83,14 @@ class ChineseLineBreaker(
|
|
|
else -> Mode.NORMAL
|
|
else -> Mode.NORMAL
|
|
|
}
|
|
}
|
|
|
var rewindClusters = 0
|
|
var rewindClusters = 0
|
|
|
- var rewindCharacters = 0
|
|
|
|
|
// 可压缩标点会把收尾标点留到下一行行首,必须回退到更早的非标点边界
|
|
// 可压缩标点会把收尾标点留到下一行行首,必须回退到更早的非标点边界
|
|
|
val needsRecheck = mode == Mode.PULL_PREVIOUS && (previousClosing || nextClosing)
|
|
val needsRecheck = mode == Mode.PULL_PREVIOUS && (previousClosing || nextClosing)
|
|
|
if (needsRecheck && index > 2) {
|
|
if (needsRecheck && index > 2) {
|
|
|
val lineStart = if (widths.isEmpty()) indentCharacters else clusterStarts.last()
|
|
val lineStart = if (widths.isEmpty()) indentCharacters else clusterStarts.last()
|
|
|
mode = Mode.NORMAL
|
|
mode = Mode.NORMAL
|
|
|
for (candidate in index downTo lineStart + 1) {
|
|
for (candidate in index downTo lineStart + 1) {
|
|
|
- if (candidate == index) {
|
|
|
|
|
- previousWidth = 0f
|
|
|
|
|
- } else {
|
|
|
|
|
|
|
+ if (candidate != index) {
|
|
|
rewindClusters++
|
|
rewindClusters++
|
|
|
- rewindCharacters += clusters[candidate].length
|
|
|
|
|
- previousWidth += widthsPx[candidate]
|
|
|
|
|
}
|
|
}
|
|
|
if (clusters[candidate] !in closing && clusters[candidate - 1] !in opening) {
|
|
if (clusters[candidate] !in closing && clusters[candidate - 1] !in opening) {
|
|
|
mode = Mode.REWIND
|
|
mode = Mode.REWIND
|
|
@@ -93,18 +99,8 @@ class ChineseLineBreaker(
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
when (mode) {
|
|
when (mode) {
|
|
|
- Mode.NORMAL -> {
|
|
|
|
|
- carriedWidth = currentWidth
|
|
|
|
|
- addStart(textLength, index)
|
|
|
|
|
- carriedCharacters = cluster.length
|
|
|
|
|
- carriedClusters = 1
|
|
|
|
|
- }
|
|
|
|
|
- Mode.PULL_PREVIOUS -> {
|
|
|
|
|
- carriedWidth = currentWidth + previousWidth
|
|
|
|
|
- addStart(textLength - clusters[index - 1].length, index - 1)
|
|
|
|
|
- carriedCharacters = clusters[index - 1].length + cluster.length
|
|
|
|
|
- carriedClusters = 2
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ Mode.NORMAL -> carryFrom(index)
|
|
|
|
|
+ Mode.PULL_PREVIOUS -> carryFrom(index - 1)
|
|
|
Mode.HANG -> {
|
|
Mode.HANG -> {
|
|
|
// 标点留在本行(行宽超出右边界,旧版正是靠这个避免标点落到下一行行首),
|
|
// 标点留在本行(行宽超出右边界,旧版正是靠这个避免标点落到下一行行首),
|
|
|
// 下一行从它之后重新开始,本行不向下一行携带任何宽度。
|
|
// 下一行从它之后重新开始,本行不向下一行携带任何宽度。
|
|
@@ -114,12 +110,7 @@ class ChineseLineBreaker(
|
|
|
carriedClusters = 0
|
|
carriedClusters = 0
|
|
|
hungLine = true
|
|
hungLine = true
|
|
|
}
|
|
}
|
|
|
- Mode.REWIND -> {
|
|
|
|
|
- carriedWidth = currentWidth + previousWidth
|
|
|
|
|
- addStart(textLength - rewindCharacters, index - rewindClusters)
|
|
|
|
|
- carriedCharacters = rewindCharacters + cluster.length
|
|
|
|
|
- carriedClusters = rewindClusters + 1
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ Mode.REWIND -> carryFrom(index - rewindClusters)
|
|
|
}
|
|
}
|
|
|
widths += lineWidth - carriedWidth
|
|
widths += lineWidth - carriedWidth
|
|
|
lineWidth = carriedWidth
|
|
lineWidth = carriedWidth
|
|
@@ -140,7 +131,6 @@ class ChineseLineBreaker(
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
textLength += cluster.length
|
|
textLength += cluster.length
|
|
|
- previousWidth = currentWidth
|
|
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -149,6 +139,52 @@ class ChineseLineBreaker(
|
|
|
clusterStarts += cluster
|
|
clusterStarts += cluster
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /** Keeps a candidate line start from splitting a Latin word that fits on a fresh line. */
|
|
|
|
|
+ private fun safeBreakStart(candidate: Int, lineStart: Int): Int {
|
|
|
|
|
+ val wordStart = latinWordStartBefore(candidate)
|
|
|
|
|
+ return if (wordStart != null && wordStart > lineStart) wordStart else candidate
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Returns the start of the Latin word containing [index], or null when the overflow
|
|
|
|
|
+ * is not inside one. A word that already starts on this visual line is deliberately not
|
|
|
|
|
+ * rewound by the caller, so an overlong word still falls back to character-level breaks.
|
|
|
|
|
+ */
|
|
|
|
|
+ private fun latinWordStartBefore(index: Int): Int? {
|
|
|
|
|
+ if (index == 0 ||
|
|
|
|
|
+ !clusters[index].isLatinWordPart() ||
|
|
|
|
|
+ !clusters[index - 1].isLatinWordPart()
|
|
|
|
|
+ ) return null
|
|
|
|
|
+
|
|
|
|
|
+ var start = index - 1
|
|
|
|
|
+ while (start > 0 && clusters[start - 1].isLatinWordPart()) start--
|
|
|
|
|
+ return start
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private fun String.isLatinWordPart(): Boolean {
|
|
|
|
|
+ var hasLatinLetter = false
|
|
|
|
|
+ var offset = 0
|
|
|
|
|
+ while (offset < length) {
|
|
|
|
|
+ val codePoint = codePointAt(offset)
|
|
|
|
|
+ val type = Character.getType(codePoint)
|
|
|
|
|
+ when {
|
|
|
|
|
+ Character.isLetter(codePoint) -> {
|
|
|
|
|
+ if (Character.UnicodeScript.of(codePoint) != Character.UnicodeScript.LATIN) {
|
|
|
|
|
+ return false
|
|
|
|
|
+ }
|
|
|
|
|
+ hasLatinLetter = true
|
|
|
|
|
+ }
|
|
|
|
|
+ Character.isDigit(codePoint) ||
|
|
|
|
|
+ type == Character.NON_SPACING_MARK.toInt() ||
|
|
|
|
|
+ type == Character.COMBINING_SPACING_MARK.toInt() ||
|
|
|
|
|
+ codePoint == '\''.code || codePoint == 0x2019 -> Unit
|
|
|
|
|
+ else -> return false
|
|
|
|
|
+ }
|
|
|
|
|
+ offset += Character.charCount(codePoint)
|
|
|
|
|
+ }
|
|
|
|
|
+ return hasLatinLetter || all { it.isDigit() || it == '\'' || it == '\u2019' }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private enum class Mode { NORMAL, PULL_PREVIOUS, REWIND, HANG }
|
|
private enum class Mode { NORMAL, PULL_PREVIOUS, REWIND, HANG }
|
|
|
|
|
|
|
|
companion object {
|
|
companion object {
|