* Centralize library management logic and introduce support for plain text and HTML formats

* Centralize library management logic and introduce support for plain text and HTML formats

* Expand unit test coverage for library state management, UI models, and MainViewModel features.

* Add comprehensive unit tests for PDF reader core logic, preferences, and data persistence

* Add unit tests for EPUB parsing, content loading, search functionality, and reader JavaScript bridges.

* Add unit tests for OPDS parsing and Smart Collection engine, and integrate Kover plugin

* Add comprehensive unit tests

* Centralize library snapshot serialization in the `shared` module and improve filtering and sorting logic.

* Implement text selection, highlighting, and reading state persistence for PDF and EPUB engines in desktop version

* Folder import support for desktop app

* Introduce Smart Shelves with rule-based filtering in desktop version

* Implement shared EPUB annotation serialization and highlight rendering

* Centralize file type capabilities and platform-specific support logic

* Refactor reader state management to use a central reducer

* Implement customizable reader toolbar and advanced formatting settings in shared

* Implement locator-based navigation and customizable highlight palette for desktop app

* Enhance reader customization and expand search functionality in desktop app

* Redesign reader settings and tools into a tabbed control panel in desktop app

* Enhance reader navigation and highlight precision in desktop app

* Implement bidirectional position synchronization and dynamic highlights in the desktop reader

* Implement shared state management and enhanced search for the PDF reader in desktop app

* Add vertical scroll support to the desktop PDF reader

* Implement ink, text, and eraser annotation support in desktop PDF viewer

* Implement PDF bookmarks, Table of Contents, and annotation editing in desktop app

* Implement link handling and navigation for PDF and EPUB readers in desktop app

* Implement PDF jump history for navigation in desktop app

* Enhance PDF ink rendering and annotation capabilities in desktop app

* Implement advanced PDF text annotations with inline editing and rich styling in desktop app

* Add move handle and movement logic for PDF text annotations in desktop app

* Implement local folder synchronization and metadata sidecar support in desktop app

* Implement book metadata extraction and drag-and-drop import for Desktop

* Implement dynamic and custom app theme management for desktop

* Introduce canonical PDF annotation codec and support for multi-segment highlights

* Implement rich text editing and pagination support for the PDF reader in desktop app

* Improve PDF rich text pagination, synchronization, and observability in desktop

* Hide trailing structural page breaks in rich text editor

* Implement a unified JVM book loader and expand supported formats on Desktop

* Add comic archive support for Desktop and enhance MOBI parsing

* Implement shared OPDS catalog support and UI for Android and Desktop

* Improve native WebView lifecycle and surface transition management on Desktop

* Enable Compose Swing interop blending and simplify Desktop WebView management

* Integrate BYOK AI features and Cloud TTS for desktop

* Enhance Desktop TTS with streaming audio and improved secure storage for AI key

* Implement scoped Cloud TTS with synchronized highlighting for EPUB and PDF in desktop app

* Implement custom font management and utility screens in desktop app

* Implement PDFium-based PDF annotation export

* Remove PdfBox dependency and standardize PDF export via Pdfium

* Implement local audio caching and playback controls for Gemini Cloud TTS in desktop app

* Implement reader themes and custom texture support in desktop app

* Redesign non-reader UI with responsive navigation and enhanced library management in desktop app

* Introduce ReaderWorkspaceShell to unify EPUB and PDF reader layouts in desktop app

* Exclude manual-only files from automated sync and import

* Implement customizable Text-to-Speech (TTS) word replacements

* Optimize reader performance with persistent layout caching and decoupled theme rendering

* Improve position restoration during reader reconfiguration in epub pagination

* Use independent thickness for eraser tool and stylus override
This commit is contained in:
Aryan 2026-05-10 10:07:37 +05:30 committed by GitHub
parent 88c7fa7b5c
commit 8366d76dcd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
214 changed files with 53372 additions and 4702 deletions

View file

