clone

fifteen puzzle games for the terminal, with daily puzzles, weekly ladders and xp, built on bubble tea

main
puzzletea / games / wordsearch / mouse.go
mouse.go 3.2K
1package wordsearch23import (4 "strings"56 "charm.land/lipgloss/v2"7 "github.com/FelineStateMachine/puzzletea/game"8)910const cellWidth = 31112// screenToGrid converts terminal screen coordinates to grid cell coordinates.13// Returns (col, row, ok) where ok is false if the click landed outside the grid.14func (m *Model) screenToGrid(screenX, screenY int) (col, row int, ok bool) {15 ox, oy := m.cachedGridOrigin()16 lx := screenX - ox17 ly := screenY - oy18 if lx < 0 || ly < 0 {19 return 0, 0, false20 }2122 // No separators: simple division by cell dimensions.23 col = lx / cellWidth24 row = ly // cellHeight is 12526 if col < 0 || col >= m.width || row < 0 || row >= m.height {27 return 0, 0, false28 }2930 return col, row, true31}3233// cachedGridOrigin returns the screen position of the top-left corner of34// cell (0,0), using a cached value when available. The cache is invalidated35// on terminal resize and solve-state changes.36func (m *Model) cachedGridOrigin() (x, y int) {37 if m.originValid {38 return m.originX, m.originY39 }40 x, y = m.gridOrigin()41 m.originX, m.originY = x, y42 m.originValid = true43 return x, y44}4546// gridOrigin computes the screen position of the top-left corner of the47// first grid cell.48//49// The root view does:50//51// lipgloss.Place(termWidth, termHeight, Center, Center, gameView)52//53// Within the game view the layout is:54//55// title56// ┌─────────────┐ spacer ┌──────────────┐57// │ letter grid │ │ word list │58// └─────────────┘ └──────────────┘59// status60//61// The grid + spacer + word list form `mainView`, which is centered as a62// block within the overall view.63func (m *Model) gridOrigin() (x, y int) {64 gameView := m.View()65 viewWidth := lipgloss.Width(gameView)66 viewHeight := lipgloss.Height(gameView)6768 if viewWidth > m.termWidth {69 viewWidth = m.termWidth70 }7172 // Centering offset applied by the root's lipgloss.Place().73 centerX := max((m.termWidth-viewWidth)/2, 0)74 centerY := max((m.termHeight-viewHeight)/2, 0)7576 // Measure the title height.77 title := game.TitleBarView("Word Search", m.modeTitle, m.solved)78 titleHeight := strings.Count(title, "\n") + 17980 // The mainView (grid + spacer + word list) is centered within the81 // overall view. Measure its width to compute left padding.82 gridContent := renderGrid(*m)83 gBorder := gridBorderStyle()84 if m.solved {85 gBorder = gridBorderSolvedStyle()86 }87 gridRendered := gBorder.Render(gridContent)8889 wordListContent := renderWordList(*m)90 wBorder := wordListBorderStyle()91 if m.solved {92 wBorder = wordListBorderSolvedStyle()93 }94 gridHeight := lipgloss.Height(gridRendered)95 wBorder = wBorder.Height(gridHeight - 2)96 wordListRendered := wBorder.Render(wordListContent)9798 spacer := strings.Repeat(" ", 2)99 mainView := lipgloss.JoinHorizontal(lipgloss.Top, gridRendered, spacer, wordListRendered)100 mainViewWidth := lipgloss.Width(mainView)101102 // The mainView block is centered within the full view width.103 mainViewPadLeft := max((viewWidth-mainViewWidth)/2, 0)104105 // Border adds 1 character on the left and 1 line on top.106 const borderLeft = 1107 const borderTop = 1108109 x = centerX + mainViewPadLeft + borderLeft110 y = centerY + titleHeight + borderTop111 return x, y112}