GridThumbnailView.swift (2201B)
1 import SwiftUI 2 3 /// A miniature, non-interactive rendering of a crossword grid for use in 4 /// game list rows. Drawn in a single `Canvas` pass: the whole thumbnail is 5 /// filled black, then non-block cells are drawn on top, so the gaps between 6 /// cells form the grid lines for free. 7 struct GridThumbnailView: View { 8 let width: Int 9 let height: Int 10 let cells: [GameThumbnailCell] 11 var size: CGFloat = 60 12 13 private let spacing: CGFloat = 0.5 14 15 /// Fill for cells with a letter entered. The thumbnail depicts the grid, 16 /// which is black-and-white in every appearance, so this stays a fixed 17 /// gray (systemGray3's light-mode value) rather than a dynamic colour — 18 /// the dark-mode variant is nearly indistinguishable from the black 19 /// blocks. 20 private let filledColor = Color(white: 0.78) 21 22 var body: some View { 23 Canvas(rendersAsynchronously: false) { ctx, canvasSize in 24 ctx.fill( 25 Path(CGRect(origin: .zero, size: canvasSize)), 26 with: .color(.black) 27 ) 28 29 let cols = CGFloat(width) 30 let rows = CGFloat(height) 31 let cellW = (canvasSize.width - spacing * (cols + 1)) / cols 32 let cellH = (canvasSize.height - spacing * (rows + 1)) / rows 33 let cell = min(cellW, cellH) 34 35 let gridW = cell * cols + spacing * (cols + 1) 36 let gridH = cell * rows + spacing * (rows + 1) 37 let originX = (canvasSize.width - gridW) / 2 38 let originY = (canvasSize.height - gridH) / 2 39 40 for index in cells.indices { 41 let cellValue = cells[index] 42 guard cellValue != .block else { continue } 43 let r = index / width 44 let c = index % width 45 let x = originX + spacing + CGFloat(c) * (cell + spacing) 46 let y = originY + spacing + CGFloat(r) * (cell + spacing) 47 ctx.fill( 48 Path(CGRect(x: x, y: y, width: cell, height: cell)), 49 with: .color(cellValue == .filled ? filledColor : .white) 50 ) 51 } 52 } 53 .frame(width: size, height: size) 54 } 55 }