| 🛟 Updated. 28d5985 k33g 18h ago | 1 | package 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 | |
| 6 | import ( |
| 7 | "github.com/gdamore/tcell/v2" |
| 8 | |
| 📦 Turbo Core f3ade8d k33g 11h ago | 9 | "rickub.com/turbo-editors/turbo-core/theme" |
| 🛟 Updated. 28d5985 k33g 18h ago | 10 | ) |
| 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. |
| 14 | const submenuMarker = "▶" |
| 15 | |
| 16 | // submenuOpen reports whether a second drop-down is showing. |
| 17 | func (b *MenuBar) submenuOpen() bool { return b.openItem >= 0 } |
| 18 | |
| 19 | // openSubmenu shows the submenu of drop-down item i. |
| 20 | func (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. |
| 32 | func (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. |
| 38 | func (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. |
| 50 | func (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. |
| 74 | func (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. |
| 92 | func (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. |
| 105 | func (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. |
| 128 | func (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. |
| 139 | func (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. |
| 151 | func (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 | } |