okami.mom/albedo

git clone https://git.nkp.pet/okami.mom/albedo

dawntui: stacked pane takes two fifths; context drops its note and facts when the list is short2f6c51d

main
4.1 KiB130 linesraw
1package tui
2
3import (
4	"cmp"
5	"slices"
6	"strings"
7
8	"github.com/charmbracelet/x/ansi"
9)
10
11// listFrame is the layout every list screen shares: the title rule, an optional
12// filter, the list beside the highlighted row's detail pane when there is
13// room, and the footer, all clipped to the terminal once. A screen supplies
14// the parts and none of the arithmetic.
15type listFrame struct {
16	title, right string // the title rule's two ends, styled
17	filter       string // the search line as drawn; empty for a screen without one
18	list         func(width, height int) []string
19	// pane is the detail beside the list; nil for a list alone.
20	pane func(width, height int) []string
21	// paneWidth is the pane's width at a terminal width; nil is sidePane.
22	paneWidth func(width int) int
23	// summary is the detail's one-line stand-in when the pane has no room at all.
24	summary string
25	// footer is the finished bottom rows: hints, status, or a confirmation.
26	footer string
27}
28
29// Side by side needs this much terminal; a narrower one with this many body
30// rows stacks the pane under the list instead, which gets the larger part.
31const (
32	paneMinWidth  = 96
33	paneMinHeight = 14
34	stackMinBody  = 12
35)
36
37// sidePane is a narrow detail column for facts about the highlighted row.
38func sidePane(width int) int { return min(max(width/3, 34), 48) }
39
40// halfPane is half the terminal, for previews that read like text.
41func halfPane(width int) int { return width - width/2 - ansi.StringWidth(svSep()) }
42
43// view draws the frame at exactly the terminal's size or less; an unsized
44// terminal is 80 by 24.
45func (f listFrame) view(width, height int) string {
46	width, height = cmp.Or(width, 80), cmp.Or(height, 24)
47	roomy := height >= 12
48
49	lines := []string{" " + titleRule(width-1, f.title, f.right)}
50	if roomy {
51		lines = append(lines, "")
52	}
53	if f.filter != "" {
54		lines = append(lines, " "+f.filter)
55		if height >= 9 {
56			lines = append(lines, "")
57		}
58	}
59
60	paned := f.pane != nil && width >= paneMinWidth && height >= paneMinHeight
61	var tail []string
62	if roomy {
63		tail = append(tail, "")
64	}
65	tail = append(tail, strings.Split(f.footer, "\n")...)
66	body := max(1, height-len(lines)-len(tail))
67	stacked := !paned && f.pane != nil && body >= stackMinBody
68	if !paned && !stacked && f.summary != "" && height >= 16 {
69		tail = slices.Insert(tail, len(tail)-strings.Count(f.footer, "\n")-1, " "+f.summary)
70		body = max(1, height-len(lines)-len(tail))
71	}
72
73	if paned {
74		paneW := sidePane(width)
75		if f.paneWidth != nil {
76			paneW = f.paneWidth(width)
77		}
78		list := f.list(width-paneW-ansi.StringWidth(svSep()), body)
79		pane := f.pane(paneW, body)
80		for i := range body {
81			lines = append(lines, list[i]+svSep()+pane[i])
82		}
83	} else if stacked {
84		paneH := body * 2 / 5
85		lines = append(lines, f.list(width, body-paneH-1)...)
86		lines = append(lines, " "+DefaultStyles.Decor.Render(strings.Repeat("─", max(0, width-2))))
87		for _, line := range f.pane(width-1, paneH) {
88			lines = append(lines, " "+line)
89		}
90	} else {
91		lines = append(lines, f.list(width, body)...)
92	}
93	lines = append(lines, tail...)
94
95	if len(lines) > height {
96		lines = append(lines[:max(0, height-1)], lines[len(lines)-1])
97	}
98	for i, line := range lines {
99		lines[i] = ansi.Truncate(line, width, "…")
100	}
101	return strings.Join(lines, "\n")
102}
103
104// footerLine is the keys on the left and a status on the right. When both
105// do not fit, an urgent status (an error, a notice, work in flight) takes
106// a row of its own above the keys, and any other status gives way to them.
107func footerLine(width int, hints []hint, status string, urgent bool) string {
108	if len(hints) == 0 {
109		return " " + status
110	}
111	room := width - 1
112	if status != "" {
113		room = width - ansi.StringWidth(status) - 3
114	}
115	left := fitHints(hints, room)
116	switch {
117	case left != "" && status != "":
118		return left + strings.Repeat(" ", width-ansi.StringWidth(left)-ansi.StringWidth(status)-1) + status
119	case left != "":
120		return left
121	}
122	keys := fitHints(hints, width-1)
123	if keys == "" {
124		keys = " " + ansi.Truncate(keyHints(hints...), max(1, width-2), "…")
125	}
126	if urgent && status != "" {
127		return " " + ansi.Truncate(status, max(1, width-2), "…") + "\n" + keys
128	}
129	return keys
130}