crossmate

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

commit 9e364976cc2580514a47c3b3bce474db46def332
parent e13c9a77f1ce2d04002a00f6e533cf709cabc3a6
Author: Michael Camilleri <[email protected]>
Date:   Fri, 15 May 2026 17:45:42 +0900

Use selection fill for remote Cursor Track

Diffstat:
MCrossmate/Views/CellView.swift | 45---------------------------------------------
MCrossmate/Views/GridView.swift | 41+++++++++--------------------------------
2 files changed, 9 insertions(+), 77 deletions(-)

diff --git a/Crossmate/Views/CellView.swift b/Crossmate/Views/CellView.swift @@ -9,7 +9,6 @@ struct CellView: View, Equatable { var isRelatedToFocus: Bool = false let specialKind: Puzzle.Special? var remoteWordTint: Color? = nil - var remoteTrackBorder: CellBorder? = nil var authorTint: Color? = nil @Environment(PlayerPreferences.self) private var preferences @@ -24,7 +23,6 @@ struct CellView: View, Equatable { && lhs.isRelatedToFocus == rhs.isRelatedToFocus && lhs.specialKind == rhs.specialKind && lhs.remoteWordTint == rhs.remoteWordTint - && lhs.remoteTrackBorder == rhs.remoteTrackBorder && lhs.authorTint == rhs.authorTint } @@ -57,10 +55,6 @@ struct CellView: View, Equatable { Rectangle() .strokeBorder(playerColor.highlightFill, lineWidth: 3) } - if let remoteTrackBorder { - CellBorderShape(edges: remoteTrackBorder.edges) - .stroke(remoteTrackBorder.color, lineWidth: 2) - } } } .frame(maxWidth: .infinity, maxHeight: .infinity) @@ -122,45 +116,6 @@ struct CellView: View, Equatable { } } -struct CellBorder: Equatable { - let color: Color - let edges: CellBorderEdges -} - -struct CellBorderEdges: OptionSet, Equatable { - let rawValue: Int - - static let top = CellBorderEdges(rawValue: 1 << 0) - static let trailing = CellBorderEdges(rawValue: 1 << 1) - static let bottom = CellBorderEdges(rawValue: 1 << 2) - static let leading = CellBorderEdges(rawValue: 1 << 3) -} - -private struct CellBorderShape: Shape { - let edges: CellBorderEdges - - func path(in rect: CGRect) -> Path { - var path = Path() - if edges.contains(.top) { - path.move(to: CGPoint(x: rect.minX, y: rect.minY)) - path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY)) - } - if edges.contains(.trailing) { - path.move(to: CGPoint(x: rect.maxX, y: rect.minY)) - path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY)) - } - if edges.contains(.bottom) { - path.move(to: CGPoint(x: rect.maxX, y: rect.maxY)) - path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY)) - } - if edges.contains(.leading) { - path.move(to: CGPoint(x: rect.minX, y: rect.maxY)) - path.addLine(to: CGPoint(x: rect.minX, y: rect.minY)) - } - return path - } -} - /// Small dot pinned to the top-leading corner of the cell, sized as a /// fraction of the shorter cell dimension. Marks cells that start a word. private struct CornerDot: Shape { diff --git a/Crossmate/Views/GridView.swift b/Crossmate/Views/GridView.swift @@ -10,7 +10,7 @@ struct GridView: View { var body: some View { let width = session.puzzle.width let height = session.puzzle.height - let (borderByCell, tintByCell) = showsSharedAnnotations ? remoteOverlays() : ([:], [:]) + let tintByCell: [GridPosition: Color] = showsSharedAnnotations ? remoteTrackTints() : [:] let authorTintByID: [String: Color] = showsSharedAnnotations ? Dictionary( uniqueKeysWithValues: roster.entries.map { ($0.authorID, $0.color.tint) } @@ -41,7 +41,6 @@ struct GridView: View { isRelatedToFocus: relatedCells.contains(pos), specialKind: session.puzzle.specialKind, remoteWordTint: tintByCell[pos], - remoteTrackBorder: borderByCell[pos], authorTint: square.entry.isEmpty ? nil : square.letterAuthorID.flatMap { authorTintByID[$0] } @@ -56,44 +55,22 @@ struct GridView: View { } /// Builds remote word-tint overlays from each peer's persisted cursor - /// track. The exact cursor reticle is intentionally local-only; CloudKit - /// presence records carry the selected answer slot, not the focused - /// square. - private func remoteOverlays() -> ( - border: [GridPosition: CellBorder], - tint: [GridPosition: Color] - ) { - var border: [GridPosition: (Date, CellBorder)] = [:] + /// track. Every cell in the peer's selected answer is filled with their + /// selection colour so the track reads as a coloured run; the exact + /// focused square is intentionally local-only. + private func remoteTrackTints() -> [GridPosition: Color] { var tint: [GridPosition: (Date, Color)] = [:] for (_, sel) in roster.remoteSelections { - let cells = session.puzzle.wordCells( + for cell in session.puzzle.wordCells( atRow: sel.row, col: sel.col, direction: sel.direction - ) - for (index, cell) in cells.enumerated() { + ) { let pos = GridPosition(row: cell.row, col: cell.col) if tint[pos].map({ $0.0 < sel.updatedAt }) ?? true { - tint[pos] = (sel.updatedAt, sel.color.highlightFill) - } - var edges: CellBorderEdges - switch sel.direction { - case .across: - edges = [.top, .bottom] - if index == cells.startIndex { edges.insert(.leading) } - if index == cells.index(before: cells.endIndex) { edges.insert(.trailing) } - case .down: - edges = [.leading, .trailing] - if index == cells.startIndex { edges.insert(.top) } - if index == cells.index(before: cells.endIndex) { edges.insert(.bottom) } - } - if border[pos].map({ $0.0 < sel.updatedAt }) ?? true { - border[pos] = ( - sel.updatedAt, - CellBorder(color: sel.color.selectionFill, edges: edges) - ) + tint[pos] = (sel.updatedAt, sel.color.selectionFill) } } } - return (border.mapValues { $0.1 }, tint.mapValues { $0.1 }) + return tint.mapValues { $0.1 } } }