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(items []shaping.Input, levels []uint8) []shaping.Input {
23 var out []shaping.Input
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 pos int
56}
57
58// visualRuns 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 visualRuns(text []rune, line shaping.Line, base bidi.BaseDirection) (shaping.Line, map[int]uint8, error) {
63 if len(line) == 0 {
64 return nil, 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, 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 := []int{}
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 levels, _, err := bidi.Resolve(text, base, breaks)
80 if err != nil {
81 return nil, nil, err
82 }
83 // The wrapper's runs are in logical order. A level change within one output
84 // requires reshaping at that boundary; splitLevels does this before wrapping.
85 runs := make([]runItem, 0, len(line))
86 for _, o := range line {
87 s, e := o.Runes.Offset, o.Runes.Offset+o.Runes.Count
88 if s < start || e > end || s >= e {
89 return nil, nil, fmt.Errorf("invalid shaped run %d:%d", s, e)
90 }
91 // L1 may reset a trailing space in a shaped RTL run. Split the output at
92 // cluster boundaries, preserving its glyph metrics and HarfBuzz order.
93 parts, err := splitWrappedLevel(o, levels)
94 if err != nil {
95 return nil, nil, err
96 }
97 for _, part := range parts {
98 runs = append(runs, runItem{output: part, level: levels[part.Runes.Offset], pos: part.Runes.Offset})
99 }
100 }
101 // L2 on run levels: from maximum down to the minimum odd level, reverse
102 // contiguous sequences whose levels are >= the current level.
103 highest, lowestOdd := uint8(0), uint8(255)
104 for _, run := range runs {
105 if run.level > highest {
106 highest = run.level
107 }
108 if run.level&1 == 1 && run.level < lowestOdd {
109 lowestOdd = run.level
110 }
111 }
112 if lowestOdd != 255 {
113 for current := int(highest); current >= int(lowestOdd); current-- {
114 for i := 0; i < len(runs); {
115 if int(runs[i].level) < current {
116 i++
117 continue
118 }
119 j := i + 1
120 for j < len(runs) && int(runs[j].level) >= current {
121 j++
122 }
123 for a, b := i, j-1; a < b; a, b = a+1, b-1 {
124 runs[a], runs[b] = runs[b], runs[a]
125 }
126 i = j
127 }
128 }
129 }
130 out := make(shaping.Line, 0, len(runs))
131 lookup := make(map[int]uint8, len(runs))
132 for _, run := range runs {
133 out = append(out, run.output)
134 lookup[run.pos] = run.level
135 }
136 return out, lookup, nil
137}
138
139// splitWrappedLevel divides only at glyph cluster boundaries; UAX#14 wraps
140// at these same boundaries. Empty/format glyphs stay attached to their cluster.
141func splitWrappedLevel(o shaping.Output, levels []uint8) ([]shaping.Output, error) {
142 start, end := o.Runes.Offset, o.Runes.Offset+o.Runes.Count
143 if start < 0 || end > len(levels) || start >= end {
144 return nil, fmt.Errorf("invalid output range")
145 }
146 same := true
147 for i := start + 1; i < end; i++ {
148 if levels[i] != levels[start] {
149 same = false
150 break
151 }
152 }
153 if same {
154 return []shaping.Output{o}, nil
155 }
156 // Group glyphs by lowest text cluster index, then restore HB visual order.
157 clusters := map[int][]shaping.Glyph{}
158 var keys []int
159 for _, g := range o.Glyphs {
160 idx := g.TextIndex()
161 if idx < start || idx >= end {
162 return nil, fmt.Errorf("glyph cluster outside run")
163 }
164 if _, ok := clusters[idx]; !ok {
165 keys = append(keys, idx)
166 }
167 clusters[idx] = append(clusters[idx], g)
168 }
169 sort.Ints(keys)
170 if len(keys) == 0 {
171 return []shaping.Output{o}, nil
172 }
173 var parts []shaping.Output
174 for i, key := range keys {
175 limit := end
176 if i+1 < len(keys) {
177 limit = keys[i+1]
178 }
179 if levels[key] != levels[limit-1] {
180 return nil, fmt.Errorf("bidi level changes within glyph cluster %d", key)
181 }
182 part := o
183 part.Runes = shaping.Range{Offset: key, Count: limit - key}
184 part.Glyphs = append([]shaping.Glyph(nil), clusters[key]...)
185 part.Direction = directionFor(levels[key])
186 part.RecomputeAdvance()
187 parts = append(parts, part)
188 }
189 // Wrapper may have absorbed leading non-glyph format runes.
190 if parts[0].Runes.Offset > start {
191 parts[0].Runes.Count += parts[0].Runes.Offset - start
192 parts[0].Runes.Offset = start
193 }
194 // Recombine adjacent same-level parts without changing glyph order.
195 var merged []shaping.Output
196 for _, part := range parts {
197 if n := len(merged); n > 0 && levels[merged[n-1].Runes.Offset] == levels[part.Runes.Offset] {
198 prev := &merged[n-1]
199 prev.Runes.Count = part.Runes.Offset + part.Runes.Count - prev.Runes.Offset
200 if o.Direction == di.DirectionRTL {
201 prev.Glyphs = append(part.Glyphs, prev.Glyphs...)
202 } else {
203 prev.Glyphs = append(prev.Glyphs, part.Glyphs...)
204 }
205 prev.RecomputeAdvance()
206 continue
207 }
208 merged = append(merged, part)
209 }
210 return merged, nil
211}