8000 Set letterSpacing to 0 for cursive writing systems by YektaDev · Pull Request #596 · androidx/androidx · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Set letterSpacing to 0 for cursive writing systems #596

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

Open
wants to merge 2 commits into
base: androidx-main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ import androidx.compose.material3.tokens.DefaultTextStyle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.Stable
import androidx.compose.runtime.structuralEqualityPolicy
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.takeOrElse
Expand All @@ -37,6 +39,9 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.isSpecified
import androidx.compose.ui.unit.isUnspecified
import androidx.compose.ui.unit.sp

/**
* High level element that displays text and provides semantics / accessibility information.
Expand Down Expand Up @@ -108,7 +113,7 @@ fun Text(
onTextLayout: ((TextLayoutResult) -> Unit)? = null,
style: TextStyle = LocalTextStyle.current
) {

val fixedLetterSpacing = rememberFixedLetterSpacing(text, letterSpacing, style.letterSpacing)
val textColor = color.takeOrElse {
style.color.takeOrElse {
LocalContentColor.current
Expand All @@ -127,7 +132,7 @@ fun Text(
fontFamily = fontFamily,
textDecoration = textDecoration,
fontStyle = fontStyle,
letterSpacing = letterSpacing
letterSpacing = fixedLetterSpacing
),
onTextLayout,
overflow,
Expand Down Expand Up @@ -254,6 +259,7 @@ fun Text(
onTextLayout: (TextLayoutResult) -> Unit = {},
style: TextStyle = LocalTextStyle.current
) {
val fixedLetterSpacing = rememberFixedLetterSpacing(text.text, letterSpacing, style.letterSpacing)
val textColor = color.takeOrElse {
style.color.takeOrElse {
LocalContentColor.current
Expand All @@ -272,7 +278,7 @@ fun Text(
fontFamily = fontFamily,
textDecoration = textDecoration,
fontStyle = fontStyle,
letterSpacing = letterSpacing
letterSpacing = fixedLetterSpacing
),
>
overflow = overflow,
Expand Down Expand Up @@ -351,3 +357,62 @@ fun ProvideTextStyle(value: TextStyle, content: @Composable () -> Unit) {
val mergedStyle = LocalTextStyle.current.merge(value)
CompositionLocalProvider(LocalTextStyle provides mergedStyle, content = content)
}

/**
* Letter spacing doesn't play well with cursive writing systems, as it can break the connection
* between letters and make the text fragmented. A simple and performant solution to this problem is
* to find the first "letter" character in the text (using `isLetter()`) and set `letterSpacing` to
* 0 if that character belongs to a cursive writing system and `letterSpacing` is greater than 0.
*/
@Composable
private fun rememberFixedLetterSpacing(
text: String,
letterSpacing: TextUnit,
styleLetterSpacing: TextUnit,
) = remember(text, letterSpacing, styleLetterSpacing) {
when (letterSpacing.isSpecified) {
true -> {
when (letterSpacing.value == 0f) {
true -> letterSpacing
false -> if (text.isFirstLetterCursive()) 0f.sp else letterSpacing
}
}
false -> {
when (styleLetterSpacing.let { it.isUnspecified || it.value == 0f }) {
true -> TextUnit.Unspecified
false -> if (text.isFirstLetterCursive()) 0f.sp else styleLetterSpacing
}
}
}
}

/**
* Returns **`true`** if the first letter of the string belongs to a cursive writing system. A good
* example is the Persian language. Letter spacing doesn't play well with cursive writing systems,
* as it can break the connection between letters and make the text fragmented.
*/
@Stable
private fun String.isFirstLetterCursive(): Boolean {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this version, we have defined a binarySearchRange extension for the range list that performs a binary search to check if a value is in any of the ranges. This drastically reduces the number of comparisons needed compared to the original approach of comparing against each range individually.

@Stable
private fun String.isFirstLetterCursive(): Boolean {
    val cursiveRanges = listOf(
        1536..1920,
        1984..2048,
        2144..2304,
        4096..4256,
        43488..43520,
        43616..43648,
        64336..65024,
        65136..65280
    )

    for (char in this) {
        if (!char.isLetter()) continue
        val c = char.code

        if (cursiveRanges.binarySearchRange(c)) {
            return true
        }
    }

    return false
}

private fun List<IntRange>.binarySearchRange(value: Int): Boolean {
    var low = 0
    var high = size - 1

    while (low <= high) {
        val mid = (low + high) / 2
        val range = this[mid]

        if (value in range) {
            return true
        }

        if (value < range.first) {
            high = mid - 1
        } else {
            low = mid + 1
        }
    }

    return false
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @miguelalegria, thanks for the suggestion. The reason I didn't incorporate binary search and ranges is to avoid unnecessary allocations given the size of our search. I assume the current workaround would be more efficient. However, this pull request isn't probably going to be merged since the problem lies in lower layer of the system.

for (char in this) {
if (!char.isLetter()) continue
val c = char.code
if (c < 1536) return false
if (c < 1920) return true
if (c < 1984) return false
if (c < 2048) return true
if (c < 2144) return false
if (c < 2304) return true
if (c < 4096) return false
if (c < 4256) return true
if (c < 43488) return false
if (c < 43520) return true
if (c < 43616) return false
if (c < 43648) return true
if (c < 64336) return false
if (c < 65024) return true
if (c < 65136) return false
if (c < 65280) return true
return false
}
return false
}
0