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:
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)
- }
}
}