1package ui
2
3import (
4 renderer "github.com/bnema/vev-vt"
5 "github.com/bnema/vev/internal/domain"
6)
7
8// VerticalAnchor controls which end of a source is retained when its height
9// differs from the destination rectangle. Source and destination aliasing is
10// unsupported.
11type VerticalAnchor uint8
12
13const (
14 VerticalAnchorTop VerticalAnchor = iota
15 VerticalAnchorBottom
16)
17
18// FrameView is a bounded, row-oriented view of cells.
19type FrameView struct {
20 Rows [][]renderer.Cell
21 Width, Height int
22}
23
24type frameSpan struct {
25 source, destination, length int
26}
27
28// BlitFrame copies the valid intersection of src and rect into dst. Uncovered
29// destination cells are left untouched. Invalid dimensions, views, rectangles,
30// or anchors are ignored.
31func BlitFrame(dst renderer.Frame, rect domain.Rect, src FrameView, anchor VerticalAnchor) {
32 if anchor != VerticalAnchorTop && anchor != VerticalAnchorBottom {
33 return
34 }
35 if dst.Validate() != nil || rect.Width <= 0 || rect.Height <= 0 || src.Width <= 0 || src.Height <= 0 {
36 return
37 }
38 if len(src.Rows) != src.Height {
39 return
40 }
41 for _, row := range src.Rows {
42 if len(row) != src.Width {
43 return
44 }
45 }
46
47 width := min(src.Width, rect.Width)
48 x, ok := clippedFrameSpan(rect.X, 0, width, dst.Width)
49 if !ok {
50 return
51 }
52
53 height := min(src.Height, rect.Height)
54 sourceY, destinationOffset := 0, 0
55 if anchor == VerticalAnchorBottom {
56 if src.Height > rect.Height {
57 sourceY = src.Height - rect.Height
58 } else {
59 destinationOffset = rect.Height - src.Height
60 }
61 }
62 destinationY, ok := frameDestinationOrigin(rect.Y, destinationOffset, dst.Height)
63 if !ok {
64 return
65 }
66 y, ok := clippedFrameSpan(destinationY, sourceY, height, dst.Height)
67 if !ok {
68 return
69 }
70
71 for dy := range y.length {
72 row := src.Rows[y.source+dy]
73 for dx := range x.length {
74 sourceX := x.source + dx
75 current := row[sourceX]
76 if current.Continuation {
77 continue
78 }
79 if renderer.RuneWidth(current.Rune) == 2 {
80 next := sourceX + 1
81 if dx+1 >= x.length || !row[next].Continuation {
82 continue
83 }
84 dst.Set(x.destination+dx, y.destination+dy, current)
85 dst.Set(x.destination+dx+1, y.destination+dy, row[next])
86 continue
87 }
88 dst.Set(x.destination+dx, y.destination+dy, current)
89 }
90 }
91}
92
93// frameDestinationOrigin adds an in-rectangle offset only after proving that
94// a non-negative origin remains in the destination's bounded coordinate range.
95func frameDestinationOrigin(origin, offset, limit int) (int, bool) {
96 if origin >= 0 {
97 if origin >= limit || offset >= limit-origin {
98 return 0, false
99 }
100 return origin + offset, true
101 }
102 // A negative integer plus a non-negative offset cannot overflow.
103 return origin + offset, true
104}
105
106// clippedFrameSpan intersects a source span placed at origin with [0, limit).
107// It avoids forming an unbounded endpoint before both operands are bounded.
108func clippedFrameSpan(origin, source, length, limit int) (frameSpan, bool) {
109 if origin < 0 {
110 if origin <= -length {
111 return frameSpan{}, false
112 }
113 trim := -origin
114 source += trim
115 length -= trim
116 origin = 0
117 }
118 if origin >= limit {
119 return frameSpan{}, false
120 }
121 length = min(length, limit-origin)
122 return frameSpan{source: source, destination: origin, length: length}, length > 0
123}