Skip to content

Commit

Permalink
Properly detect content based text direction on native (#514)
Browse files Browse the repository at this point in the history
* Require from platform only unicode data

* Detect RTL from locale

* Use skiko icu binding for non-jvm targets

* Add TODOs

* Use more correct algorithm based only on strong types

* Update skiko version and add simple test

* Fix POP_DIRECTIONAL_ISOLATE_CODE_POINT value

* Add example to test text directions

* Move test ui to mpp demo

* Add unit test for resolveTextDirection
  • Loading branch information
MatkovIvan committed Apr 21, 2023
1 parent 1cfbb6d commit b2f35ae
Show file tree
Hide file tree
Showing 17 changed files with 526 additions and 86 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class App(
Screen("Example1") { Example1() },
Screen("ImageViewer") { ImageViewer() },
Screen("RoundedCornerCrashOnJS") { RoundedCornerCrashOnJS() },
Screen("TextDirection") { TextDirection() },
)

private class Screen(val title: String, val content: @Composable () -> Unit)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package androidx.compose.mpp.demo

import androidx.compose.foundation.Canvas
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFontFamilyResolver
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.text.Paragraph
import androidx.compose.ui.text.ParagraphIntrinsics
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.text.intl.LocaleList
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.unit.*

val LabelWidth = 150.dp
val LineHeight = 20.dp

@Composable
fun TextDirection() {
MaterialTheme {
val state = rememberScrollState()
Column(Modifier
.fillMaxSize()
.padding(10.dp)
.verticalScroll(state)) {

val textDirections = sequenceOf(
TextDirection.Ltr,
TextDirection.Rtl,
TextDirection.Content,
TextDirection.ContentOrLtr,
TextDirection.ContentOrRtl
)

Text("Latin letters (Strong characters)")
for (textDirection in textDirections) {
testTextDirection("Hello World", textDirection)
}

Spacer(Modifier.height(LineHeight))
Text("Arabic letters (Strong characters)")
for (textDirection in textDirections) {
testTextDirection("مرحبا بالعالم", textDirection)
}

Spacer(Modifier.height(LineHeight))
Text("Arabic letters EMBEDDING")
for (textDirection in textDirections) {
testTextDirection("\u202Bمرحبا بالعالم\u202C Hello World", textDirection)
}

Spacer(Modifier.height(LineHeight))
Text("Arabic letters OVERRIDE")
for (textDirection in textDirections) {
testTextDirection("\u202Eمرحبا بالعالم\u202C Hello World", textDirection)
}

Spacer(Modifier.height(LineHeight))
Text("Arabic letters ISOLATE")
for (textDirection in textDirections) {
testTextDirection("\u2067مرحبا بالعالم\u2069 Hello World", textDirection)
}

Spacer(Modifier.height(LineHeight))
Text("Weak characters")
for (textDirection in textDirections) {
testTextDirection("12345", textDirection)
}

Spacer(Modifier.height(LineHeight))
Text("LayoutDirection fallback")
testLayoutDirectionFallback("12345", LayoutDirection.Ltr)
testLayoutDirectionFallback("12345", LayoutDirection.Rtl)

Spacer(Modifier.height(LineHeight))
Text("Locale fallback")
testContentDirectionLocaleFallback("12345", "en")
testContentDirectionLocaleFallback("12345", "ar")
}
}
}

@Composable
fun testTextDirection(text: String, textDirection: TextDirection) {
Row {
Text(
text = textDirection.toString(),
modifier = Modifier
.width(LabelWidth)
.height(LineHeight)
)
Text(
text = text,
style = MaterialTheme.typography.body1.copy(
textDirection = textDirection
),
modifier = Modifier
.fillMaxWidth()
.height(LineHeight)
.border(1.dp, Color.Black)
)
}
}

@Composable
fun testLayoutDirectionFallback(text: String, layoutDirection: LayoutDirection) {
Row {
Text(
text = "Layout: $layoutDirection",
modifier = Modifier
.width(LabelWidth)
.height(LineHeight)
)
CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) {
Text(
text = text,
style = MaterialTheme.typography.body1.copy(
textDirection = TextDirection.Content
),
modifier = Modifier
.fillMaxWidth()
.height(LineHeight)
.border(1.dp, Color.Black)
)
}
}
}

