15

Background. "Bidi" refers to the bidirectional text, that is a text where traditional left-to-right scripts are intermixed with right-to-left ones, such as Arabic, Hebrew, Farsi, Thaana, etc. Such text may look like this: "Hello އުސެރް އެކްސްޕެރިއްނެސެ". Since most readers of this board do not know these languages, I will use the same convention as the Unicode documents: in example strings the lowercase letters will denote an ltr language, and uppercase letters will be an rtl script. Thus, the example above is "hello RESU ECNEIREPXE" (note that I manually switched words order because this is how word-wrap in rtl text is supposed to work).

The Question. I'll put it here explicitly, or otherwise it might get lost behind all the minute details of the subsequent exposition. So: What considerations should be taken into account when making a bidi-aware text editor? How should those elements function around the bidi text?
Next I will point out the issues I'm aware of, and their possible solutions. I'm seeking an expert opinion on which problems I overlooked, and whether there are better solutions than I came up with. Also it might be worth mentioning that I'm working on an IDE, that is a text editor for programming languages. Thus, the bulk of the text will always be ltr, and only small fragments (string literals and comments) right-to-left.

Also, there are very detailed documents from Unicode workgroup on how the bidi text should be stored, displayed, rendered, and searched. This is not an attempt to trespass on the territory of Unicode's authority. Rather, I'd like to understand how should the interaction between the user and the bidi text be organized – something which Unicode standard omits.

Text cursor. The function of the text cursor is to indicate the place where the next character you type will appear. The cursor guides the eye: as an exercise try writing a reply to this post by looking at the preview window only, where there is no cursor. The unfortunate feature of the bidi text is that it is not always known where the next character will appear. For example, if you type a logical string abcABC, it will appear on your screen as "abc|CBA". If at this point you type D, it will appear where the cursor is, just as we need. But if you continue with an ltr letter d, it should become appended to the end, like this: abcCBAd|. This is the correct placement by the Unicode standard, and the only problem is that the letter appeared not where the cursor promised it should have had.

The potential solution is to display two cursors in such case: abc|CBA|. The second one will probably be less prominent, since it is less likely that the next typed letter will go there. It would actually be an interesting exercise to make the brightness of the second cursor based on an estimated probability of the next character appearing there.

Some editors show a different type of cursor inside the rtl text: a vertical line with a small triangle pointing left. This indicates that the cursor will move left after you type something. However I'm not exactly sure if this is useful, since an rtl user will already expect that the cursor will move in that direction. There is no need to indicate that your software will behave in the wave you expect it to.

Text cursor, movement. Suppose you typed abcDCBAd|, what should happen as you press the key? It seems to me that the most logical behavior is to move the caret leftwards, one character at a time: abcDCBA|d, abcDCB|Ad, abcDC|BAd, etc. However in many editors the behavior is different: they move the cursor backwards, which within rtl text means to the right: abcDCBAd|, abc|DCBAd, abcD|CBAd, abcDC|BAd, abcDCB|Ad, abc|DCBAd, ab|cDCBAd, etc (yes, the cursor never visits the position between A and d). I don't know if this is a bug or deliberate decision, but I don't like it. Could an actual rtl speaker provide an opinion on which behavior is more natural to them?

Text cursor, explicit. Let's say you entered abcDCBAdef|, and then you click your mouse on the position between A and d. You probably want the cursor to go there: abcDCBA|def (you'd be surprised, but in many editors it goes elsewhere!) Now you type something – where should your text go? Probably exactly in the same place where you clicked your cursor, that is between A and d? Haven't seen a single text editor which gets this right... The problem is that there in the underlying text ("abcABCDdef") there is no position between letters A and d – such position is a visual artifact. But the user doesn't care about the problems with underlying representation. He wants either to append something to the beginning of def, or to the start of DCBA (which is on the right). The editor should place the character at such position in the text, that when rendered that character appears exactly at the same position where the user explicitly indicated.

Home and End keys. As the names indicate, they should move the cursor to the beginning / end of the line respectively. In the rtl context, the beginning of the line is on the right edge, and the end is on the left. However what if your line is long and word-wrapped? Usually in such case the Home / End keys go to the start/end of the visual sub-line, but what do you do if the line has lrt/rtl text intermixed? Is the direction of movement determined by the main ltr/rtl directionality of the entire line? Or is it based on the first character of the visual subline? Or on the type of text your cursor is currently at? Or is it governed by directionality of the entire (logical) line with the exception when the entire subline has different direction? I cannot decide on this...

Del and Backspace keys. Traditionally Del deletes the next character after the cursor, and Backspace deletes the previous character. In the rtl context the "next" means on the left, and "previous" is on the right – reverse from what an ltr person expects. But again, complications arise. What's a character (note that this applies to European ltr languages as well)? For example, ޝަ is a single character consisting of two Unicode codepoints: "Sh" ޝ and "a" ަ. Should they be deleted as one character, with a single Backspace keypress, or separately? On one hand we want to match the user's expectation of what a character is, and avoid creation of broken-off character pieces; on the other hand if the user made a typo and instead of "a" ަ pressed "e" ެ, he probably wouldn't want to retype the first character.

