1package ansi
2
3import (
4 "bytes"
5 "strconv"
6
7 "github.com/bnema/vev-vt/core"
8)
9
10func findSafeScroll(frame CellSource, damage []Damage) (Damage, bool) {
11 for _, d := range damage {
12 if (d.Kind == DamageScrollUp || d.Kind == DamageScrollDown) && isSafeScroll(frame, d) {
13 return d, true
14 }
15 }
16 return Damage{}, false
17}
18
19func isSafeScroll(frame CellSource, d Damage) bool {
20 return d.X == 0 && d.Width == frame.Columns() && d.Y >= 0 && d.Y <= frame.Rows() && d.Height > 0 && d.Height <= frame.Rows()-d.Y && d.Count > 0 && d.Count < d.Height
21}
22
23func emitScroll(out *bytes.Buffer, d Damage) {
24 out.WriteString("\x1b[0m")
25 // \x1b[%d;%dr — set scroll region
26 out.WriteString("\x1b[")
27 var b [16]byte
28 n := strconv.AppendInt(b[:0], int64(d.Y+1), 10)
29 out.Write(n)
30 out.WriteByte(';')
31 n = strconv.AppendInt(b[:0], int64(d.Y+d.Height), 10)
32 out.Write(n)
33 out.WriteByte('r')
34 // SD scrolls down without moving content outside the chosen region.
35 if d.Kind == DamageScrollDown {
36 out.WriteString("\x1b[")
37 n = strconv.AppendInt(b[:0], int64(d.Count), 10)
38 out.Write(n)
39 out.WriteByte('T')
40 out.WriteString("\x1b[r")
41 return
42 }
43 // Position cursor at bottom of scroll region.
44 writeCursor(out, d.Y+d.Height-1, 0)
45 if d.Count == 1 {
46 out.WriteByte('\n')
47 } else {
48 out.WriteString("\x1b[")
49 n = strconv.AppendInt(b[:0], int64(d.Count), 10)
50 out.Write(n)
51 out.WriteByte('S')
52 }
53 out.WriteString("\x1b[r")
54}
55
56func canApplyScrollAgainst(frame CellSource, scroll Damage, damage []Damage, committed Frame) bool {
57 // Scrolling fills exposed rows with default-style blanks. Every target
58 // cell that differs from that fill must be repainted, in either direction.
59 exposed := scroll.Y + scroll.Height - scroll.Count
60 if scroll.Kind == DamageScrollDown {
61 exposed = scroll.Y
62 }
63 blank := BlankCell()
64 for y := exposed; y < exposed+scroll.Count; y++ {
65 for x := range scroll.Width {
66 column := scroll.X + x
67 if !damageCoversCell(damage, column, y) && !frame.Cell(column, y).Equal(blank) {
68 return false
69 }
70 }
71 }
72 // The plan emits one scroll plus text/clear spans. Rows outside the chosen
73 // region must therefore already match the committed shadow wherever damage
74 // does not repaint them; another scroll region in the same batch moves rows
75 // without text damage and must fall back to a snapshot.
76 if !outsideScrollMatches(frame, scroll, damage, committed) {
77 return false
78 }
79 if dense, ok := asFrame(frame); ok {
80 return canApplyDenseScrollAgainst(dense, scroll, damage, committed)
81 }
82 start, end, offset := scrollRetainedRows(scroll)
83 for y := start; y < end; y++ {
84 for x := range scroll.Width {
85 column := scroll.X + x
86 committedCell, frameCell := committed.Cell(column, y+offset), frame.Cell(column, y)
87 if committedCell == frameCell || committedCell.Equal(frameCell) {
88 continue
89 }
90 if !damageCoversCell(damage, column, y) {
91 return false
92 }
93 }
94 }
95 return true
96}
97
98func canApplyDenseScrollAgainst(frame Frame, scroll Damage, damage []Damage, committed Frame) bool {
99 start, end, offset := scrollRetainedRows(scroll)
100 for y := start; y < end; y++ {
101 // isSafeScroll guarantees full-width scrolls, so a retained row that
102 // matches whole needs no per-cell damage lookups.
103 if core.RowsEqualAt(committed, y+offset, frame, y) {
104 continue
105 }
106 for x := range scroll.Width {
107 column := scroll.X + x
108 committedCell, frameCell := committed.At(column, y+offset), frame.At(column, y)
109 if committedCell == frameCell || committedCell.Equal(frameCell) {
110 continue
111 }
112 if !damageCoversCell(damage, column, y) {
113 return false
114 }
115 }
116 }
117 return true
118}
119
120func outsideScrollMatches(frame CellSource, scroll Damage, damage []Damage, committed Frame) bool {
121 dense, isDense := asFrame(frame)
122 columns := frame.Columns()
123 for y := range frame.Rows() {
124 if y >= scroll.Y && y < scroll.Y+scroll.Height {
125 continue
126 }
127 if isDense && core.RowsEqualAt(committed, y, dense, y) {
128 continue
129 }
130 for x := range columns {
131 committedCell, frameCell := committed.Cell(x, y), frame.Cell(x, y)
132 if committedCell == frameCell || committedCell.Equal(frameCell) {
133 continue
134 }
135 if !damageCoversCell(damage, x, y) {
136 return false
137 }
138 }
139 }
140 return true
141}
142
143func scrollRetainedRows(scroll Damage) (start, end, offset int) {
144 if scroll.Kind == DamageScrollDown {
145 return scroll.Y + scroll.Count, scroll.Y + scroll.Height, -scroll.Count
146 }
147 return scroll.Y, scroll.Y + scroll.Height - scroll.Count, scroll.Count
148}
149
150func damageCoversCell(damage []Damage, x, y int) bool {
151 for _, d := range damage {
152 if (d.Kind == DamageText || d.Kind == DamageClear) && x >= d.X && x < d.X+d.Width && y >= d.Y && y < d.Y+d.Height {
153 return true
154 }
155 }
156 return false
157}