diff --git a/app/src/main/java/com/m57/hermescontrol/ui/bots/group/GroupChatScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/bots/group/GroupChatScreen.kt index 7c95c70f..948ed979 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/bots/group/GroupChatScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/bots/group/GroupChatScreen.kt @@ -292,9 +292,9 @@ fun GroupChatScreen( MaterialTheme.typography.bodySmall.copy( textDirection = if (isThinkingRtl) { - TextDirection.ContentOrRtl + TextDirection.Rtl } else { - TextDirection.ContentOrLtr + TextDirection.Ltr }, ), color = MaterialTheme.colorScheme.primary, @@ -914,9 +914,9 @@ private fun GroupMessageCard( MaterialTheme.typography.labelSmall.copy( textDirection = if (isSystemRtl) { - TextDirection.ContentOrRtl + TextDirection.Rtl } else { - TextDirection.ContentOrLtr + TextDirection.Ltr }, ), color = MaterialTheme.colorScheme.onSurfaceVariant, @@ -946,9 +946,9 @@ private fun GroupMessageCard( MaterialTheme.typography.bodyMedium.copy( textDirection = if (isUserRtl) { - TextDirection.ContentOrRtl + TextDirection.Rtl } else { - TextDirection.ContentOrLtr + TextDirection.Ltr }, ), color = MaterialTheme.colorScheme.onPrimary, @@ -1017,9 +1017,9 @@ private fun GroupMessageCard( MaterialTheme.typography.labelMedium.copy( textDirection = if (isBotNameRtl) { - TextDirection.ContentOrRtl + TextDirection.Rtl } else { - TextDirection.ContentOrLtr + TextDirection.Ltr }, ), fontWeight = FontWeight.Bold, @@ -1168,7 +1168,7 @@ private fun GroupChatToolChip( text = label, style = MaterialTheme.typography.labelSmall.copy( - textDirection = if (isLabelRtl) TextDirection.ContentOrRtl else TextDirection.ContentOrLtr, + textDirection = if (isLabelRtl) TextDirection.Rtl else TextDirection.Ltr, ), color = MaterialTheme.colorScheme.onSurface, maxLines = 1, @@ -1246,7 +1246,7 @@ private fun GroupChatToolChip( fontFamily = FontFamily.Monospace, textDirection = if (isOutputRtl) { - TextDirection.ContentOrRtl + TextDirection.Rtl } else { TextDirection.Ltr }, diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatBubble.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatBubble.kt index 67c2552e..779788d2 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatBubble.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatBubble.kt @@ -197,9 +197,9 @@ fun ChatBubble( MaterialTheme.typography.bodyMedium.copy( textDirection = if (isRtl) { - TextDirection.ContentOrRtl + TextDirection.Rtl } else { - TextDirection.ContentOrLtr + TextDirection.Ltr }, ), ) diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/MarkdownText.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/MarkdownText.kt index 2b8b701e..71d8c628 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/chat/MarkdownText.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/chat/MarkdownText.kt @@ -71,8 +71,7 @@ private val BULLET_LINE_RE = Regex("""^(\s*)[-*+]\s+(.*)$""") private val ORDERED_LINE_RE = Regex("""^(\s*)(\d+)\.\s+(.*)$""") private val LIST_ITEM_LINE_RE = Regex("""^(\s*)(?:[-*+]\s+(?:\[([ xX])\]\s+)?|(\d+)\.\s+)(.*)$""") -private fun bidiTextDirection(isRtl: Boolean): TextDirection = - if (isRtl) TextDirection.ContentOrRtl else TextDirection.ContentOrLtr +private fun bidiTextDirection(isRtl: Boolean): TextDirection = if (isRtl) TextDirection.Rtl else TextDirection.Ltr /** * Renders chat assistant text as Markdown — but ONLY once the message has finished streaming. @@ -551,7 +550,7 @@ private fun MarkdownInlineText( val direction = if (isRtl) LayoutDirection.Rtl else LocalLayoutDirection.current val resolvedStyle = style.copy( - textDirection = if (isRtl) TextDirection.ContentOrRtl else TextDirection.ContentOrLtr, + textDirection = if (isRtl) TextDirection.Rtl else TextDirection.Ltr, ) val processedText = remember(text, isRtl) { @@ -1499,6 +1498,34 @@ private fun parseInlineSource( i = match.range.last + 1 } + // Plain text / words: when inside RTL text, wrap sequences of Latin/LTR words in LTR isolate + // so an English word at the start/middle of an Arabic line doesn't flip the layout direction. + isRtl && Character.isLetterOrDigit(src.codePointAt(i)) && + Character.getDirectionality(src.codePointAt(i)) == Character.DIRECTIONALITY_LEFT_TO_RIGHT -> { + var end = i + Character.charCount(src.codePointAt(i)) + while (end < src.length) { + val cp = src.codePointAt(end) + val dir = Character.getDirectionality(cp) + if (dir == Character.DIRECTIONALITY_LEFT_TO_RIGHT || + dir == Character.DIRECTIONALITY_EUROPEAN_NUMBER || + dir == Character.DIRECTIONALITY_EUROPEAN_NUMBER_SEPARATOR || + dir == Character.DIRECTIONALITY_EUROPEAN_NUMBER_TERMINATOR || + ( + dir == Character.DIRECTIONALITY_WHITESPACE && end + 1 < src.length && + Character.getDirectionality(src.codePointAt(end + 1)) == + Character.DIRECTIONALITY_LEFT_TO_RIGHT + ) + ) { + end += Character.charCount(cp) + } else { + break + } + } + val ltrSnippet = src.substring(i, end) + append(BidiUtils.wrapLtrIsolate(ltrSnippet)) + i = end + } + // search highlight searchQuery.isNotEmpty() && src.regionMatches(i, searchQuery, 0, searchQuery.length, ignoreCase = true) -> { diff --git a/app/src/test/java/com/m57/hermescontrol/ui/chat/MarkdownTextFeatureTest.kt b/app/src/test/java/com/m57/hermescontrol/ui/chat/MarkdownTextFeatureTest.kt index ac37ca12..c531107b 100644 --- a/app/src/test/java/com/m57/hermescontrol/ui/chat/MarkdownTextFeatureTest.kt +++ b/app/src/test/java/com/m57/hermescontrol/ui/chat/MarkdownTextFeatureTest.kt @@ -563,4 +563,12 @@ class MarkdownTextFeatureTest { val parsed = parseInline(input, Color.Black, "", false, Color.Blue, DEFAULT_HIGHLIGHTS) assertEquals("Run val x = 1 here", parsed.toString()) } + + @Test + fun testArabicStartingWithEnglishWord_wrapsEnglishInLtrIsolate() { + val input = "Okay سكرت كلشي وصار كامل عربي متل ما بدك يا بوبو" + val parsed = parseInline(input, Color.Black, "", false, Color.Blue, DEFAULT_HIGHLIGHTS) + val expected = "${BidiUtils.LRI}Okay${BidiUtils.PDI} سكرت كلشي وصار كامل عربي متل ما بدك يا بوبو" + assertEquals(expected, parsed.toString()) + } }