aboutsummaryrefslogtreecommitdiff
path: root/internal/tui/dialogs.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-06-29 04:03:38 -0400
committerhistoria <[not public]>2026-06-29 04:03:38 -0400
commitba34490aaed5f7c242c2b0453130fefc07d0d5d0 (patch)
treedb5818757d5080221494cead1c7e61a576d1cb65 /internal/tui/dialogs.go
parentb1e252c996a6332d391f96624a7d6f149eb097c4 (diff)
downloadtui-ascii-mapper-ba34490aaed5f7c242c2b0453130fefc07d0d5d0.tar.gz
restructured projectHEADmain
Diffstat (limited to 'internal/tui/dialogs.go')
-rw-r--r--internal/tui/dialogs.go526
1 files changed, 526 insertions, 0 deletions
diff --git a/internal/tui/dialogs.go b/internal/tui/dialogs.go
new file mode 100644
index 0000000..014951d
--- /dev/null
+++ b/internal/tui/dialogs.go
@@ -0,0 +1,526 @@
+package tui
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+
+ "tui-ascii-mapper/internal/mapio"
+ "tui-ascii-mapper/internal/model"
+
+ tea "github.com/charmbracelet/bubbletea"
+)
+
+func (m *AppModel) handleDialogKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
+ key := msg.String()
+
+ isFilePicker := m.dialog == model.DialogFileSave || m.dialog == model.DialogFileOpen || m.dialog == model.DialogSaveAs || m.dialog == model.DialogOpenMap
+ if isFilePicker && m.filePicker != nil {
+ switch key {
+ case "esc":
+ m.mode = model.ModeNormal
+ m.dialog = model.DialogNone
+ m.filePicker = nil
+ return m, nil
+ case "up":
+ if m.filePicker.Selected > 0 {
+ m.filePicker.Selected--
+ }
+ return m, nil
+ case "down":
+ if m.filePicker.Selected < len(m.filePicker.Files)-1 {
+ m.filePicker.Selected++
+ }
+ return m, nil
+ case "pgup":
+ m.filePicker.Selected -= 10
+ if m.filePicker.Selected < 0 {
+ m.filePicker.Selected = 0
+ }
+ return m, nil
+ case "pgdown":
+ m.filePicker.Selected += 10
+ if m.filePicker.Selected >= len(m.filePicker.Files) {
+ m.filePicker.Selected = len(m.filePicker.Files) - 1
+ }
+ return m, nil
+ case "left":
+ parent := filepath.Dir(m.filePicker.CurDir)
+ m.filePicker.CurDir = parent
+ m.filePicker.Selected = 0
+ m.refreshFilePicker()
+ return m, nil
+ case "enter":
+ tiVal := strings.TrimSpace(m.ti.Value())
+ if tiVal != "" {
+ m.filePicker = nil
+ return m.doFileAction(tiVal)
+ }
+ if m.filePicker.Selected >= 0 && m.filePicker.Selected < len(m.filePicker.Files) {
+ entry := m.filePicker.Files[m.filePicker.Selected]
+ if entry.IsDir() {
+ m.filePicker.CurDir = filepath.Join(m.filePicker.CurDir, entry.Name())
+ m.filePicker.Selected = 0
+ m.refreshFilePicker()
+ return m, nil
+ }
+ fullPath := filepath.Join(m.filePicker.CurDir, entry.Name())
+ m.ti.SetValue(fullPath)
+ m.filePicker = nil
+ return m.doFileAction(fullPath)
+ }
+ return m, nil
+ }
+ var cmd tea.Cmd
+ m.ti, cmd = m.ti.Update(msg)
+ return m, cmd
+ }
+
+ switch key {
+ case "esc":
+ if m.dialog == model.DialogQuitConfirm {
+ m.quitting = true
+ return m, tea.Quit
+ }
+ m.mode = model.ModeNormal
+ m.dialog = model.DialogNone
+ return m, nil
+ case "q":
+ if m.dialog == model.DialogQuitConfirm {
+ m.quitting = true
+ return m, tea.Quit
+ }
+ fallthrough
+ default:
+ var cmd tea.Cmd
+ m.ti, cmd = m.ti.Update(msg)
+ return m, cmd
+ case "enter":
+ switch m.dialog {
+ case model.DialogSaveAs:
+ name := m.ti.Value()
+ if name != "" {
+ m.rootMap.Filename = name
+ if err := mapio.SerializeMap(m.rootMap, name); err != nil {
+ m.dialogMsg = err.Error()
+ } else {
+ m.dialogMsg = fmt.Sprintf("Saved %s", name)
+ m.dirty = false
+ m.undoPosAtSave = m.undo.Pos()
+ }
+ }
+ case model.DialogResize:
+ var w, h int
+ val := m.ti.Value()
+ if n, _ := fmt.Sscanf(val, "%dx%d", &w, &h); n == 2 && w > 0 && h > 0 && w < 1000 && h < 1000 {
+ m.resizeMap(w, h)
+ }
+ case model.DialogQuitConfirm:
+ m.quitting = true
+ return m, tea.Quit
+ case model.DialogDeleteSubmapConfirm:
+ m.dirty = true
+ m.undo.Push(m.curMap())
+ delete(m.curMap().Submaps, m.cursor)
+ case model.DialogRenameSymbol:
+ name := m.ti.Value()
+ if name != "" && m.selected < len(m.curPalette()) {
+ m.curPalette()[m.selected].Name = name
+ }
+ case model.DialogRenameMap:
+ name := m.ti.Value()
+ if name != "" {
+ m.curMap().Name = name
+ }
+ case model.DialogOpenMap:
+ name := m.ti.Value()
+ if name != "" {
+ return m, m.loadMapCmd(name)
+ }
+ }
+ m.mode = model.ModeNormal
+ m.dialog = model.DialogNone
+ return m, nil
+ }
+}
+
+func (m *AppModel) doFileAction(path string) (tea.Model, tea.Cmd) {
+ switch m.dialog {
+ case model.DialogFileSave, model.DialogSaveAs:
+ m.rootMap.Filename = path
+ if err := mapio.SerializeMap(m.rootMap, path); err != nil {
+ m.dialogMsg = err.Error()
+ } else {
+ m.dialogMsg = fmt.Sprintf("Saved %s", path)
+ m.dirty = false
+ m.undoPosAtSave = m.undo.Pos()
+ }
+ case model.DialogFileOpen, model.DialogOpenMap:
+ m.mode = model.ModeNormal
+ m.dialog = model.DialogNone
+ return m, m.loadMapCmd(path)
+ }
+ m.mode = model.ModeNormal
+ m.dialog = model.DialogNone
+ return m, nil
+}
+
+func (m *AppModel) handleFilePickerMouse(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
+ _, y := msg.X, msg.Y
+ if msg.Button != tea.MouseButtonLeft || msg.Action != tea.MouseActionPress {
+ return m, nil
+ }
+ entryY := y - m.filePicker.PopupY
+ if entryY >= 0 && entryY < len(m.filePicker.Files) {
+ idx := entryY + m.filePicker.ListTop
+ if idx >= 0 && idx < len(m.filePicker.Files) {
+ m.filePicker.Selected = idx
+ entry := m.filePicker.Files[idx]
+ if entry.IsDir() {
+ m.filePicker.CurDir = filepath.Join(m.filePicker.CurDir, entry.Name())
+ m.filePicker.Selected = 0
+ m.refreshFilePicker()
+ } else {
+ fullPath := filepath.Join(m.filePicker.CurDir, entry.Name())
+ m.ti.SetValue(fullPath)
+ m.filePicker = nil
+ return m.doFileAction(fullPath)
+ }
+ }
+ }
+ return m, nil
+}
+
+func (m *AppModel) saveMap() {
+ if m.rootMap.Filename == "" {
+ m.mode = model.ModeDialog
+ m.dialog = model.DialogFileSave
+ m.openFilePicker()
+ m.ti.Focus()
+ return
+ }
+ if err := mapio.SerializeMap(m.rootMap, m.rootMap.Filename); err != nil {
+ m.dialogMsg = fmt.Sprintf("Save error: %v", err)
+ } else {
+ m.dialogMsg = fmt.Sprintf("Saved %s", m.rootMap.Filename)
+ m.dirty = false
+ m.undoPosAtSave = m.undo.Pos()
+ }
+}
+
+func (m *AppModel) openFilePicker() {
+ cur := "."
+ m.filePicker = &FilePickerState{CurDir: cur, Selected: 0}
+ m.refreshFilePicker()
+ m.ti.SetValue("")
+}
+
+func (m *AppModel) refreshFilePicker() {
+ entries, err := os.ReadDir(m.filePicker.CurDir)
+ if err != nil {
+ m.filePicker.Files = nil
+ return
+ }
+ sort.Slice(entries, func(i, j int) bool {
+ if entries[i].IsDir() != entries[j].IsDir() {
+ return entries[i].IsDir()
+ }
+ return entries[i].Name() < entries[j].Name()
+ })
+ m.filePicker.Files = entries
+ if m.filePicker.Selected >= len(entries) {
+ m.filePicker.Selected = len(entries) - 1
+ }
+ if m.filePicker.Selected < 0 {
+ m.filePicker.Selected = 0
+ }
+}
+
+func (m *AppModel) doFilePickerSelect() {
+ if m.filePicker == nil || len(m.filePicker.Files) == 0 {
+ return
+ }
+ entry := m.filePicker.Files[m.filePicker.Selected]
+ if entry.IsDir() {
+ m.filePicker.CurDir = filepath.Join(m.filePicker.CurDir, entry.Name())
+ m.filePicker.Selected = 0
+ m.refreshFilePicker()
+ return
+ }
+ m.ti.SetValue(filepath.Join(m.filePicker.CurDir, entry.Name()))
+}
+
+func (m *AppModel) drillDown() {
+ m.cancelPreview()
+ sub, ok := m.curMap().Submaps[m.cursor]
+ if !ok {
+ sub = model.NewMap(fmt.Sprintf("%s/sub", m.curMap().Name),
+ m.cfg.DefaultMapWidth, m.cfg.DefaultMapHeight,
+ m.curMap().Palette)
+ sub.Parent = m.curMap()
+ sub.Filename = m.rootMap.Filename
+ m.curMap().Submaps[m.cursor] = sub
+ }
+ m.prevCursor = m.cursor
+ m.map_ = sub
+ m.cursor = model.Point{X: 0, Y: 0}
+ m.offset = model.Point{X: 0, Y: 0}
+}
+
+func (m *AppModel) drillUp() {
+ if m.curMap().Parent == nil {
+ return
+ }
+ if m.isMapBlank(m.curMap()) {
+ delete(m.curMap().Parent.Submaps, m.prevCursor)
+ }
+ parent := m.curMap().Parent
+ m.map_ = parent
+ if m.prevCursor.X >= 0 {
+ m.cursor = m.prevCursor
+ } else {
+ m.cursor = model.Point{X: -1, Y: -1}
+ }
+ m.offset = model.Point{X: 0, Y: 0}
+ m.prevCursor = model.Point{X: -1, Y: -1}
+}
+
+func (m *AppModel) isMapBlank(mm *model.Map) bool {
+ for y := range mm.Grid {
+ for x := range mm.Grid[y] {
+ if mm.Grid[y][x].Terrain >= 0 || mm.Grid[y][x].Text != "" {
+ return false
+ }
+ }
+ }
+ return len(mm.Submaps) == 0
+}
+
+func (m *AppModel) handleToolbarClick(x int) {
+ widths := []int{16, 10, 21, 5, 5, 5, 6, 6, 6}
+ pos := 0
+ for i, w := range widths {
+ if x >= pos && x < pos+w {
+ switch i {
+ case 0:
+ m.mode = model.ModeDialog
+ m.dialog = model.DialogRenameMap
+ m.ti.SetValue(m.curMap().Name)
+ m.ti.Focus()
+ case 1:
+ m.mode = model.ModeDialog
+ m.dialog = model.DialogResize
+ m.ti.SetValue(fmt.Sprintf("%dx%d", m.curMap().Width, m.curMap().Height))
+ m.ti.Focus()
+ case 2:
+ m.mode = model.ModeDialog
+ m.dialog = model.DialogFileSave
+ m.openFilePicker()
+ m.ti.SetValue(m.rootMap.Filename)
+ m.ti.Focus()
+ case 3:
+ m.unicode = !m.unicode
+ case 4:
+ m.colorMode = !m.colorMode
+ case 5:
+ m.fillShapes = !m.fillShapes
+ case 6:
+ m.saveMap()
+ case 7:
+ m.mode = model.ModeDialog
+ m.dialog = model.DialogFileOpen
+ m.openFilePicker()
+ m.ti.Focus()
+ case 8:
+ if m.dirty {
+ m.mode = model.ModeDialog
+ m.dialog = model.DialogQuitConfirm
+ } else {
+ m.quitting = true
+ }
+ }
+ return
+ }
+ pos += w
+ }
+}
+
+func (m *AppModel) handleSidebarClick(x, y int) {
+ relY := y - 1
+
+ if relY >= 1 && relY <= 10 {
+ idx := m.palettePage*10 + (relY - 1)
+ if idx < len(m.curPalette()) {
+ m.selected = idx
+ }
+ return
+ }
+
+ if relY == 11 {
+ if x < m.width-sidebarW+7 {
+ m.palettePagePrev()
+ } else {
+ m.palettePageNext()
+ }
+ return
+ }
+
+ if relY == 12 {
+ if x < m.width-sidebarW+8 {
+ m.addSymbol()
+ } else {
+ m.removeSymbol()
+ }
+ return
+ }
+ if relY == 13 {
+ if x < m.width-sidebarW+8 {
+ m.moveSymbolUp()
+ } else {
+ m.moveSymbolDown()
+ }
+ return
+ }
+ if relY == 14 {
+ if x < m.width-sidebarW+10 {
+ m.mode = model.ModeDialog
+ m.dialog = model.DialogRenameSymbol
+ m.ti.SetValue(m.curPalette()[model.Clamp(m.selected, 0, len(m.curPalette())-1)].Name)
+ m.ti.Focus()
+ } else {
+ m.openColorPicker(false)
+ }
+ return
+ }
+
+ toolRow := relY - 16
+ if toolRow >= 0 && toolRow < 8 {
+ newTool := model.Tool(toolRow)
+ if m.tool != newTool && m.mode == model.ModeTextEdit {
+ m.mode = model.ModeNormal
+ m.movingLabel = -1
+ m.textEditing = false
+ m.textInput.Blur()
+ }
+ m.tool = newTool
+ }
+ bwRow := relY - 25
+ if bwRow >= 0 && bwRow < 3 {
+ m.brushWidth = []int{1, 3, 5}[bwRow]
+ }
+}
+
+func (m *AppModel) addSymbol() {
+ p := m.curPalette()
+ if len(p) >= 100 {
+ return
+ }
+ m.dirty = true
+ m.undo.Push(m.curMap())
+ m.curMap().Palette = append(p, model.Terrain{Name: "new", Symbol: "?", ASCII: "?", Colors: []model.TerrainColor{{Color: "255", Weight: 100}}})
+}
+
+func (m *AppModel) removeSymbol() {
+ if len(m.curPalette()) <= 1 {
+ return
+ }
+ m.dirty = true
+ m.undo.Push(m.curMap())
+ idx := model.Clamp(m.selected, 0, len(m.curPalette())-1)
+ for y := range m.curMap().Grid {
+ for x := range m.curMap().Grid[y] {
+ c := &m.curMap().Grid[y][x]
+ if c.Terrain == idx {
+ c.Terrain = -1
+ } else if c.Terrain > idx {
+ c.Terrain--
+ }
+ }
+ }
+ m.curMap().Palette = append(m.curMap().Palette[:idx], m.curMap().Palette[idx+1:]...)
+ if m.selected >= len(m.curPalette()) {
+ m.selected = len(m.curPalette()) - 1
+ }
+}
+
+func (m *AppModel) moveSymbolUp() {
+ if m.selected <= 0 || m.selected >= len(m.curPalette()) {
+ return
+ }
+ m.dirty = true
+ m.undo.Push(m.curMap())
+ i := m.selected
+ m.curPalette()[i], m.curPalette()[i-1] = m.curPalette()[i-1], m.curPalette()[i]
+ for y := range m.curMap().Grid {
+ for x := range m.curMap().Grid[y] {
+ c := &m.curMap().Grid[y][x]
+ if c.Terrain == i {
+ c.Terrain = i - 1
+ } else if c.Terrain == i-1 {
+ c.Terrain = i
+ }
+ }
+ }
+ m.selected = i - 1
+}
+
+func (m *AppModel) moveSymbolDown() {
+ if m.selected < 0 || m.selected >= len(m.curPalette())-1 {
+ return
+ }
+ m.dirty = true
+ m.undo.Push(m.curMap())
+ i := m.selected
+ m.curPalette()[i], m.curPalette()[i+1] = m.curPalette()[i+1], m.curPalette()[i]
+ for y := range m.curMap().Grid {
+ for x := range m.curMap().Grid[y] {
+ c := &m.curMap().Grid[y][x]
+ if c.Terrain == i {
+ c.Terrain = i + 1
+ } else if c.Terrain == i+1 {
+ c.Terrain = i
+ }
+ }
+ }
+ m.selected = i + 1
+}
+
+func (m *AppModel) palettePagePrev() {
+ totalPages := (len(m.curPalette()) + 9) / 10
+ if totalPages <= 1 {
+ return
+ }
+ m.palettePage = (m.palettePage - 1 + totalPages) % totalPages
+}
+
+func (m *AppModel) palettePageNext() {
+ totalPages := (len(m.curPalette()) + 9) / 10
+ if totalPages <= 1 {
+ return
+ }
+ m.palettePage = (m.palettePage + 1) % totalPages
+}
+
+func (m *AppModel) resizeMap(w, h int) {
+ m.dirty = true
+ m.undo.Push(m.curMap())
+ old := m.curMap()
+ newGrid := make([][]model.Cell, h)
+ for y := range newGrid {
+ newGrid[y] = make([]model.Cell, w)
+ for x := range newGrid[y] {
+ if y < old.Height && x < old.Width {
+ newGrid[y][x] = old.Grid[y][x]
+ } else {
+ newGrid[y][x].Terrain = -1
+ }
+ }
+ }
+ old.Grid = newGrid
+ old.Width = w
+ old.Height = h
+ m.cursor.X = model.Clamp(m.cursor.X, 0, w-1)
+ m.cursor.Y = model.Clamp(m.cursor.Y, 0, h-1)
+}