main 7a3b92c

view.go RawHistoryFolder

1package popup
2
3import (
4	"strconv"
5	"strings"
6
7	"github.com/bnema/nefergui"
8
9	"github.com/bnema/neferbar/internal/module"
10)
11
12// tooltipModel is what a tooltip shows.
13type tooltipModel struct {
14	title string
15	lines []string   // the body, one entry per line; an empty one is a blank line
16	rows  [][]string // the table
17	cols  int        // cells of the longest row
18}
19
20func newTooltip(title, body string, rows [][]string) *tooltipModel {
21	m := &tooltipModel{title: title, rows: rows}
22	if body = strings.Trim(body, "\n"); body != "" {
23		m.lines = strings.Split(body, "\n")
24	}
25	for _, r := range rows {
26		m.cols = max(m.cols, len(r))
27	}
28	return m
29}
30
31// tooltipView draws the title in bold, the body, then the table. The table is
32// laid out column by column, so every column is as wide as its widest cell
33// and the cells of a row line up, as long as no cell wraps: a cell wider than
34// the tooltip can be takes two lines and shifts the rest of its column.
35func tooltipView(f *nefergui.Frame, m *tooltipModel) {
36	col := f.Root().Column()
37	if m.title != "" {
38		col.Text(m.title, nefergui.Class("title"), nefergui.Key("title"))
39	}
40	for i, l := range m.lines {
41		if l == "" {
42			l = " " // keeps the height of a line
43		}
44		col.Text(l, nefergui.Class("body"), nefergui.Key("b"+strconv.Itoa(i)))
45	}
46	if m.cols == 0 {
47		return
48	}
49	table := col.Row(nefergui.Class("table"), nefergui.Key("table"))
50	for c := 0; c < m.cols; c++ {
51		class := "value"
52		if c == 0 && m.cols > 1 {
53			class = "key"
54		}
55		cells := table.Column(nefergui.Class(class), nefergui.Key("c"+strconv.Itoa(c)))
56		for r, row := range m.rows {
57			s := " "
58			if c < len(row) && row[c] != "" {
59				s = row[c]
60			}
61			cells.Text(s, nefergui.Key("r"+strconv.Itoa(r)))
62		}
63	}
64}
65
66// menuModel is a menu and where the user is in it. Submenus open in place:
67// path holds the indexes of the submenus entered, and only the level at the
68// end of the path is shown.
69type menuModel struct {
70	items  []module.MenuItem
71	path   []int
72	token  uint32
73	chosen int32 // the id of the activated item, -1 for none
74	// changed is set when the level changed: the popup must be resized.
75	changed bool
76	// scroll wraps the items in a scrolling box of scrollH pixels: they are
77	// taller than the popup.
78	scroll  bool
79	scrollH int
80}
81
82// fitTo is the fallback when the popup could not be resized for a level that
83// needs wantH pixels: if the surface keeps curH, the items scroll inside it.
84func (m *menuModel) fitTo(wantH, curH int32) {
85	if wantH > curH {
86		m.scroll, m.scrollH = true, max(int(curH)-2*Inset, 1)
87	}
88}
89
90func newMenu(items []module.MenuItem, token uint32) *menuModel {
91	return &menuModel{items: items, token: token, chosen: -1}
92}
93
94// level is the items of the submenu the path leads to.
95func (m *menuModel) level() []module.MenuItem {
96	items := m.items
97	for _, i := range m.path {
98		if i < 0 || i >= len(items) {
99			return nil
100		}
101		items = items[i].Items
102	}
103	return items
104}
105
106// inSubmenu reports whether the shown level is a submenu.
107func (m *menuModel) inSubmenu() bool { return len(m.path) > 0 }
108
109// push enters the submenu at index i of the shown level.
110func (m *menuModel) push(i int) bool {
111	lv := m.level()
112	if i < 0 || i >= len(lv) || len(lv[i].Items) == 0 || len(m.path) >= module.MaxMenuDepth {
113		return false
114	}
115	m.path = append(m.path, i)
116	m.changed = true
117	return true
118}
119
120// pop goes back to the parent level.
121func (m *menuModel) pop() bool {
122	if len(m.path) == 0 {
123		return false
124	}
125	m.path = m.path[:len(m.path)-1]
126	m.changed = true
127	return true
128}
129
130// choose records the activation of a leaf item.
131func (m *menuModel) choose(id int32) { m.chosen = id }
132
133// levelKey prefixes the keys of a level, so a control keeps no hover or focus
134// state from another level.
135func (m *menuModel) levelKey() string {
136	k := "l"
137	for _, i := range m.path {
138		k += "." + strconv.Itoa(i)
139	}
140	return k + "/"
141}
142
143// menuContentHeight is the height of the box that scrolls in a popup of
144// MaxHeight: the popup height minus the stylesheet's padding and border.
145const menuContentHeight = MaxHeight - 2*Inset
146
147const (
148	backLabel   = "‹ Back"
149	submenuMark = "  ›"
150)
151
152// menuView draws the shown level. Activations happen here, while the view
153// runs: Back and submenu entries move the path, other entries record the id.
154func menuView(f *nefergui.Frame, m *menuModel) {
155	parent := f.Root()
156	if m.scroll {
157		parent = parent.Scroll(nefergui.Key("scroll"), nefergui.Inline("height: "+strconv.Itoa(m.scrollH)+"px"))
158	}
159	col := parent.Column()
160	if m.inSubmenu() {
161		if col.Button(backLabel, nefergui.Key("back"), nefergui.Class("item")).Activated() {
162			m.pop()
163		}
164	}
165	prefix := m.levelKey()
166	for i, it := range m.level() {
167		key := nefergui.Key(prefix + strconv.Itoa(i))
168		switch {
169		case it.Kind == module.KindSeparator:
170			col.Separator(key)
171		case len(it.Items) > 0:
172			if col.Button(it.Label+submenuMark, key, nefergui.Class("item"), nefergui.Disabled(!it.Enabled)).Activated() {
173				m.push(i)
174			}
175		case it.Kind == module.KindCheck || it.Kind == module.KindRadio:
176			checked := it.Checked
177			if col.Checkbox(it.Label, &checked, key, nefergui.Class("item"), nefergui.Disabled(!it.Enabled)).Changed() {
178				m.choose(it.ID)
179			}
180		default:
181			if col.Button(it.Label, key, nefergui.Class("item"), nefergui.Disabled(!it.Enabled)).Activated() {
182				m.choose(it.ID)
183			}
184		}
185	}
186}

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 5

Tags 4

↑↓ moveenter openesc close