iced-rs / iced

A cross-platform GUI library for Rust, inspired by Elm
https://iced.rs
MIT License
24.56k stars 1.15k forks source link

Right-to-left script not showing in `TextInput` widget #1877

Open menoua opened 1 year ago

menoua commented 1 year ago

Is there an existing issue for this?

Is this issue related to iced?

What happened?

Thanks to #1830, regular Text widgets can handle complex scripts, including left-to-right (LTR), right-to-left (RTL), and mixed LTR/RTL. But this is only partially true for the TextInput widget, such that complex scripts will be displayed properly only if the content of the text input starts with an LTR (alphabet) character. In other words, the content of text input is only displayed when the content should be aligned LTR.

The screen capture below demonstrates the issue using the Todos example of the library, but this issue is reproducible with any TextInput instance. The video covers LTR, RTL, LTR->RTL, and RTL->LTR.

https://github.com/iced-rs/iced/assets/39179450/899d857a-61bb-43d7-ab0c-63dc7941c82c

What is the expected behavior?

Ultimately, if the user input starts with an RTL (alphabet) character, the entire content should be displayed RTL and be right-aligned. However, since TextInput does not yet have horizontal text alignment support, a first step would be to just make the text show up, even if left-aligned.

I don't have enough knowledge of text rendering or the internals of this library to pinpoint where the issue stems from and how to fix it. But I did try a couple of basic modifications to this function call:

I also tried rendering with different values of offset with renderer.with_translation(...). None of my attempts so far has made the missing text appear.

I would be happy to look into fixing the issue if someone can point me to the potential location of the issue in the codebase.

Version

master

Operative System

macOS

Do you have any log output?

No response

menoua commented 1 year ago

@hecrj @mtkennerly I have dug deeper into this since posting the issue and identified the root cause. I think it is upstream in glyphon. FWIW, here's what I have found so far.

Original Issue

Cause

This happens because glyphon (I'm assuming that's where the text rendering happens) renders all right-to-left text as right-aligned, regardless of the horizontal_alignment value provided. That is the text is rendered from the rightmost end of the bounds (bounds.x + bounds.width) towards the left. Since TextInput has bounds.width = infinity, when you try to input RTL text into a TextInput, it tries to render the text at x = infinity which obviously will never show up on screen. What is different now compared to when I opened the issue is that performing this action simply leads to a crash in glyphon of "attempt to add with overflow" when trying to do math with infinity here.

Naive solution

Setting the bounds.width value here to f32::max(text_width, text_bounds.width) makes TextInput work with LTR scripts without problem (so far as I can tell), and RTL scripts display correctly too, although making the cursor work properly in this case requires some additional tweaking. I'm not making a PR on this yet because I haven't found the time to work on the cursor stuff and text selection, but I'll keep looking into it.

https://github.com/iced-rs/iced/assets/39179450/ba417ae5-e173-4c25-bebf-87cee277718d

NOTE: the clipping at the end happens because renderer.measure_width returns a width of zero for RTL text having again to do with bounds.x = infinity. Setting bounds = Size {width: 1e6, height: 1e6} instead of bounds = Size::INFINITY in the measure_width function makes it return a non-zero value, but this is an ugly solution.

A more fundamental issue

Cause

This is also causing an even more serious issue with the regular Text widget, because the bounds.x value of text is updated here assuming the renderer will respect the horizontal_alignment paramter. But because that doesn't happen for RTL text, the text is rendered completely out of bounds if any alignment other than Horizontal::Left is provided. As you can see below, RTL left-aligned acts like LTR right-aligned, and the other RTLs are out of the specified widget width, because their bounds.x is being moved to the right.

Screenshot 2023-07-31 at 11 30 56 AM

Naive solution

A downstream solution for this would be to update the bounds parameter differently for different cases of alignment when the text is RTL, for example:

bounds.width = text_width;
let x = match horizontal_alignment {
    alignment::Horizontal::Left => bounds.x,
    alignment::Horizontal::Center => bounds.center_x() - text_width / 2.0,
    alignment::Horizontal::Right => bounds.x + bounds.width - text_width,
};

Since I didn't find a function within iced or glyphon that returns the direction of a language, I couldn't test it out (without adding an explicit direction parameter to text widgets). The Horizontal::Left case works for both LTR and RTL however:

Screenshot 2023-07-31 at 11 31 24 AM

I'm not making a PR on this because I think it needs a fix upstream.