diff --git a/main.go b/main.go index b9f094d..872fa8c 100644 --- a/main.go +++ b/main.go @@ -70,12 +70,12 @@ func main() { barFocused := false - fileMenu := ui.NewMenu("File", &theme) + fileMenu := ui.NewMenu("_File", &theme) - fileMenu.AddItems([]ui.Item{&ui.ItemEntry{Name: "New File", Callback: func() { + 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() { + }}, &ui.ItemEntry{Name: "_Open...", Callback: func() { callback := func(filePaths []string) { for _, path := range filePaths { file, err := os.Open(path) @@ -109,7 +109,7 @@ func main() { }, ) changeFocus(fileSelector) - }}, &ui.ItemEntry{Name: "Save", Callback: func() { + }}, &ui.ItemEntry{Name: "_Save", Callback: func() { if tabContainer.GetTabCount() > 0 { tab := tabContainer.GetTab(tabContainer.GetSelectedTabIdx()) te := tab.Child.(*ui.TextEdit) @@ -124,7 +124,7 @@ func main() { } } } - }}, &ui.ItemEntry{Name: "Save As...", Callback: func() { + }}, &ui.ItemEntry{Name: "Save _As...", Callback: func() { // TODO: implement a "Save as" dialog system, and show that when trying to save noname files callback := func(filePaths []string) { fileSelector = nil // Hide the file selector @@ -142,14 +142,14 @@ func main() { }, ) changeFocus(fileSelector) - }}, &ui.ItemSeparator{}, &ui.ItemEntry{Name: "Exit", Callback: func() { + }}, &ui.ItemSeparator{}, &ui.ItemEntry{Name: "E_xit", Callback: func() { s.Fini() os.Exit(0) }}}) - editMenu := ui.NewMenu("Edit", &theme) + editMenu := ui.NewMenu("_Edit", &theme) - editMenu.AddItems([]ui.Item{&ui.ItemEntry{Name: "Cut", Callback: func() { + editMenu.AddItems([]ui.Item{&ui.ItemEntry{Name: "_Cut", Callback: func() { if tabContainer.GetTabCount() > 0 { tab := tabContainer.GetTab(tabContainer.GetSelectedTabIdx()) te := tab.Child.(*ui.TextEdit) @@ -160,7 +160,7 @@ func main() { _ = ClipWrite(selectedStr) // Add the selectedStr to clipboard } } - }}, &ui.ItemEntry{Name: "Copy", Callback: func() { + }}, &ui.ItemEntry{Name: "_Copy", Callback: func() { if tabContainer.GetTabCount() > 0 { tab := tabContainer.GetTab(tabContainer.GetSelectedTabIdx()) te := tab.Child.(*ui.TextEdit) @@ -169,7 +169,7 @@ func main() { _ = ClipWrite(selectedStr) // Add selectedStr to clipboard } } - }}, &ui.ItemEntry{Name: "Paste", Callback: func() { + }}, &ui.ItemEntry{Name: "_Paste", Callback: func() { if tabContainer.GetTabCount() > 0 { tab := tabContainer.GetTab(tabContainer.GetSelectedTabIdx()) te := tab.Child.(*ui.TextEdit) @@ -182,7 +182,7 @@ func main() { } }}}) - searchMenu := ui.NewMenu("Search", &theme) + searchMenu := ui.NewMenu("_Search", &theme) searchMenu.AddItems([]ui.Item{&ui.ItemEntry{Name: "New", Callback: func() { s.Beep() diff --git a/ui/drawfunctions.go b/ui/drawfunctions.go index 9585186..574fa76 100644 --- a/ui/drawfunctions.go +++ b/ui/drawfunctions.go @@ -15,11 +15,33 @@ func DrawRect(s tcell.Screen, x, y, width, height int, char rune, style tcell.St // DrawStr will render each character of a string at `x` and `y`. func DrawStr(s tcell.Screen, x, y int, str string, style tcell.Style) { runes := []rune(str) - for idx := 0; idx < len(runes); idx++ { - s.SetContent(x+idx, y, runes[idx], nil, style) + for idx, r := range runes { + s.SetContent(x+idx, y, r, nil, style) } } +// DrawQuickCharStr renders a string very similar to how DrawStr works, but stylizes the +// quick char (any rune after an underscore) with an underline. Returned is the number of +// columns that were drawn to the screen. This is useful to know the length of the string +// drawn, minus the underscore. +func DrawQuickCharStr(s tcell.Screen, x, y int, str string, style tcell.Style) int { + runes := []rune(str) + col := 0 + for i := 0; i < len(runes); i++ { + r := runes[i] + if r == '_' && i+1 < len(runes) { + i++ + sty := style.Underline(true) + + s.SetContent(x+col, y, runes[i], nil, sty) + } else { + s.SetContent(x+col, y, r, nil, style) + } + col++ + } + return col +} + // DrawRectOutline draws only the outline of a rectangle, using `ul`, `ur`, `bl`, and `br` // for the corner runes, and `hor` and `vert` for the horizontal and vertical runes, respectively. func DrawRectOutline(s tcell.Screen, x, y, _width, _height int, ul, ur, bl, br, hor, vert rune, style tcell.Style) { diff --git a/ui/menu.go b/ui/menu.go index d180eba..3820237 100644 --- a/ui/menu.go +++ b/ui/menu.go @@ -90,13 +90,14 @@ func (b *MenuBar) Draw(s tcell.Screen) { 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 + + sty := normalStyle + if b.focused && b.selected == i { + sty = b.Theme.GetOrDefault("MenuBarSelected") // Use special style for selected item } - DrawStr(s, col, b.y, str, sty) + + DrawQuickCharStr(s, col, b.y, str, sty) + col += len(str) } @@ -185,14 +186,16 @@ func (b *MenuBar) HandleEvent(event tcell.Event) bool { 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 { + for i, m := range b.Menus { found, r := QuickCharInString(m.Name) if found && r == ev.Rune() { b.menusVisible = true + b.selected = i menu := &b.Menus[b.selected] (*menu).SetPos(b.GetMenuXPos(b.selected), b.y+1) (*menu).SetFocused(true) + break } } } else { @@ -306,9 +309,11 @@ func (m *Menu) Draw(s tcell.Screen) { } else { sty = defaultStyle } - itemName := item.GetName() - str := fmt.Sprintf("%s%s", itemName, strings.Repeat(" ", m.width-2-len(itemName))) - DrawStr(s, m.x+1, m.y+1+i, str, sty) + + len := DrawQuickCharStr(s, m.x+1, m.y+1+i, item.GetName(), sty) + + str := strings.Repeat(" ", m.width-2-len) // Fill space after menu names to border + DrawStr(s, m.x+1+len, m.y+1+i, str, sty) } } } @@ -368,10 +373,13 @@ func (m *Menu) HandleEvent(event tcell.Event) bool { case tcell.KeyRune: // TODO: support quick chars for sub-menus for i, item := range m.Items { + if m.selected == i { + continue // Skip the item we're on + } found, r := QuickCharInString(item.GetName()) if found && r == ev.Rune() { m.selected = i - m.ActivateItemUnderCursor() + break } } diff --git a/ui/theme.go b/ui/theme.go index d9efe2e..c6fae9f 100644 --- a/ui/theme.go +++ b/ui/theme.go @@ -36,7 +36,6 @@ var DefaultTheme = Theme{ "MenuBarSelected": tcell.Style{}.Foreground(tcell.ColorSilver).Background(tcell.ColorBlack), "Menu": tcell.Style{}.Foreground(tcell.ColorBlack).Background(tcell.ColorSilver), "MenuSelected": tcell.Style{}.Foreground(tcell.ColorSilver).Background(tcell.ColorBlack), - "QuickChar": tcell.Style{}.Foreground(tcell.ColorYellow).Background(tcell.ColorBlack), "Tab": tcell.Style{}.Foreground(tcell.ColorSilver).Background(tcell.ColorBlack), "TabContainer": tcell.Style{}.Foreground(tcell.ColorSilver).Background(tcell.ColorBlack), "TabSelected": tcell.Style{}.Foreground(tcell.ColorBlack).Background(tcell.ColorSilver),