commit 4ce3988c737c98c74b88adf13db65886bb65c215
parent e3618621cc52091c1e7e67be44b5d4127596bc9c
Author: Michael Camilleri <[email protected]>
Date: Sun, 7 Jun 2026 00:23:10 +0900
Move iPadOS's Clue List during replay
The replay clue animation previously only reached the Clue Bar. On
iPadOS, where the sidebar Clue List replaces the Clue Bar, replay
playback and scrubbing continued to highlight the live selected clue
instead of following the replay playhead.
This commit lets ClueList accept the current replay frame and derive its
effective clue from the replay cursor and recorded direction when available.
The existing highlight and scroll-to-current-row behavior then applies to
replay just as it does to normal clue selection, while falling back to the live
session clue when replay has no focused clue.
Co-Authored-By: Codex GPT 5.5 <[email protected]>
Diffstat:
2 files changed, 38 insertions(+), 5 deletions(-)
diff --git a/Crossmate/Views/ClueList.swift b/Crossmate/Views/ClueList.swift
@@ -3,6 +3,7 @@ import SwiftUI
struct ClueList: View {
@Bindable var session: PlayerSession
var presentation: Presentation = .sheet
+ var replayFrame: ReplayFrame?
@Environment(PlayerPreferences.self) private var preferences
@Environment(\.dismiss) private var dismiss
@Environment(\.scenePhase) private var scenePhase
@@ -36,9 +37,10 @@ struct ClueList: View {
@ViewBuilder
private var clueList: some View {
- let current = session.currentClue()
- let currentDirection = session.direction
- let currentID = current.map { rowID(direction: currentDirection, number: $0.number) }
+ let currentDisplay = replayClueDisplay ?? liveClueDisplay
+ let current = currentDisplay.clue
+ let currentDirection = currentDisplay.direction
+ let currentID = currentDisplay.currentID
if presentation == .sheet {
list(
@@ -186,6 +188,37 @@ struct ClueList: View {
"\(direction == .across ? "A" : "D")-\(number)"
}
+ private var liveClueDisplay: ClueDisplay {
+ ClueDisplay(clue: session.currentClue(), direction: session.direction)
+ }
+
+ private var replayClueDisplay: ClueDisplay? {
+ guard let position = replayFrame?.cursor,
+ let direction = replayFrame?.cursorDirection
+ else { return nil }
+ return ClueDisplay(
+ clue: clue(atRow: position.row, col: position.col, direction: direction),
+ direction: direction
+ )
+ }
+
+ private struct ClueDisplay {
+ let clue: Puzzle.Clue?
+ let direction: Puzzle.Direction
+
+ var currentID: String? {
+ clue.map { "\(direction == .across ? "A" : "D")-\($0.number)" }
+ }
+ }
+
+ private func clue(atRow row: Int, col: Int, direction: Puzzle.Direction) -> Puzzle.Clue? {
+ guard let number = session.puzzle.wordCells(atRow: row, col: col, direction: direction).first?.number else {
+ return nil
+ }
+ let clues = direction == .across ? session.puzzle.acrossClues : session.puzzle.downClues
+ return clues.first { $0.number == number }
+ }
+
@ViewBuilder
private func row(
for clue: Puzzle.Clue,
diff --git a/Crossmate/Views/PuzzleView.swift b/Crossmate/Views/PuzzleView.swift
@@ -177,7 +177,7 @@ struct PuzzleView: View {
Divider()
}
- ClueList(session: session, presentation: .sidebar)
+ ClueList(session: session, presentation: .sidebar, replayFrame: replay.frame)
}
.frame(minWidth: 300, idealWidth: 360, maxWidth: 420)
.background(Color(.secondarySystemBackground))
@@ -213,7 +213,7 @@ struct PuzzleView: View {
Divider()
}
- ClueList(session: session, presentation: .sidebar)
+ ClueList(session: session, presentation: .sidebar, replayFrame: replay.frame)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.background(Color(.secondarySystemBackground))