terminal_renderer_hardscroll.go

  1package uv
  2
  3import (
  4	"strings"
  5
  6	"github.com/charmbracelet/x/ansi"
  7)
  8
  9// scrollOptimize optimizes the screen to transform the old buffer into the new
 10// buffer.
 11func (s *TerminalRenderer) scrollOptimize(newbuf *Buffer) {
 12	height := newbuf.Height()
 13	if s.oldnum == nil || len(s.oldnum) < height {
 14		s.oldnum = append(s.oldnum, make([]int, height-len(s.oldnum))...)
 15	}
 16
 17	// Calculate the indices
 18	s.updateHashmap(newbuf)
 19	if len(s.hashtab) < height {
 20		return
 21	}
 22
 23	// Pass 1 - from top to bottom scrolling up
 24	for i := 0; i < height; {
 25		for i < height && (s.oldnum[i] == newIndex || s.oldnum[i] <= i) {
 26			i++
 27		}
 28		if i >= height {
 29			break
 30		}
 31
 32		shift := s.oldnum[i] - i // shift > 0
 33		start := i
 34
 35		i++
 36		for i < height && s.oldnum[i] != newIndex && s.oldnum[i]-i == shift {
 37			i++
 38		}
 39		end := i - 1 + shift
 40
 41		if !s.scrolln(newbuf, shift, start, end, height-1) {
 42			continue
 43		}
 44	}
 45
 46	// Pass 2 - from bottom to top scrolling down
 47	for i := height - 1; i >= 0; {
 48		for i >= 0 && (s.oldnum[i] == newIndex || s.oldnum[i] >= i) {
 49			i--
 50		}
 51		if i < 0 {
 52			break
 53		}
 54
 55		shift := s.oldnum[i] - i // shift < 0
 56		end := i
 57
 58		i--
 59		for i >= 0 && s.oldnum[i] != newIndex && s.oldnum[i]-i == shift {
 60			i--
 61		}
 62
 63		start := i + 1 - (-shift)
 64		if !s.scrolln(newbuf, shift, start, end, height-1) {
 65			continue
 66		}
 67	}
 68}
 69
 70// scrolln scrolls the screen up by n lines.
 71func (s *TerminalRenderer) scrolln(newbuf *Buffer, n, top, bot, maxY int) (v bool) { //nolint:unparam
 72	const (
 73		nonDestScrollRegion = false
 74		memoryBelow         = false
 75	)
 76
 77	blank := s.clearBlank()
 78	if n > 0 {
 79		// Scroll up (forward)
 80		v = s.scrollUp(newbuf, n, top, bot, 0, maxY, blank)
 81		if !v {
 82			s.buf.WriteString(ansi.SetTopBottomMargins(top+1, bot+1))
 83
 84			// XXX: How should we handle this in inline mode when not using alternate screen?
 85			s.cur.X, s.cur.Y = -1, -1
 86			v = s.scrollUp(newbuf, n, top, bot, top, bot, blank)
 87			s.buf.WriteString(ansi.SetTopBottomMargins(1, maxY+1))
 88			s.cur.X, s.cur.Y = -1, -1
 89		}
 90
 91		if !v {
 92			v = s.scrollIdl(newbuf, n, top, bot-n+1, blank)
 93		}
 94
 95		// Clear newly shifted-in lines.
 96		if v &&
 97			(nonDestScrollRegion || (memoryBelow && bot == maxY)) {
 98			if bot == maxY {
 99				s.move(newbuf, 0, bot-n+1)
100				s.clearToBottom(nil)
101			} else {
102				for i := 0; i < n; i++ {
103					s.move(newbuf, 0, bot-i)
104					s.clearToEnd(newbuf, nil, false)
105				}
106			}
107		}
108	} else if n < 0 {
109		// Scroll down (backward)
110		v = s.scrollDown(newbuf, -n, top, bot, 0, maxY, blank)
111		if !v {
112			s.buf.WriteString(ansi.SetTopBottomMargins(top+1, bot+1))
113
114			// XXX: How should we handle this in inline mode when not using alternate screen?
115			s.cur.X, s.cur.Y = -1, -1
116			v = s.scrollDown(newbuf, -n, top, bot, top, bot, blank)
117			s.buf.WriteString(ansi.SetTopBottomMargins(1, maxY+1))
118			s.cur.X, s.cur.Y = -1, -1
119
120			if !v {
121				v = s.scrollIdl(newbuf, -n, bot+n+1, top, blank)
122			}
123
124			// Clear newly shifted-in lines.
125			if v &&
126				(nonDestScrollRegion || (memoryBelow && top == 0)) {
127				for i := 0; i < -n; i++ {
128					s.move(newbuf, 0, top+i)
129					s.clearToEnd(newbuf, nil, false)
130				}
131			}
132		}
133	}
134
135	if !v {
136		return
137	}
138
139	s.scrollBuffer(s.curbuf, n, top, bot, blank)
140
141	// shift hash values too, they can be reused
142	s.scrollOldhash(n, top, bot)
143
144	return true
145}
146
147// scrollBuffer scrolls the buffer by n lines.
148func (s *TerminalRenderer) scrollBuffer(b *Buffer, n, top, bot int, blank *Cell) {
149	if top < 0 || bot < top || bot >= b.Height() {
150		// Nothing to scroll
151		return
152	}
153
154	if n < 0 {
155		// shift n lines downwards
156		limit := top - n
157		for line := bot; line >= limit && line >= 0 && line >= top; line-- {
158			copy(b.Lines[line], b.Lines[line+n])
159		}
160		for line := top; line < limit && line <= b.Height()-1 && line <= bot; line++ {
161			b.FillArea(blank, Rect(0, line, b.Width(), 1))
162		}
163	}
164
165	if n > 0 {
166		// shift n lines upwards
167		limit := bot - n
168		for line := top; line <= limit && line <= b.Height()-1 && line <= bot; line++ {
169			copy(b.Lines[line], b.Lines[line+n])
170		}
171		for line := bot; line > limit && line >= 0 && line >= top; line-- {
172			b.FillArea(blank, Rect(0, line, b.Width(), 1))
173		}
174	}
175
176	s.touchLine(b, top, bot-top+1, true)
177}
178
179// touchLine marks the line as touched.
180func (s *TerminalRenderer) touchLine(newbuf *Buffer, y, n int, changed bool) {
181	height := newbuf.Height()
182	if n < 0 || y < 0 || y >= height || newbuf.Touched == nil || len(newbuf.Touched) < height {
183		return // Nothing to touch
184	}
185
186	width := newbuf.Width()
187	for i := y; i < y+n && i < height && i < len(newbuf.Touched); i++ {
188		if changed {
189			newbuf.TouchLine(0, i, width)
190		} else {
191			newbuf.Touched[i] = nil
192		}
193	}
194}
195
196// scrollUp scrolls the screen up by n lines.
197func (s *TerminalRenderer) scrollUp(newbuf *Buffer, n, top, bot, minY, maxY int, blank *Cell) bool {
198	if n == 1 && top == minY && bot == maxY {
199		s.move(newbuf, 0, bot)
200		s.updatePen(blank)
201		s.buf.WriteByte('\n')
202	} else if n == 1 && bot == maxY {
203		s.move(newbuf, 0, top)
204		s.updatePen(blank)
205		s.buf.WriteString(ansi.DeleteLine(1))
206	} else if top == minY && bot == maxY {
207		supportsSU := s.caps.Contains(capSU)
208		if supportsSU {
209			s.move(newbuf, 0, bot)
210		} else {
211			s.move(newbuf, 0, top)
212		}
213		s.updatePen(blank)
214		if supportsSU {
215			s.buf.WriteString(ansi.ScrollUp(n))
216		} else {
217			s.buf.WriteString(strings.Repeat("\n", n))
218		}
219	} else if bot == maxY {
220		s.move(newbuf, 0, top)
221		s.updatePen(blank)
222		s.buf.WriteString(ansi.DeleteLine(n))
223	} else {
224		return false
225	}
226	return true
227}
228
229// scrollDown scrolls the screen down by n lines.
230func (s *TerminalRenderer) scrollDown(newbuf *Buffer, n, top, bot, minY, maxY int, blank *Cell) bool {
231	if n == 1 && top == minY && bot == maxY {
232		s.move(newbuf, 0, top)
233		s.updatePen(blank)
234		s.buf.WriteString(ansi.ReverseIndex)
235	} else if n == 1 && bot == maxY {
236		s.move(newbuf, 0, top)
237		s.updatePen(blank)
238		s.buf.WriteString(ansi.InsertLine(1))
239	} else if top == minY && bot == maxY {
240		s.move(newbuf, 0, top)
241		s.updatePen(blank)
242		if s.caps.Contains(capSD) {
243			s.buf.WriteString(ansi.ScrollDown(n))
244		} else {
245			s.buf.WriteString(strings.Repeat(ansi.ReverseIndex, n))
246		}
247	} else if bot == maxY {
248		s.move(newbuf, 0, top)
249		s.updatePen(blank)
250		s.buf.WriteString(ansi.InsertLine(n))
251	} else {
252		return false
253	}
254	return true
255}
256
257// scrollIdl scrolls the screen n lines by using [ansi.DL] at del and using
258// [ansi.IL] at ins.
259func (s *TerminalRenderer) scrollIdl(newbuf *Buffer, n, del, ins int, blank *Cell) bool {
260	if n < 0 {
261		return false
262	}
263
264	// Delete lines
265	s.move(newbuf, 0, del)
266	s.updatePen(blank)
267	s.buf.WriteString(ansi.DeleteLine(n))
268
269	// Insert lines
270	s.move(newbuf, 0, ins)
271	s.updatePen(blank)
272	s.buf.WriteString(ansi.InsertLine(n))
273
274	return true
275}