1package vt
2
3import (
4 "errors"
5 "fmt"
6 "math"
7
8 renderer "github.com/bnema/vev-vt/core"
9 "github.com/bnema/vev-vt/graphics"
10)
11
12// index moves to the next physical row, scrolling the region when the cursor
13// already sits on its last row. It reports whether the row the cursor was on
14// survived that scroll by moving up to s.Row-1, which only a deferred wrap
15// cares about: a wrap must relink to the row it flowed out of, and
16// buffer.scrollUp severs that link on the way. Callers that merely advance,
17// such as LF and ESC D, ignore the result.
18func (s *Screen) index() (movedUp bool) {
19 if s.frame.Height == 0 {
20 return false
21 }
22 if s.Row == s.scrollBottom && s.Row >= s.scrollTop {
23 return s.scrollUpRegion(s.scrollTop, s.scrollBottom, 1)
24 }
25 if s.Row+1 < s.frame.Height {
26 s.Row++
27 return false
28 }
29 s.Row = s.frame.Height - 1
30 return false
31}
32
33func (s *Screen) nextLine() {
34 s.buffer.hard(s.Row)
35 s.Col = 0
36 s.index()
37}
38
39func (s *Screen) reverseIndex() {
40 if s.frame.Height == 0 {
41 return
42 }
43 if s.Row == s.scrollTop && s.Row <= s.scrollBottom {
44 s.scrollDownRegion(s.scrollTop, s.scrollBottom, 1)
45 return
46 }
47 if s.Row > 0 {
48 s.Row--
49 }
50}
51
52// scrollUpBy scrolls the current scroll region up by n lines (CSI S / SU).
53// The cursor position is preserved.
54func (s *Screen) scrollUpBy(n int) {
55 if n <= 0 {
56 return
57 }
58 s.scrollUpRegion(s.scrollTop, s.scrollBottom, n)
59}
60
61func (s *Screen) scrollDownBy(n int) {
62 if n <= 0 {
63 return
64 }
65 s.scrollDownRegion(s.scrollTop, s.scrollBottom, n)
66}
67
68// scrollUpRegion scrolls [top,bottom] up by n. It reports whether any row
69// survived by moving up: a region exactly n rows tall is blanked in place, so
70// the receiving range [top,bottom-n] is empty and nothing survives. Reporting
71// what the scroll observed keeps callers from re-deriving a predicate that this
72// function may have declined to act on at all.
73func (s *Screen) scrollUpRegion(top, bottom, n int) (shifted bool) {
74 if s.frame.Width == 0 || s.frame.Height == 0 || n <= 0 {
75 return false
76 }
77 top, bottom, ok := s.normalizedRegion(top, bottom)
78 if !ok {
79 return false
80 }
81 w := s.frame.Width
82 height := bottom - top + 1
83 graphicsRows := n
84 if n > height {
85 n = height
86 }
87 s.scrollGraphicsRegion(top, bottom, -graphicsRows)
88 // VT scroll regions always span the full frame width, so we rotate the
89 // frame's line offsets (recycling and blanking the evicted rows in place)
90 // instead of copying cells. See renderer.Frame.ScrollUp.
91 s.emitLineEvicted(top, n)
92 s.frame.ScrollUp(top, bottom, n)
93 s.buffer.scrollUp(top, bottom, n)
94 s.fillMissingRowIDs(s.buffer)
95 s.record(renderer.Damage{Kind: renderer.DamageScrollUp, X: 0, Y: top, Width: w, Height: height, Count: n})
96 s.record(renderer.Damage{Kind: renderer.DamageText, X: 0, Y: bottom - n + 1, Width: w, Height: n, Count: 1})
97 return bottom-n >= top
98}
99
100func (s *Screen) emitLineEvicted(top, n int) {
101 // Only rows leaving the top edge of the primary screen belong to global
102 // scrollback. Interior DECSTBM scroll regions are local mutations.
103 if s.alternate != nil || top != 0 {
104 return
105 }
106 // Without a history or an observer nothing consumes the evicted rows, so
107 // skip materialising them.
108 if s.history == nil && s.OnLineEvicted == nil {
109 return
110 }
111 // Drop scratch sized for a much wider past geometry.
112 if cap(s.evictScratch) > 2*s.frame.Width {
113 s.evictScratch = nil
114 }
115 // Read boundaries and IDs before the caller rotates the frame: a soft link
116 // belongs to the row it follows, and rotation reassigns row indices.
117 for y := top; y < top+n; y++ {
118 // The scratch row is only valid for this call: History copies appended
119 // rows and OnLineEvicted receives its own copy (see recordEvicted).
120 row := s.evictScratch[:0]
121 for x := range s.frame.Width {
122 row = append(row, s.frame.Cell(x, y))
123 }
124 s.evictScratch = row
125 s.recordEvicted(row, s.buffer.bound(y), s.buffer.rowIDs[y])
126 clear(s.evictScratch)
127 }
128}
129
130func (s *Screen) recordEvicted(row []renderer.Cell, bound LineBound, id RowID) {
131 if s.history != nil {
132 err := s.history.AppendWithID(row, bound, id)
133 if err != nil && !errors.Is(err, ErrHistoryRowTooLarge) {
134 panic(err)
135 }
136 // A nil error records the row; ErrHistoryRowTooLarge explicitly leaves it
137 // unrecorded. Both cases continue to the eviction observer below.
138 }
139 if s.OnLineEvicted != nil {
140 s.OnLineEvicted(append([]renderer.Cell(nil), row...))
141 }
142}
143
144func (s *Screen) scrollDownRegion(top, bottom, n int) {
145 if s.frame.Width == 0 || s.frame.Height == 0 || n <= 0 {
146 return
147 }
148 top, bottom, ok := s.normalizedRegion(top, bottom)
149 if !ok {
150 return
151 }
152 w := s.frame.Width
153 height := bottom - top + 1
154 graphicsRows := n
155 if n > height {
156 n = height
157 }
158 s.scrollGraphicsRegion(top, bottom, graphicsRows)
159 // Full-width region: rotate line offsets instead of copying cells.
160 s.frame.ScrollDown(top, bottom, n)
161 s.buffer.scrollDown(top, bottom, n)
162 s.fillMissingRowIDs(s.buffer)
163 s.record(renderer.Damage{Kind: renderer.DamageText, X: 0, Y: top, Width: w, Height: height, Count: 1})
164}
165
166func (s *Screen) scrollGraphicsRegion(top, bottom, rows int) {
167 if rows == 0 || s.graphics == nil || s.graphics.scene == nil {
168 return
169 }
170 _, cellHeight, ok := s.kittyCellPixels()
171 if !ok || cellHeight <= 0 {
172 return
173 }
174 delta := int64(rows) * int64(cellHeight)
175 fullScreen := top == 0 && bottom == s.frame.Height-1
176 snapshot := s.graphics.scene.Snapshot()
177 placements := snapshot.Placements()
178 operations := make([]graphics.Operation, 0, len(placements))
179 for _, placement := range placements {
180 destination := placement.Destination()
181 if !fullScreen {
182 end, valid := destination.Bottom()
183 if !valid || destination.Y < 0 || end <= destination.Y {
184 continue
185 }
186 startRow := int(destination.Y / int64(cellHeight))
187 endRow := int((end - 1) / int64(cellHeight))
188 if startRow < top || endRow > bottom {
189 continue
190 }
191 }
192 if delta > 0 && destination.Y > math.MaxInt64-delta || delta < 0 && destination.Y < math.MinInt64-delta {
193 continue
194 }
195 destination.Y += delta
196 if _, valid := destination.Bottom(); !valid {
197 continue
198 }
199 spec := placement.Spec()
200 spec.Destination = destination
201 operations = append(operations, graphics.Operation{Kind: graphics.OperationUpdatePlacement, PlacementID: placement.ID(), Placement: spec})
202 }
203 if len(operations) != 0 {
204 if err := s.graphics.scene.Apply(operations...); err != nil {
205 panic(fmt.Errorf("scroll graphics placements: %w", err))
206 }
207 }
208}
209
210func (s *Screen) normalizedRegion(top, bottom int) (int, int, bool) {
211 if s.frame.Height == 0 || top > bottom {
212 return 0, 0, false
213 }
214 top = clamp(top, 0, s.frame.Height-1)
215 bottom = clamp(bottom, 0, s.frame.Height-1)
216 return top, bottom, top <= bottom
217}