1package text
2
3import (
4 "fmt"
5 "sort"
6
7 "github.com/bnema/nefergui/internal/bidi"
8 "github.com/go-text/typesetting/di"
9 "github.com/go-text/typesetting/shaping"
10)
11
12// splitLevels prevents a single shaping run from crossing resolved paragraph
13// levels (not merely direction parity). The wrapper can subsequently split
14// further at UAX#14 break opportunities.
15func splitLevels(out, items []shaping.Input, levels []uint8) []shaping.Input {
16 out = out[:0]
17 for _, item := range items {
18 if item.RunStart >= item.RunEnd {
19 continue
20 }
21 start := item.RunStart
22 for i := start + 1; i < item.RunEnd; i++ {
23 if levels[i] != levels[start] {
24 part := item
25 part.RunStart = start
26 part.RunEnd = i
27 part.Direction = directionFor(levels[start])
28 out = append(out, part)
29 start = i
30 }
31 }
32 item.RunStart = start
33 item.Direction = directionFor(levels[start])
34 out = append(out, item)
35 }
36 return out
37}
38func directionFor(level uint8) di.Direction {
39 if level&1 != 0 {
40 return di.DirectionRTL
41 }
42 return di.DirectionLTR
43}
44
45type runItem struct {
46 output shaping.Output
47 level uint8
48}
49
50// visualScratch holds order buffers reused across lines; the returned runs
51// are valid until the next call.
52type visualScratch struct {
53 breaks []int
54 levels []uint8
55 runs []runItem
56}
57
58// order resets trailing whitespace to the paragraph level (L1) by
59// resolving levels against the *actual wrapped line breaks*, then applies L2
60// to complete runs. The shaper already emits glyphs in visual order within an
61// odd-level run: never reverse its glyph slice a second time.
62func (v *visualScratch) order(text []rune, line shaping.Line, base bidi.BaseDirection) ([]runItem, error) {
63 if len(line) == 0 {
64 return nil, nil
65 }
66 start, end := line[0].Runes.Offset, line[len(line)-1].Runes.Offset+line[len(line)-1].Runes.Count
67 if start < 0 || end > len(text) || end <= start {
68 return nil, fmt.Errorf("invalid line range %d:%d", start, end)
69 }
70 // Include earlier lines so weak and neutral resolution use paragraph context.
71 breaks := v.breaks[:0]
72 if start > 0 {
73 breaks = append(breaks, start)
74 }
75 breaks = append(breaks, end)
76 if end < len(text) {
77 breaks = append(breaks, len(text))
78 }
79 v.breaks = breaks
80 levels, _, err := bidi.ResolveInto(v.levels, text, base, breaks)
81 if err != nil {
82 return nil, err
83 }
84 v.levels = levels
85 // The wrapper's runs are in logical order. A level change within one output
86 // requires reshaping at that boundary; splitLevels does this before wrapping.
87 clear(v.runs) // drop glyph references from the previous line
88 runs := v.runs[:0]
89 for _, o := range line {
90 s, e := o.Runes.Offset, o.Runes.Offset+o.Runes.Count
91 if s < start || e > end || s >= e {
92 return nil, fmt.Errorf("invalid shaped run %d:%d", s, e)
93 }
94 if sameLevel(levels, s, e) {
95 runs = append(runs, runItem{output: o, level: levels[s]})
96 continue
97 }
98 // L1 may reset a trailing space in a shaped RTL run. Split the output at
99 // cluster boundaries, preserving its glyph metrics and HarfBuzz order.
100 parts, err := splitWrappedLevel(o, levels)
101 if err != nil {
102 return nil, err
103 }
104 for _, part := range parts {
105 runs = append(runs, runItem{output: part, level: levels[part.Runes.Offset]})
106 }
107 }
108 v.runs = runs
109 // L2 on run levels: from maximum down to the minimum odd level, reverse
110 // contiguous sequences whose levels are >= the current level.
111 highest, lowestOdd := uint8(0), uint8(255)
112 for _, run := range runs {
113 if run.level > highest {
114 highest = run.level
115 }
116 if run.level&1 == 1 && run.level < lowestOdd {
117 lowestOdd = run.level
118 }
119 }
120 if lowestOdd != 255 {
121 for current := int(highest); current >= int(lowestOdd); current-- {
122 for i := 0; i < len(runs); {
123 if int(runs[i].level) < current {
124 i++
125 continue
126 }
127 j := i + 1
128 for j < len(runs) && int(runs[j].level) >= current {
129 j++
130 }
131 for a, b := i, j-1; a < b; a, b = a+1, b-1 {
132 runs[a], runs[b] = runs[b], runs[a]
133 }
134 i = j
135 }
136 }
137 }
138 return runs, nil
139}
140
141func sameLevel(levels []uint8, start, end int) bool {
142 for i := start + 1; i < end; i++ {
143 if levels[i] != levels[start] {
144 return false
145 }
146 }
147 return true
148}
149
150// splitWrappedLevel divides only at glyph cluster boundaries; UAX#14 wraps
151// at these same boundaries. Empty/format glyphs stay attached to their cluster.
152func splitWrappedLevel(o shaping.Output, levels []uint8) ([]shaping.Output, error) {
153 start, end := o.Runes.Offset, o.Runes.Offset+o.Runes.Count
154 if start < 0 || end > len(levels) || start >= end {
155 return nil, fmt.Errorf("invalid output range")
156 }
157 if sameLevel(levels, start, end) {
158 return []shaping.Output{o}, nil
159 }
160 // Group glyphs by lowest text cluster index, then restore HB visual order.
161 clusters := map[int][]shaping.Glyph{}
162 var keys []int
163 for _, g := range o.Glyphs {
164 idx := g.TextIndex()
165 if idx < start || idx >= end {
166 return nil, fmt.Errorf("glyph cluster outside run")
167 }
168 if _, ok := clusters[idx]; !ok {
169 keys = append(keys, idx)
170 }
171 clusters[idx] = append(clusters[idx], g)
172 }
173 sort.Ints(keys)
174 if len(keys) == 0 {
175 return []shaping.Output{o}, nil
176 }
177 var parts []shaping.Output
178 for i, key := range keys {
179 limit := end
180 if i+1 < len(keys) {
181 limit = keys[i+1]
182 }
183 if levels[key] != levels[limit-1] {
184 return nil, fmt.Errorf("bidi level changes within glyph cluster %d", key)
185 }
186 part := o
187 part.Runes = shaping.Range{Offset: key, Count: limit - key}
188 part.Glyphs = append([]shaping.Glyph(nil), clusters[key]...)
189 part.Direction = directionFor(levels[key])
190 part.RecomputeAdvance()
191 parts = append(parts, part)
192 }
193 // Wrapper may have absorbed leading non-glyph format runes.
194 if parts[0].Runes.Offset > start {
195 parts[0].Runes.Count += parts[0].Runes.Offset - start
196 parts[0].Runes.Offset = start
197 }
198 // Recombine adjacent same-level parts without changing glyph order.
199 var merged []shaping.Output
200 for _, part := range parts {
201 if n := len(merged); n > 0 && levels[merged[n-1].Runes.Offset] == levels[part.Runes.Offset] {
202 prev := &merged[n-1]
203 prev.Runes.Count = part.Runes.Offset + part.Runes.Count - prev.Runes.Offset
204 if o.Direction == di.DirectionRTL {
205 prev.Glyphs = append(part.Glyphs, prev.Glyphs...)
206 } else {
207 prev.Glyphs = append(prev.Glyphs, part.Glyphs...)
208 }
209 prev.RecomputeAdvance()
210 continue
211 }
212 merged = append(merged, part)
213 }
214 return merged, nil
215}