1package daemon
2
3import (
4 renderer "github.com/bnema/vev-vt"
5 "github.com/bnema/vev/internal/domain"
6 scopy "github.com/bnema/vev/internal/usecase/copy"
7)
8
9// Only successfully emitted composition caches carry this metadata forward.
10// It describes the terminal shadow, not the unadorned live-pane cache.
11type copyViewportState struct {
12 document *scopy.Document
13 target domain.Rect
14 top int
15 cursor scopy.Pos
16 frame renderer.Frame
17}
18
19func captureCopyViewport(state capturedRenderState, content domain.Rect) copyViewportState {
20 mode := state.overlays.copyMode
21 if mode == nil || mode.Document() == nil || mode.Selection().Enabled || mode.SearchQuery != "" {
22 return copyViewportState{}
23 }
24 return copyViewportState{document: mode.Document(), target: capturedCopyTarget(state, content), top: mode.ViewportTop, cursor: mode.Cursor()}
25}
26
27// A scroll hint is safe only for a whole-width, unchanged document viewport.
28// The ANSI renderer independently verifies retained cells against its committed
29// shadow and falls back to a snapshot if search or other adornments changed.
30func copyViewportDamage(previous, next copyViewportState, frame renderer.Frame) []renderer.Damage {
31 target := next.target
32 if next.document == nil || previous.document != next.document || previous.target != target ||
33 target.X != 0 || target.Width != frame.Width || target.Y < 0 || target.Height <= 0 ||
34 target.Y >= frame.Height || target.Height >= frame.Height-target.Y ||
35 target.Height != next.document.Height() || target.Width != next.document.Width() {
36 return nil
37 }
38 delta := next.top - previous.top
39 if delta == 0 || delta <= -target.Height || delta >= target.Height {
40 return nil
41 }
42 kind, count, exposedY := renderer.DamageScrollUp, delta, target.Y+target.Height-delta
43 if delta < 0 {
44 kind, count, exposedY = renderer.DamageScrollDown, -delta, target.Y
45 }
46 damage := []renderer.Damage{
47 {Kind: kind, Y: target.Y, Width: target.Width, Height: target.Height, Count: count},
48 {Kind: renderer.DamageText, Y: exposedY, Width: frame.Width, Height: count},
49 }
50 // Refresh the old cursor after shifting it, and paint the new cursor. The
51 // highlighted cursor need not occupy the same screen row after navigation.
52 for _, cursor := range []scopy.Pos{previous.cursor, next.cursor} {
53 y := cursor.Row - next.top
54 if y >= 0 && y < target.Height {
55 damage = append(damage, renderer.Damage{Kind: renderer.DamageText, Y: target.Y + y, Width: frame.Width, Height: 1})
56 }
57 }
58 // Preserve all chrome and other panes above/below the scroll region, even
59 // when live PTY output or status updates arrive together with the wheel.
60 if target.Y > 0 {
61 damage = append(damage, renderer.Damage{Kind: renderer.DamageText, Width: frame.Width, Height: target.Y})
62 }
63 bottom := target.Y + target.Height
64 damage = append(damage, renderer.Damage{Kind: renderer.DamageText, Y: bottom, Width: frame.Width, Height: frame.Height - bottom})
65 return damage
66}
67
68// Reuse retained compact rows instead of decoding and interning the whole
69// immutable viewport. The result is built in spare, the alternate viewport
70// buffer, so both committed frames remain untouched until send succeeds.
71func composeScrolledCopyViewport(state capturedRenderState, base, spare renderer.Frame, previous, next copyViewportState, damage []renderer.Damage) renderer.Frame {
72 frame := spare
73 frame.CopyFrom(previous.frame)
74 target := next.target
75 scroll := damage[0]
76 if scroll.Kind == renderer.DamageScrollDown {
77 frame.ScrollDown(target.Y, target.Y+target.Height-1, scroll.Count)
78 } else {
79 frame.ScrollUp(target.Y, target.Y+target.Height-1, scroll.Count)
80 }
81 for y := range frame.Height {
82 if y < target.Y || y >= target.Y+target.Height {
83 for x := range frame.Width {
84 frame.Set(x, y, base.Cell(x, y))
85 }
86 }
87 }
88 paint := func(y int, row []renderer.Cell) {
89 if y == next.document.Height() {
90 frame.WriteRow(frame.Height-1, 0, row)
91 } else {
92 frame.WriteRow(target.Y+y, 0, row)
93 }
94 }
95 for _, d := range damage[1:] {
96 start, end := max(d.Y-target.Y, 0), min(d.Y+d.Height-target.Y, target.Height)
97 if start < end {
98 state.overlays.copyMode.RenderRowsRange(start, end, paint, state.styles.CopyStatus, state.styles.Selection)
99 }
100 }
101 return frame
102}