go-text / typesetting

High quality text shaping in pure Go.
Other
88 stars 11 forks source link

Line wrapper bidi output ordering #29

Closed whereswaldon closed 1 year ago

whereswaldon commented 1 year ago

So I've been implementing bidirectional text support in Gio, or at least a rough approximation of it. I've realized a tricky nuance of our current line wrapping. We return shaping.Line in which the runs are ordered logically, not visually. I find this ordering to be quite convenient, as you can iterate through the data in order of the underlying runes by iterating the returned line. However, this requires some post-processing to determine the correct visual order. If you have this logical output for a paragraph with an overall LTR text direction:

LTRrunA, RTLrunB, RTLrunC, RTLrunD, LTRrunE

The correct display order is:

LTRrunA, RTLrunD, RTLrunC, RTLrunB, LTRrunE

Essentially, you need to search every line for consecutive runs going against the dominant text direction. If you find a sequence of two or more runs going against that text direction, their order must be reversed visually within the line.

Here's some code I've written in Gio for this purpose (it operates on the lines after they've been converted to Gio's text types, so it's not 100% ready to be used on go-text's output):

// computeVisualOrder will populate the Line's VisualOrder field and the
// VisualPosition field of each element in Runs.
func computeVisualOrder(l *text.Line) {
    l.VisualOrder = make([]int, len(l.Runs))
    const none = -1
    bidiRangeStart := none

    // visPos returns the visual position for an individual logically-indexed
    // run in this line, taking only the line's overall text direction into
    // account.
    visPos := func(logicalIndex int) int {
        if l.Direction.Progression() == system.TowardOrigin {
            return len(l.Runs) - 1 - logicalIndex
        }
        return logicalIndex
    }

    // resolveBidi populated the line's VisualOrder fields for the elements in the
    // half-open range [bidiRangeStart:bidiRangeEnd) indicating that those elements
    // should be displayed in reverse-visual order.
    resolveBidi := func(bidiRangeStart, bidiRangeEnd int) {
        firstVisual := bidiRangeEnd - 1
        // Just found the end of a bidi range.
        for startIdx := bidiRangeStart; startIdx < bidiRangeEnd; startIdx++ {
            pos := visPos(firstVisual)
            l.Runs[startIdx].VisualPosition = pos
            l.VisualOrder[pos] = startIdx
            firstVisual--
        }
        bidiRangeStart = none
    }
    for runIdx, run := range l.Runs {
        if run.Direction.Progression() != l.Direction.Progression() {
            if bidiRangeStart == none {
                bidiRangeStart = runIdx
            }
            continue
        } else if bidiRangeStart != none {
            // Just found the end of a bidi range.
            resolveBidi(bidiRangeStart, runIdx)
            bidiRangeStart = none
        }
        pos := visPos(runIdx)
        l.Runs[runIdx].VisualPosition = pos
        l.VisualOrder[pos] = runIdx
    }
    if bidiRangeStart != none {
        // We ended iteration within a bidi segment, resolve it.
        resolveBidi(bidiRangeStart, len(l.Runs))
    }
}

Anyway, I'm opening this issue to discuss this API design decision: What order do we want to return the runs in? Logical order is convenient for GUI use-cases like implementing text editors, but less convenient for simple use-cases like displaying a string with no post-processing.

Note: So far as I can tell, this isn't a fatal flaw in our line wrapping strategy. Because the line wrapper considers the text purely in logical order (order of the underlying runes) we always ensure that the logical "beginning" of a bidirectional run is preserved on a line when we must break that run. So we always generate the correct broken runs for a line, they're simply not in visual order.

andydotxyz commented 1 year ago

Good point, I wonder if it relates to rendering - because if when we have a renderer it will take the info and do the transform to graphics for the developer. So perhaps the simple case is even simpler as they won't have to worry about the data order at all. My personal feeling is that it should match the flow of runes, but I have not yet got to that level of complexity on coding a complete renderer so I could be chasing the wrong approach.

whereswaldon commented 1 year ago

My personal feeling is that it should match the flow of runes, but I have not yet got to that level of complexity on coding a complete renderer so I could be chasing the wrong approach.

Assuming you mean "it should match the logical order of runes" (what we have now), I agree. For Gio's needs, it's pretty easy to manage the visual ordering separately with a little extra metadata.

andydotxyz commented 1 year ago

Assuming you mean "it should match the logical order of runes"

Yeah that is what I meant, sorry.

whereswaldon commented 1 year ago

I don't think there's any action to take here, so I'm going to close this issue.