package ui // The menu bar's types and its state. What it draws is in menu_draw.go, what it // answers to is in menu_events.go, and the second-level panels are in // submenu.go. // MenuItem is one line of a drop-down menu. // // Label may carry a hot key written with tildes, as in "~O~pen…". Shortcut is // the key combination shown right-aligned; the menu only displays it, since // the shortcut itself is handled wherever global keys are. type MenuItem struct { Label string Shortcut string Action func() Enabled func() bool Separator bool // Items makes this a submenu: choosing it opens a second drop-down beside // the first rather than running anything, and Action is ignored. // // One level deep is all there is. The menus that need it — snippets // grouped by kind — are exactly one level, and a general depth would mean // replacing the bar's two indices with a path for a need nobody has. Items []*MenuItem } // hasSubmenu reports whether choosing this item opens another drop-down. func (m *MenuItem) hasSubmenu() bool { return len(m.Items) > 0 } // enabled reports whether the item can be chosen right now. An item with no // Enabled function is always available, which is the common case. func (m *MenuItem) enabled() bool { if m.Separator { return false } if m.Action == nil && !m.hasSubmenu() { return false } return m.Enabled == nil || m.Enabled() } // width returns how many cells the item needs, shortcut or marker included. func (m *MenuItem) width() int { if m.Separator { return 0 } width := LabelWidth(m.Label) switch { case m.Shortcut != "": width += len([]rune(m.Shortcut)) + 3 case m.hasSubmenu(): width += len([]rune(submenuMarker)) + 2 } return width } // Menu is one drop-down: a title on the bar and the items under it. type Menu struct { Label string Items []*MenuItem // OnOpen is called just before the menu drops down, so its owner can // refill Items first. // // A menu built from a file, or from what the front window holds, cannot be // decided once at start-up. This is the hook for that, and it runs at the // only moment the contents are about to be seen. OnOpen func() } // MenuBar is the row of menu titles across the top of the screen, and the // drop-down currently open under it. // // It is drawn last and offered events first, so an open menu takes precedence // over everything behind it. type MenuBar struct { Box menus []*Menu openMenu int // -1 when nothing is open highlight int // openItem is the index of the drop-down item whose submenu is showing, or // -1 when none is, and subHighlight is the highlighted line inside it. openItem int subHighlight int } // NewMenuBar returns a closed menu bar holding the given menus. func NewMenuBar(menus ...*Menu) *MenuBar { return &MenuBar{menus: menus, openMenu: -1, openItem: -1} } // Menus returns the menus on the bar. func (b *MenuBar) Menus() []*Menu { return b.menus } // SetMenus replaces the menus on the bar, closing whatever was open. // // Menu.OnOpen refills one menu; this is for the rarer case where the *set* of // menus changes — a project file that names menus of its own can add or remove // one while the editor is running. Closing first matters: the open index refers // to the old slice, and keeping it would drop down whichever menu happened to // land at that position. // // bar.SetMenus(append(fixedMenus(), toolMenus()...)) func (b *MenuBar) SetMenus(menus []*Menu) { b.Close() b.menus = menus } // Open reports whether a drop-down is currently showing. func (b *MenuBar) Open() bool { return b.openMenu >= 0 } // OpenMenu drops down menu i and highlights its first usable item. // // The menu's OnOpen runs first, so a menu whose contents come from elsewhere is // filled in before anyone sees it. func (b *MenuBar) OpenMenu(i int) { if i < 0 || i >= len(b.menus) { return } if b.menus[i].OnOpen != nil { b.menus[i].OnOpen() } b.openMenu = i b.openItem = -1 b.highlight = b.nextUsableIn(b.menus[i].Items, -1, +1) } // Close puts the open drop-down away, and the submenu with it. func (b *MenuBar) Close() { b.openMenu = -1 b.openItem = -1 b.highlight = 0 b.subHighlight = 0 }