diff options
Diffstat (limited to 'internal/tui/dialogs.go')
| -rw-r--r-- | internal/tui/dialogs.go | 526 |
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) +} |
