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) } } }