1package ansi
2
3import "github.com/bnema/vev-vt/core"
4
5// Span identifies a changed horizontal range in a logical frame row.
6type Span struct {
7 Y int
8 X int
9 Width int
10}
11
12// Scroll identifies a safe full-width scroll.
13type Scroll struct {
14 Y int
15 Height int
16 Count int
17 Down bool
18}
19
20// DeltaPlan describes how to advance a committed frame to the candidate frame.
21// Snapshot requests replacement of the committed frame; Spans, when present,
22// are the canonical ranges to encode before that replacement.
23type DeltaPlan struct {
24 Snapshot bool
25 Scroll Scroll
26 Spans []Span
27}
28
29// DeltaCandidate holds a plan and owns a source frame snapshot when the plan
30// has work to commit. Empty candidates are true no-ops.
31type DeltaCandidate struct {
32 Plan DeltaPlan
33 frame Frame
34}
35
36// newDeltaCandidate builds a candidate whose snapshot, when the plan has work to
37// commit, is copied into scratch (reusing its storage) or into a fresh frame
38// when scratch is nil.
39func newDeltaCandidate(frame CellSource, plan DeltaPlan, scratch *Frame) DeltaCandidate {
40 candidate := DeltaCandidate{Plan: plan}
41 if plan.Snapshot || plan.Scroll.Height != 0 || len(plan.Spans) != 0 {
42 candidate.frame = snapshotCellSource(scratch, frame)
43 }
44 return candidate
45}
46
47// PlanDelta prepares a non-mutating update from committed to frame. The
48// returned candidate owns an independent snapshot of frame.
49func PlanDelta(frame CellSource, damage []Damage, committed Frame, reset bool) (DeltaCandidate, error) {
50 return planDelta(frame, damage, committed, reset, nil)
51}
52
53// planDelta implements PlanDelta. A non-nil scratch receives the candidate's
54// snapshot in place of a fresh allocation; the caller then owns its lifetime.
55func planDelta(frame CellSource, damage []Damage, committed Frame, reset bool, scratch *Frame) (DeltaCandidate, error) {
56 if err := validateCellSource(frame); err != nil {
57 return DeltaCandidate{}, err
58 }
59
60 if reset || frame.Columns() != committed.Columns() || frame.Rows() != committed.Rows() {
61 return newDeltaCandidate(frame, DeltaPlan{Snapshot: true}, scratch), nil
62 }
63 if err := committed.Validate(); err != nil {
64 return DeltaCandidate{}, err
65 }
66
67 if len(damage) == 1 && (damage[0].Kind == DamageText || damage[0].Kind == DamageClear) {
68 return newDeltaCandidate(frame, planSingleDamage(frame, damage[0]), scratch), nil
69 }
70
71 if needsFull(damage) {
72 dirty, full := buildDirtyLinePlan(frame, committed)
73 if len(dirty) > 0 || full {
74 return newDeltaCandidate(frame, DeltaPlan{Snapshot: true, Spans: dirty}, scratch), nil
75 }
76 return DeltaCandidate{}, nil
77 }
78
79 var plan DeltaPlan
80 var skip *Damage
81 if scroll, ok := findSafeScroll(frame, damage); ok && canApplyScrollAgainst(frame, scroll, damage, committed) {
82 plan.Scroll = Scroll{Y: scroll.Y, Height: scroll.Height, Count: scroll.Count, Down: scroll.Kind == DamageScrollDown}
83 skip = &scroll
84 } else if hasScrollDamage(damage) {
85 return newDeltaCandidate(frame, DeltaPlan{Snapshot: true}, scratch), nil
86 }
87
88 var spans []Span
89 var full bool
90 if len(damage) == 0 {
91 spans, full = buildDirtyLinePlan(frame, committed)
92 } else {
93 spans, full = buildDamagePlan(frame, damage, skip)
94 }
95 if full {
96 return newDeltaCandidate(frame, DeltaPlan{Snapshot: true}, scratch), nil
97 }
98 if deltaCostsSnapshot(frame, spans, plan.Scroll.Height != 0) {
99 if plan.Scroll.Height != 0 {
100 return newDeltaCandidate(frame, DeltaPlan{Snapshot: true}, scratch), nil
101 }
102 return newDeltaCandidate(frame, DeltaPlan{Snapshot: true, Spans: spans}, scratch), nil
103 }
104 plan.Spans = spans
105 return newDeltaCandidate(frame, plan, scratch), nil
106}
107
108func planSingleDamage(frame CellSource, d Damage) DeltaPlan {
109 spans, full := buildSingleDamageSpans(frame, d)
110 if full {
111 return DeltaPlan{Snapshot: true}
112 }
113 plan := DeltaPlan{Spans: spans}
114 if deltaCostsSnapshot(frame, spans, false) {
115 plan.Snapshot = true
116 }
117 return plan
118}
119
120func buildDirtyLinePlan(frame CellSource, committed Frame) ([]Span, bool) {
121 var spans []Span
122 dense, isDense := asFrame(frame)
123 for y := range frame.Rows() {
124 dirty := false
125 if isDense {
126 dirty = !core.RowsEqualAt(dense, y, committed, y)
127 } else {
128 for x := range frame.Columns() {
129 if !frame.Cell(x, y).Equal(committed.Cell(x, y)) {
130 dirty = true
131 break
132 }
133 }
134 }
135 if dirty {
136 if len(spans) == maxPlannedDamageSpans {
137 return nil, true
138 }
139 spans = append(spans, Span{Y: y, Width: frame.Columns()})
140 }
141 }
142 return spans, false
143}
144
145func deltaCostsSnapshot(frame CellSource, spans []Span, hasScroll bool) bool {
146 if !hasScroll && frame.Rows() > 1 && len(spans) == 1 {
147 return false
148 }
149
150 var deltaCost int64
151 if hasScroll {
152 deltaCost = 6
153 }
154 for _, span := range spans {
155 deltaCost += 12 + int64(span.Width)
156 }
157
158 minimumSnapshotCost := int64(frame.Rows()) * (12 + int64(frame.Columns()))
159 if deltaCost < minimumSnapshotCost {
160 return false
161 }
162 maximumSnapshotCost := int64(frame.Rows()) * (8 + 5*int64(frame.Columns()))
163 if deltaCost >= maximumSnapshotCost {
164 return true
165 }
166 if !hasScroll && fullWidthRows(frame, spans) {
167 return true
168 }
169
170 for _, span := range spans {
171 deltaCost += 4 * int64(styleRunCountIn(frame, span.Y, span.X, span.Width)-1)
172 }
173 var snapshotCost int64
174 for y := range frame.Rows() {
175 snapshotCost += spanCost(frame, y, 0, frame.Columns())
176 }
177 return deltaCost >= snapshotCost
178}
179
180func fullWidthRows(frame CellSource, spans []Span) bool {
181 if len(spans) != frame.Rows() {
182 return false
183 }
184 for y, span := range spans {
185 if span.Y != y || span.X != 0 || span.Width != frame.Columns() {
186 return false
187 }
188 }
189 return true
190}
191
192func spanCost(frame CellSource, y, x, width int) int64 {
193 return 8 + int64(width) + 4*int64(styleRunCountIn(frame, y, x, width))
194}
195
196func styleRunCountIn(frame CellSource, y, x, width int) int {
197 if width == 0 {
198 return 0
199 }
200 runs := 1
201 previous := frame.Cell(x, y).Style
202 for column := x + 1; column < x+width; column++ {
203 style := frame.Cell(column, y).Style
204 if previous != style && !previous.Equal(style) {
205 runs++
206 }
207 previous = style
208 }
209 return runs
210}
211
212// Commit advances dst to the frame snapshot owned by the candidate.
213func (c DeltaCandidate) Commit(dst *Frame) {
214 if !c.Plan.Snapshot && c.Plan.Scroll.Height == 0 && len(c.Plan.Spans) == 0 {
215 return
216 }
217 if c.Plan.Snapshot || dst.Width != c.frame.Width || dst.Height != c.frame.Height {
218 // dst may be shared with the caller, so give it a fresh page rather
219 // than overwriting storage other Frame values might still reference.
220 dst.Replace(c.frame)
221 return
222 }
223 if c.Plan.Scroll.Height != 0 {
224 scroll := c.Plan.Scroll
225 if scroll.Down {
226 dst.ScrollDown(scroll.Y, scroll.Y+scroll.Height-1, scroll.Count)
227 } else {
228 dst.ScrollUp(scroll.Y, scroll.Y+scroll.Height-1, scroll.Count)
229 }
230 }
231 for _, span := range c.Plan.Spans {
232 for x := span.X; x < span.X+span.Width; x++ {
233 dst.Set(x, span.Y, c.frame.Cell(x, span.Y))
234 }
235 }
236}