1package netwalk23import (4 "image/color"5 "strconv"6 "strings"78 "charm.land/lipgloss/v2"9 "github.com/FelineStateMachine/puzzletea/game"10 "github.com/FelineStateMachine/puzzletea/theme"11)1213const (14 cellWidth = 515 cellHeight = 316)1718func gridView(m Model) string {19 colors := game.DefaultBorderColors()20 rows := make([]string, 0, m.puzzle.Size*cellHeight+2)21 rows = append(rows, game.HBorderRow(m.puzzle.Size, -1, cellWidth, "┌", "┐", colors, m.state.solved))22 for y := range m.puzzle.Size {23 for inner := range cellHeight {24 rows = append(rows, gridContentRow(m, y, inner, colors))25 }26 }27 rows = append(rows, game.HBorderRow(m.puzzle.Size, -1, cellWidth, "└", "┘", colors, m.state.solved))28 return strings.Join(rows, "\n")29}3031func gridContentRow(m Model, y, inner int, colors game.GridBorderColors) string {32 var b strings.Builder33 b.WriteString(game.BorderChar("│", colors, m.state.solved, false))34 for x := range m.puzzle.Size {35 b.WriteString(cellRowView(m, x, y, inner))36 }37 b.WriteString(game.BorderChar("│", colors, m.state.solved, false))38 return b.String()39}4041func cellRowView(m Model, x, y, inner int) string {42 rows := cellRows(m, x, y)43 t := m.puzzle.Tiles[y][x]44 if x == m.cursor.X && y == m.cursor.Y && !isActive(t) {45 rows = blankCursorRows()46 }47 style := lipgloss.NewStyle().48 Width(cellWidth).49 Background(cellBackground(m, x, y)).50 Foreground(cellForeground(m, x, y))5152 if x == m.cursor.X && y == m.cursor.Y && isActive(t) {53 style = style.Bold(true)54 }55 if t.Kind == serverCell || degree(m.state.rotatedMasks[y][x]) == 1 || m.state.tileHasDangling[y][x] {56 style = style.Bold(true)57 }5859 return style.Render(rows[inner])60}6162func cellRows(m Model, x, y int) [cellHeight]string {63 t := m.puzzle.Tiles[y][x]64 if !isActive(t) {65 return [cellHeight]string{" ", " ", " "}66 }6768 mask := m.state.rotatedMasks[y][x]69 center := centerGlyph(m, x, y, t.Kind, mask)7071 return [cellHeight]string{72 verticalCellRow(mask&north != 0),73 horizontalCellRow(mask&west != 0, center, mask&east != 0),74 verticalCellRow(mask&south != 0),75 }76}7778func centerGlyph(m Model, x, y int, kind cellKind, mask directionMask) string {79 switch {80 case kind == serverCell:81 return "◆"82 case degree(mask) == 1:83 return "●"84 default:85 return maskGlyph(mask)86 }87}8889func blankCursorRows() [cellHeight]string {90 return [cellHeight]string{91 " ",92 game.CursorLeft + " " + game.CursorRight,93 " ",94 }95}9697func verticalCellRow(on bool) string {98 if !on {99 return " "100 }101 return " │ "102}103104func horizontalCellRow(left bool, center string, right bool) string {105 leftArm := " "106 if left {107 leftArm = "──"108 }109 rightArm := " "110 if right {111 rightArm = "──"112 }113 return leftArm + center + rightArm114}115116func cellBackground(m Model, x, y int) color.Color {117 t := m.puzzle.Tiles[y][x]118 if m.state.solved {119 return theme.Current().SuccessBG120 }121 if x == m.cursor.X && y == m.cursor.Y {122 return theme.Blend(theme.Current().BG, theme.Current().Accent, 0.18)123 }124 if !isActive(t) {125 return theme.Current().BG126 }127 if t.Locked {128 return theme.Blend(theme.Current().BG, theme.Current().Surface, 0.60)129 }130 return theme.Current().BG131}132133func cellForeground(m Model, x, y int) color.Color {134 return pipeForeground(m, point{X: x, Y: y})135}136137func pipeForeground(m Model, cells ...point) color.Color {138 if m.state.solved {139 return theme.Current().SolvedFG140 }141142 hasConnected := false143 for _, cell := range cells {144 if !m.puzzle.activeAt(cell.X, cell.Y) {145 continue146 }147 tile := m.puzzle.Tiles[cell.Y][cell.X]148 if tile.Kind == serverCell {149 return theme.Current().AccentSoft150 }151 if stateBoolAt(m.state.tileHasDangling, cell.X, cell.Y) {152 return theme.Current().Error153 }154 if stateBoolAt(m.state.connectedToRoot, cell.X, cell.Y) {155 hasConnected = true156 }157 }158 if hasConnected {159 return theme.Current().Secondary160 }161 return theme.Current().FG162}163164func hasStateCell(cells [][]bool, x, y int) bool {165 return y >= 0 && y < len(cells) && x >= 0 && x < len(cells[y])166}167168func stateBoolAt(cells [][]bool, x, y int) bool {169 return hasStateCell(cells, x, y) && cells[y][x]170}171172func statusBarView(m Model, full bool) string {173 info := "connected " + strconv.Itoa(m.state.connected) + "/" + strconv.Itoa(m.state.nonEmpty) +174 " dangling " + strconv.Itoa(m.state.dangling) +175 " locks " + strconv.Itoa(m.state.locked)176 if !full {177 return game.StatusBarStyle().Render(info + "\nspace: rotate enter: lock")178 }179 return game.StatusBarStyle().Render(info + "\nspace: rotate backspace: reverse\nenter: toggle lock ctrl+r: reset")180}