package ui // Submenus: the second drop-down that an item with Items opens beside the // first. One level deep is all there is — see MenuItem.Items for why. import ( "github.com/gdamore/tcell/v2" "rickub.com/turbo-editors/turbo-core/theme" ) // submenuMarker is drawn at the right of an item that opens a submenu. It is // one column wide and three bytes long, so its width is counted in runes. const submenuMarker = "▶" // submenuOpen reports whether a second drop-down is showing. func (b *MenuBar) submenuOpen() bool { return b.openItem >= 0 } // openSubmenu shows the submenu of drop-down item i. func (b *MenuBar) openSubmenu(i int) { items := b.menus[b.openMenu].Items if i < 0 || i >= len(items) || !items[i].hasSubmenu() { return } b.highlight = i b.openItem = i b.subHighlight = b.nextUsableIn(items[i].Items, -1, +1) } // closeSubmenu puts the second drop-down away, leaving the first showing. func (b *MenuBar) closeSubmenu() { b.openItem = -1 b.subHighlight = 0 } // submenuItems returns the items of the open submenu, or nil when none is. func (b *MenuBar) submenuItems() []*MenuItem { if !b.submenuOpen() { return nil } return b.menus[b.openMenu].Items[b.openItem].Items } // submenuBounds returns where the open submenu goes: to the right of the // drop-down, level with the item that opened it. // // It flips to the left of the drop-down when there is not room on the right, // because a menu drawn past the edge of the screen is a menu nobody can read. func (b *MenuBar) submenuBounds() Rect { parent := b.dropdownBounds() items := b.menus[b.openMenu].Items[b.openItem].Items width := 0 for _, item := range items { width = max(width, item.width()) } // Capped to the screen as well as flipped, because a panel wider than the // terminal cannot be made to fit by moving it. Long labels are then clipped // by the painter, which is better than a frame with no right-hand edge. width = min(width+4, b.Bounds().W) x := parent.Right() if x+width+ShadowWidth > b.Bounds().Right() { x = parent.X - width } rightmost := max(b.Bounds().Right()-width, b.Bounds().X) x = min(max(x, b.Bounds().X), rightmost) return Rect{X: x, Y: parent.Y + b.openItem, W: width, H: len(items) + 2} } // drawSubmenu paints the second drop-down, on top of the first. func (b *MenuBar) drawSubmenu(p *Painter, th *theme.Theme) { bounds := b.submenuBounds() DrawShadow(p, bounds, th.Style(theme.KeyShadow)) panel := p.Sub(bounds) panel.Clear(th.Style(theme.KeyMenuItem)) DrawFrame(panel, panel.Size(), FrameSingle, th.Style(theme.KeyMenuItem)) for i, item := range b.submenuItems() { b.drawLine(panel, th, i, item, i == b.subHighlight) } } // stepRight opens the highlighted item's submenu, or moves to the next menu // when it has none. // // That order is what makes right mean "further in" wherever you are, rather // than sometimes skipping past a branch you were about to open. func (b *MenuBar) stepRight() { items := b.menus[b.openMenu].Items if b.highlight >= 0 && b.highlight < len(items) && items[b.highlight].hasSubmenu() { b.openSubmenu(b.highlight) return } b.OpenMenu((b.openMenu + 1) % len(b.menus)) } // handleSubmenuKey works the second drop-down. // // Left and Escape differ here: left steps back out to the parent, while Escape // puts the whole menu away — which is what "cancel" should do from anywhere. func (b *MenuBar) handleSubmenuKey(ev *tcell.EventKey) bool { items := b.submenuItems() switch ev.Key() { case tcell.KeyEscape: b.Close() case tcell.KeyLeft: b.closeSubmenu() case tcell.KeyUp: b.subHighlight = b.nextUsableIn(items, b.subHighlight, -1) case tcell.KeyDown: b.subHighlight = b.nextUsableIn(items, b.subHighlight, +1) case tcell.KeyRight, tcell.KeyEnter: b.activateSubmenu(b.subHighlight) case tcell.KeyRune: return b.selectSubmenuByHotKey(ev.Rune()) default: return false } return true } // selectSubmenuByHotKey runs the submenu item whose hot key was typed. func (b *MenuBar) selectSubmenuByHotKey(key rune) bool { for i, item := range b.submenuItems() { if item.enabled() && MatchesHotKey(item.Label, key) { b.activateSubmenu(i) return true } } return true } // activateSubmenu closes the whole menu and runs a submenu item's action. func (b *MenuBar) activateSubmenu(i int) { items := b.submenuItems() if i < 0 || i >= len(items) || !items[i].enabled() { return } action := items[i].Action b.Close() action() } // handleSubmenuMouse reacts to the pointer over the second drop-down. func (b *MenuBar) handleSubmenuMouse(x, y int, pressed bool) bool { index := y - b.submenuBounds().Y - 1 items := b.submenuItems() if index < 0 || index >= len(items) || !items[index].enabled() { return true } b.subHighlight = index if pressed { b.activateSubmenu(index) } return true }