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 }