1package renderer
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
43func (f Frame) Validate() error {
44 if f.Width <= 0 || f.Height <= 0 {
45 return fmt.Errorf("invalid frame size %dx%d", f.Width, f.Height)
46 }
47 if len(f.Cells) != f.Width*f.Height {
48 return fmt.Errorf("invalid cell count: got %d want %d", len(f.Cells), f.Width*f.Height)
49 }
50 if len(f.lineOffset) != f.Height {
51 return fmt.Errorf("invalid lineOffset length: got %d want %d", len(f.lineOffset), f.Height)
52 }
53 return nil
54}
55
56// CheckInvariants verifies that lineOffset is a permutation of the canonical
57// row offsets: every entry is a non-negative multiple of Width within bounds,
58// and every logical row maps to a distinct physical row. It is used by tests
59// and assertions; the hot path uses the lighter Validate.
60func (f Frame) CheckInvariants() error {
61 if f.Width <= 0 {
62 return fmt.Errorf("invalid width %d", f.Width)
63 }
64 if len(f.lineOffset) != f.Height {
65 return fmt.Errorf("lineOffset length %d != height %d", len(f.lineOffset), f.Height)
66 }
67 seen := make([]bool, f.Height)
68 for y, off := range f.lineOffset {
69 if off < 0 || off%f.Width != 0 {
70 return fmt.Errorf("row %d: offset %d is not a non-negative multiple of width %d", y, off, f.Width)
71 }
72 phys := off / f.Width
73 if phys >= f.Height {
74 return fmt.Errorf("row %d: offset %d maps to physical row %d out of range [0,%d)", y, off, phys, f.Height)
75 }
76 if seen[phys] {
77 return fmt.Errorf("row %d: physical row %d already mapped by another logical row", y, phys)
78 }
79 seen[phys] = true
80 }
81 return nil
82}
83
84func (f Frame) At(x, y int) Cell {
85 return f.Cells[f.lineOffset[y]+x]
86}
87
88func (f Frame) Set(x, y int, cell Cell) {
89 f.Cells[f.lineOffset[y]+x] = cell
90}
91
92// Row returns the backing slice for logical row y. Mutating the returned slice
93// mutates the frame in place. The slice is valid until the next scroll or
94// resize that reassigns offsets.
95func (f Frame) Row(y int) []Cell {
96 base := f.lineOffset[y]
97 return f.Cells[base : base+f.Width]
98}
99
100// ScrollUp scrolls the logical rows in [top,bottom] up by n lines by rotating
101// their physical offsets: no per-cell copying. Rows scrolled off the top are
102// recycled to the bottom and blanked in place. It assumes the full frame width
103// and 0 <= top <= bottom < Height and 0 < n <= bottom-top+1 (the caller
104// clamps). The receiver is a value, but lineOffset and Cells alias the caller's
105// backing arrays, so in-place mutations are visible to the caller.
106func (f Frame) ScrollUp(top, bottom, n int) {
107 for ; n > 0; n-- {
108 recycled := f.lineOffset[top]
109 for y := top; y < bottom; y++ {
110 f.lineOffset[y] = f.lineOffset[y+1]
111 }
112 f.lineOffset[bottom] = recycled
113 blankRow(f.Cells[recycled : recycled+f.Width])
114 }
115}
116
117// ScrollDown scrolls the logical rows in [top,bottom] down by n lines by
118// rotating their physical offsets. Rows scrolled off the bottom are recycled to
119// the top and blanked in place. See ScrollUp for preconditions.
120func (f Frame) ScrollDown(top, bottom, n int) {
121 for ; n > 0; n-- {
122 recycled := f.lineOffset[bottom]
123 for y := bottom; y > top; y-- {
124 f.lineOffset[y] = f.lineOffset[y-1]
125 }
126 f.lineOffset[top] = recycled
127 blankRow(f.Cells[recycled : recycled+f.Width])
128 }
129}
130
131func blankRow(row []Cell) {
132 for i := range row {
133 row[i] = BlankCell()
134 }
135}