My idea is to make the behavior of Backspace depend on whether we have the "running" cursor or "explicit" (see discussion above). In the former case the user has just typed the wrong sub-character, so he wants to delete just it. In the letter case the character was already sitting there, it probably makes no sense to split it apart. The Del key should always delete the next character entirely.

Weak characters. As I mentioned, there are ltr characters, and rtl characters. But there are also so-called "weak" characters, whose directionality depends on the context. For example the space character. Unicode has very specific and detailed algorithm describing how to assign the directionality to such characters. The problem is that as you enter the text, the context changes and the application of those rules also changes! Here's an example: you have abc|CBA|, and you press a space bar. Where should the space go? Depends on what you type after. If you type D, then the text will become abc|D CBA|, but if you type d, the text will be abcCBA d|. So what should we display after the space bar? (There is a rule in Unicode standard that the logical string "abcABC " should be rendered as "abcCBA ", but using this rule causes jumpy behavior in >50% of cases).

I'm thinking that a possible solution to this dilemma is to display both: abc| CBA |. This way the natural flow of user's sight is not interrupted: as you type the text your eyes are focused on the "correct" cursor, and you see the text appearing where it is "supposed to". As you keep typing, the phantom text on the opposite site disappears, but the overall experience is that nothing jumps around. Of course, special treatment has to be given to the situation when you stop typing with both spaces still present. One of them will have to be discarded, and it will be the program's judgement call as to which one.

Ltr inside rtl. Suppose you have typed abc DCCCCBAAAA def, and now you want to insert x in the middle of the rtl string. You probably expect that it will look like this: abc DCCCCxBAAAA def, but in all editors that I tried it actually becomes abc BAAAAxDCCCC def. I think this should be considered a bug, which will be the more annoying the longer the original rtl text fragment was. The reason this bug exists is because this is how Unicode standard requires to render the logical string "abc AAAABxCCCCD def". The standard also mentions the existence of explicit overrides (embedding marks), so that "abc AAAAB{x}CCCCD def" should be rendered as expected: abc DCCCCxBAAAA def. Unfortunately the standard doesn't place any recommendations as to when the embedding marks has to be used, but I believe that here we have exactly such a case. What do you think?

Selection. Selections are tricky: in a text like this abc ED 1|234 CBA def if you select everything from the cursor to the start of the string, the following parts will be highlighted: [abc ]ED [1]234[ CBA] def. This is because the underlying logical string is "abc ABC 1234 DE def", and you want to select according to the logical text or otherwise your fragment will make no sense when copied to a new location. However discontinuous and jumpy selection even if correct from technical perspective, presents very poor user experience (which is what we care about on this site).

One way to fix this would be to forbid the user from selecting parts of mixed-directionality fragments. That is, you can select freely within the ltr piece, or within the rtl-only piece. But when your selection originates at ltr position and then you hit the rtl fragment, it gets immediately selected as a whole: [abc ED 1234 CBA] def. This is probably a desired behavior for the majority of use cases, and the rest can just delete the extraneous parts manually. What do you think?

Strings and comments. Since I'm writing an IDE, there are some programming-languages specific issues too. Here's how a program with rtl comments should be rendered:

          !DETNEMUCOD LLEW YLBANOSAER MA I TSAEL TA TUB ,MARGORP YCNAF YLBIRRET A TON M'I \\
int main() { return 0; }

I think the comment sigil should be inverted, which is especially important for the block comments like /* abc */ => \* CBA *\. Finally, the quotation marks surrounding an rtl string should also acquire the rtl property, so that the word-wrap would work properly:

std::string description_string = TI .ENIL ENO NI TIF TONNAC DNA GNOL OOT SI GNIRTS SIHT"
    ".ENIL DNOCES EHT OTNO DEPPARW YLLACITAMOTUA STEG ;

Looks bizarre, but it's better than quotation marks randomly appearing in the middle of the string...


Ok, I think that's all. Did I miss anything?

Note: there was a previous question with the same topic on UX, however it was not detailed enough for my needs.

1
  • 4
    Damn. That is the longest question that I have ever read on here, but also one of the more interesting ones.
    – JohnGB
    Mar 21, 2013 at 12:38

3 Answers 3

6

Apple used to solve this in Carbon (on page 34) in two ways:

1. Use a double cursor

When you are on a space between the LTR and RTL parts of the text and you type LTR characters, it will appended to the LTR part of the text, when you type RTL characters you will append it to the RTL part. Caret position in a bidi string

The cursor is positioned at the end of the Arabic text and the beginning of the Roman text, so when you would want to visualize this with a cursor you get the following image:

Primary and secondary cursors

In this example the primary caret is based on the primary line direction and the secondary based on the line direction on the Roman text. You could also base the primary caret on the current input type, as seen on the top bar with the flag to indicate the input is currently set to Roman text.

