From a2876c2086ee357cc3a8c5561760674db1265dfb Mon Sep 17 00:00:00 2001 From: EnumDev Date: Wed, 4 Jun 2025 16:00:37 +0300 Subject: [PATCH] Add basic dropdown functionality --- src/buffer.go | 2 + src/dropdown.go | 72 ++++++++++++++++++++++++ src/main.go | 3 +- src/top_menu.go | 77 ++++++++++++++++++++++++- src/utils.go | 36 ++++++++++++ src/window.go | 146 ++++++++++++++++++++++++++++++++++-------------- 6 files changed, 291 insertions(+), 45 deletions(-) create mode 100644 src/dropdown.go diff --git a/src/buffer.go b/src/buffer.go index 7f8d155..ce55c26 100644 --- a/src/buffer.go +++ b/src/buffer.go @@ -68,6 +68,7 @@ func CreateFileBuffer(filename string) (*Buffer, error) { } Buffers[buffer.Id] = &buffer + LastBufferId++ return &buffer, nil } @@ -82,6 +83,7 @@ func CreateBuffer(bufferName string) *Buffer { } Buffers[buffer.Id] = &buffer + LastBufferId++ return &buffer } diff --git a/src/dropdown.go b/src/dropdown.go new file mode 100644 index 0000000..47d408d --- /dev/null +++ b/src/dropdown.go @@ -0,0 +1,72 @@ +package main + +import ( + "github.com/gdamore/tcell" +) + +type Dropdown struct { + Active bool + Selected int + Options []string + PosX, PosY int + Width int + Action func(int) +} + +var dropdowns = make([]*Dropdown, 0) + +func CreateDropdownMenu(options []string, posX, posY, dropdownWidth int, action func(int)) *Dropdown { + if len(options) == 0 { + return nil + } + + width := 0 + + if dropdownWidth <= 0 { + for _, option := range options { + if len(option) > width { + width = len(option) + } + } + } + + d := &Dropdown{ + Active: false, + Selected: 0, + Options: options, + PosX: posX, + PosY: posY, + Width: width, + Action: action, + } + + dropdowns = append(dropdowns, d) + + return d +} + +func GetActiveDropdown() *Dropdown { + for _, dropdown := range dropdowns { + if dropdown.Active { + return dropdown + } + } + return nil +} + +func drawDropdowns(window *Window) { + dropdownStyle := tcell.StyleDefault.Foreground(tcell.ColorBlack).Background(tcell.ColorWhite) + for _, d := range dropdowns { + drawBox(window.screen, d.PosX, d.PosY, d.PosX+d.Width+1, d.PosY+len(d.Options)+1, dropdownStyle) + line := d.PosY + for i, option := range d.Options { + if d.Selected == i { + drawText(window.screen, d.PosX+1, d.PosY+line, d.PosX+d.Width+1, d.PosY+line, dropdownStyle.Background(tcell.Color250), option) + } else { + drawText(window.screen, d.PosX+1, d.PosY+line, d.PosX+d.Width+1, d.PosY+line, dropdownStyle, option) + } + + line++ + } + } +} diff --git a/src/main.go b/src/main.go index 4a7ca5e..c4f18a1 100644 --- a/src/main.go +++ b/src/main.go @@ -20,7 +20,7 @@ func main() { PrintMessage(window, "Could not open file: "+file) continue } - Buffers[b.Id] = b + if initialBuffer == nil { initialBuffer = b } @@ -28,7 +28,6 @@ func main() { } if initialBuffer != nil { - delete(Buffers, window.textArea.CurrentBuffer.Id) window.textArea.CurrentBuffer = initialBuffer } diff --git a/src/top_menu.go b/src/top_menu.go index 65526db..13e4bcd 100644 --- a/src/top_menu.go +++ b/src/top_menu.go @@ -1,11 +1,18 @@ package main import ( + "fmt" "github.com/gdamore/tcell" + "maps" + "slices" + "strconv" + "strings" ) type TopMenuButton struct { - Name string + Name string + Key rune + Action func(w *Window) } var TopMenuButtons = make([]TopMenuButton, 0) @@ -14,12 +21,80 @@ func initTopMenu() { // Buttons fileButton := TopMenuButton{ Name: "File", + Key: 'f', + Action: func(window *Window) { + dropdowns = make([]*Dropdown, 0) + d := CreateDropdownMenu([]string{"New", "Save", "Open", "Close", "Quit"}, 0, 1, 0, func(i int) { + switch i { + case 0: + number := 1 + for _, buffer := range Buffers { + if strings.HasPrefix(buffer.Name, "New File ") { + number++ + } + } + buffer := CreateBuffer(fmt.Sprintf("New File %d", number)) + window.textArea.CurrentBuffer = buffer + window.SetCursorPos(0) + case 1: + case 2: + case 3: + delete(Buffers, window.textArea.CurrentBuffer.Id) + buffersSlice := slices.Collect(maps.Values(Buffers)) + if len(buffersSlice) == 0 { + window.Close() + return + } + window.textArea.CurrentBuffer = buffersSlice[0] + window.SetCursorPos(0) + case 4: + window.Close() + } + dropdowns = make([]*Dropdown, 0) + window.textArea.Typing = true + }) + d.Active = true + window.textArea.Typing = false + }, } EditButton := TopMenuButton{ Name: "Edit", + Key: 'e', } Buffers := TopMenuButton{ Name: "Buffers", + Key: 'b', + Action: func(window *Window) { + dropdowns = make([]*Dropdown, 0) + buffersSlice := make([]string, 0) + for _, buffer := range Buffers { + if window.textArea.CurrentBuffer == buffer { + buffersSlice = append(buffersSlice, fmt.Sprintf("[%d] * %s", buffer.Id, buffer.Name)) + } else { + buffersSlice = append(buffersSlice, fmt.Sprintf("[%d] %s", buffer.Id, buffer.Name)) + } + } + + slices.Sort(buffersSlice) + + d := CreateDropdownMenu(buffersSlice, 0, 1, 0, func(i int) { + start := strings.Index(buffersSlice[i], "[") + end := strings.Index(buffersSlice[i], "]") + + id, err := strconv.Atoi(buffersSlice[i][start+1 : end]) + if err != nil { + PrintMessage(window, fmt.Sprintf("Cannot convert buffer id '%s' to int", buffersSlice[i][start:end])) + return + } + + window.textArea.CurrentBuffer = Buffers[id] + window.SetCursorPos(0) + dropdowns = make([]*Dropdown, 0) + window.textArea.Typing = true + }) + d.Active = true + window.textArea.Typing = false + }, } // Append buttons diff --git a/src/utils.go b/src/utils.go index 869e2a3..16573f8 100644 --- a/src/utils.go +++ b/src/utils.go @@ -17,3 +17,39 @@ func drawText(s tcell.Screen, x1, y1, x2, y2 int, style tcell.Style, text string } } } + +func drawBox(s tcell.Screen, x1, y1, x2, y2 int, style tcell.Style) { + if y2 < y1 { + y1, y2 = y2, y1 + } + if x2 < x1 { + x1, x2 = x2, x1 + } + + // Fill background + for row := y1; row <= y2; row++ { + for col := x1; col <= x2; col++ { + s.SetContent(col, row, ' ', nil, style) + } + } + + // Draw borders + for col := x1; col <= x2; col++ { + s.SetContent(col, y1, tcell.RuneHLine, nil, style) + s.SetContent(col, y2, tcell.RuneHLine, nil, style) + } + for row := y1 + 1; row < y2; row++ { + s.SetContent(x1, row, tcell.RuneVLine, nil, style) + s.SetContent(x2, row, tcell.RuneVLine, nil, style) + } + + // Only draw corners if necessary + if y1 != y2 && x1 != x2 { + s.SetContent(x1, y1, tcell.RuneULCorner, nil, style) + s.SetContent(x2, y1, tcell.RuneURCorner, nil, style) + s.SetContent(x1, y2, tcell.RuneLLCorner, nil, style) + s.SetContent(x2, y2, tcell.RuneLRCorner, nil, style) + } + + drawText(s, x1+1, y1+1, x2-1, y2-1, style, " ") +} diff --git a/src/window.go b/src/window.go index f83a905..1d33c34 100644 --- a/src/window.go +++ b/src/window.go @@ -3,6 +3,8 @@ package main import ( "github.com/gdamore/tcell" "log" + "maps" + "slices" ) type Window struct { @@ -16,6 +18,7 @@ type Window struct { type TextArea struct { CursorPos int + Typing bool CurrentBuffer *Buffer } @@ -26,6 +29,7 @@ func CreateWindow() (*Window, error) { textArea: TextArea{ CursorPos: 0, + Typing: true, CurrentBuffer: nil, }, @@ -33,7 +37,7 @@ func CreateWindow() (*Window, error) { } // Create empty buffer if nil - window.textArea.CurrentBuffer = CreateBuffer("New File") + window.textArea.CurrentBuffer = CreateBuffer("New File 1") // Create tcell screen screen, err := tcell.NewScreen() @@ -109,8 +113,15 @@ func (window *Window) Draw() { // Draw message bar drawMessageBar(window) + // Draw dropdowns + drawDropdowns(window) + // Draw cursor - window.screen.ShowCursor(window.GetAbsoluteCursorPos()) + if window.textArea.Typing { + window.screen.ShowCursor(window.GetAbsoluteCursorPos()) + } else { + window.screen.HideCursor() + } // Update screen window.screen.Show() @@ -123,46 +134,93 @@ func (window *Window) Draw() { case *tcell.EventResize: window.screen.Sync() case *tcell.EventKey: - // Navigation Keys - if ev.Key() == tcell.KeyRight { + window.input(ev) + } +} + +func (window *Window) input(ev *tcell.EventKey) { + if ev.Key() == tcell.KeyRight { // Navigation Keys + if window.textArea.Typing { window.SetCursorPos(window.textArea.CursorPos + 1) - } else if ev.Key() == tcell.KeyLeft { + } + } else if ev.Key() == tcell.KeyLeft { + if window.textArea.Typing { window.SetCursorPos(window.textArea.CursorPos - 1) - } else if ev.Key() == tcell.KeyUp { + } + } else if ev.Key() == tcell.KeyUp { + if window.textArea.Typing { x, y := window.GetCursorPos2D() window.SetCursorPos2D(x, y-1) - } else if ev.Key() == tcell.KeyDown { + } else if GetActiveDropdown() != nil { + dropdown := GetActiveDropdown() + dropdown.Selected-- + if dropdown.Selected < 0 { + dropdown.Selected = 0 + } + } + } else if ev.Key() == tcell.KeyDown { + if window.textArea.Typing { x, y := window.GetCursorPos2D() window.SetCursorPos2D(x, y+1) + } else if GetActiveDropdown() != nil { + dropdown := GetActiveDropdown() + dropdown.Selected++ + if dropdown.Selected >= len(dropdown.Options) { + dropdown.Selected = len(dropdown.Options) - 1 + } } - - // Exit key - if ev.Key() == tcell.KeyCtrlC { + } else if ev.Key() == tcell.KeyEscape { + dropdowns = make([]*Dropdown, 0) + window.textArea.Typing = true + } else if ev.Key() == tcell.KeyCtrlC { // Close buffer key + delete(Buffers, window.textArea.CurrentBuffer.Id) + buffersSlice := slices.Collect(maps.Values(Buffers)) + if len(buffersSlice) == 0 { window.Close() + return + } + window.textArea.CurrentBuffer = buffersSlice[0] + window.SetCursorPos(0) + dropdowns = make([]*Dropdown, 0) + window.textArea.Typing = true + } else if ev.Key() == tcell.KeyCtrlQ { // Exit key + window.Close() + } else if ev.Modifiers()&tcell.ModAlt != 0 { // Menu Bar + for _, button := range TopMenuButtons { + if ev.Rune() == button.Key { + button.Action(window) + break + } + } + } else if ev.Key() == tcell.KeyBackspace2 { // Typing + str := window.textArea.CurrentBuffer.Contents + index := window.textArea.CursorPos + + if index != 0 { + str = str[:index-1] + str[index:] + window.textArea.CursorPos-- + window.textArea.CurrentBuffer.Contents = str + } + } else if ev.Key() == tcell.KeyTab { + if GetActiveDropdown() != nil { + return } - // Typing - if ev.Key() == tcell.KeyBackspace2 { - str := window.textArea.CurrentBuffer.Contents - index := window.textArea.CursorPos + str := window.textArea.CurrentBuffer.Contents + index := window.textArea.CursorPos - if index != 0 { - str = str[:index-1] + str[index:] - window.textArea.CursorPos-- - window.textArea.CurrentBuffer.Contents = str - } - } else if ev.Key() == tcell.KeyTab { - str := window.textArea.CurrentBuffer.Contents - index := window.textArea.CursorPos - - if index == len(str) { - str += "\t" - } else { - str = str[:index] + "\t" + str[index:] - } - window.textArea.CursorPos++ - window.textArea.CurrentBuffer.Contents = str - } else if ev.Key() == tcell.KeyEnter { + if index == len(str) { + str += "\t" + } else { + str = str[:index] + "\t" + str[index:] + } + window.textArea.CursorPos++ + window.textArea.CurrentBuffer.Contents = str + } else if ev.Key() == tcell.KeyEnter { + if GetActiveDropdown() != nil { + d := GetActiveDropdown() + d.Action(d.Selected) + } else { str := window.textArea.CurrentBuffer.Contents index := window.textArea.CursorPos @@ -173,18 +231,22 @@ func (window *Window) Draw() { } window.textArea.CursorPos++ window.textArea.CurrentBuffer.Contents = str - } else if ev.Key() == tcell.KeyRune { - str := window.textArea.CurrentBuffer.Contents - index := window.textArea.CursorPos - - if index == len(str) { - str += string(ev.Rune()) - } else { - str = str[:index] + string(ev.Rune()) + str[index:] - } - window.textArea.CursorPos++ - window.textArea.CurrentBuffer.Contents = str } + } else if ev.Key() == tcell.KeyRune { + if GetActiveDropdown() != nil { + return + } + + str := window.textArea.CurrentBuffer.Contents + index := window.textArea.CursorPos + + if index == len(str) { + str += string(ev.Rune()) + } else { + str = str[:index] + string(ev.Rune()) + str[index:] + } + window.textArea.CursorPos++ + window.textArea.CurrentBuffer.Contents = str } }