Did a lot of work off-repo, basically init
This commit is contained in:
290
ui/menu.go
Normal file
290
ui/menu.go
Normal file
@ -0,0 +1,290 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
)
|
||||
|
||||
// Item is an interface implemented by ItemEntry and ItemMenu to be listed in Menus.
|
||||
type Item interface {
|
||||
isItem()
|
||||
GetName() string
|
||||
}
|
||||
|
||||
// An ItemSeparator is like a blank Item that cannot actually be selected. It is useful
|
||||
// for separating items in a Menu.
|
||||
type ItemSeparator struct{}
|
||||
|
||||
func (i *ItemSeparator) isItem() {}
|
||||
|
||||
// GetName returns an empty string.
|
||||
func (i *ItemSeparator) GetName() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// ItemEntry is a listing in a Menu with a name and callback.
|
||||
type ItemEntry struct {
|
||||
Name string
|
||||
Callback func()
|
||||
}
|
||||
|
||||
func (i *ItemEntry) isItem() {}
|
||||
|
||||
// GetName returns the name of the ItemEntry.
|
||||
func (i *ItemEntry) GetName() string {
|
||||
return i.Name
|
||||
}
|
||||
|
||||
func (m *Menu) isItem() {}
|
||||
|
||||
// GetName returns the name of the Menu.
|
||||
func (m *Menu) GetName() string {
|
||||
return m.Name
|
||||
}
|
||||
|
||||
// A MenuBar is a horizontal list of menus.
|
||||
type MenuBar struct {
|
||||
Menus []Menu
|
||||
|
||||
x, y int
|
||||
width, height int
|
||||
menuExpanded bool
|
||||
focused bool
|
||||
selected int // Index of selection in MenuBar
|
||||
|
||||
Theme *Theme
|
||||
}
|
||||
|
||||
func NewMenuBar(menus []Menu, theme *Theme) *MenuBar {
|
||||
if menus == nil {
|
||||
menus = make([]Menu, 0, 6)
|
||||
}
|
||||
|
||||
return &MenuBar{
|
||||
Menus: menus,
|
||||
Theme: theme,
|
||||
}
|
||||
}
|
||||
|
||||
// GetMenuXPos returns the X position of the name of Menu at `idx` visually.
|
||||
func (b *MenuBar) GetMenuXPos(idx int) int {
|
||||
x := 1
|
||||
for i := 0; i < idx; i++ {
|
||||
x += len(b.Menus[i].Name) + 2 // two for padding
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
// Draw renders the MenuBar and its sub-menus.
|
||||
func (b *MenuBar) Draw(s tcell.Screen) {
|
||||
normalStyle := b.Theme.GetOrDefault("MenuBar")
|
||||
|
||||
// Draw menus based on whether b.focused and which is selected
|
||||
DrawRect(s, b.x, b.y, 200, 1, ' ', normalStyle) // TODO: calculate actual width
|
||||
col := b.x + 1
|
||||
for i, item := range b.Menus {
|
||||
str := fmt.Sprintf(" %s ", item.Name) // Surround the name in spaces
|
||||
var sty tcell.Style
|
||||
if b.selected == i && b.focused { // If we are drawing the selected item ...
|
||||
sty = b.Theme.GetOrDefault("MenuBarSelected") // Use style for selected items
|
||||
} else {
|
||||
sty = normalStyle
|
||||
}
|
||||
DrawStr(s, col, b.y, str, sty)
|
||||
col += len(str)
|
||||
}
|
||||
|
||||
if b.menuExpanded {
|
||||
menu := b.Menus[b.selected]
|
||||
menu.Draw(s) // Draw menu when it is expanded / visible
|
||||
}
|
||||
}
|
||||
|
||||
// SetFocused highlights the MenuBar and focuses any sub-menus.
|
||||
func (b *MenuBar) SetFocused(v bool) {
|
||||
b.focused = v
|
||||
if !v {
|
||||
b.menuExpanded = false
|
||||
}
|
||||
}
|
||||
|
||||
func (b *MenuBar) SetTheme(theme *Theme) {
|
||||
b.Theme = theme
|
||||
}
|
||||
|
||||
// GetPos returns the position of the MenuBar.
|
||||
func (b *MenuBar) GetPos() (int, int) {
|
||||
return b.x, b.y
|
||||
}
|
||||
|
||||
// SetPos sets the position of the MenuBar.
|
||||
func (b *MenuBar) SetPos(x, y int) {
|
||||
b.x, b.y = x, y
|
||||
}
|
||||
|
||||
func (b *MenuBar) GetMinSize() (int, int) {
|
||||
return 0, 1
|
||||
}
|
||||
|
||||
// GetSize returns the size of the MenuBar.
|
||||
func (b *MenuBar) GetSize() (int, int) {
|
||||
return b.width, b.height
|
||||
}
|
||||
|
||||
// SetSize sets the size of the MenuBar.
|
||||
func (b *MenuBar) SetSize(width, height int) {
|
||||
b.width, b.height = width, height
|
||||
}
|
||||
|
||||
// HandleEvent will propogate events to sub-menus and returns true if
|
||||
// any of them handled the event.
|
||||
func (b *MenuBar) HandleEvent(event tcell.Event) bool {
|
||||
switch ev := event.(type) {
|
||||
case *tcell.EventKey:
|
||||
if ev.Key() == tcell.KeyEnter && !b.menuExpanded {
|
||||
menu := &b.Menus[b.selected]
|
||||
menu.SetPos(b.GetMenuXPos(b.selected), b.y+1)
|
||||
menu.SetFocused(true)
|
||||
|
||||
b.menuExpanded = true // Tells Draw() to render the menu
|
||||
} else if ev.Key() == tcell.KeyLeft {
|
||||
if b.selected <= 0 {
|
||||
b.selected = len(b.Menus) - 1 // Wrap to end
|
||||
} else {
|
||||
b.selected--
|
||||
}
|
||||
// Update position of new menu after changing menu selection
|
||||
b.Menus[b.selected].SetPos(b.GetMenuXPos(b.selected), b.y+1)
|
||||
} else if ev.Key() == tcell.KeyRight {
|
||||
if b.selected >= len(b.Menus)-1 {
|
||||
b.selected = 0 // Wrap to beginning
|
||||
} else {
|
||||
b.selected++
|
||||
}
|
||||
// Update position of new menu after changing menu selection
|
||||
b.Menus[b.selected].SetPos(b.GetMenuXPos(b.selected), b.y+1)
|
||||
} else {
|
||||
if b.menuExpanded {
|
||||
return b.Menus[b.selected].HandleEvent(event)
|
||||
} else {
|
||||
return false // Nobody to propogate our event to
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// A Menu contains one or more ItemEntry or ItemMenus.
|
||||
type Menu struct {
|
||||
Name string
|
||||
Items []Item
|
||||
|
||||
x, y int
|
||||
width, height int // Size may not be settable
|
||||
selected int // Index of selected Item
|
||||
|
||||
Theme *Theme
|
||||
}
|
||||
|
||||
// New creates a new Menu. `items` can be `nil`.
|
||||
func NewMenu(name string, theme *Theme, items []Item) Menu {
|
||||
if items == nil {
|
||||
items = make([]Item, 0, 6)
|
||||
}
|
||||
|
||||
return Menu{
|
||||
Name: name,
|
||||
Items: items,
|
||||
Theme: theme,
|
||||
}
|
||||
}
|
||||
|
||||
// Draw renders the Menu at its position.
|
||||
func (m *Menu) Draw(s tcell.Screen) {
|
||||
defaultStyle := m.Theme.GetOrDefault("Menu")
|
||||
|
||||
m.GetSize() // Call this to update internal width and height
|
||||
DrawRect(s, m.x, m.y, m.width, m.height, ' ', defaultStyle) // Fill background
|
||||
DrawRectOutlineDefault(s, m.x, m.y, m.width, m.height, defaultStyle) // Draw outline
|
||||
|
||||
// Draw items based on whether m.focused and which is selected
|
||||
for i, item := range m.Items {
|
||||
var sty tcell.Style
|
||||
if m.selected == i {
|
||||
sty = m.Theme.GetOrDefault("MenuSelected")
|
||||
} else {
|
||||
sty = defaultStyle
|
||||
}
|
||||
DrawStr(s, m.x+1, m.y+1+i, item.GetName(), sty)
|
||||
}
|
||||
}
|
||||
|
||||
// SetFocused does not do anything for a Menu.
|
||||
func (m *Menu) SetFocused(v bool) {
|
||||
// TODO: wat do
|
||||
}
|
||||
|
||||
// GetPos returns the position of the Menu.
|
||||
func (m *Menu) GetPos() (int, int) {
|
||||
return m.x, m.y
|
||||
}
|
||||
|
||||
// SetPos sets the position of the Menu.
|
||||
func (m *Menu) SetPos(x, y int) {
|
||||
m.x, m.y = x, y
|
||||
}
|
||||
|
||||
// GetSize returns the size of the Menu.
|
||||
func (m *Menu) GetSize() (int, int) {
|
||||
// TODO: no, pls don't do this
|
||||
maxLen := 0
|
||||
for _, item := range m.Items {
|
||||
len := len(item.GetName())
|
||||
if len > maxLen {
|
||||
maxLen = len
|
||||
}
|
||||
}
|
||||
m.width = maxLen + 2 // Add two for padding
|
||||
m.height = len(m.Items) + 2 // And another two for the same reason ...
|
||||
return m.width, m.height
|
||||
}
|
||||
|
||||
// SetSize sets the size of the Menu.
|
||||
func (m *Menu) SetSize(width, height int) {
|
||||
// Cannot set the size of a Menu
|
||||
}
|
||||
|
||||
// HandleEvent will handle events for a Menu and may propogate them
|
||||
// to sub-menus. Returns true if the event was handled.
|
||||
func (m *Menu) HandleEvent(event tcell.Event) bool {
|
||||
// TODO: simplify this function
|
||||
switch ev := event.(type) {
|
||||
case *tcell.EventKey:
|
||||
if ev.Key() == tcell.KeyEnter {
|
||||
switch item := m.Items[m.selected].(type) {
|
||||
case *ItemEntry:
|
||||
item.Callback()
|
||||
case *Menu:
|
||||
// TODO: implement sub-menus ...
|
||||
}
|
||||
return true
|
||||
} else if ev.Key() == tcell.KeyUp {
|
||||
if m.selected <= 0 {
|
||||
m.selected = len(m.Items) - 1 // Wrap to end
|
||||
} else {
|
||||
m.selected--
|
||||
}
|
||||
return true
|
||||
} else if ev.Key() == tcell.KeyDown {
|
||||
if m.selected >= len(m.Items)-1 {
|
||||
m.selected = 0 // Wrap to beginning
|
||||
} else {
|
||||
m.selected++
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
Reference in New Issue
Block a user