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