AvaloniaUI / Avalonia

Develop Desktop, Embedded, Mobile and WebAssembly apps with C# and XAML. The most popular .NET UI client technology
https://avaloniaui.net
MIT License
25.16k stars 2.18k forks source link

Detect FlowDirection from text content #16008

Open Bip901 opened 2 months ago

Bip901 commented 2 months ago

Is your feature request related to a problem? Please describe.

Currently, we have FlowDirection.LeftToRight and FlowDirection.RightToLeft, which are good for single-line, static content. However, text controls that display user-controlled text or text where some lines are RTL and others are LTR often need to detect the flow direction based on the content, such that lines starting with RTL characters have FlowDirection.RightToLeft. This is similar to the DetectFromContent value of TextAlignment, just for the character order instead of the alignment.

For example, the text "שלום, John Dough" starts with the RTL character "ש", so this textbox should be right-to-left:

Incorrect behavior

incorrect

Correct behavior

correct

Strong RTL Unicode characters are given by RFC 3454 section D.

Describe the solution you'd like

A new enum value for FlowDirection, e.g. FlowDirection.DetectFromContent, or a boolean property on TextBox and TextBlock, e.g. DetectFlowDirectionFromContent.

Describe alternatives you've considered

I'm currently using a custom converter and a style. This solution is limited because it sets the FlowDirection for the entire textbox based on the first character, instead of the more granular control of a line-by-line solution similar to TextAlignment.DetectFromContent.

public class TextFlowDirectionConverter : IValueConverter
{
    // From https://www.ietf.org/rfc/rfc3454.txt Section D.1
    private static readonly Range[] strongRtlRanges = new Range[] {
        0x05BE..0x05BE, 0x05C0..0x05C0, 0x05C3..0x05C3, 0x05D0..0x05EA,
        0x05F0..0x05F4, 0x061B..0x061B, 0x061F..0x061F, 0x0621..0x063A,
        0x0640..0x064A, 0x066D..0x066F, 0x0671..0x06D5, 0x06DD..0x06DD,
        0x06E5..0x06E6, 0x06FA..0x06FE, 0x0700..0x070D, 0x0710..0x0710,
        0x0712..0x072C, 0x0780..0x07A5, 0x07B1..0x07B1, 0x200F..0x200F,
        0xFB1D..0xFB1D, 0xFB1F..0xFB28, 0xFB2A..0xFB36, 0xFB38..0xFB3C,
        0xFB3E..0xFB3E, 0xFB40..0xFB41, 0xFB43..0xFB44, 0xFB46..0xFBB1,
        0xFBD3..0xFD3D, 0xFD50..0xFD8F, 0xFD92..0xFDC7, 0xFDF0..0xFDFC,
        0xFE70..0xFE74, 0xFE76..0xFEFC
    };

    private static bool IsStrongRTLChar(int codePoint)
    {
        foreach (Range range in strongRtlRanges)
        {
            if (range.Start.Value <= codePoint && codePoint <= range.End.Value)
            {
                return true;
            }
        }
        return false;
    }

    public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
    {
        if (value is string text && text.Length > 0)
        {
            if (IsStrongRTLChar(char.ConvertToUtf32(text, 0)))
            {
                return FlowDirection.RightToLeft;
            }
        }
        FlowDirection defaultFlowDirection = (parameter as FlowDirection?) ?? FlowDirection.LeftToRight;
        return defaultFlowDirection;
    }

    public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
    {
        throw new NotSupportedException();
    }
}
<Style Selector="TextBox">
    <Setter
                Property="FlowDirection"
                Value="{Binding $self.Text, Converter={StaticResource MyTextFlowDirectionConverter}, ConverterParameter=LeftToRight}"/>
</Style>

(I herby license the code snippets above as CC0 so feel free to use them).

Additional context

No response

Bip901 commented 2 months ago

Example:

This line is left-to-right because it starts with "T". שלום! This line is right-to-left because it starts with "ש". This line is left-to-right even though it contains Hebrew characters: שלום, because it starts with "T".

EDIT: Seems like GitHub messed this up too, it's only correct in the preview 😄

github_add_comment_preview

Gillibald commented 2 months ago

The bidi level of each line already indicates the strong direction. FlowDirection.DetectFromContent should be simple.

Gillibald commented 2 months ago
 var text = "שלום, John Dough";

 var codepointEnumerator = new CodepointEnumerator(text);

 var codepoint = Codepoint.ReplacementCodepoint;

 while (codepointEnumerator.MoveNext(out codepoint))
 {
     var flowDirection = GetFlowDirectionDirection(codepoint.BiDiClass);

     if (flowDirection != null)
     {
         break;
     }
 }

 return;

 static FlowDirection? GetFlowDirectionDirection(BidiClass bidiClass)
 {
     switch (bidiClass)
     {
         case BidiClass.ArabicLetter:
         case BidiClass.ArabicNumber:
         case BidiClass.RightToLeft:
         {
             return FlowDirection.RightToLeft;
         }
         case BidiClass.LeftToRight:
         {
             return FlowDirection.LeftToRight;
         }
     }

     return null;
 }