Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 9f7a4442 authored by Doug Felt's avatar Doug Felt Committed by Kenny Root
Browse files

Enable nested bidi levels in a paragraph.

Changes the internal representation of direction information in the Directions object to be a visually-ordered list of start/length+direction pairs instead of a list of directionality inversion offsets.

Rewrite Layout.getOffsetToLeft/RightOf to use run information instead of width metrics.

Remove java Bidi, use native.  Switch bidi tests to test native, expect levels instead of dirs.

Add test of directionality.  Leave in switch to turn new code off and restore previous behavior for now.

Change-Id: Iea8bb46c678a18820e237c90f76007a084c83051
parent 5833ae3c
Loading
Loading
Loading
Loading
+0 −1
Original line number Diff line number Diff line
@@ -310,7 +310,6 @@ extends Layout

        Directions[] objects = new Directions[1];


        for (int i = 0; i < n; i++) {
            ints[START] = reflowed.getLineStart(i) |
                          (reflowed.getParagraphDirection(i) << DIR_SHIFT) |
+368 −308

File changed.

Preview size limit exceeded, changes collapsed.

+110 −268
Original line number Diff line number Diff line
@@ -224,7 +224,7 @@ extends Layout

            boolean easy = true;
            boolean altered = false;
            int dir = DEFAULT_DIR; // XXX
            int dir = DEFAULT_DIR; // XXX pass value in

            for (int i = 0; i < n; i++) {
                if (chs[i] >= FIRST_RIGHT_TO_LEFT) {
@@ -253,7 +253,8 @@ extends Layout

            if (!easy) {
                // XXX put override flags, etc. into chdirs
                dir = bidi(dir, chs, chdirs, n, false);
                // XXX supply dir rather than force
                dir = AndroidBidi.bidi(DIR_REQUEST_DEFAULT_LTR, chs, chdirs, n, false);

                // Do mirroring for right-to-left segments

@@ -606,239 +607,6 @@ extends Layout
        }
    }

    /**
     * Runs the unicode bidi algorithm on the first n chars in chs, returning
     * the char dirs in chInfo and the base line direction of the first
     * paragraph.
     * 
     * XXX change result from dirs to levels
     *  
     * @param dir the direction flag, either DIR_REQUEST_LTR,
     * DIR_REQUEST_RTL, DIR_REQUEST_DEFAULT_LTR, or DIR_REQUEST_DEFAULT_RTL.
     * @param chs the text to examine
     * @param chInfo on input, if hasInfo is true, override and other flags 
     * representing out-of-band embedding information. On output, the generated 
     * dirs of the text.
     * @param n the length of the text/information in chs and chInfo
     * @param hasInfo true if chInfo has input information, otherwise the
     * input data in chInfo is ignored.
     * @return the resolved direction level of the first paragraph, either
     * DIR_LEFT_TO_RIGHT or DIR_RIGHT_TO_LEFT.
     */
    /* package */ static int bidi(int dir, char[] chs, byte[] chInfo, int n, 
            boolean hasInfo) {
        
        AndroidCharacter.getDirectionalities(chs, chInfo, n);

        /*
         * Determine primary paragraph direction if not specified
         */
        if (dir != DIR_REQUEST_LTR && dir != DIR_REQUEST_RTL) {
            // set up default
            dir = dir >= 0 ? DIR_LEFT_TO_RIGHT : DIR_RIGHT_TO_LEFT;
            for (int j = 0; j < n; j++) {
                int d = chInfo[j];

                if (d == Character.DIRECTIONALITY_LEFT_TO_RIGHT) {
                    dir = DIR_LEFT_TO_RIGHT;
                    break;
                }
                if (d == Character.DIRECTIONALITY_RIGHT_TO_LEFT) {
                    dir = DIR_RIGHT_TO_LEFT;
                    break;
                }
            }
        }

        final byte SOR = dir == DIR_LEFT_TO_RIGHT ?
                Character.DIRECTIONALITY_LEFT_TO_RIGHT :
                Character.DIRECTIONALITY_RIGHT_TO_LEFT;

        /*
         * XXX Explicit overrides should go here
         */

        /*
         * Weak type resolution
         */

        // dump(chdirs, n, "initial");

        // W1 non spacing marks
        for (int j = 0; j < n; j++) {
            if (chInfo[j] == Character.NON_SPACING_MARK) {
                if (j == 0)
                    chInfo[j] = SOR;
                else
                    chInfo[j] = chInfo[j - 1];
            }
        }

        // dump(chdirs, n, "W1");

        // W2 european numbers
        byte cur = SOR;
        for (int j = 0; j < n; j++) {
            byte d = chInfo[j];

            if (d == Character.DIRECTIONALITY_LEFT_TO_RIGHT ||
                d == Character.DIRECTIONALITY_RIGHT_TO_LEFT ||
                d == Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC)
                cur = d;
            else if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER) {
                 if (cur ==
                    Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC)
                    chInfo[j] = Character.DIRECTIONALITY_ARABIC_NUMBER;
            }
        }

        // dump(chdirs, n, "W2");

        // W3 arabic letters
        for (int j = 0; j < n; j++) {
            if (chInfo[j] == Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC)
                chInfo[j] = Character.DIRECTIONALITY_RIGHT_TO_LEFT;
        }

        // dump(chdirs, n, "W3");

        // W4 single separator between numbers
        for (int j = 1; j < n - 1; j++) {
            byte d = chInfo[j];
            byte prev = chInfo[j - 1];
            byte next = chInfo[j + 1];

            if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER_SEPARATOR) {
                if (prev == Character.DIRECTIONALITY_EUROPEAN_NUMBER &&
                    next == Character.DIRECTIONALITY_EUROPEAN_NUMBER)
                    chInfo[j] = Character.DIRECTIONALITY_EUROPEAN_NUMBER;
            } else if (d == Character.DIRECTIONALITY_COMMON_NUMBER_SEPARATOR) {
                if (prev == Character.DIRECTIONALITY_EUROPEAN_NUMBER &&
                    next == Character.DIRECTIONALITY_EUROPEAN_NUMBER)
                    chInfo[j] = Character.DIRECTIONALITY_EUROPEAN_NUMBER;
                if (prev == Character.DIRECTIONALITY_ARABIC_NUMBER &&
                    next == Character.DIRECTIONALITY_ARABIC_NUMBER)
                    chInfo[j] = Character.DIRECTIONALITY_ARABIC_NUMBER;
            }
        }

        // dump(chdirs, n, "W4");

        // W5 european number terminators
        boolean adjacent = false;
        for (int j = 0; j < n; j++) {
            byte d = chInfo[j];

            if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER)
                adjacent = true;
            else if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER_TERMINATOR && adjacent)
                chInfo[j] = Character.DIRECTIONALITY_EUROPEAN_NUMBER;
            else
                adjacent = false;
        }

        //dump(chdirs, n, "W5");

        // W5 european number terminators part 2,
        // W6 separators and terminators
        adjacent = false;
        for (int j = n - 1; j >= 0; j--) {
            byte d = chInfo[j];

            if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER)
                adjacent = true;
            else if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER_TERMINATOR) {
                if (adjacent)
                    chInfo[j] = Character.DIRECTIONALITY_EUROPEAN_NUMBER;
                else
                    chInfo[j] = Character.DIRECTIONALITY_OTHER_NEUTRALS;
            }
            else {
                adjacent = false;

                if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER_SEPARATOR ||
                    d == Character.DIRECTIONALITY_COMMON_NUMBER_SEPARATOR ||
                    d == Character.DIRECTIONALITY_PARAGRAPH_SEPARATOR ||
                    d == Character.DIRECTIONALITY_SEGMENT_SEPARATOR)
                    chInfo[j] = Character.DIRECTIONALITY_OTHER_NEUTRALS;
            }
        }

        // dump(chdirs, n, "W6");

        // W7 strong direction of european numbers
        cur = SOR;
        for (int j = 0; j < n; j++) {
            byte d = chInfo[j];

            if (d == SOR ||
                d == Character.DIRECTIONALITY_LEFT_TO_RIGHT ||
                d == Character.DIRECTIONALITY_RIGHT_TO_LEFT)
                cur = d;

            if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER)
                chInfo[j] = cur;
        }

        // dump(chdirs, n, "W7");

        // N1, N2 neutrals
        cur = SOR;
        for (int j = 0; j < n; j++) {
            byte d = chInfo[j];

            if (d == Character.DIRECTIONALITY_LEFT_TO_RIGHT ||
                d == Character.DIRECTIONALITY_RIGHT_TO_LEFT) {
                cur = d;
            } else if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER ||
                       d == Character.DIRECTIONALITY_ARABIC_NUMBER) {
                cur = Character.DIRECTIONALITY_RIGHT_TO_LEFT;
            } else {
                byte dd = SOR;
                int k;

                for (k = j + 1; k < n; k++) {
                    dd = chInfo[k];

                    if (dd == Character.DIRECTIONALITY_LEFT_TO_RIGHT ||
                        dd == Character.DIRECTIONALITY_RIGHT_TO_LEFT) {
                        break;
                    }
                    if (dd == Character.DIRECTIONALITY_EUROPEAN_NUMBER ||
                        dd == Character.DIRECTIONALITY_ARABIC_NUMBER) {
                        dd = Character.DIRECTIONALITY_RIGHT_TO_LEFT;
                        break;
                    }
                }

                for (int y = j; y < k; y++) {
                    if (dd == cur)
                        chInfo[y] = cur;
                    else
                        chInfo[y] = SOR;
                }

                j = k - 1;
            }
        }

        // dump(chdirs, n, "final");

        // extra: enforce that all tabs and surrogate characters go the
        // primary direction
        // TODO: actually do directions right for surrogates

        for (int j = 0; j < n; j++) {
            char c = chs[j];

            if (c == '\t' || (c >= 0xD800 && c <= 0xDFFF)) {
                chInfo[j] = SOR;
            }
        }
        
        return dir;
    }

    private static final char FIRST_CJK = '\u2E80';
    /**
     * Returns true if the specified character is one of those specified
@@ -1062,49 +830,123 @@ extends Layout
        if (tab)
            lines[off + TAB] |= TAB_MASK;

        {
        lines[off + DIR] |= dir << DIR_SHIFT;
        Directions linedirs = DIRS_ALL_LEFT_TO_RIGHT;
        // easy means all chars < the first RTL, so no emoji, no nothing
        // XXX a run with no text or all spaces is easy but might be an empty 
        // RTL paragraph.  Make sure easy is false if this is the case.
        if (easy) {
            mLineDirections[j] = linedirs;
        } else {
            int startOff = start - pstart;
            int baseLevel = dir == DIR_LEFT_TO_RIGHT ? 0 : 1;
            int curLevel = chdirs[startOff];
            int minLevel = curLevel;
            int runCount = 1;
            for (int i = start + 1; i < end; ++i) {
                int level = chdirs[i - pstart];
                if (level != curLevel) {
                    curLevel = level;
                    ++runCount;
                }
            }
            
            // add final run for trailing counter-directional whitespace
            int visEnd = end;
            if ((curLevel & 1) != (baseLevel & 1)) {
                // look for visible end
                while (--visEnd >= start) {
                    char ch = text.charAt(visEnd);

                    if (ch == '\n') {
                        --visEnd;
                        break;
                    }

            int cur = Character.DIRECTIONALITY_LEFT_TO_RIGHT;
            int count = 0;

            if (!easy) {
                for (int k = start; k < end; k++) {
                    if (chdirs[k - pstart] != cur) {
                        count++;
                        cur = chdirs[k - pstart];
                    if (ch != ' ' && ch != '\t') {
                        break;
                    }
                }
                ++visEnd;
                if (visEnd != end) {
                    ++runCount;
                }

            Directions linedirs;

            if (count == 0) {
                linedirs = DIRS_ALL_LEFT_TO_RIGHT;
            } else {
                short[] ld = new short[count + 1];

                cur = Character.DIRECTIONALITY_LEFT_TO_RIGHT;
                count = 0;
                int here = start;

                for (int k = start; k < end; k++) {
                    if (chdirs[k - pstart] != cur) {
                        // XXX check to make sure we don't
                        //     overflow short
                        ld[count++] = (short) (k - here);
                        cur = chdirs[k - pstart];
                        here = k;
            }
                }

                ld[count] = (short) (end - here);
            
                if (count == 1 && ld[0] == 0) {
            if (runCount == 1 && minLevel == baseLevel) {
                if ((minLevel & 1) != 0) {
                    linedirs = DIRS_ALL_RIGHT_TO_LEFT;
                }
                // we're done, only one run on this line
            } else {
                    linedirs = new Directions(ld);
                int[] ld = new int[runCount * 2];
                int maxLevel = minLevel;
                int levelBits = minLevel << RUN_LEVEL_SHIFT;
                {
                    // Start of first pair is always 0, we write
                    // length then start at each new run, and the
                    // last run length after we're done.
                    int n = 1;
                    int prev = start;
                    curLevel = minLevel;
                    for (int i = start; i < visEnd; ++i) {
                        int level = chdirs[i - pstart];
                        if (level != curLevel) {
                            curLevel = level;
                            if (level > maxLevel) {
                                maxLevel = level;
                            } else if (level < minLevel) {
                                minLevel = level;
                            }
                            // XXX ignore run length limit of 2^RUN_LEVEL_SHIFT
                            ld[n++] = (i - prev) | levelBits;
                            ld[n++] = i - start;
                            levelBits = curLevel << RUN_LEVEL_SHIFT;
                            prev = i;
                        }
                    }
                    ld[n] = (visEnd - prev) | levelBits;
                    if (visEnd < end) {
                        ld[++n] = visEnd - start;
                        ld[++n] = (end - visEnd) | (baseLevel << RUN_LEVEL_SHIFT);
                    }
                }

                // See if we need to swap any runs.
                // If the min level run direction doesn't match the base 
                // direction, we always need to swap (at this point
                // we have more than one run).
                // Otherwise, we don't need to swap the lowest level.
                // Since there are no logically adjacent runs at the same 
                // level, if the max level is the same as the (new) min
                // level, we have a series of alternating levels that
                // is already in order, so there's no more to do.
                // 
                boolean swap;
                if ((minLevel & 1) == baseLevel) {
                    minLevel += 1;
                    swap = maxLevel > minLevel;
                } else {
                    swap = runCount > 1;
                }
                if (swap) {
                    for (int level = maxLevel - 1; level >= minLevel; --level) {
                        for (int i = 0; i < ld.length; i += 2) {
                            if (chdirs[startOff + ld[i]] >= level) {
                                int e = i + 2;
                                while (e < ld.length && chdirs[startOff + ld[e]] >= level) {
                                    e += 2;
                                }
                                for (int low = i, hi = e - 2; low < hi; low += 2, hi -= 2) {
                                    int x = ld[low]; ld[low] = ld[hi]; ld[hi] = x;
                                    x = ld[low+1]; ld[low+1] = ld[hi+1]; ld[hi+1] = x;
                                }
                                i = e + 2;
                            }
                        }
                    }
                }
                linedirs = new Directions(ld);
            }

            mLineDirections[j] = linedirs;
+10 −36
Original line number Diff line number Diff line
@@ -22,7 +22,7 @@ import android.util.Log;
import junit.framework.TestCase;

/**
 * Tests StaticLayout bidi implementation.
 * Quick check of native bidi implementation.
 */
