turbo-editors/turbo-corepublic Fork 0
v1.0.0
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

submenu.go · 163 lines · 4.7 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g 20h ago1package ui
2
3// Submenus: the second drop-down that an item with Items opens beside the
4// first. One level deep is all there is — see MenuItem.Items for why.
5
6import (
7 "github.com/gdamore/tcell/v2"
8
📦 Turbo Core f3ade8d k33g 13h ago9 "rickub.com/turbo-editors/turbo-core/theme"
🛟 Updated. 28d5985 k33g 20h ago10)
11
12// submenuMarker is drawn at the right of an item that opens a submenu. It is
13// one column wide and three bytes long, so its width is counted in runes.
14const submenuMarker = "▶"
15
16// submenuOpen reports whether a second drop-down is showing.
17func (b *MenuBar) submenuOpen() bool { return b.openItem >= 0 }
18
19// openSubmenu shows the submenu of drop-down item i.
20func (b *MenuBar) openSubmenu(i int) {
21 items := b.menus[b.openMenu].Items
22 if i < 0 || i >= len(items) || !items[i].hasSubmenu() {
23 return
24 }
25
26 b.highlight = i
27 b.openItem = i
28 b.subHighlight = b.nextUsableIn(items[i].Items, -1, +1)
29}
30
31// closeSubmenu puts the second drop-down away, leaving the first showing.
32func (b *MenuBar) closeSubmenu() {
33 b.openItem = -1
34 b.subHighlight = 0
35}
36
37// submenuItems returns the items of the open submenu, or nil when none is.
38func (b *MenuBar) submenuItems() []*MenuItem {
39 if !b.submenuOpen() {
40 return nil
41 }
42 return b.menus[b.openMenu].Items[b.openItem].Items
43}
44
45// submenuBounds returns where the open submenu goes: to the right of the
46// drop-down, level with the item that opened it.
47//
48// It flips to the left of the drop-down when there is not room on the right,
49// because a menu drawn past the edge of the screen is a menu nobody can read.
50func (b *MenuBar) submenuBounds() Rect {
51 parent := b.dropdownBounds()
52 items := b.menus[b.openMenu].Items[b.openItem].Items
53
54 width := 0
55 for _, item := range items {
56 width = max(width, item.width())
57 }
58 // Capped to the screen as well as flipped, because a panel wider than the
59 // terminal cannot be made to fit by moving it. Long labels are then clipped
60 // by the painter, which is better than a frame with no right-hand edge.
61 width = min(width+4, b.Bounds().W)
62
63 x := parent.Right()
64 if x+width+ShadowWidth > b.Bounds().Right() {
65 x = parent.X - width
66 }
67 rightmost := max(b.Bounds().Right()-width, b.Bounds().X)
68 x = min(max(x, b.Bounds().X), rightmost)
69
70 return Rect{X: x, Y: parent.Y + b.openItem, W: width, H: len(items) + 2}
71}
72
73// drawSubmenu paints the second drop-down, on top of the first.
74func (b *MenuBar) drawSubmenu(p *Painter, th *theme.Theme) {
75 bounds := b.submenuBounds()
76 DrawShadow(p, bounds, th.Style(theme.KeyShadow))
77
78 panel := p.Sub(bounds)
79 panel.Clear(th.Style(theme.KeyMenuItem))
80 DrawFrame(panel, panel.Size(), FrameSingle, th.Style(theme.KeyMenuItem))
81
82 for i, item := range b.submenuItems() {
83 b.drawLine(panel, th, i, item, i == b.subHighlight)
84 }
85}
86
87// stepRight opens the highlighted item's submenu, or moves to the next menu
88// when it has none.
89//
90// That order is what makes right mean "further in" wherever you are, rather
91// than sometimes skipping past a branch you were about to open.
92func (b *MenuBar) stepRight() {
93 items := b.menus[b.openMenu].Items
94 if b.highlight >= 0 && b.highlight < len(items) && items[b.highlight].hasSubmenu() {
95 b.openSubmenu(b.highlight)
96 return
97 }
98 b.OpenMenu((b.openMenu + 1) % len(b.menus))
99}
100
101// handleSubmenuKey works the second drop-down.
102//
103// Left and Escape differ here: left steps back out to the parent, while Escape
104// puts the whole menu away — which is what "cancel" should do from anywhere.
105func (b *MenuBar) handleSubmenuKey(ev *tcell.EventKey) bool {
106 items := b.submenuItems()
107
108 switch ev.Key() {
109 case tcell.KeyEscape:
110 b.Close()
111 case tcell.KeyLeft:
112 b.closeSubmenu()
113 case tcell.KeyUp:
114 b.subHighlight = b.nextUsableIn(items, b.subHighlight, -1)
115 case tcell.KeyDown:
116 b.subHighlight = b.nextUsableIn(items, b.subHighlight, +1)
117 case tcell.KeyRight, tcell.KeyEnter:
118 b.activateSubmenu(b.subHighlight)
119 case tcell.KeyRune:
120 return b.selectSubmenuByHotKey(ev.Rune())
121 default:
122 return false
123 }
124 return true
125}
126
127// selectSubmenuByHotKey runs the submenu item whose hot key was typed.
128func (b *MenuBar) selectSubmenuByHotKey(key rune) bool {
129 for i, item := range b.submenuItems() {
130 if item.enabled() && MatchesHotKey(item.Label, key) {
131 b.activateSubmenu(i)
132 return true
133 }
134 }
135 return true
136}
137
138// activateSubmenu closes the whole menu and runs a submenu item's action.
139func (b *MenuBar) activateSubmenu(i int) {
140 items := b.submenuItems()
141 if i < 0 || i >= len(items) || !items[i].enabled() {
142 return
143 }
144
145 action := items[i].Action
146 b.Close()
147 action()
148}
149
150// handleSubmenuMouse reacts to the pointer over the second drop-down.
151func (b *MenuBar) handleSubmenuMouse(x, y int, pressed bool) bool {
152 index := y - b.submenuBounds().Y - 1
153 items := b.submenuItems()
154 if index < 0 || index >= len(items) || !items[index].enabled() {
155 return true
156 }
157
158 b.subHighlight = index
159 if pressed {
160 b.activateSubmenu(index)
161 }
162 return true
163}