crossmate

A collaborative crossword app for iOS
Log | Files | Refs | LICENSE

commit 2a593b08d9d0923931b903884407a6b0747b1b96
parent 1e3c2f9ae64f5bbbde3424da344d4ea0c0a2ee80
Author: Michael Camilleri <[email protected]>
Date:   Fri,  1 May 2026 18:20:45 +0900

Switch to using canvas for thumbnail previews

This commit uses Canvas to draw the thumbnail previews.

Co-Authored-By: Claude Opus 4.7 <[email protected]>

Diffstat:
MCrossmate/Views/GridThumbnailView.swift | 96++++++++++++++++++++++++++-----------------------------------------------------
1 file changed, 31 insertions(+), 65 deletions(-)

diff --git a/Crossmate/Views/GridThumbnailView.swift b/Crossmate/Views/GridThumbnailView.swift @@ -1,8 +1,9 @@ import SwiftUI /// A miniature, non-interactive rendering of a crossword grid for use in -/// game list rows. Each cell is a colored rectangle — black for blocks, -/// white for empty cells, tinted for cells with entries. +/// game list rows. Drawn in a single `Canvas` pass: the whole thumbnail is +/// filled black, then non-block cells are drawn on top, so the gaps between +/// cells form the grid lines for free. struct GridThumbnailView: View { let width: Int let height: Int @@ -12,71 +13,36 @@ struct GridThumbnailView: View { private let spacing: CGFloat = 0.5 var body: some View { - ThumbnailGridLayout(columns: width, rows: height, spacing: spacing) { - ForEach(0..<cells.count, id: \.self) { index in - Rectangle() - .fill(fillColor(for: cells[index])) + Canvas(rendersAsynchronously: false) { ctx, canvasSize in + ctx.fill( + Path(CGRect(origin: .zero, size: canvasSize)), + with: .color(.black) + ) + + let cols = CGFloat(width) + let rows = CGFloat(height) + let cellW = (canvasSize.width - spacing * (cols + 1)) / cols + let cellH = (canvasSize.height - spacing * (rows + 1)) / rows + let cell = min(cellW, cellH) + + let gridW = cell * cols + spacing * (cols + 1) + let gridH = cell * rows + spacing * (rows + 1) + let originX = (canvasSize.width - gridW) / 2 + let originY = (canvasSize.height - gridH) / 2 + + for index in cells.indices { + let cellValue = cells[index] + guard cellValue != .block else { continue } + let r = index / width + let c = index % width + let x = originX + spacing + CGFloat(c) * (cell + spacing) + let y = originY + spacing + CGFloat(r) * (cell + spacing) + ctx.fill( + Path(CGRect(x: x, y: y, width: cell, height: cell)), + with: .color(cellValue == .filled ? Color(.systemGray3) : .white) + ) } } .frame(width: size, height: size) - .background(Color.black) - .clipShape(RoundedRectangle(cornerRadius: 6)) - } - - private func fillColor(for cell: GameThumbnailCell) -> Color { - switch cell { - case .block: return .black - case .empty: return .white - case .filled: return Color(.systemGray3) - } - } -} - -// MARK: - Layout - -/// Simplified grid layout for thumbnails. Fills the proposed size exactly, -/// computing cell size from the available space. -private struct ThumbnailGridLayout: Layout { - let columns: Int - let rows: Int - let spacing: CGFloat - - func sizeThatFits( - proposal: ProposedViewSize, - subviews: Subviews, - cache: inout () - ) -> CGSize { - let w = proposal.width ?? 60 - let h = proposal.height ?? 60 - return CGSize(width: w, height: h) - } - - func placeSubviews( - in bounds: CGRect, - proposal: ProposedViewSize, - subviews: Subviews, - cache: inout () - ) { - let cols = CGFloat(columns) - let rs = CGFloat(rows) - let totalSpacingW = spacing * (cols + 1) - let totalSpacingH = spacing * (rs + 1) - let cellW = (bounds.width - totalSpacingW) / cols - let cellH = (bounds.height - totalSpacingH) / rs - let cellSize = min(cellW, cellH) - - let gridW = cellSize * cols + spacing * (cols + 1) - let gridH = cellSize * rs + spacing * (rs + 1) - let originX = bounds.minX + (bounds.width - gridW) / 2 - let originY = bounds.minY + (bounds.height - gridH) / 2 - - let cellProposal = ProposedViewSize(width: cellSize, height: cellSize) - for (index, subview) in subviews.enumerated() { - let r = index / columns - let c = index % columns - let x = originX + spacing + CGFloat(c) * (cellSize + spacing) - let y = originY + spacing + CGFloat(r) * (cellSize + spacing) - subview.place(at: CGPoint(x: x, y: y), anchor: .topLeading, proposal: cellProposal) - } } }