Menu & MenuBar experience improvements

This commit is contained in:
Luke I. Wilson 2021-03-18 23:37:42 -05:00
parent 1d77aae277
commit 83567c1341
3 changed files with 134 additions and 41 deletions

23
main.go
View File

@ -70,8 +70,9 @@ func main() {
barFocused := false
// TODO: load menus in another function
bar.AddMenu(ui.NewMenu("File", &theme, []ui.Item{&ui.ItemEntry{Name: "New File", Callback: func() {
fileMenu := ui.NewMenu("File", &theme)
fileMenu.AddItems([]ui.Item{&ui.ItemEntry{Name: "New File", Callback: func() {
textEdit := ui.NewTextEdit(&s, "", "", &theme) // No file path, no contents
tabContainer.AddTab("noname", textEdit)
}}, &ui.ItemEntry{Name: "Open...", Callback: func() {
@ -144,9 +145,11 @@ func main() {
}}, &ui.ItemSeparator{}, &ui.ItemEntry{Name: "Exit", Callback: func() {
s.Fini()
os.Exit(0)
}}}))
}}})
bar.AddMenu(ui.NewMenu("Edit", &theme, []ui.Item{&ui.ItemEntry{Name: "Cut", Callback: func() {
editMenu := ui.NewMenu("Edit", &theme)
editMenu.AddItems([]ui.Item{&ui.ItemEntry{Name: "Cut", Callback: func() {
if tabContainer.GetTabCount() > 0 {
tab := tabContainer.GetTab(tabContainer.GetSelectedTabIdx())
te := tab.Child.(*ui.TextEdit)
@ -177,11 +180,17 @@ func main() {
}
te.Insert(contents)
}
}}}))
}}})
bar.AddMenu(ui.NewMenu("Search", &theme, []ui.Item{&ui.ItemEntry{Name: "New", Callback: func() {
searchMenu := ui.NewMenu("Search", &theme)
searchMenu.AddItems([]ui.Item{&ui.ItemEntry{Name: "New", Callback: func() {
s.Beep()
}}}))
}}})
bar.AddMenu(fileMenu)
bar.AddMenu(editMenu)
bar.AddMenu(searchMenu)
changeFocus(tabContainer) // TabContainer is focused by default

View File

@ -52,6 +52,7 @@ type MenuBar struct {
width, height int
focused bool
selected int // Index of selection in MenuBar
menusVisible bool // Whether to draw the selected menu
Theme *Theme
}
@ -65,7 +66,8 @@ func NewMenuBar(theme *Theme) *MenuBar {
func (b *MenuBar) AddMenu(menu *Menu) {
menu.itemSelectedCallback = func() {
// TODO: figure out what im doing here
b.menusVisible = false
menu.SetFocused(false)
}
b.Menus = append(b.Menus, menu)
}
@ -84,7 +86,7 @@ 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
DrawRect(s, b.x, b.y, b.width, 1, ' ', normalStyle)
col := b.x + 1
for i, item := range b.Menus {
str := fmt.Sprintf(" %s ", item.Name) // Surround the name in spaces
@ -98,7 +100,7 @@ func (b *MenuBar) Draw(s tcell.Screen) {
col += len(str)
}
if b.Menus[b.selected].Visible {
if b.menusVisible {
menu := b.Menus[b.selected]
menu.Draw(s) // Draw menu when it is expanded / visible
}
@ -107,8 +109,12 @@ func (b *MenuBar) Draw(s tcell.Screen) {
// SetFocused highlights the MenuBar and focuses any sub-menus.
func (b *MenuBar) SetFocused(v bool) {
b.focused = v
b.Menus[b.selected].SetFocused(v)
if !v {
b.Menus[b.selected].SetFocused(false)
b.selected = 0 // Reset cursor position every time component is unfocused
if b.menusVisible {
b.menusVisible = false
}
}
}
@ -145,11 +151,19 @@ func (b *MenuBar) SetSize(width, height int) {
func (b *MenuBar) HandleEvent(event tcell.Event) bool {
switch ev := event.(type) {
case *tcell.EventKey:
if ev.Key() == tcell.KeyEnter && !b.Menus[b.selected].Visible {
switch ev.Key() {
case tcell.KeyEnter:
if !b.menusVisible { // If menus are not visible...
b.menusVisible = true
menu := &b.Menus[b.selected]
(*menu).SetPos(b.GetMenuXPos(b.selected), b.y+1)
(*menu).SetFocused(true) // Makes .Visible true for the Menu
} else if ev.Key() == tcell.KeyLeft {
} else { // The selected Menu is visible, send the event to it
return b.Menus[b.selected].HandleEvent(event)
}
case tcell.KeyLeft:
b.Menus[b.selected].SetFocused(false) // Unfocus current menu
if b.selected <= 0 {
b.selected = len(b.Menus) - 1 // Wrap to end
} else {
@ -157,7 +171,9 @@ func (b *MenuBar) HandleEvent(event tcell.Event) bool {
}
// 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 {
b.Menus[b.selected].SetFocused(true) // Focus new menu
case tcell.KeyRight:
b.Menus[b.selected].SetFocused(false)
if b.selected >= len(b.Menus)-1 {
b.selected = 0 // Wrap to beginning
} else {
@ -165,8 +181,26 @@ func (b *MenuBar) HandleEvent(event tcell.Event) bool {
}
// Update position of new menu after changing menu selection
b.Menus[b.selected].SetPos(b.GetMenuXPos(b.selected), b.y+1)
b.Menus[b.selected].SetFocused(true) // Focus new menu
case tcell.KeyRune: // Search for the matching quick char in menu names
if !b.menusVisible { // If the selected Menu is not open/visible
for _, m := range b.Menus {
found, r := QuickCharInString(m.Name)
if found && r == ev.Rune() {
b.menusVisible = true
menu := &b.Menus[b.selected]
(*menu).SetPos(b.GetMenuXPos(b.selected), b.y+1)
(*menu).SetFocused(true)
}
}
} else {
if b.Menus[b.selected].Visible {
return b.Menus[b.selected].HandleEvent(event) // Have menu handle quick char event
}
default:
if b.menusVisible {
return b.Menus[b.selected].HandleEvent(event)
} else {
return false // Nobody to propogate our event to
@ -181,7 +215,6 @@ func (b *MenuBar) HandleEvent(event tcell.Event) bool {
type Menu struct {
Name string
Items []Item
Visible bool // True when focused
x, y int
width, height int // Size may not be settable
@ -192,18 +225,40 @@ type Menu struct {
}
// 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)
}
func NewMenu(name string, theme *Theme) *Menu {
return &Menu{
Name: name,
Items: items,
Items: make([]Item, 0, 6),
Theme: theme,
}
}
func (m *Menu) AddItem(item Item) {
switch typ := item.(type) {
case *Menu:
typ.itemSelectedCallback = func() {
m.itemSelectedCallback()
}
}
m.Items = append(m.Items, item)
}
func (m *Menu) AddItems(items []Item) {
for _, item := range items {
m.AddItem(item)
}
}
func (m *Menu) ActivateItemUnderCursor() {
switch item := m.Items[m.selected].(type) {
case *ItemEntry:
item.Callback()
m.itemSelectedCallback()
case *Menu:
// TODO: implement sub-menus ...
}
}
func (m *Menu) CursorUp() {
if m.selected <= 0 {
m.selected = len(m.Items) - 1 // Wrap to end
@ -260,7 +315,10 @@ func (m *Menu) Draw(s tcell.Screen) {
// SetFocused does not do anything for a Menu.
func (m *Menu) SetFocused(v bool) {
m.Visible = v
// TODO: when adding sub-menus, set all focus to v
if !v {
m.selected = 0
}
}
// GetPos returns the position of the Menu.
@ -299,22 +357,28 @@ func (m *Menu) HandleEvent(event tcell.Event) bool {
// TODO: simplify this function
switch ev := event.(type) {
case *tcell.EventKey:
if ev.Key() == tcell.KeyEnter {
m.SetFocused(false) // Hides the menu
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 {
switch ev.Key() {
case tcell.KeyEnter:
m.ActivateItemUnderCursor()
case tcell.KeyUp:
m.CursorUp()
return true
} else if ev.Key() == tcell.KeyDown {
case tcell.KeyDown:
m.CursorDown()
return true
case tcell.KeyRune:
// TODO: support quick chars for sub-menus
for i, item := range m.Items {
found, r := QuickCharInString(item.GetName())
if found && r == ev.Rune() {
m.selected = i
m.ActivateItemUnderCursor()
}
}
default:
return false
}
return true
}
return false
}

View File

@ -1,5 +1,25 @@
package ui
import "unicode"
// QuickCharInString is used for finding the "quick char" in a string. A quick char
// suffixes a '_' (underscore). So basically, this function returns any rune after
// an underscore. The rune is always made lowercase. The bool returned is whether
// the rune was found.
func QuickCharInString(s string) (bool, rune) {
runes := []rune(s)
for i, r := range runes {
if r == '_' {
if i+1 < len(runes) {
return true, unicode.ToLower(runes[i+1])
} else {
return false, ' '
}
}
}
return false, ' '
}
// Max returns the larger integer.
func Max(a, b int) int {
if a > b {