diff options
| author | historia <[not public]> | 2026-06-29 04:03:38 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-06-29 04:03:38 -0400 |
| commit | ba34490aaed5f7c242c2b0453130fefc07d0d5d0 (patch) | |
| tree | db5818757d5080221494cead1c7e61a576d1cb65 /internal/tui/handlers.go | |
| parent | b1e252c996a6332d391f96624a7d6f149eb097c4 (diff) | |
| download | tui-ascii-mapper-ba34490aaed5f7c242c2b0453130fefc07d0d5d0.tar.gz | |
Diffstat (limited to 'internal/tui/handlers.go')
| -rw-r--r-- | internal/tui/handlers.go | 501 |
1 files changed, 501 insertions, 0 deletions
diff --git a/internal/tui/handlers.go b/internal/tui/handlers.go new file mode 100644 index 0000000..6f78922 --- /dev/null +++ b/internal/tui/handlers.go @@ -0,0 +1,501 @@ +package tui + +import ( + "fmt" + + "tui-ascii-mapper/internal/model" + "tui-ascii-mapper/internal/tools" + + tea "github.com/charmbracelet/bubbletea" +) + +func (m *AppModel) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { + m.dialogMsg = "" + if m.mode == model.ModeDialog { + return m.handleDialogKey(msg) + } + if m.colorPicker != nil && m.colorPicker.Active { + return m.handleColorPickerKey(msg) + } + if m.mode == model.ModeTextEdit { + return m.handleTextEditKey(msg) + } + + key := msg.String() + cfg := m.cfg.Keybindings + + switch key { + case cfg.Quit: + if m.curMap().Parent != nil { + m.drillUp() + } else if m.dirty { + m.mode = model.ModeDialog + m.dialog = model.DialogQuitConfirm + } else { + m.quitting = true + return m, tea.Quit + } + return m, nil + case cfg.Save: + m.saveMap() + return m, nil + case cfg.Undo: + if entry := m.undo.Undo(); entry != nil { + *entry.Target = *entry.State + m.dirty = m.undo.Pos() != m.undoPosAtSave + } + return m, nil + case cfg.Redo: + if entry := m.undo.Redo(); entry != nil { + *entry.Target = *entry.State + m.dirty = m.undo.Pos() != m.undoPosAtSave + } + return m, nil + case cfg.UnicodeToggle: + m.unicode = !m.unicode + return m, nil + case cfg.ColorToggle: + m.colorMode = !m.colorMode + return m, nil + case cfg.FillToggle, "f": + m.fillShapes = !m.fillShapes + return m, nil + case cfg.Resize: + m.mode = model.ModeDialog + m.dialog = model.DialogResize + m.ti.SetValue(fmt.Sprintf("%dx%d", m.curMap().Width, m.curMap().Height)) + m.ti.Focus() + return m, nil + case "!": + m.tool = model.ToolBrush + m.drawHeld, m.eraseHeld = false, false + case "@": + m.tool = model.ToolSelect + m.drawHeld, m.eraseHeld = false, false + case "#": + m.tool = model.ToolErase + m.drawHeld, m.eraseHeld = false, false + case "$": + m.tool = model.ToolFill + m.drawHeld, m.eraseHeld = false, false + case "%": + m.tool = model.ToolLine + m.drawHeld, m.eraseHeld = false, false + case "^": + m.tool = model.ToolRect + m.drawHeld, m.eraseHeld = false, false + case "&": + m.tool = model.ToolCircle + m.drawHeld, m.eraseHeld = false, false + case "*": + m.tool = model.ToolText + m.drawHeld, m.eraseHeld = false, false + case "(": + m.tool = model.ToolText + m.drawHeld, m.eraseHeld = false, false + + case "ctrl+1": + m.brushWidth = 1 + case "ctrl+2": + m.brushWidth = 3 + case "ctrl+3": + m.brushWidth = 5 + case "[": + if m.brushWidth > 1 { + m.brushWidth -= 2 + } + case "]": + if m.brushWidth < 5 { + m.brushWidth += 2 + } + case "enter": + if m.tool == model.ToolText { + return m.handleSpace() + } + if m.movingLabel >= 0 { + m.placeMovingLabel() + return m, nil + } + if m.mode == model.ModeLinePreview { + m.finalizeLinePreview() + } else if m.mode == model.ModeRectPreview { + m.finalizeRectPreview() + } else if m.mode == model.ModeCirclePreview { + m.finalizeCirclePreview() + } else if "enter" == cfg.DrillDown { + m.drillDown() + } + return m, nil + case cfg.DrillDown: + m.drillDown() + return m, nil + case cfg.DrillUp: + m.drillUp() + return m, nil + case cfg.DeleteSubmap: + if _, ok := m.curMap().Submaps[m.cursor]; ok { + m.mode = model.ModeDialog + m.dialog = model.DialogDeleteSubmapConfirm + } + return m, nil + case " ", "space": + return m.handleSpace() + case "backspace", "x": + return m.handleBackspace() + case "e": + if m.tool == model.ToolText { + return m.editTextAtCursor() + } + case "c": + if m.tool == model.ToolText { + idx := tools.FindTextLabelAt(m.curMap(), m.cursor) + if idx >= 0 { + m.openColorPicker(true) + return m, nil + } + } + case "esc": + if m.movingLabel >= 0 { + m.movingLabel = -1 + m.dragLabelOrigin = model.Point{X: 0, Y: 0} + m.dragMouseOrigin = model.Point{X: 0, Y: 0} + return m, nil + } + m.cancelPreview() + return m, nil + case "up", "down", "left", "right", "h", "j", "k", "l": + switch key { + case "up", "k": + m.moveCursor(0, -1) + case "down", "j": + m.moveCursor(0, 1) + case "left", "h": + m.moveCursor(-1, 0) + case "right", "l": + m.moveCursor(1, 0) + } + if m.movingLabel < 0 && m.drawHeld { + m.dirty = true + m.undo.Push(m.curMap()) + m.applyBrush(m.cursor) + } else if m.movingLabel < 0 && m.eraseHeld { + m.dirty = true + m.undo.Push(m.curMap()) + m.applyBrush(m.cursor, true) + } + return m, nil + default: + if len(msg.Runes) == 1 { + r := msg.Runes[0] + switch r { + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + hk := (int(r-'0') + 9) % 10 + var indices []int + for i := range m.curPalette() { + if i%10 == hk { + indices = append(indices, i) + } + } + if len(indices) == 0 { + return m, nil + } + curOff := m.hotkeySelect[hk] + if m.selected%10 == hk && model.ContainsInt(indices, m.selected) { + curOff = (curOff + 1) % len(indices) + } else { + curOff = 0 + } + m.hotkeySelect[hk] = curOff + m.selected = indices[curOff] + m.palettePage = m.selected / 10 + case '=': + m.palettePageNext() + case '-': + m.palettePagePrev() + case '<', ',': + m.moveSymbolUp() + case '>', '.': + m.moveSymbolDown() + } + } + } + return m, nil +} + +func (m *AppModel) handleSpace() (tea.Model, tea.Cmd) { + m.drawHeld = false + m.eraseHeld = false + + if m.mode == model.ModeLinePreview { + m.finalizeLinePreview() + return m, nil + } + if m.mode == model.ModeRectPreview { + m.finalizeRectPreview() + return m, nil + } + if m.mode == model.ModeCirclePreview { + m.finalizeCirclePreview() + return m, nil + } + + if m.tool == model.ToolSelect { + return m, nil + } + + if m.tool == model.ToolLine && m.mode == model.ModeNormal { + m.lineStart = m.cursor + m.mode = model.ModeLinePreview + m.linePreview = nil + return m, nil + } + if m.tool == model.ToolRect && m.mode == model.ModeNormal { + m.rectStart = m.cursor + m.mode = model.ModeRectPreview + m.rectPreview = nil + return m, nil + } + if m.tool == model.ToolCircle && m.mode == model.ModeNormal { + m.circleCenter = m.cursor + m.mode = model.ModeCirclePreview + m.circlePreview = nil + return m, nil + } + if m.tool == model.ToolText { + if m.movingLabel >= 0 { + if m.movingLabel < len(m.curMap().TextLabels) { + old := m.curMap().TextLabels[m.movingLabel].Start + newPos := m.labelDragPos() + if old != newPos && m.curMap().InBounds(newPos) { + m.dirty = true + m.undo.Push(m.curMap()) + tools.MoveTextLabel(m.curMap(), old, newPos) + } + } + m.movingLabel = -1 + return m, nil + } + idx := tools.FindTextLabelAt(m.curMap(), m.cursor) + if idx >= 0 { + m.movingLabel = idx + tl := m.curMap().TextLabels[idx] + m.dragLabelOrigin = tl.Start + m.dragMouseOrigin = m.cursor + } else { + m.startTextEdit(m.cursor) + } + return m, nil + } + if m.tool == model.ToolErase { + m.dirty = true + m.undo.Push(m.curMap()) + m.applyBrush(m.cursor, true) + return m, nil + } + if m.tool == model.ToolFill { + m.dirty = true + m.undo.Push(m.curMap()) + m.fillAt(m.cursor) + return m, nil + } + m.dirty = true + m.undo.Push(m.curMap()) + m.applyBrush(m.cursor) + return m, nil +} + +func (m *AppModel) handleBackspace() (tea.Model, tea.Cmd) { + m.drawHeld = false + m.eraseHeld = false + if m.mode == model.ModeLinePreview || m.mode == model.ModeRectPreview || m.mode == model.ModeCirclePreview { + m.cancelPreview() + return m, nil + } + if m.tool == model.ToolText { + idx := tools.FindTextLabelAt(m.curMap(), m.cursor) + if idx >= 0 { + m.dirty = true + m.undo.Push(m.curMap()) + tools.RemoveTextLabel(m.curMap(), m.curMap().TextLabels[idx].Start) + } + return m, nil + } + m.dirty = true + m.undo.Push(m.curMap()) + m.applyBrush(m.cursor, true) + return m, nil +} + +func (m *AppModel) finalizeLinePreview() { + m.dirty = true + m.undo.Push(m.curMap()) + tools.ApplyPoints(m.curMap(), m.linePreview, m.selected, m.curPalette()) + m.linePreview = nil + m.mode = model.ModeNormal +} + +func (m *AppModel) finalizeRectPreview() { + m.dirty = true + m.undo.Push(m.curMap()) + tools.ApplyPoints(m.curMap(), m.rectPreview, m.selected, m.curPalette()) + m.rectPreview = nil + m.mode = model.ModeNormal +} + +func (m *AppModel) finalizeCirclePreview() { + m.dirty = true + m.undo.Push(m.curMap()) + tools.ApplyPoints(m.curMap(), m.circlePreview, m.selected, m.curPalette()) + m.circlePreview = nil + m.mode = model.ModeNormal +} + +func (m *AppModel) cancelPreview() { + m.linePreview = nil + m.rectPreview = nil + m.circlePreview = nil + m.mode = model.ModeNormal +} + +func (m *AppModel) moveCursor(dx, dy int) { + m.cursor.X = model.Clamp(m.cursor.X+dx, 0, m.curMap().Width-1) + m.cursor.Y = model.Clamp(m.cursor.Y+dy, 0, m.curMap().Height-1) + if m.mode == model.ModeLinePreview { + m.linePreview = tools.ThickenPoints(tools.BresenhamLine(m.lineStart, m.cursor), m.brushWidth) + } else if m.mode == model.ModeRectPreview { + m.rectPreview = tools.ThickenPoints(tools.DrawRect(m.rectStart, m.cursor, m.fillShapes), m.brushWidth) + } else if m.mode == model.ModeCirclePreview { + m.circlePreview = tools.ThickenPoints(tools.DrawCircle(m.circleCenter, m.cursor, m.fillShapes), m.brushWidth) + } +} + +func (m *AppModel) applyBrush(center model.Point, erase ...bool) { + if !m.curMap().InBounds(center) { + return + } + terrain := m.selected + if len(erase) > 0 && erase[0] { + terrain = -1 + } + palette := m.curPalette() + switch m.tool { + case model.ToolBrush: + tools.Brush(m.curMap(), center, terrain, m.brushWidth, palette) + case model.ToolErase: + tools.Brush(m.curMap(), center, terrain, m.brushWidth, palette) + default: + color := "" + if terrain >= 0 && terrain < len(palette) { + color = palette[terrain].PickColor() + } + m.curMap().SetCell(center, terrain, color) + } +} + +func (m *AppModel) fillAt(p model.Point) { + if m.selected >= 0 && m.selected < len(m.curPalette()) { + tools.FloodFill(m.curMap(), p, m.selected, m.curPalette()) + } +} + +func (m *AppModel) clampCursor() { + m.cursor.X = model.Clamp(m.cursor.X, 0, m.curMap().Width-1) + m.cursor.Y = model.Clamp(m.cursor.Y, 0, m.curMap().Height-1) +} + +func (m *AppModel) startTextEdit(p model.Point) { + m.mode = model.ModeTextEdit + m.textInput.SetValue("") + m.textInput.Focus() + m.cursor = p + m.textCursorStart = p + m.movingLabel = -1 +} + +func (m *AppModel) startTextEditText(idx int) { + if idx < 0 || idx >= len(m.curMap().TextLabels) { + return + } + m.mode = model.ModeTextEdit + m.textInput.SetValue(m.curMap().TextLabels[idx].Text) + m.textInput.Focus() + m.textCursorStart = m.curMap().TextLabels[idx].Start + m.movingLabel = idx + m.textEditing = true +} + +func (m *AppModel) editTextAtCursor() (tea.Model, tea.Cmd) { + idx := tools.FindTextLabelAt(m.curMap(), m.cursor) + if idx < 0 { + return m, nil + } + m.startTextEditText(idx) + return m, nil +} + +func (m *AppModel) placeMovingLabel() { + if m.movingLabel >= 0 && m.movingLabel < len(m.curMap().TextLabels) { + old := m.curMap().TextLabels[m.movingLabel].Start + newPos := m.labelDragPos() + if old != newPos && m.curMap().InBounds(newPos) { + m.dirty = true + m.undo.Push(m.curMap()) + tools.MoveTextLabel(m.curMap(), old, newPos) + } + } + m.movingLabel = -1 + m.dragLabelOrigin = model.Point{X: 0, Y: 0} + m.dragMouseOrigin = model.Point{X: 0, Y: 0} +} + +func (m *AppModel) labelDragPos() model.Point { + return model.Point{ + X: m.dragLabelOrigin.X + (m.cursor.X - m.dragMouseOrigin.X), + Y: m.dragLabelOrigin.Y + (m.cursor.Y - m.dragMouseOrigin.Y), + } +} + +func (m *AppModel) handleTextEditKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { + key := msg.String() + + switch key { + case "esc": + m.mode = model.ModeNormal + m.movingLabel = -1 + m.textEditing = false + m.textInput.Blur() + return m, nil + case "backspace": + val := m.textInput.Value() + runes := []rune(val) + if len(runes) > 0 { + m.textInput.SetValue(string(runes[:len(runes)-1])) + } + return m, nil + case "enter": + m.commitTextEdit() + return m, nil + } + var cmd tea.Cmd + m.textInput, cmd = m.textInput.Update(msg) + return m, cmd +} + +func (m *AppModel) commitTextEdit() { + text := m.textInput.Value() + if text != "" { + m.dirty = true + m.undo.Push(m.curMap()) + start := m.cursor + if m.textEditing && m.movingLabel >= 0 && m.movingLabel < len(m.curMap().TextLabels) { + tools.RemoveTextLabel(m.curMap(), m.curMap().TextLabels[m.movingLabel].Start) + start = m.textCursorStart + m.textEditing = false + } + tools.PlaceTextLabel(m.curMap(), start, text, m.textColor) + } + m.mode = model.ModeNormal + m.movingLabel = -1 + m.textEditing = false + m.textInput.Blur() +} |
