1package ansi
2
3import (
4 "bytes"
5 "strconv"
6)
7
8func findSafeScroll(frame CellSource, damage []Damage) (Damage, bool) {
9 for _, d := range damage {
10 if (d.Kind == DamageScrollUp || d.Kind == DamageScrollDown) && isSafeScroll(frame, d) {
11 return d, true
12 }
13 }
14 return Damage{}, false
15}
16
17func isSafeScroll(frame CellSource, d Damage) bool {
18 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
19}
20
21func emitScroll(out *bytes.Buffer, d Damage) {
22 out.WriteString("\x1b[0m")
23 // \x1b[%d;%dr — set scroll region
24 out.WriteString("\x1b[")
25 var b [16]byte
26 n := strconv.AppendInt(b[:0], int64(d.Y+1), 10)
27 out.Write(n)
28 out.WriteByte(';')
29 n = strconv.AppendInt(b[:0], int64(d.Y+d.Height), 10)
30 out.Write(n)
31 out.WriteByte('r')
32 // SD scrolls down without moving content outside the chosen region.
33 if d.Kind == DamageScrollDown {
34 out.WriteString("\x1b[")
35 n = strconv.AppendInt(b[:0], int64(d.Count), 10)
36 out.Write(n)
37 out.WriteByte('T')
38 out.WriteString("\x1b[r")
39 return
40 }
41 // Position cursor at bottom of scroll region.
42 writeCursor(out, d.Y+d.Height-1, 0)
43 if d.Count == 1 {
44 out.WriteByte('\n')
45 } else {
46 out.WriteString("\x1b[")
47 n = strconv.AppendInt(b[:0], int64(d.Count), 10)
48 out.Write(n)
49 out.WriteByte('S')
50 }
51 out.WriteString("\x1b[r")
52}
53
54func canApplyScrollAgainst(frame CellSource, scroll Damage, damage []Damage, committed Frame) bool {
55 // Scrolling fills exposed rows with default-style blanks. Every target
56 // cell that differs from that fill must be repainted, in either direction.
57 exposed := scroll.Y + scroll.Height - scroll.Count
58 if scroll.Kind == DamageScrollDown {
59 exposed = scroll.Y
60 }
61 blank := BlankCell()
62 for y := exposed; y < exposed+scroll.Count; y++ {
63 for x := range scroll.Width {
64 column := scroll.X + x
65 if !damageCoversCell(damage, column, y) && !frame.Cell(column, y).Equal(blank) {
66 return false
67 }
68 }
69 }
70 switch frame := frame.(type) {
71 case Frame:
72 return canApplyDenseScrollAgainst(frame, scroll, damage, committed)
73 case *Frame:
74 return canApplyDenseScrollAgainst(*frame, scroll, damage, committed)
75 }
76 start, end, offset := scrollRetainedRows(scroll)
77 for y := start; y < end; y++ {
78 for x := range scroll.Width {
79 column := scroll.X + x
80 committedCell, frameCell := committed.Cell(column, y+offset), frame.Cell(column, y)
81 if committedCell == frameCell || committedCell.Equal(frameCell) {
82 continue
83 }
84 if !damageCoversCell(damage, column, y) {
85 return false
86 }
87 }
88 }
89 return true
90}
91
92func canApplyDenseScrollAgainst(frame Frame, scroll Damage, damage []Damage, committed Frame) bool {
93 start, end, offset := scrollRetainedRows(scroll)
94 for y := start; y < end; y++ {
95 for x := range scroll.Width {
96 column := scroll.X + x
97 committedCell, frameCell := committed.At(column, y+offset), frame.At(column, y)
98 if committedCell == frameCell || committedCell.Equal(frameCell) {
99 continue
100 }
101 if !damageCoversCell(damage, column, y) {
102 return false
103 }
104 }
105 }
106 return true
107}
108
109func scrollRetainedRows(scroll Damage) (start, end, offset int) {
110 if scroll.Kind == DamageScrollDown {
111 return scroll.Y + scroll.Count, scroll.Y + scroll.Height, -scroll.Count
112 }
113 return scroll.Y, scroll.Y + scroll.Height - scroll.Count, scroll.Count
114}
115
116func damageCoversCell(damage []Damage, x, y int) bool {
117 for _, d := range damage {
118 if (d.Kind == DamageText || d.Kind == DamageClear) && x >= d.X && x < d.X+d.Width && y >= d.Y && y < d.Y+d.Height {
119 return true
120 }
121 }
122 return false
123}