@Composable
fun testContentDirectionLocaleFallback(text: String, locale: String) {
Row {
Text(
text = "Locale: $locale",
modifier = Modifier
.width(LabelWidth)
.height(LineHeight)
)
var size by remember { mutableStateOf(IntSize.Zero) }
Box(Modifier
.onSizeChanged { size = it }
.fillMaxWidth()
.height(LineHeight)
.border(1.dp, Color.Black)) {
val paragraph = Paragraph(
paragraphIntrinsics = ParagraphIntrinsics(
text = text,
style = MaterialTheme.typography.body1.copy(
localeList = LocaleList(Locale(locale)),
textDirection = TextDirection.Content
),
density = LocalDensity.current,
fontFamilyResolver = LocalFontFamilyResolver.current,
),
constraints = Constraints.fixedWidth(size.width)
)
Canvas(
Modifier.size(
with(LocalDensity.current) { DpSize(size.width.toDp(), size.height.toDp()) }
)) {
paragraph.paint(canvas = drawContext.canvas, color = Color.Black)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package androidx.compose.ui.text


internal actual class WeakKeysCache<K : Any, V> : Cache<K, V> {
private val cache = java.util.WeakHashMap<K, V>()

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.compose.ui.text

internal actual fun strongDirectionType(codePoint: Int): StrongDirectionType =
codePoint.getDirectionality().toStrongDirectionType()

/**
* Get the Unicode directionality of a character.
*/
private fun Int.getDirectionality(): CharDirectionality =
CharDirectionality.valueOf(Character.getDirectionality(this).toInt())

/**
* Get strong (R, L or AL) direction type.
* See https://www.unicode.org/reports/tr9/
*/
private fun CharDirectionality.toStrongDirectionType() = when (this) {
CharDirectionality.LEFT_TO_RIGHT -> StrongDirectionType.Ltr

CharDirectionality.RIGHT_TO_LEFT,
CharDirectionality.RIGHT_TO_LEFT_ARABIC -> StrongDirectionType.Rtl

else -> StrongDirectionType.None
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,29 +16,34 @@

package androidx.compose.ui.text.intl

import java.util.Locale
import java.awt.ComponentOrientation
import java.util.Locale as JavaLocale

internal class DesktopLocale(val locale: Locale) : PlatformLocale {
internal class DesktopLocale(val javaLocale: JavaLocale) : PlatformLocale {
override val language: String
get() = locale.language
get() = javaLocale.language

override val script: String
get() = locale.script
get() = javaLocale.script

override val region: String
get() = locale.country
get() = javaLocale.country

override fun toLanguageTag(): String = locale.toLanguageTag()
override fun toLanguageTag(): String = javaLocale.toLanguageTag()
}

internal actual fun createPlatformLocaleDelegate() = object : PlatformLocaleDelegate {
override val current: LocaleList
get() = LocaleList(listOf(Locale(DesktopLocale(Locale.getDefault()))))
get() = LocaleList(listOf(Locale(DesktopLocale(JavaLocale.getDefault()))))

override fun parseLanguageTag(languageTag: String): PlatformLocale =
DesktopLocale(
Locale.forLanguageTag(
JavaLocale.forLanguageTag(
languageTag
)
)
}

internal actual fun PlatformLocale.isRtl(): Boolean =
// TODO Get rid of AWT reference here
!ComponentOrientation.getOrientation((this as DesktopLocale).javaLocale).isLeftToRight

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,21 @@ import androidx.compose.ui.text.intl.PlatformLocale
*/
internal class DesktopStringDelegate : PlatformStringDelegate {
override fun toUpperCase(string: String, locale: PlatformLocale): String =
string.uppercase((locale as DesktopLocale).locale)
string.uppercase((locale as DesktopLocale).javaLocale)

override fun toLowerCase(string: String, locale: PlatformLocale): String =
string.lowercase((locale as DesktopLocale).locale)
string.lowercase((locale as DesktopLocale).javaLocale)

override fun capitalize(string: String, locale: PlatformLocale): String =
string.replaceFirstChar {
if (it.isLowerCase())
it.titlecase((locale as DesktopLocale).locale)
it.titlecase((locale as DesktopLocale).javaLocale)
else
it.toString()
}

override fun decapitalize(string: String, locale: PlatformLocale): String =
string.replaceFirstChar { it.lowercase((locale as DesktopLocale).locale) }
string.replaceFirstChar { it.lowercase((locale as DesktopLocale).javaLocale) }
}

internal actual fun ActualStringDelegate(): PlatformStringDelegate =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,4 @@ internal actual fun createPlatformLocaleDelegate(): PlatformLocaleDelegate =
}
}


internal actual fun PlatformLocale.isRtl(): Boolean = false // TODO
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,23 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.compose.ui.text.platform
package androidx.compose.ui.text

import androidx.compose.ui.text.style.ResolvedTextDirection
import org.jetbrains.skia.icu.CharDirection

internal actual fun String.contentBasedTextDirection(): ResolvedTextDirection? {
// TODO: implement native contentBasedTextDirection
return null
}

internal actual fun strongDirectionType(codePoint: Int): StrongDirectionType =
CharDirection.of(codePoint).toStrongDirectionType()

/**
* Get strong (R, L or AL) direction type.
* See https://www.unicode.org/reports/tr9/
*/
private fun Int.toStrongDirectionType() = when (this) {
CharDirection.LEFT_TO_RIGHT -> StrongDirectionType.Ltr

CharDirection.RIGHT_TO_LEFT,
CharDirection.RIGHT_TO_LEFT_ARABIC -> StrongDirectionType.Rtl

else -> StrongDirectionType.None
}

0 comments on commit b2f35ae

Please sign in to comment.