Two bugs fixed: 1. Mouse hit-testing: gridOrigin() now renders the actual View components and measures them with lipgloss.Width/Height instead of computing offsets from first principles. This accounts for JoinVertical(Center) padding, styled text widths, and all other lipgloss layout behavior that made the manual calculation inaccurate. 2. Paint brush release: KeyReleaseMsg now matches on msg.String() instead of key.Matches(). The key.Matches() function checks if the binding is enabled, but updateKeyBindings() disables FillTile after filling a cell, so the release event for z would never match and paintBrush was never cleared.
- commit
- ccba74ad8fd14ad9aa3c1c76fc766a5986b54064
- parent
- 68ab28e0bdd35083d52a73daf2391fb953fdc418
- author
- Dami <dami.etoile@gmail.com>
| M | nonogram/ | 6 | ++- |
| M | nonogram/ | 89 | ++++++++++++++++-------------- |
@@ -110,7 +110,11 @@ func (m Model) Update(msg tea.Msg) (game.Gamer, tea.Cmd) {
110110 }111111 112112 case tea.KeyReleaseMsg:113- if key.Matches(msg, m.keys.FillTile) || key.Matches(msg, m.keys.MarkTile) {113+ // Clear paint brush on key release. Cannot use key.Matches here114+ // because updateKeyBindings may have disabled the binding after the115+ // key was pressed (e.g. filling a cell disables FillTile).116+ switch msg.String() {117+ case "z", "x":114118 m.paintBrush = 0115119 }116120 @@ -1,5 +1,10 @@
11 package nonogram22 3+import (4+ "charm.land/lipgloss/v2"5+ "github.com/FelineStateMachine/puzzletea/game"6+)7+38 // screenToGrid converts terminal screen coordinates to grid cell coordinates.49 // Returns (col, row, ok) where ok is false if the click landed outside the grid510 // or on a separator.@@ -63,49 +68,47 @@ func localToCell(pos, count, unitWidth int) int {
6368 }6469 6570 // gridOrigin computes the screen position of the top-left corner of the66-// first grid cell. This depends on the hint areas, title bar, centering67-// offsets, and the status bar.71+// first grid cell by rendering the same components as View() and measuring72+// their dimensions.6873 func (m *Model) gridOrigin() (x, y int) {69- rowHintWidth := m.rowHints.RequiredLen() * cellWidth70- colHintHeight := m.colHints.RequiredLen()71-72- // Grid content dimensions (the entire View output of the nonogram).73- gridRenderWidth := rowHintWidth + gridPixelWidth(m.width)74- gridRenderHeight := colHintHeight + gridPixelHeight(m.height)75-76- // Title bar is 1 line of text. The overall View joins:77- // title (1 line) + grid block + status bar (2 lines with MarginTop 1).78- titleHeight := 179- statusHeight := 2 // 1 line text + 1 line margin80- totalContentHeight := titleHeight + gridRenderHeight + statusHeight81-82- // The content width is the max of the title width and grid block width.83- // Title is short, so grid block dominates.84- totalContentWidth := gridRenderWidth85-86- // CenterView uses lipgloss.Place which centers within termWidth x termHeight.87- centerOffsetX := max((m.termWidth-totalContentWidth)/2, 0)88- centerOffsetY := max((m.termHeight-totalContentHeight)/2, 0)89-90- x = centerOffsetX + rowHintWidth91- y = centerOffsetY + titleHeight + colHintHeight74+ // Render the same components View() renders to get exact measurements.75+ maxWidth := m.rowHints.RequiredLen() * cellWidth76+ maxHeight := m.colHints.RequiredLen()77+78+ title := game.TitleBarView("Nonogram", m.modeTitle, m.solved)79+ rowHints := rowHintView(m.rowHints, maxWidth, m.currentHints.rows)80+ colHints := colHintView(m.colHints, maxHeight, m.currentHints.cols)81+ spacer := baseStyle.Width(maxWidth).Height(maxHeight).Render("")82+ g := gridView(m.grid, m.cursor, m.solved)83+ status := statusBarView(m.showFullHelp)84+85+ // Reproduce the exact layout from View().86+ s1 := lipgloss.JoinHorizontal(lipgloss.Bottom, spacer, colHints)87+ s2 := lipgloss.JoinHorizontal(lipgloss.Top, rowHints, g)88+ gridBlock := lipgloss.JoinVertical(lipgloss.Center, s1, s2)89+ fullContent := lipgloss.JoinVertical(lipgloss.Center, title, gridBlock, status)90+91+ // Measure the full content dimensions for centering.92+ contentWidth := lipgloss.Width(fullContent)93+ contentHeight := lipgloss.Height(fullContent)94+95+ // CenterView uses lipgloss.Place(termWidth, termHeight, Center, Center, ...).96+ centerOffsetX := max((m.termWidth-contentWidth)/2, 0)97+ centerOffsetY := max((m.termHeight-contentHeight)/2, 0)98+99+ // Grid cell (0,0) is offset from the content's top-left by:100+ // - X: the row hint area width101+ // - Y: title height + column hint area height102+ hintAreaWidth := lipgloss.Width(rowHints)103+ titleAreaHeight := lipgloss.Height(title)104+ colHintAreaHeight := lipgloss.Height(s1)105+106+ // Within the full content, JoinVertical(Center) pads all lines to max107+ // width. The grid block may be centered within that width.108+ gridBlockWidth := lipgloss.Width(gridBlock)109+ gridBlockPadLeft := max((contentWidth-gridBlockWidth)/2, 0)110+111+ x = centerOffsetX + gridBlockPadLeft + hintAreaWidth112+ y = centerOffsetY + titleAreaHeight + colHintAreaHeight92113 return x, y93114 }94-95-// gridPixelWidth returns the terminal character width of the grid area.96-func gridPixelWidth(w int) int {97- pw := w * cellWidth98- if w > spacerEvery {99- pw += (w - 1) / spacerEvery100- }101- return pw102-}103-104-// gridPixelHeight returns the terminal line height of the grid area.105-func gridPixelHeight(h int) int {106- ph := h107- if h > spacerEvery {108- ph += (h - 1) / spacerEvery109- }110- return ph111-}