Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Properly detect content based text direction on native #514

Merged
merged 11 commits into from
Apr 21, 2023
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
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,7 @@

package androidx.compose.ui.text.intl

import platform.Foundation.countryCode
import platform.Foundation.currentLocale
import platform.Foundation.languageCode
import platform.Foundation.NSLocale
import platform.Foundation.localeIdentifier
import platform.Foundation.scriptCode
import platform.Foundation.*

internal class NativeLocale(val locale: NSLocale) : PlatformLocale {
override val language: String
Expand All @@ -48,4 +43,5 @@ internal actual fun createPlatformLocaleDelegate(): PlatformLocaleDelegate =
}
}


internal actual fun PlatformLocale.isRtl(): Boolean =
NSLocale.characterDirectionForLanguage(language) == NSLocaleLanguageDirectionRightToLeft
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2021 The Android Open Source Project
* 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.
Expand All @@ -16,6 +16,7 @@

package androidx.compose.ui.text

import kotlin.jvm.JvmInline
import org.jetbrains.skia.BreakIterator

internal actual fun String.findPrecedingBreak(index: Int): Int {
Expand All @@ -28,4 +29,94 @@ internal actual fun String.findFollowingBreak(index: Int): Int {
val it = BreakIterator.makeCharacterInstance()
it.setText(this)
return it.following(index)
}
}

/**
* See https://www.unicode.org/reports/tr9/
*/
@JvmInline
internal value class StrongDirectionType private constructor(val value: Int) {
companion object {
val None = StrongDirectionType(0)
val Ltr = StrongDirectionType(1)
val Rtl = StrongDirectionType(2)
}
}

internal expect fun strongDirectionType(codePoint: Int): StrongDirectionType

/**
* Determine direction based on the first strong directional character.
* Only considers the characters outside isolate pairs.
*/
internal fun String.firstStrongDirectionType(): StrongDirectionType {
for (codePoint in codePointsOutsideDirectionalIsolate) {
return when (val strongDirectionType = strongDirectionType(codePoint)) {
StrongDirectionType.None -> continue
else -> strongDirectionType
}
}
return StrongDirectionType.None
}

/**
* U+2066 LEFT-TO-RIGHT ISOLATE (LRI)
* U+2067 RIGHT-TO-LEFT ISOLATE (RLI)
* U+2068 FIRST STRONG ISOLATE (FSI)
*/
private val PUSH_DIRECTIONAL_ISOLATE_RANGE: IntRange = 0x2066..0x2068

/**
* U+2069 POP DIRECTIONAL ISOLATE (PDI)
*/
private const val POP_DIRECTIONAL_ISOLATE_CODE_POINT: Int = 0x10000

private val String.codePointsOutsideDirectionalIsolate get() = sequence {
var openIsolateCount = 0
for (codePoint in codePoints) {
if (codePoint in PUSH_DIRECTIONAL_ISOLATE_RANGE) {
openIsolateCount++
} else if (codePoint == POP_DIRECTIONAL_ISOLATE_CODE_POINT) {
if (openIsolateCount > 0) {
openIsolateCount--
}
} else if (openIsolateCount == 0) {
yield(codePoint)
}
}
}

private val String.codePoints get() = sequence {
var index = 0
while (index < length) {
val codePoint = codePointAt(index)
yield(codePoint)
index += codePoint.charCount()
}
}

// TODO Remove once it's available in common stdlib https://youtrack.jetbrains.com/issue/KT-23251
private fun String.codePointAt(index: Int): Int {
val high = this[index]
if (high.isHighSurrogate() && index + 1 < this.length) {
val low = this[index + 1]
if (low.isLowSurrogate()) {
return Char.toCodePoint(high, low)
}
}
return high.code
}

/**
* Converts a surrogate pair to a unicode code point.
*/
private fun Char.Companion.toCodePoint(high: Char, low: Char): Int =
(((high - MIN_HIGH_SURROGATE) shl 10) or (low - MIN_LOW_SURROGATE)) + 0x10000
MatkovIvan marked this conversation as resolved.
Show resolved Hide resolved

/**
* The minimum value of a supplementary code point, `\u0x10000`.
*/
private const val MIN_SUPPLEMENTARY_CODE_POINT: Int = 0x10000

// TODO Remove once it's available in common stdlib https://youtrack.jetbrains.com/issue/KT-23251
private fun Int.charCount(): Int = if (this >= MIN_SUPPLEMENTARY_CODE_POINT) 2 else 1
Original file line number Diff line number Diff line change
Expand Up @@ -336,8 +336,8 @@ internal class SkiaParagraph(

override fun getBidiRunDirection(offset: Int): ResolvedTextDirection =
when (getBoxForwardByOffset(offset)?.direction) {
org.jetbrains.skia.paragraph.Direction.RTL -> ResolvedTextDirection.Rtl
org.jetbrains.skia.paragraph.Direction.LTR -> ResolvedTextDirection.Ltr
Direction.RTL -> ResolvedTextDirection.Rtl
Direction.LTR -> ResolvedTextDirection.Ltr
null -> ResolvedTextDirection.Ltr
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2020 The Android Open Source Project
* 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.
Expand All @@ -13,12 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.compose.ui.text.platform

import androidx.compose.ui.text.style.ResolvedTextDirection

internal actual fun String.contentBasedTextDirection(): ResolvedTextDirection? {
// TODO: implement js contentBasedTextDirection
return null
}
package androidx.compose.ui.text.intl

internal expect fun PlatformLocale.isRtl(): Boolean