crossmate

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

commit 856ee7589cd0136c65ed0d18731be519550ce25c
parent 39eb714f6676e7a1cfc230ceaae7341fe4e1c100
Author: Michael Camilleri <[email protected]>
Date:   Sun, 16 Aug 2026 00:19:58 +0900

Keep the cursor movable on a finished puzzle

Prior to this commit, a completed puzzle ignored every key on a hardware
keyboard. The user could still tap a cell to move the caret but could
not reach it with the cursor arrows, Tab or the direction toggles. The
same held wherever the grid was read-only for another reason — revoked
access, an unsupported protocol, or a sticky announcement — because the
block sat at the top of the handler rather than on the keys that write.

This commit narrows the block to text entry.
handleHardwareKeyboardEvent now gates the letter, digit and delete cases
alone, leaving the arrows, ⌘-arrows, Tab and the Space and Return
direction toggles live; a blocked key still returns false, so it bubbles
to the app menu rather than being swallowed. GameMutator.isEditable
remains the write lock behind every mutation, and
skipsFilledCellsDuringNavigation already stops skipping filled cells
once a game is solved, so navigation over a full grid is literal.

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

Diffstat:
MCrossmate/Views/Puzzle/PuzzleView.swift | 17++++++++++++-----
1 file changed, 12 insertions(+), 5 deletions(-)

diff --git a/Crossmate/Views/Puzzle/PuzzleView.swift b/Crossmate/Views/Puzzle/PuzzleView.swift @@ -123,9 +123,10 @@ struct PuzzleView: View { /// Whether the mutator is read-only (access revoked, unsupported protocol, /// completed) or a sticky, input-blocking announcement is showing. Greys - /// out the custom keyboard, makes the hardware-key handler a no-op, and - /// disables the toolbar's editing menus. Reads the mutator's own predicate - /// so the block holds even if the matching banner was never posted. + /// out the custom keyboard, drops the hardware keys that write to the grid + /// (navigation keys still move the cursor), and disables the toolbar's + /// editing menus. Reads the mutator's own predicate so the block holds + /// even if the matching banner was never posted. private var isInputBlocked: Bool { !session.mutator.isEditable || announcements.isInputBlocked(forGame: session.mutator.gameID) @@ -563,7 +564,11 @@ struct PuzzleView: View { } private func handleHardwareKeyboardEvent(_ event: HardwareKeyboardEvent) -> Bool { - guard !isSolved, !isInputBlocked else { return false } + // Blocks the keys that write to the grid, not the ones that move the + // cursor: a finished (or read-only) puzzle still lets a player tap + // their way around the grid, so the hardware arrows, Tab and the + // direction toggles stay live to match. + let isEntryBlocked = isSolved || isInputBlocked // Undo/redo (⌘Z, ⇧⌘Z) live in the app menu (see PuzzleCommands) so they // appear in the hold-⌘ shortcut overlay. A ⌘-modified Z falls through @@ -578,7 +583,8 @@ struct PuzzleView: View { .keyboardZ, .keyboard0, .keyboard1, .keyboard2, .keyboard3, .keyboard4, .keyboard5, .keyboard6, .keyboard7, .keyboard8, .keyboard9: - guard !event.modifierFlags.contains(.command), + guard !isEntryBlocked, + !event.modifierFlags.contains(.command), !event.modifierFlags.contains(.control), !event.modifierFlags.contains(.alternate), let character = hardwareKeyboardCharacter(from: event) else { @@ -592,6 +598,7 @@ struct PuzzleView: View { return true case .keyboardDeleteOrBackspace, .keyboardDeleteForward: + guard !isEntryBlocked else { return false } if session.isRebusActive { session.deleteRebusLetter() } else {