modal.go RawHistoryFolder

1package ui
2
3import (
4	renderer "github.com/bnema/vev-vt"
5	"github.com/bnema/vev/internal/domain"
6)
7
8// Modal describes a rectangular overlay.
9type Modal struct {
10	WidthPct    int
11	HeightPct   int
12	MinWidth    int
13	MinHeight   int
14	Title       string
15	Anchor      domain.Anchor
16	Margins     Margins
17	FixedWidth  int
18	FixedHeight int
19}
20
21func (m Modal) preferredBounds(base domain.Size) domain.Rect {
22	width := percentOf(base.Cols, m.WidthPct)
23	height := percentOf(base.Rows, m.HeightPct)
24	if m.FixedWidth > 0 {
25		width = m.FixedWidth
26	}
27	if m.FixedHeight > 0 {
28		height = m.FixedHeight
29	}
30	if width < m.MinWidth {
31		width = m.MinWidth
32	}
33	if height < m.MinHeight {
34		height = m.MinHeight
35	}
36	return Place(base, domain.Size{Cols: width, Rows: height}, m.Anchor, m.Margins)
37}
38
39// Resolve computes the modal's responsive presentation from one preferred
40// bounds and inner pair.
41func (m Modal) Resolve(base domain.Size) Presentation {
42	bounds := m.preferredBounds(base)
43	return ResolvePresentation(base, bounds, modalInner(bounds))
44}
45
46// CompositePresentation draws an already-resolved modal presentation and
47// returns its inner content rectangle.
48func (m Modal) CompositePresentation(f renderer.Frame, p Presentation, border, interior renderer.Style) domain.Rect {
49	FillRect(f, p.Inner, renderer.Cell{Rune: ' ', Style: interior})
50	drawBorderEdges(f, p.Bounds, p.Borders, border)
51	if p.Borders&BorderTop != 0 {
52		m.drawTitle(f, p.Bounds, border)
53	}
54	return p.Inner
55}
56
57func (m Modal) drawTitle(f renderer.Frame, bounds domain.Rect, style renderer.Style) {
58	if bounds.Width <= 2 || bounds.Height <= 0 || m.Title == "" {
59		return
60	}
61	left := bounds.X + 1
62	right := bounds.X + bounds.Width - 1
63	start := max(left, bounds.X+(bounds.Width-textWidth(m.Title))/2)
64	DrawText(f, start, bounds.Y, right, m.Title, style)
65}
66
67func modalInner(bounds domain.Rect) domain.Rect {
68	return domain.Rect{
69		X:      bounds.X + 1,
70		Y:      bounds.Y + 1,
71		Width:  max(0, bounds.Width-2),
72		Height: max(0, bounds.Height-2),
73	}
74}
75
76// FillRect fills rect with cell, clipped to the frame bounds.
77func FillRect(f renderer.Frame, rect domain.Rect, cell renderer.Cell) {
78	left := clamp(rect.X, 0, f.Width)
79	top := clamp(rect.Y, 0, f.Height)
80	right := clamp(rect.X+rect.Width, 0, f.Width)
81	bottom := clamp(rect.Y+rect.Height, 0, f.Height)
82	for y := top; y < bottom; y++ {
83		for x := left; x < right; x++ {
84			f.Set(x, y, cell)
85		}
86	}
87}
88
89// DrawBox draws a single-cell box clipped to the frame bounds.
90func DrawBox(f renderer.Frame, rect domain.Rect, style renderer.Style) {
91	drawBorderEdges(f, rect, BorderAll, style)
92}
93
94func drawBorderEdges(f renderer.Frame, rect domain.Rect, edges BorderEdges, style renderer.Style) {
95	if rect.Width <= 0 || rect.Height <= 0 || edges == 0 {
96		return
97	}
98	left := rect.X
99	top := rect.Y
100	right := rect.X + rect.Width - 1
101	bottom := rect.Y + rect.Height - 1
102
103	if edges&BorderTop != 0 {
104		for x := left; x <= right; x++ {
105			setCell(f, x, top, '─', style)
106		}
107	}
108	if edges&BorderBottom != 0 {
109		for x := left; x <= right; x++ {
110			setCell(f, x, bottom, '─', style)
111		}
112	}
113	if edges&BorderLeft != 0 {
114		for y := top; y <= bottom; y++ {
115			setCell(f, left, y, '│', style)
116		}
117	}
118	if edges&BorderRight != 0 {
119		for y := top; y <= bottom; y++ {
120			setCell(f, right, y, '│', style)
121		}
122	}
123	if edges&(BorderTop|BorderLeft) == BorderTop|BorderLeft {
124		setCell(f, left, top, '┌', style)
125	}
126	if edges&(BorderTop|BorderRight) == BorderTop|BorderRight {
127		setCell(f, right, top, '┐', style)
128	}
129	if edges&(BorderBottom|BorderLeft) == BorderBottom|BorderLeft {
130		setCell(f, left, bottom, '└', style)
131	}
132	if edges&(BorderBottom|BorderRight) == BorderBottom|BorderRight {
133		setCell(f, right, bottom, '┘', style)
134	}
135}
136
137// DrawText draws text starting at x,y before exclusive clipX and returns the
138// next x position. Wide runes that would cross clipX are dropped.
139func DrawText(f renderer.Frame, x, y, clipX int, text string, style renderer.Style) int {
140	if y < 0 || y >= f.Height {
141		return x
142	}
143	clipX = clamp(clipX, 0, f.Width)
144	pos := x
145	for _, r := range text {
146		w := renderer.RuneWidth(r)
147		if w == 0 {
148			continue
149		}
150		if pos >= clipX {
151			break
152		}
153		if pos+w > clipX {
154			break
155		}
156		if pos >= 0 && pos < f.Width {
157			f.Set(pos, y, renderer.Cell{Rune: r, Style: style})
158		}
159		if w == 2 && pos+1 >= 0 && pos+1 < f.Width {
160			f.Set(pos+1, y, renderer.Cell{Style: style, Continuation: true})
161		}
162		pos += w
163	}
164	return pos
165}
166
167func setCell(f renderer.Frame, x, y int, r rune, style renderer.Style) {
168	if x < 0 || x >= f.Width || y < 0 || y >= f.Height {
169		return
170	}
171	f.Set(x, y, renderer.Cell{Rune: r, Style: style})
172}
173
174func percentOf(n, pct int) int {
175	if pct <= 0 {
176		return n
177	}
178	return n * pct / 100
179}
180
181func textWidth(text string) int {
182	width := 0
183	for _, r := range text {
184		width += renderer.RuneWidth(r)
185	}
186	return width
187}

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