@ -32,6 +32,7 @@ import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.SoftwareKeyboardController
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextMeasurer
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.TextStyle
@ -56,12 +57,16 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.json.JSONArray
import org.json.JSONObject
import com.aryan.reader.shared.pdf.SHARED_PDF_RICH_TEXT_LOG_TAG
import timber.log.Timber
import java.io.File
const val PAGE_BREAK_CHAR = '\u000C'
private const val ZWSP = "\u200B"
internal fun String.hasRenderableRichText(): Boolean =
any { it != PAGE_BREAK_CHAR && !it.isWhitespace() }
object PdfFontCache {
private val cache = ConcurrentHashMap<String, FontFamily>()
private var assetManager: android.content.res.AssetManager? = null
@ -262,126 +267,200 @@ class TextPaginationEngine {
dirtyGlobalIndex: Int = 0
): List<PageTextLayout> {
val totalLen = globalText.length
if (totalLen == 0) return listOf(
PageTextLayout(0, AnnotatedString(""), 0, 0, pageHeightPx)
Timber.tag(SHARED_PDF_RICH_TEXT_LOG_TAG).d(
"android.paginate start textLen=$totalLen page=${pageWidthPx.richAndroidLogFloat()}x${pageHeightPx.richAndroidLogFloat()} " +
"margin=${marginX.richAndroidLogFloat()},${marginY.richAndroidLogFloat()} prev=${previousLayouts.size} dirty=$dirtyGlobalIndex"
)
if (pageWidthPx <= 0 || pageHeightPx <= 0) return emptyList()
val validPages = if (dirtyGlobalIndex > 0 && previousLayouts.isNotEmpty()) {
previousLayouts.takeWhile { it.globalEndIndex < dirtyGlobalIndex }
} else {
emptyList()
if (totalLen == 0) {
Timber.tag(SHARED_PDF_RICH_TEXT_LOG_TAG).d("android.paginate empty -> p0:0-0")
return listOf(
PageTextLayout(0, AnnotatedString(""), 0, 0, pageHeightPx)
)
}
if (pageWidthPx <= 0 || pageHeightPx <= 0) {
Timber.tag(SHARED_PDF_RICH_TEXT_LOG_TAG).d("android.paginate aborted invalid page size")
return emptyList()
}
val startPageIndex = validPages.size
val measurementStartIndex = validPages.lastOrNull()?.globalEndIndex ?: 0
if (measurementStartIndex >= totalLen) return validPages
val textToMeasure = globalText.subSequence(measurementStartIndex, totalLen)
val fullString = textToMeasure.text
val editorWidth = (pageWidthPx - (marginX * 2)).coerceAtLeast(10f)
val editorHeight = (pageHeightPx - (marginY * 2)).coerceAtLeast(10f)
val measureResult = textMeasurer.measure(
text = textToMeasure,
style = TextStyle(fontSize = 16.sp, color = Color.Black),
constraints = Constraints(maxWidth = editorWidth.toInt(), maxHeight = Constraints.Infinity),
density = density
)
val newPages = mutableListOf<PageTextLayout>()
var currentPageIndex = startPageIndex
var currentPageStartRel = 0
var currentPageAccumulatedHeight = 0f
var currentPageIndex = 0
var segmentStart = 0
val rawText = globalText.text
var currentLineIndex = 0
val totalLines = measureResult.lineCount
while (segmentStart < totalLen) {
val breakIndex = rawText.indexOf(PAGE_BREAK_CHAR, startIndex = segmentStart)
val hasExplicitBreak = breakIndex != -1
val contentEnd = if (hasExplicitBreak) breakIndex else totalLen
val segmentEnd = if (hasExplicitBreak) breakIndex + 1 else totalLen
Timber.tag("RichTextFlow").d("Pagination: Measuring ${fullString.length} chars from Global $measurementStartIndex. Lines: $totalLines")
while (currentLineIndex < totalLines) {
val lineTop = measureResult.getLineTop(currentLineIndex)
val lineBottom = measureResult.getLineBottom(currentLineIndex)
val lineHeight = lineBottom - lineTop
val lineStartRel = measureResult.getLineStart(currentLineIndex)
val lineEndRel = measureResult.getLineEnd(currentLineIndex)
val localStartOffset = (currentPageStartRel - lineStartRel).coerceAtLeast(0)
if (lineStartRel + localStartOffset >= lineEndRel && currentLineIndex < totalLines - 1) {
currentLineIndex++
continue
}
val safeEndRel = lineEndRel.coerceAtMost(fullString.length)
val lineContent = fullString.substring(lineStartRel, safeEndRel)
val breakIndexInLine = lineContent.indexOf(PAGE_BREAK_CHAR, localStartOffset)
val hasPageBreak = breakIndexInLine != -1
val isStartOfPage = (currentPageAccumulatedHeight == 0f)
val willOverflow = !isStartOfPage && (currentPageAccumulatedHeight + lineHeight > editorHeight)
if (hasPageBreak) {
val splitRelIndex = lineStartRel + breakIndexInLine + 1
val globalStart = measurementStartIndex + currentPageStartRel
val globalEnd = measurementStartIndex + splitRelIndex
Timber.tag("RichTextMigration").v("PaginationEngine: Found PAGE_BREAK_CHAR at relative ${breakIndexInLine}. Breaking Page $currentPageIndex at Global Index $globalEnd")
if (globalEnd > globalStart) {
val visibleText = globalText.subSequence(globalStart, globalEnd)
newPages.add(PageTextLayout(currentPageIndex, visibleText, globalStart, globalEnd, pageHeightPx))
currentPageIndex++
}
currentPageStartRel = splitRelIndex
currentPageAccumulatedHeight = 0f
continue
}
else if (willOverflow) {
val globalStart = measurementStartIndex + currentPageStartRel
val globalEnd = measurementStartIndex + lineStartRel
if (globalEnd > globalStart) {
val visibleText = globalText.subSequence(globalStart, globalEnd)
newPages.add(PageTextLayout(currentPageIndex, visibleText, globalStart, globalEnd, pageHeightPx))
Timber.tag("RichTextFlow").v("Page $currentPageIndex Created (Overflow): $globalStart -> $globalEnd")
currentPageIndex++
}
currentPageStartRel = lineStartRel
currentPageAccumulatedHeight = 0f
continue
}
currentPageAccumulatedHeight += lineHeight
currentLineIndex++
currentPageIndex = newPages.appendMeasuredAndroidRichTextSegment(
globalText = globalText,
segmentStart = segmentStart,
contentEnd = contentEnd,
explicitBreakEnd = if (hasExplicitBreak) segmentEnd else null,
pageIndex = currentPageIndex,
pageHeightPx = pageHeightPx,
editorWidth = editorWidth,
editorHeight = editorHeight,
textMeasurer = textMeasurer,
density = density
)
segmentStart = segmentEnd
}
if (currentPageStartRel < fullString.length) {
val globalStart = measurementStartIndex + currentPageStartRel
val globalEnd = measurementStartIndex + fullString.length
val visibleText = globalText.subSequence(globalStart, globalEnd)
newPages.add(PageTextLayout(currentPageIndex, visibleText, globalStart, globalEnd, pageHeightPx))
}
val resultLayouts = validPages + newPages
val resultLayouts = newPages.withTrailingAndroidBlankRichTextPageIfNeeded(
globalText = globalText,
pageHeightPx = pageHeightPx
)
val mapLog = resultLayouts.joinToString("\n") {
" Page ${it.pageIndex}: Global[${it.globalStartIndex}..${it.globalEndIndex}]"
}
Timber.tag("RichTextMigration").i("Pagination Map Generated:\n$mapLog")
Timber.tag(SHARED_PDF_RICH_TEXT_LOG_TAG).d("android.paginate done -> ${resultLayouts.richAndroidLayoutSummary()}")
return resultLayouts
}
}
private fun MutableList<PageTextLayout>.appendMeasuredAndroidRichTextSegment(
globalText: AnnotatedString,
segmentStart: Int,
contentEnd: Int,
explicitBreakEnd: Int?,
pageIndex: Int,
pageHeightPx: Float,
editorWidth: Float,
editorHeight: Float,
textMeasurer: TextMeasurer,
density: Density
): Int {
var nextPageIndex = pageIndex
if (segmentStart >= contentEnd) {
val breakEnd = explicitBreakEnd ?: return nextPageIndex
add(
PageTextLayout(
pageIndex = nextPageIndex,
visibleText = globalText.subSequence(segmentStart, breakEnd),
globalStartIndex = segmentStart,
globalEndIndex = breakEnd,
pageHeightPx = pageHeightPx
)
)
Timber.tag(SHARED_PDF_RICH_TEXT_LOG_TAG).d(
"android.paginate pageBreakOnly page=$nextPageIndex global=$segmentStart..$breakEnd"
)
return nextPageIndex + 1
}
val contentLength = contentEnd - segmentStart
var relativeStart = 0
while (relativeStart < contentLength) {
val globalStart = segmentStart + relativeStart
val remainingText = globalText.subSequence(globalStart, contentEnd)
val measureResult = textMeasurer.measure(
text = remainingText,
style = TextStyle(fontSize = 16.sp, color = Color.Black),
constraints = Constraints(maxWidth = editorWidth.toInt(), maxHeight = Constraints.Infinity),
density = density
)
val fitsOnPage = measureResult.size.height.toFloat() <= editorHeight || measureResult.lineCount <= 1
var overflowLineIndex: Int? = null
val relativeEnd = if (fitsOnPage) {
contentLength
} else {
val lineIndex = measureResult.richAndroidLastFittingLineIndex(editorHeight)
overflowLineIndex = lineIndex
val localEnd = measureResult.getLineEnd(lineIndex)
.coerceIn(0, remainingText.length)
.coerceAtLeast(1)
(relativeStart + localEnd)
.coerceAtLeast(relativeStart + 1)
.coerceAtMost(contentLength)
}
val isLastContentPage = relativeEnd >= contentLength
val globalEnd = if (isLastContentPage && explicitBreakEnd != null) {
explicitBreakEnd
} else {
segmentStart + relativeEnd
}
add(
PageTextLayout(
pageIndex = nextPageIndex,
visibleText = globalText.subSequence(globalStart, globalEnd),
globalStartIndex = globalStart,
globalEndIndex = globalEnd,
pageHeightPx = pageHeightPx
)
)
if (isLastContentPage && explicitBreakEnd != null) {
Timber.tag(SHARED_PDF_RICH_TEXT_LOG_TAG).d(
"android.paginate pageBreak page=$nextPageIndex global=$globalStart..$globalEnd"
)
} else if (!fitsOnPage) {
Timber.tag(SHARED_PDF_RICH_TEXT_LOG_TAG).d(
"android.paginate overflow page=$nextPageIndex global=$globalStart..$globalEnd line=$overflowLineIndex"
)
}
nextPageIndex++
relativeStart = relativeEnd
}
return nextPageIndex
}
private fun TextLayoutResult.richAndroidLastFittingLineIndex(editorHeight: Float): Int {
var lastFitting = 0
for (lineIndex in 0 until lineCount) {
if (lineIndex == 0 || getLineBottom(lineIndex) <= editorHeight) {
lastFitting = lineIndex
} else {
break
}
}
return lastFitting.coerceIn(0, (lineCount - 1).coerceAtLeast(0))
}
private fun List<PageTextLayout>.withTrailingAndroidBlankRichTextPageIfNeeded(
globalText: AnnotatedString,
pageHeightPx: Float
): List<PageTextLayout> {
if (globalText.text.lastOrNull() != PAGE_BREAK_CHAR) return this
val lastLayout = lastOrNull()
val trailingStart = globalText.length
if (lastLayout != null &&
lastLayout.globalStartIndex == trailingStart &&
lastLayout.globalEndIndex == trailingStart
) {
return this
}
return this + PageTextLayout(
pageIndex = (lastLayout?.pageIndex ?: -1) + 1,
visibleText = AnnotatedString(""),
globalStartIndex = trailingStart,
globalEndIndex = trailingStart,
pageHeightPx = pageHeightPx
)
}
private fun AnnotatedString.withoutTrailingAndroidPageBreak(): AnnotatedString {
return if (text.lastOrNull() == PAGE_BREAK_CHAR) {
subSequence(0, length - 1)
} else {
this
}
}
private fun AnnotatedString.withRestoredTrailingAndroidPageBreak(shouldRestore: Boolean): AnnotatedString {
if (!shouldRestore) return this
if (text.lastOrNull() == PAGE_BREAK_CHAR) return this
return this + AnnotatedString(PAGE_BREAK_CHAR.toString())
}
class PdfRichTextRepository(private val context: Context) {
private val _document = MutableStateFlow<GlobalRichDocument?>(null)
val document = _document.asStateFlow()
@ -396,8 +475,12 @@ class PdfRichTextRepository(private val context: Context) {
suspend fun load(bookId: String) {
withContext(Dispatchers.IO) {
val file = getFile(bookId)
Timber.tag(SHARED_PDF_RICH_TEXT_LOG_TAG).d(
"android.repository.load start book=$bookId exists=${file.exists()} path=${file.absolutePath}"
)
if (!file.exists()) {
_document.value = GlobalRichDocument("", emptyList())
Timber.tag(SHARED_PDF_RICH_TEXT_LOG_TAG).d("android.repository.load missing -> empty book=$bookId")
return@withContext
}
try {
@ -425,7 +508,11 @@ class PdfRichTextRepository(private val context: Context) {
)
}
_document.value = GlobalRichDocument(text, spans)
Timber.tag(SHARED_PDF_RICH_TEXT_LOG_TAG).d(
"android.repository.load decoded book=$bookId rawLen=${jsonString.length} textLen=${text.length} spans=${spans.size}"
)
} catch (e: Exception) {
Timber.tag(SHARED_PDF_RICH_TEXT_LOG_TAG).e(e, "android.repository.load failed book=$bookId")
Timber.e(e, "Failed to load rich text doc")
_document.value = GlobalRichDocument("", emptyList())
}
@ -436,6 +523,9 @@ class PdfRichTextRepository(private val context: Context) {
_document.value = document
withContext(Dispatchers.IO) {
try {
Timber.tag(SHARED_PDF_RICH_TEXT_LOG_TAG).d(
"android.repository.save start book=$bookId textLen=${document.text.length} spans=${document.spans.size}"
)
val obj = JSONObject().apply {
put("text", document.text)
val spansArray = JSONArray()
@ -456,14 +546,35 @@ class PdfRichTextRepository(private val context: Context) {
}
put("spans", spansArray)
}
getFile(bookId).writeText(obj.toString())
val file = getFile(bookId)
file.writeText(obj.toString())
Timber.tag(SHARED_PDF_RICH_TEXT_LOG_TAG).d(
"android.repository.save done book=$bookId bytes=${file.length()} path=${file.absolutePath}"
)
} catch (e: Exception) {
Timber.tag(SHARED_PDF_RICH_TEXT_LOG_TAG).e(e, "android.repository.save failed book=$bookId")
Timber.e(e, "Failed to save rich text doc")
}
}
}
}
private fun Float.richAndroidLogFloat(): String {
return if (isFinite()) {
val rounded = kotlin.math.round(this * 10f) / 10f
rounded.toString()
} else {
toString()
}
}
private fun List<PageTextLayout>.richAndroidLayoutSummary(): String {
if (isEmpty()) return "[]"
return joinToString(prefix = "[", postfix = "]", limit = 8, truncated = "...") { layout ->
"p${layout.pageIndex}:${layout.globalStartIndex}-${layout.globalEndIndex}/len${layout.visibleText.length}"
}
}
@Stable
class RichTextController(
private val repository: PdfRichTextRepository,
@ -485,6 +596,9 @@ class RichTextController(
var pageLayouts by mutableStateOf(emptyList<PageTextLayout>())
private set
val hasRenderableText: Boolean
get() = globalTextFieldValue.text.hasRenderableRichText()
var currentStyle: SpanStyle by mutableStateOf(SpanStyle(color = Color.Black, fontSize = 16.sp))
private set
@ -720,9 +834,11 @@ class RichTextController(
val currentGlobal = globalTextFieldValue.annotatedString
// FIX: Strip ZWSP (index 0) from local text
val localText = if (localTextFieldValue.annotatedString.isNotEmpty()) {
val localEditableText = if (localTextFieldValue.annotatedString.isNotEmpty()) {
localTextFieldValue.annotatedString.subSequence(1, localTextFieldValue.annotatedString.length)
} else AnnotatedString("")
val shouldPreservePageBreak = layout.visibleText.text.lastOrNull() == PAGE_BREAK_CHAR
val localText = localEditableText.withRestoredTrailingAndroidPageBreak(shouldPreservePageBreak)
Timber.tag("RichTextFlow").d("Sync: Page $activePageIndex, GlobalRange [$globalStart..$globalEnd], LocalLen ${localText.length}")
@ -776,7 +892,7 @@ class RichTextController(
activePageIndex = newActiveLayout.pageIndex
val reExtractedText = newGlobalAnnotated.subSequence(
newActiveLayout.globalStartIndex, newActiveLayout.globalEndIndex
)
).withoutTrailingAndroidPageBreak()
val textWithZwsp = AnnotatedString(ZWSP) + reExtractedText
val newLocalCursor = (newGlobalCursorPos - newActiveLayout.globalStartIndex + 1)
.coerceIn(0, textWithZwsp.length)
@ -865,28 +981,26 @@ class RichTextController(
val editorWidth = (lastPageWidth - (margin * 2)).coerceAtLeast(10f)
val vText = currentLayout.visibleText
val editableText = vText.withoutTrailingAndroidPageBreak()
// FIX: Prepend ZWSP to the visible text
val textWithZwsp = AnnotatedString(ZWSP) + vText
val safeLen = if (vText.isNotEmpty() && vText.last() == PAGE_BREAK_CHAR) vText.length - 1 else vText.length
val textWithZwsp = AnnotatedString(ZWSP) + editableText
val safeLen = editableText.length
// FIX: Adjust initial selection by +1 because of ZWSP
localTextFieldValue = TextFieldValue(textWithZwsp, TextRange(safeLen + 1))
val measureResult = measurer.measure(
text = currentLayout.visibleText, // We measure the original for layout tap calc
text = editableText, // We measure editable text, not the hidden page-break sentinel
style = TextStyle(fontSize = 16.sp, color = Color.Black),
constraints = Constraints(maxWidth = editorWidth.toInt()),
density = density
)
val textHeight = measureResult.size.height.toFloat()
val textHeight = if (editableText.isEmpty()) 0f else measureResult.size.height.toFloat()
if (localTapOffset.y <= textHeight) {
if (editableText.isNotEmpty() && localTapOffset.y <= textHeight) {
var localIndex = measureResult.getOffsetForPosition(localTapOffset)
if (vText.isNotEmpty() && vText.last() == PAGE_BREAK_CHAR && localIndex >= vText.length) {
localIndex = vText.length - 1
}
localIndex = localIndex.coerceIn(0, editableText.length)
localTextFieldValue = localTextFieldValue.copy(selection = TextRange(localIndex + 1))
} else {
val gap = localTapOffset.y - textHeight
@ -1132,7 +1246,9 @@ class RichTextController(
val currentGlobal = globalTextFieldValue.annotatedString
val localAnnotatedRaw = localTextFieldValue.annotatedString
val localAnnotated = if (localAnnotatedRaw.isNotEmpty()) localAnnotatedRaw.subSequence(1, localAnnotatedRaw.length) else AnnotatedString("")
val localEditableAnnotated = if (localAnnotatedRaw.isNotEmpty()) localAnnotatedRaw.subSequence(1, localAnnotatedRaw.length) else AnnotatedString("")
val shouldPreservePageBreak = layout.visibleText.text.lastOrNull() == PAGE_BREAK_CHAR
val localAnnotated = localEditableAnnotated.withRestoredTrailingAndroidPageBreak(shouldPreservePageBreak)
val charBeforeSync = if (globalStart > 0) currentGlobal.text[globalStart - 1] else "START"
val charAfterSync = if (globalEnd < currentGlobal.length) currentGlobal.text[globalEnd] else "END"
@ -1197,7 +1313,7 @@ class RichTextController(
val reExtracted = newGlobalAnnotated.subSequence(
newActiveLayout.globalStartIndex, newActiveLayout.globalEndIndex
)
).withoutTrailingAndroidPageBreak()
val textWithZwsp = AnnotatedString(ZWSP) + reExtracted
val newLocalCursor = (newGlobalCursorPos - newActiveLayout.globalStartIndex + 1).coerceIn(0, textWithZwsp.length)
@ -1301,7 +1417,7 @@ class RichTextController(
activePageIndex = finalActiveLayout.pageIndex
val reExtracted = intermediateGlobal.subSequence(
finalActiveLayout.globalStartIndex, finalActiveLayout.globalEndIndex
)
).withoutTrailingAndroidPageBreak()
val textWithZwsp = AnnotatedString(ZWSP) + reExtracted
val localCursor = (newCursorPos - finalActiveLayout.globalStartIndex + 1).coerceIn(0, textWithZwsp.length)
@ -1351,7 +1467,7 @@ class RichTextController(
val reExtracted = newGlobalText.subSequence(
finalActiveLayout.globalStartIndex, finalActiveLayout.globalEndIndex
)
).withoutTrailingAndroidPageBreak()
val textWithZwsp = AnnotatedString(ZWSP) + reExtracted
val localCursor = (newCursorPos - finalActiveLayout.globalStartIndex + 1).coerceIn(0, textWithZwsp.length)
@ -1400,4 +1516,4 @@ class RichTextController(
isSaving = false
}
}
}
}