1package vtcore
2
3import "fmt"
4
5// Frame is a fixed-size grid of cells. Logical rows are decoupled from their
6// physical storage through lineOffset: logical row y lives at
7// Cells[lineOffset[y] : lineOffset[y]+Width]. This indirection lets full-width
8// scrolls rotate offsets instead of copying cell memory (NeoVim's grid
9// technique; cf. nvim grid.c line_offset[]). All cell access must go through
10// the accessors (At/Set/Row) so callers never assume a physical layout.
11type Frame struct {
12 Width int
13 Height int
14 Cells []Cell
15 // lineOffset maps a logical row index to its physical base index into
16 // Cells. It is always a permutation of the canonical offsets y*Width.
17 lineOffset []int
18}
19
20func NewFrame(width, height int) Frame {
21 cells := make([]Cell, width*height)
22 for i := range cells {
23 cells[i] = BlankCell()
24 }
25 lineOffset := make([]int, height)
26 for y := range lineOffset {
27 lineOffset[y] = y * width
28 }
29 return Frame{Width: width, Height: height, Cells: cells, lineOffset: lineOffset}
30}
31
32// Clone returns an independent copy preserving logical row contents. The clone
33// uses canonical row storage so later scrolls or writes to the source frame do
34// not affect it.
35func (f Frame) Clone() Frame {
36 clone := NewFrame(f.Width, f.Height)
37 for y := range f.Height {
38 copy(clone.Row(y), f.Row(y))
39 }
40 return clone
41}
42
43// Replace copies src into f while reusing canonical storage when dimensions
44// permit. src is read through logical rows so its physical row rotation is
45// preserved in the resulting contents without exposing layout internals.
46func (f *Frame) Replace(src Frame) {
47 if f == nil {
48 return
49 }
50 if src.Width <= 0 || src.Height <= 0 || len(src.Cells) != src.Width*src.Height {
51 *f = Frame{}
52 return
53 }
54
55 sameDimensions := f.Width == src.Width && f.Height == src.Height
56 cellCount := src.Width * src.Height
57 if sameDimensions && cap(f.Cells) >= cellCount {
58 f.Cells = f.Cells[:cellCount]
59 } else {
60 f.Cells = make([]Cell, cellCount)
61 }
62 if sameDimensions && cap(f.lineOffset) >= src.Height {
63 f.lineOffset = f.lineOffset[:src.Height]
64 } else {
65 f.lineOffset = make([]int, src.Height)
66 }
67 f.Width = src.Width
68 f.Height = src.Height
69 for y := range f.lineOffset {
70 f.lineOffset[y] = y * f.Width
71 }
72 for y := range src.Height {
73 copy(f.Row(y), src.Row(y))
74 }
75}
76
77func (f Frame) Validate() error {
78 if f.Width <= 0 || f.Height <= 0 {
79 return fmt.Errorf("invalid frame size %dx%d", f.Width, f.Height)
80 }
81 if len(f.Cells) != f.Width*f.Height {
82 return fmt.Errorf("invalid cell count: got %d want %d", len(f.Cells), f.Width*f.Height)
83 }
84 if len(f.lineOffset) != f.Height {
85 return fmt.Errorf("invalid lineOffset length: got %d want %d", len(f.lineOffset), f.Height)
86 }
87 return nil
88}
89
90// CheckInvariants verifies that lineOffset is a permutation of the canonical
91// row offsets: every entry is a non-negative multiple of Width within bounds,
92// and every logical row maps to a distinct physical row. It is used by tests
93// and assertions; the hot path uses the lighter Validate.
94func (f Frame) CheckInvariants() error {
95 if f.Width <= 0 {
96 return fmt.Errorf("invalid width %d", f.Width)
97 }
98 if len(f.lineOffset) != f.Height {
99 return fmt.Errorf("lineOffset length %d != height %d", len(f.lineOffset), f.Height)
100 }
101 seen := make([]bool, f.Height)
102 for y, off := range f.lineOffset {
103 if off < 0 || off%f.Width != 0 {
104 return fmt.Errorf("row %d: offset %d is not a non-negative multiple of width %d", y, off, f.Width)
105 }
106 phys := off / f.Width
107 if phys >= f.Height {
108 return fmt.Errorf("row %d: offset %d maps to physical row %d out of range [0,%d)", y, off, phys, f.Height)
109 }
110 if seen[phys] {
111 return fmt.Errorf("row %d: physical row %d already mapped by another logical row", y, phys)
112 }
113 seen[phys] = true
114 }
115 return nil
116}
117
118func (f Frame) At(x, y int) Cell {
119 return f.Cells[f.lineOffset[y]+x]
120}
121
122func (f Frame) Set(x, y int, cell Cell) {
123 f.Cells[f.lineOffset[y]+x] = cell
124}
125
126// Row returns the backing slice for logical row y. Mutating the returned slice
127// mutates the frame in place. The slice is valid until the next scroll or
128// resize that reassigns offsets.
129func (f Frame) Row(y int) []Cell {
130 base := f.lineOffset[y]
131 return f.Cells[base : base+f.Width]
132}
133
134// ScrollUp scrolls the logical rows in [top,bottom] up by n lines by rotating
135// their physical offsets: no per-cell copying. Rows scrolled off the top are
136// recycled to the bottom and blanked in place. It assumes the full frame width
137// and 0 <= top <= bottom < Height and 0 < n <= bottom-top+1 (the caller
138// clamps). The receiver is a value, but lineOffset and Cells alias the caller's
139// backing arrays, so in-place mutations are visible to the caller.
140func (f Frame) ScrollUp(top, bottom, n int) {
141 for ; n > 0; n-- {
142 recycled := f.lineOffset[top]
143 for y := top; y < bottom; y++ {
144 f.lineOffset[y] = f.lineOffset[y+1]
145 }
146 f.lineOffset[bottom] = recycled
147 blankRow(f.Cells[recycled : recycled+f.Width])
148 }
149}
150
151// ScrollDown scrolls the logical rows in [top,bottom] down by n lines by
152// rotating their physical offsets. Rows scrolled off the bottom are recycled
153// to the top and blanked in place. See ScrollUp for preconditions.
154func (f Frame) ScrollDown(top, bottom, n int) {
155 for ; n > 0; n-- {
156 recycled := f.lineOffset[bottom]
157 for y := bottom; y > top; y-- {
158 f.lineOffset[y] = f.lineOffset[y-1]
159 }
160 f.lineOffset[top] = recycled
161 blankRow(f.Cells[recycled : recycled+f.Width])
162 }
163}
164
165func blankRow(row []Cell) {
166 for i := range row {
167 row[i] = BlankCell()
168 }
169}