tl;dr: Is there a way to hook into the native text...
# compose
k
tl;dr: Is there a way to hook into the native text line-breaking algorithm (if there is one)? I created a Composable for handling HTML ruby text. It's basically markup that can be used to add additional information to text. It's often used for providing readings in Japanese for example. For that the text is displayed above the Japanese characters (see image). My composable if made out of
RubyText
that contains text and optionally a ruby text that dictate it's reading. These are packed inside a
FlowRow
.The problem I am facing is that I can't control line breaking properly this way. If there is a long slice of text without reading annotations they are packed into the same
RubyText
composable. Thus they cannot break in the middle. Is there a way to hook into the native line-breaking and take advantage of it? Or do I have to re-implement it myself to make my rubied text work properly?
Copy code
@Composable
fun RubyText(
    text: String,
    ruby: String? = null,
    rubyVisible: Boolean = true,
    style: TextStyle = TextStyle.Default,
) {
    val rubyFontSize = LocalTextStyle.current.fontSize / 2
    val boxHeight = rubyFontSize.toDp()

    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        Box(modifier = Modifier.requiredHeight(boxHeight + 3.dp)) {
            ruby?.let {
                if (rubyVisible) {
                    Text(text = it, style = TextStyle(fontSize = rubyFontSize))
                }
            }
        }
        Text(text = text, style = style)
    }
}

// This renders the second image (nside of the thread).
fun example2() {
        RubyText(text = "このルールを")
        RubyText(text = "守", ruby = "まも")
        RubyText(text = "るらない")
        RubyText(text = "人", ruby = "ひと")
        RubyText(text = "は")
        RubyText(text = "旅行", ruby = "りょこう")
        RubyText(text = "ができなくなることもあります。")
}
The problem is visible pretty clearly here. Compared to how android natively renders it (third image).
z
There's no way to hook into the algorithm, but this seems like something we might be able to support with some other api? Please file a feature request
e
HACK:
Copy code
val (annotatedString, inlineContent) = "このルールを守まもるらない人ひとは旅行りょこうができなくなることもあります。".toRuby()
Text(text = annotatedString, inlineContent = inlineContent)

private val RUBY_REGEX = "\uFFF9([^\uFFF9\uFFFA\uFFFB]*)\uFFFA([^\uFFF9\uFFFA\uFFFB]*)\uFFFB".toRegex()
private fun String.toRuby(): Pair<AnnotatedString, Map<String, InlineTextContent>> {
    val inlineContent = mutableMapOf<String, InlineTextContent>()
    return buildAnnotatedString {
        var position = 0
        for (match in RUBY_REGEX.findAll(this@toRuby)) {
            append(substring(startIndex = position, endIndex = match.range.first))
            position = match.range.last + 1
            val id = match.value
            val (text, annotation) = match.destructured
            appendInlineContent(id, text)
            inlineContent[id] = InlineTextContent(
                placeholder = Placeholder((2 * text.length).em, 3.em, PlaceholderVerticalAlign.Bottom),
                children = {
                    Column(
                        modifier = Modifier.fillMaxHeight(),
                        horizontalAlignment = Alignment.CenterHorizontally,
                        verticalArrangement = Arrangement.Bottom,
                    ) {
                        BasicText(text = annotation)
                        BasicText(text = text)
                    }
                }
            )
        }
        append(substring(startIndex = position))
    } to inlineContent
}
of course, something built-in could work much much better than this
you could possibly tweak this to look more-or-less passable on some simple usages, but it really needs more to be built into the text layout so that styling, selection, etc. all work
if you've created a feature request, drop it here so I can it :)
k
@ephemient I didn't create a feature request since I lacked an account for the issue tracker. Could you explain the example above a bit more clearly? What exactly should
toRuby
do?
e
it's replacing
\uFFF9..\uFFFA..\uFFFB
(the Unicode interlinear annotations for ruby text, but you could of course use whatever else you want, this is just an example) with inline content inline content means that the string contains "旅行" (which is important for accessibility) but when it is drawn, the given
@Composable
is used instead of that substring https://developer.android.com/reference/kotlin/androidx/compose/foundation/text/InlineTextContent
there isn't a good way to get the placeholder sizing right without having some integration with text layout too, so this is not a great solution, just a stopgap that might work in some use cases
z
For future reference, anyone can make an account for free, it's helpful to be able to file requests and bugs.
k
@Zach Klippenstein (he/him) [MOD] I know, I moved away from Google products a couple of years ago. I just don't feel comfortable to have to create a new account just to file a feature request. I am thankful for ephemient.
@ephemient Would you be so kind to edit your feature description to add that, depending on how the text is rendered, the annotation position changes? Japanese (as well as Chinese IIRC) can also be written top to bottom. And vertical lines are then written right to left. In these cases readings are rendered on the right side of the characters (see here: https://en.wikipedia.org/wiki/Furigana). I am not sure if Android/Compose even supports top to bottom text via
Text
or some sibling class.
e
neither Android nor web supports top-to-bottom text as far as I know
👍 1
k
Web not directly, but a website could use CSS to render it properly. But this might be out-of-scope for Compose I suppose.
e
I am wrong, CSS
writing-mode: vertical-rl
has been implemented in many web engines (very recently): https://caniuse.com/mdn-css_properties_writing-mode_horizontal_vertical_values
and it looks like CSS
ruby-position
has similar support: https://caniuse.com/mdn-css_properties_ruby-position
in any case, I've added a link to https://netflixtechblog.com/implementing-japanese-subtitles-on-netflix-c165fbe61989 to the issue, as it discusses these details about rubies (although unfortunately, their implementation does not appear to be public)
👀 1