Skip to content

Commit a50aa0c

Browse files
committed
[shaping] better face selection for mixed RTL/LTR with marks
1 parent b7af2ec commit a50aa0c

2 files changed

Lines changed: 41 additions & 5 deletions

File tree

shaping/input.go

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ func SplitByFontGlyphs(input Input, availableFaces []*font.Face) []Input {
117117
// the return value of the [Fontmap.ResolveFace] call.
118118
// The 'Face' field of 'input' is ignored: only 'availableFaces' is used to select the face.
119119
func SplitByFace(input Input, availableFaces Fontmap) []Input {
120-
return splitByFace(input, availableFaces, nil)
120+
return splitByFace(input, availableFaces, nil, true)
121121
}
122122

123123
// Segmenter holds a state used to split input
@@ -395,22 +395,40 @@ func (seg *Segmenter) splitByVertOrientation() {
395395
// assume [splitByScript] has been called
396396
func (seg *Segmenter) splitByFace(faces Fontmap) {
397397
withScript, hasScriptSupport := faces.(FontmapScript)
398-
for _, input := range seg.input {
398+
lastRunWithoutFace := -1
399+
for i, input := range seg.input {
399400
if hasScriptSupport {
400401
withScript.SetScript(input.Script)
401402
}
402-
seg.output = splitByFace(input, faces, seg.output)
403+
isLast := i == len(seg.input)-1
404+
L := len(seg.output)
405+
seg.output = splitByFace(input, faces, seg.output, isLast)
406+
if face := seg.output[L].Face; face != nil {
407+
if lastRunWithoutFace != -1 {
408+
// apply it back
409+
for k := lastRunWithoutFace; k < L; k++ {
410+
seg.output[k].Face = face
411+
}
412+
// reset the marker
413+
lastRunWithoutFace = -1
414+
}
415+
} else {
416+
// in this case, only one run has been added by splitByFace
417+
if lastRunWithoutFace == -1 {
418+
lastRunWithoutFace = L
419+
}
420+
}
403421
}
404422
}
405423

406-
func splitByFace(input Input, availableFaces Fontmap, buffer []Input) []Input {
424+
func splitByFace(input Input, availableFaces Fontmap, buffer []Input, isLast bool) []Input {
407425
currentInput := input
408426
for i := input.RunStart; i < input.RunEnd; i++ {
409427
r := input.Text[i]
410428
// We can safely ignore characters if we have a face or if there is more text,
411429
// but we must force the choice of a face if we still don't have one and we reach
412430
// the final rune. Otherwise strings like all-whitespace are never assigned a face.
413-
if ignoreFaceChange(r) && (currentInput.Face != nil || i < input.RunEnd-1) {
431+
if ignoreFaceChange(r) && (currentInput.Face != nil || !isLast || i < input.RunEnd-1) {
414432
// add the rune to the current input
415433
continue
416434
}

shaping/input_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ func Test_ignoreFaceChange(t *testing.T) {
2626
{'\ufe02', true},
2727
{'\U000E0100', true},
2828
{'\u06DD', false},
29+
{'\u200f', true},
2930
}
3031
for _, tt := range tests {
3132
if got := ignoreFaceChange(tt.args); got != tt.want {
@@ -659,6 +660,23 @@ func TestSplit(t *testing.T) {
659660
{0, 8, sideways, language.Mongolian, "mn", latinFont},
660661
},
661662
},
663+
{
664+
"\u200fabc", // the mark should not trigger a face change, even "across" different directions/scripts
665+
di.DirectionLTR,
666+
[]run{
667+
{0, 1, di.DirectionRTL, language.Common, "fr", latinFont},
668+
{1, 4, di.DirectionLTR, language.Latin, "fr", latinFont},
669+
},
670+
},
671+
{
672+
"a\u200fabc", // the mark should not trigger a face change, even "across" different directions/scripts
673+
di.DirectionLTR,
674+
[]run{
675+
{0, 1, di.DirectionLTR, language.Latin, "fr", latinFont},
676+
{1, 2, di.DirectionRTL, language.Common, "fr", latinFont},
677+
{2, 5, di.DirectionLTR, language.Latin, "fr", latinFont},
678+
},
679+
},
662680
} {
663681
inputs := seg.Split(Input{
664682
Text: []rune(test.text),

0 commit comments

Comments
 (0)