public class StaticLayoutBidiTest extends TestCase {
    
@@ -41,73 +41,47 @@ public class StaticLayoutBidiTest extends TestCase {
    
    //@SmallTest
    public void testAllLtr() {
        expectBidi(REQ_DL, "a test", "000000", L);
        expectNativeBidi(REQ_DL, "a test", "000000", L);
    }
    
    //@SmallTest
    public void testLtrRtl() {
        expectBidi(REQ_DL, "abc " + ALEF + BET + GIMEL, "0000111", L);
        expectNativeBidi(REQ_DL, "abc " + ALEF + BET + GIMEL, "0000111", L);
    }
    
    //@SmallTest
    public void testAllRtl() {
        expectBidi(REQ_DL, ALEF + SP + ALEF + BET + GIMEL + DALET, "111111", R);
        expectNativeBidi(REQ_DL, ALEF + SP + ALEF + BET + GIMEL + DALET, "111111", R);
    }
    
    //@SmallTest
    public void testRtlLtr() {
        expectBidi(REQ_DL,  ALEF + BET + GIMEL + " abc", "1111000", R);
        expectNativeBidi(REQ_DL,  ALEF + BET + GIMEL + " abc", "1111222", R);
    }
    
    //@SmallTest
    public void testRAllLtr() {
        expectBidi(REQ_R, "a test", "000000", R);
        expectNativeBidi(REQ_R, "a test", "222222", R);
    }
    
    //@SmallTest
    public void testRLtrRtl() {
        expectBidi(REQ_R, "abc " + ALEF + BET + GIMEL, "0001111", R);
        expectNativeBidi(REQ_R, "abc " + ALEF + BET + GIMEL, "2221111", R);
    }
    
    //@SmallTest
    public void testLAllRtl() {
        expectBidi(REQ_L, ALEF + SP + ALEF + BET + GIMEL + DALET, "111111", L);
        expectNativeBidi(REQ_L, ALEF + SP + ALEF + BET + GIMEL + DALET, "111111", L);
    }
    
    //@SmallTest
    public void testLRtlLtr() {
        expectBidi(REQ_L,  ALEF + BET + GIMEL + " abc", "1110000", L);
    }
    
    private void expectBidi(int dir, String text, 
            String expectedLevels, int expectedDir) {
        char[] chs = text.toCharArray();
        int n = chs.length;
        byte[] chInfo = new byte[n];
        
        int resultDir = StaticLayout.bidi(dir, chs, chInfo, n, false);
        
        {
            StringBuilder sb = new StringBuilder("info:");
            for (int i = 0; i < n; ++i) {
                sb.append(" ").append(String.valueOf(chInfo[i]));
            }
            Log.i("BIDI", sb.toString());
        }
        
        char[] resultLevelChars = new char[n];
        for (int i = 0; i < n; ++i) {
            resultLevelChars[i] = (char)('0' + chInfo[i]);
        }
        String resultLevels = new String(resultLevelChars);
        assertEquals("direction", expectedDir, resultDir);
        assertEquals("levels", expectedLevels, resultLevels);
        expectNativeBidi(REQ_DL,  ALEF + BET + GIMEL + " abc", "1111222", R);
    }
    
    //@SmallTest
    public void testNativeBidi() {
        // native bidi returns levels, not simply directions
        expectNativeBidi(REQ_DL,  ALEF + BET + GIMEL + " abc", "1111222", R);
        expectNativeBidi(REQ_L,  ALEF + BET + GIMEL + " abc", "1110000", L);
    }
    
    private void expectNativeBidi(int dir, String text, 
+233 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading