aboutsummaryrefslogtreecommitdiff
path: root/internal/tui/handlers.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/handlers.go
parentb1e252c996a6332d391f96624a7d6f149eb097c4 (diff)
downloadtui-ascii-mapper-main.tar.gz
restructured projectHEADmain
Diffstat (limited to 'internal/tui/handlers.go')
-rw-r--r--internal/tui/handlers.go501
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()
+}