From 14c9863a837157a807789f717e32e4a8728a7595 Mon Sep 17 00:00:00 2001 From: sergeych Date: Sun, 17 May 2026 02:48:53 +0300 Subject: [PATCH] hyphenate text fix for desktop --- .../net/sergeych/toread/ReaderContent.kt | 62 ++++++++++++++++++- gradle/libs.versions.toml | 4 +- 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/net/sergeych/toread/ReaderContent.kt b/composeApp/src/commonMain/kotlin/net/sergeych/toread/ReaderContent.kt index c2f0d2e..38cfb1c 100644 --- a/composeApp/src/commonMain/kotlin/net/sergeych/toread/ReaderContent.kt +++ b/composeApp/src/commonMain/kotlin/net/sergeych/toread/ReaderContent.kt @@ -26,15 +26,22 @@ import androidx.compose.material3.Card import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontStyle @@ -48,6 +55,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.isSpecified import androidx.compose.ui.unit.sp import net.sergeych.toread.fb2.Fb2Block import net.sergeych.toread.fb2.Fb2Book @@ -57,6 +65,9 @@ import net.sergeych.toread.fb2.Fb2Text import net.sergeych.toread.fb2.Fb2TextSpan import net.sergeych.toread.fb2.Fb2TextStyle import net.sergeych.toread.text.HyphenationRegistry +import net.sergeych.toread.text.SoftHyphen +import kotlin.math.max +import kotlin.math.min @Composable internal fun ContinuousBookReader( @@ -343,11 +354,50 @@ private fun ReaderText( textAlign: TextAlign, modifier: Modifier = Modifier, ) { + val annotatedText = text.toAnnotatedString(language, hyphenation) + val needsSoftHyphenPaintWorkaround = isDesktopPlatform() + var textLayout by remember(annotatedText) { mutableStateOf(null) } + val desktopHyphenColor = MaterialTheme.colorScheme.onSurface + val desktopHyphenGutter = 8.dp + val textModifier = modifier + .fillMaxWidth() + .then( + if (needsSoftHyphenPaintWorkaround) { + Modifier.drawWithContent { + drawContent() + + val layout = textLayout ?: return@drawWithContent + val layoutText = layout.layoutInput.text.text + val fontSizePx = if (style.fontSize.isSpecified) style.fontSize.toPx() else 18.sp.toPx() + val hyphenLength = fontSizePx * 0.36f + val strokeWidth = max(1f, fontSizePx * 0.055f) + + for (line in 0 until layout.lineCount) { + if (!layout.endsAtSoftHyphen(layoutText, line)) continue + + val lineRight = layout.getLineRight(line) + val x = min(lineRight + hyphenLength * 0.12f, size.width - hyphenLength) + val y = layout.getLineBaseline(line) - fontSizePx * 0.32f + drawLine( + color = desktopHyphenColor, + start = Offset(x, y), + end = Offset(x + hyphenLength, y), + strokeWidth = strokeWidth, + cap = StrokeCap.Square, + ) + } + }.padding(end = desktopHyphenGutter) + } else { + Modifier + }, + ) + Text( - text = text.toAnnotatedString(language, hyphenation), + text = annotatedText, style = style, textAlign = textAlign, - modifier = modifier.fillMaxWidth(), + modifier = textModifier, + onTextLayout = { textLayout = it }, ) } @@ -364,6 +414,14 @@ private fun readerParagraphTextStyle(language: String?): TextStyle = private fun isAndroidPlatform(): Boolean = getPlatform().name.startsWith("Android") +private fun isDesktopPlatform(): Boolean = + getPlatform().name.startsWith("Java") + +private fun TextLayoutResult.endsAtSoftHyphen(text: String, line: Int): Boolean { + val end = getLineEnd(line, visibleEnd = false) + return text.getOrNull(end - 1) == SoftHyphen || text.getOrNull(end) == SoftHyphen +} + @Composable private fun BookImage( book: Fb2Book, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8d9a9ea..7bc16c5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,13 +11,13 @@ androidx-lifecycle = "2.10.0" androidx-testExt = "1.3.0" composeHotReload = "1.1.0" composeMaterialIcons = "1.7.3" -composeMultiplatform = "1.10.3" +composeMultiplatform = "1.11.0" junit = "4.13.2" kotlin = "2.3.21" kotlinx-coroutines = "1.10.2" ktor = "3.4.3" logback = "1.5.32" -material3 = "1.10.0-alpha05" +material3 = "1.11.0-alpha07" h2 = "2.4.240" [libraries]