1package text
2
3import (
4 "errors"
5 "github.com/bnema/nefergui/internal/bidi"
6)
7
8type paragraphRange struct{ start, end int }
9
10// paragraphRanges excludes paragraph separators from shaping. CRLF is a
11// single separator but consumes two source runes, so Run offsets stay global.
12// The terminal empty range is a caret line only when the input ends in a
13// separator; the empty input retains Measure's existing zero-height behavior.
14func paragraphRanges(text []rune) ([]paragraphRange, bool) {
15 var out []paragraphRange
16 start := 0
17 split := false
18 for i := 0; i < len(text); i++ {
19 r := text[i]
20 if !paragraphSeparator(r) {
21 continue
22 }
23 out = append(out, paragraphRange{start, i})
24 split = true
25 if r == '\r' && i+1 < len(text) && text[i+1] == '\n' {
26 i++
27 }
28 start = i + 1
29 }
30 if split {
31 out = append(out, paragraphRange{start, len(text)})
32 }
33 return out, split
34}
35func paragraphSeparator(r rune) bool {
36 switch r {
37 case '\n', '\r', 0x001c, 0x001d, 0x001e, 0x0085, 0x2029:
38 return true
39 }
40 return false
41}
42
43func (e *Engine) measureParagraphs(text []rune, paragraphs []paragraphRange, r Request, width float64) (Layout, error) {
44 var result Layout
45 if r.Direction > bidi.RTL {
46 return result, errors.New("invalid paragraph direction")
47 }
48 for index, p := range paragraphs {
49 // Uncached: part is modified in place below.
50 part, err := e.measure(string(text[p.start:p.end]), r, width)
51 if err != nil {
52 return Layout{}, err
53 }
54 if index == 0 {
55 result.Direction = part.Direction
56 result.Baseline = part.Baseline
57 result.LineHeight = part.LineHeight
58 }
59 if len(part.Lines) == 0 {
60 part.Lines = []Line{{Direction: part.Direction, Height: part.LineHeight, Baseline: part.Baseline}}
61 }
62 for _, line := range part.Lines {
63 line.Baseline += result.Height
64 for i := range line.Runs {
65 run := &line.Runs[i]
66 run.Start += p.start
67 run.End += p.start
68 run.Y += result.Height
69 for j := range run.Glyphs {
70 run.Glyphs[j].Y += result.Height
71 run.Glyphs[j].Cluster += p.start
72 }
73 }
74 result.Lines = append(result.Lines, line)
75 if line.Width > result.Width {
76 result.Width = line.Width
77 }
78 }
79 result.Height += float64(len(part.Lines)) * part.LineHeight
80 }
81 return result, nil
82}
83
84// A layout's Direction refers to the first paragraph. Each Line has its own
85// Direction so subsequent auto-detected RTL paragraphs retain their basis.