2. Show only the primary cursor

The document makes clear that a single cursor is simpler: Single cursor based on input method

When the input method is changed to Roman then the cursor appears at the spot Roman characters would appear, and when it is changed to Arabic for example, the cursor is moved to the spot where Arabic characters would be inserted.

The solution would depend on your users, would they like to know both spaces where characters could be inserted and are they able to grasp the dual cursors you could go for the first option. When an input method is changed to insert characters the second option appears simpler to the user.

1
  • 1
    +1 for the best answer here. Great solution to the problem.
    – JohnGB
    May 14, 2013 at 17:57
4

Text cursor

I would consider using a block cursor to avoid any ambiguity between whether the character being entered in part of ltr or rtl text. A cursor kept at the point of insertion will always fail at the boundaries between ltr and rtl text.
enter image description here

Navigation and selection

I think one area where a small change can greatly simplify things, is to reconsider how you think of navigation and selection. Stop thinking of it as relative, and start thinking of it as absolute. This should solve most of the problems (but not all).

So when I hit my left arrow key, move the cursor absolute left. If I hit backspace, consider it as a delete absolute left, and consider 'delete` as delete absolute right.

The same then applies to home and end. Home is absolute far left, and end is absolute far right.

Selection should also be graphical. If I select an area, I want that area selected, not some interpretation of what I meant when I selected it. If my selection is abc|defZYX|NET then I want any copy or paste action to copy 'defZYX` for me.

Whatever happens with the text then, at least the person using it is always sure about their navigation and selection.

Weak characters

If you are using a block cursor, the direction of weak characters will be clear because the context should be clear. So if the cursor is on a rtl character, then tab or space move left, and vice versa.

Compound characters

Many languages use compound characters, but every time that I have seen them used, a delete or backspace will delete the entire character. If would opt for the simpler option of deleting the whole compound character, and then if there is significant feedback that this is problematic, I would consider splitting it to only a portion of the character.

Strings and comments

I would consider all characters that mark comments as being ltr regardless of what language they were entered in. That way, // (or /* or */) should always be in the correct position.

Other changes

I think you should consider marking rtl languages areas in a visual way. Possibly by changing the background colour slightly so that people can see what is what, even if there are similar characters between ltr and rtl.

TL;DR: Use a block cursor. Make all navigation keys, backspace, and delete, absolute not relative to the text direction. Keep all visual selection visually accurate.

12
  • Thanks for the reply! However I do not completely understand your block cursor idea. Say, if I wanted to write "abcABC" -- where will the cursor be positioned at the end? ab[c]CBA, or abc[C]BA ? How is this indicative that typing ` d` would result in abcCBA d ?
    – Pasha
    Mar 21, 2013 at 23:20
  • @user958624 The idea of a block cursor is that it clearly indicates whether you are inserting into a ltr or a rtl text element. If the cursor is on a rtl element, you will insert on the left. If it's on a ltr element, you will insert on the right. If you had ab[c]CBA and entered a d it should become abc[d]CBA. If it were abc[C]BA, it would also become abc[d]CBA.
    – JohnGB
    Mar 21, 2013 at 23:43
  • But what if the user wants to type abcCBAdef? After he gets to abc[C]BA, the next key press will give him abc[d]CBA, and then abcde[f]CBA – not what he intended! Forcing the user to move the cursor in order to type a contiguous string is a really bad UX...
    – Pasha
    Mar 22, 2013 at 4:53
  • @user958624 There is no 'nice' solution for every metric when you are combining two systems that were never designed to work together. It's never going to be clear UX wise, so the next goal is to make it simpler to understand.
    – JohnGB
    Mar 22, 2013 at 11:58
  • @JohnGB With a block cursor and you have ab[c]CBA and you press d you would get abd[c]CBA. The character gets inserted on the place the block cursor is standing. So you would still have the problem that it is ambiguous where the character is inserted.
    – Pesikar
    May 13, 2013 at 9:16
1

I assume the user is toggling between 2 modes of his keyboard to switch from english to the rtl language here.

If the user is already using the toggle or some mechanism to switch while entering the text, the same mechanism's rules should apply while editing. So, if you are in the rtl mode, backspace moves towards right, home is on the right and end is on the left. Similar rule will apply to the weak characters, selections, text cursor movements. And if you use this method, the representation of the cursor should not matter that much since even if you use the _ as you cursor, in ltr mode text is typed to the left and in rtl mode text is typed to the right.

2
  • This may not always work, since you can have ltr characters in an rtl keyboard layout. For example, the number "123" will always appear like that even within an arabic text: "FED 123 CBA", and you can obviously type those digits while in the arabic keyboard mode.
    – Pasha
    Mar 21, 2013 at 23:07
  • Well, whenever the character set is available in both languages, I would urge the user to default to English. Like here, since the numbers are same in Arabic and English, the user should default to ltr. Maybe you can make the IDE recognize such patterns.
    – rk.
    Mar 22, 2013 at 14:42

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.