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 // Read boundaries and IDs before the caller rotates the frame: a soft link
107 // belongs to the row it follows, and rotation reassigns row indices.
108 for y := top; y < top+n; y++ {
109 s.recordEvicted(s.frame.Row(y), s.buffer.bound(y), s.buffer.rowIDs[y])
110 }
111}
112
113func (s *Screen) recordEvicted(row []renderer.Cell, bound LineBound, id RowID) {
114 if s.history != nil {
115 err := s.history.AppendWithID(row, bound, id)
116 if err != nil && !errors.Is(err, ErrHistoryRowTooLarge) {
117 panic(err)
118 }
119 // A nil error records the row; ErrHistoryRowTooLarge explicitly leaves it
120 // unrecorded. Both cases continue to the eviction observer below.
121 }
122 if s.OnLineEvicted != nil {
123 s.OnLineEvicted(append([]renderer.Cell(nil), row...))
124 }
125}
126
127func (s *Screen) scrollDownRegion(top, bottom, n int) {
128 if s.frame.Width == 0 || s.frame.Height == 0 || n <= 0 {
129 return
130 }
131 top, bottom, ok := s.normalizedRegion(top, bottom)
132 if !ok {
133 return
134 }
135 w := s.frame.Width
136 height := bottom - top + 1
137 graphicsRows := n
138 if n > height {
139 n = height
140 }
141 s.scrollGraphicsRegion(top, bottom, graphicsRows)
142 // Full-width region: rotate line offsets instead of copying cells.
143 s.frame.ScrollDown(top, bottom, n)
144 s.buffer.scrollDown(top, bottom, n)
145 s.fillMissingRowIDs(s.buffer)
146 s.record(renderer.Damage{Kind: renderer.DamageText, X: 0, Y: top, Width: w, Height: height, Count: 1})
147}
148
149func (s *Screen) scrollGraphicsRegion(top, bottom, rows int) {
150 if rows == 0 || s.graphics == nil || s.graphics.scene == nil {
151 return
152 }
153 _, cellHeight, ok := s.kittyCellPixels()
154 if !ok || cellHeight <= 0 {
155 return
156 }
157 delta := int64(rows) * int64(cellHeight)
158 fullScreen := top == 0 && bottom == s.frame.Height-1
159 snapshot := s.graphics.scene.Snapshot()
160 placements := snapshot.Placements()
161 operations := make([]graphics.Operation, 0, len(placements))
162 for _, placement := range placements {
163 destination := placement.Destination()
164 if !fullScreen {
165 end, valid := destination.Bottom()
166 if !valid || destination.Y < 0 || end <= destination.Y {
167 continue
168 }
169 startRow := int(destination.Y / int64(cellHeight))
170 endRow := int((end - 1) / int64(cellHeight))
171 if startRow < top || endRow > bottom {
172 continue
173 }
174 }
175 if delta > 0 && destination.Y > math.MaxInt64-delta || delta < 0 && destination.Y < math.MinInt64-delta {
176 continue
177 }
178 destination.Y += delta
179 if _, valid := destination.Bottom(); !valid {
180 continue
181 }
182 spec := placement.Spec()
183 spec.Destination = destination
184 operations = append(operations, graphics.Operation{Kind: graphics.OperationUpdatePlacement, PlacementID: placement.ID(), Placement: spec})
185 }
186 if len(operations) != 0 {
187 if err := s.graphics.scene.Apply(operations...); err != nil {
188 panic(fmt.Errorf("scroll graphics placements: %w", err))
189 }
190 }
191}
192
193func (s *Screen) normalizedRegion(top, bottom int) (int, int, bool) {
194 if s.frame.Height == 0 || top > bottom {
195 return 0, 0, false
196 }
197 top = clamp(top, 0, s.frame.Height-1)
198 bottom = clamp(bottom, 0, s.frame.Height-1)
199 return top, bottom, top <= bottom
200}