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}