v0.2.0 80c61e3

frame.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close