aboutsummaryrefslogtreecommitdiff
path: root/internal/tui/update.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/update.go
parentb1e252c996a6332d391f96624a7d6f149eb097c4 (diff)
downloadtui-ascii-mapper-main.tar.gz
restructured projectHEADmain
Diffstat (limited to 'internal/tui/update.go')
-rw-r--r--internal/tui/update.go296
1 files changed, 296 insertions, 0 deletions
diff --git a/internal/tui/update.go b/internal/tui/update.go
new file mode 100644
index 0000000..87e0917
--- /dev/null
+++ b/internal/tui/update.go
@@ -0,0 +1,296 @@
+package tui
+
+import (
+ "fmt"
+
+ "tui-ascii-mapper/internal/model"
+ "tui-ascii-mapper/internal/tools"
+
+ tea "github.com/charmbracelet/bubbletea"
+)
+
+func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ switch msg := msg.(type) {
+ case tea.WindowSizeMsg:
+ m.width = msg.Width
+ m.height = msg.Height
+ return m, nil
+
+ case mapLoadedMsg:
+ if msg.err != nil {
+ m.dialogMsg = fmt.Sprintf("Load error: %v", msg.err)
+ m.mode = model.ModeNormal
+ m.dialog = model.DialogNone
+ return m, nil
+ }
+ m.map_ = msg.data
+ m.rootMap = msg.data
+ m.undo = &model.UndoStack{}
+ m.undo.Push(m.map_)
+ m.cursor = model.Point{X: 0, Y: 0}
+ m.offset = model.Point{X: 0, Y: 0}
+ m.dialogMsg = fmt.Sprintf("Loaded %s", msg.data.Filename)
+ m.mode = model.ModeNormal
+ m.dialog = model.DialogNone
+ m.dirty = false
+ m.undoPosAtSave = m.undo.Pos()
+ return m, nil
+
+ case tea.MouseMsg:
+ return m.handleMouse(msg)
+
+ case tea.KeyMsg:
+ return m.handleKey(msg)
+ }
+
+ if m.mode == model.ModeDialog || m.mode == model.ModeTextEdit {
+ var cmd tea.Cmd
+ m.ti, cmd = m.ti.Update(msg)
+ if cmd != nil {
+ return m, cmd
+ }
+ m.textInput, cmd = m.textInput.Update(msg)
+ if cmd != nil {
+ return m, cmd
+ }
+ }
+ return m, nil
+}
+
+func (m *AppModel) handleMouse(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
+ x, y := msg.X, msg.Y
+
+ if m.colorPicker != nil && m.colorPicker.Active {
+ return m.handleColorPickerMouse(msg)
+ }
+
+ isFilePicker := m.dialog == model.DialogFileSave || m.dialog == model.DialogFileOpen || m.dialog == model.DialogSaveAs || m.dialog == model.DialogOpenMap
+ if isFilePicker && m.filePicker != nil && m.filePicker.PopupY > 0 {
+ return m.handleFilePickerMouse(msg)
+ }
+
+ isPress := msg.Button == tea.MouseButtonLeft && msg.Action == tea.MouseActionPress
+ isRelease := msg.Action == tea.MouseActionRelease
+ isRightPress := msg.Button == tea.MouseButtonRight && msg.Action == tea.MouseActionPress
+
+ gridAreaW := m.width - sidebarW
+ gridH := m.height - 1
+ if m.height > 6 {
+ gridH -= 2
+ }
+ if gridH < 1 {
+ gridH = 1
+ }
+ if gridAreaW < 1 {
+ gridAreaW = 1
+ }
+
+ if isPress && y == 0 {
+ m.handleToolbarClick(x)
+ if m.quitting {
+ return m, tea.Quit
+ }
+ return m, nil
+ }
+
+ gridY := y - 1
+ onSidebar := gridAreaW > 0 && x >= gridAreaW
+
+ if isPress && onSidebar && gridY >= 0 {
+ m.handleSidebarClick(x, y)
+ return m, nil
+ }
+
+ gx := x + m.offset.X
+ gy := gridY + m.offset.Y
+ inGrid := gridY >= 0 && gridY < gridH && x < gridAreaW
+
+ if isPress {
+ m.mouseDown = true
+ m.lastPaint = model.Point{X: -1, Y: -1}
+ if inGrid {
+ p := model.Point{X: gx, Y: gy}
+ switch m.tool {
+ case model.ToolLine:
+ m.mouseStart = p
+ m.mode = model.ModeLinePreview
+ m.linePreview = nil
+ case model.ToolRect:
+ m.mouseStart = p
+ m.mode = model.ModeRectPreview
+ m.rectPreview = nil
+ case model.ToolCircle:
+ m.mouseStart = p
+ m.mode = model.ModeCirclePreview
+ m.circlePreview = nil
+ case model.ToolSelect:
+ m.cursor = p
+ m.clampCursor()
+ default:
+ return m.mouseDraw(p.X, p.Y)
+ }
+ }
+ return m, nil
+ }
+
+ if isRelease {
+ if m.mouseDown {
+ m.mouseRelease(gx, gy)
+ }
+ m.mouseDown = false
+ m.lastPaint = model.Point{X: -1, Y: -1}
+ return m, nil
+ }
+
+ if isRightPress {
+ if inGrid {
+ if m.tool == model.ToolText {
+ idx := tools.FindTextLabelAt(m.curMap(), model.Point{X: gx, Y: gy})
+ if idx >= 0 {
+ m.dirty = true
+ m.undo.Push(m.curMap())
+ tools.RemoveTextLabel(m.curMap(), m.curMap().TextLabels[idx].Start)
+ m.movingLabel = -1
+ }
+ } else {
+ m.dirty = true
+ m.undo.Push(m.curMap())
+ m.applyBrush(model.Point{X: gx, Y: gy}, true)
+ m.lastPaint = model.Point{X: gx, Y: gy}
+ }
+ }
+ return m, nil
+ }
+
+ if m.mouseDown && inGrid && (gx != m.lastPaint.X || gy != m.lastPaint.Y) {
+ if m.movingLabel >= 0 && m.tool == model.ToolText {
+ m.cursor = model.Point{X: gx, Y: gy}
+ m.clampCursor()
+ } else if m.tool == model.ToolLine {
+ m.linePreview = tools.ThickenPoints(tools.BresenhamLine(m.mouseStart, model.Point{X: gx, Y: gy}), m.brushWidth)
+ m.lastPaint = model.Point{X: gx, Y: gy}
+ } else if m.tool == model.ToolRect {
+ m.rectPreview = tools.ThickenPoints(tools.DrawRect(m.mouseStart, model.Point{X: gx, Y: gy}, m.fillShapes), m.brushWidth)
+ m.lastPaint = model.Point{X: gx, Y: gy}
+ } else if m.tool == model.ToolCircle {
+ m.circlePreview = tools.ThickenPoints(tools.DrawCircle(m.mouseStart, model.Point{X: gx, Y: gy}, m.fillShapes), m.brushWidth)
+ m.lastPaint = model.Point{X: gx, Y: gy}
+ } else if m.tool != model.ToolText {
+ m.mouseDraw(gx, gy)
+ }
+ }
+
+ switch msg.Button {
+ case tea.MouseButtonWheelUp:
+ if m.offset.Y > 0 {
+ m.offset.Y--
+ }
+ case tea.MouseButtonWheelDown:
+ maxY := m.curMap().Height - gridH
+ if maxY < 0 {
+ maxY = 0
+ }
+ if m.offset.Y < maxY {
+ m.offset.Y++
+ }
+ }
+
+ return m, nil
+}
+
+func (m *AppModel) mouseDraw(gx, gy int) (tea.Model, tea.Cmd) {
+ p := model.Point{X: gx, Y: gy}
+ switch m.tool {
+ case model.ToolBrush:
+ m.dirty = true
+ m.undo.Push(m.curMap())
+ m.applyBrush(p)
+ m.lastPaint = p
+ case model.ToolErase:
+ m.dirty = true
+ m.undo.Push(m.curMap())
+ m.applyBrush(p, true)
+ m.lastPaint = p
+ case model.ToolFill:
+ m.dirty = true
+ m.undo.Push(m.curMap())
+ m.fillAt(p)
+ m.lastPaint = p
+ case model.ToolText:
+ idx := tools.FindTextLabelAt(m.curMap(), p)
+ if idx >= 0 {
+ m.movingLabel = idx
+ tl := m.curMap().TextLabels[idx]
+ m.dragLabelOrigin = tl.Start
+ m.dragMouseOrigin = p
+ m.lastPaint = p
+ }
+ }
+ m.cursor = p
+ m.clampCursor()
+ return m, nil
+}
+
+func (m *AppModel) mouseRelease(gx, gy int) {
+ p := model.Point{X: gx, Y: gy}
+ switch m.tool {
+ case model.ToolLine:
+ m.dirty = true
+ m.undo.Push(m.curMap())
+ m.finishShape(m.mouseStart, p, model.ToolLine)
+ m.linePreview = nil
+ m.mode = model.ModeNormal
+ case model.ToolRect:
+ m.dirty = true
+ m.undo.Push(m.curMap())
+ m.finishShape(m.mouseStart, p, model.ToolRect)
+ m.rectPreview = nil
+ m.mode = model.ModeNormal
+ case model.ToolCircle:
+ m.dirty = true
+ m.undo.Push(m.curMap())
+ m.finishShape(m.mouseStart, p, model.ToolCircle)
+ m.circlePreview = nil
+ m.mode = model.ModeNormal
+ case model.ToolText:
+ 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
+ } else {
+ m.startTextEditText(m.movingLabel)
+ }
+ } else if m.cursor == p && m.curMap().InBounds(p) {
+ m.startTextEdit(p)
+ }
+ m.dragLabelOrigin = model.Point{X: 0, Y: 0}
+ m.dragMouseOrigin = model.Point{X: 0, Y: 0}
+ }
+}
+
+func (m *AppModel) finishShape(a, b model.Point, tool model.Tool) {
+ palette := m.curPalette()
+ terrain := m.selected
+ if terrain < 0 || terrain >= len(palette) {
+ return
+ }
+ var pts []model.Point
+ switch tool {
+ case model.ToolLine:
+ pts = tools.BresenhamLine(a, b)
+ case model.ToolRect:
+ pts = tools.DrawRect(a, b, m.fillShapes)
+ case model.ToolCircle:
+ pts = tools.DrawCircle(a, b, m.fillShapes)
+ }
+ tools.ApplyPoints(m.curMap(), pts, terrain, palette)
+ if m.brushWidth > 1 {
+ for _, pt := range pts {
+ tools.Brush(m.curMap(), pt, terrain, m.brushWidth, palette)
+ }
+ }
+}