commit 97f69a88839beca6da39c9774ee50b64c81a9416
parent 57bfdf38421d7fd20f202f1cb285c8f4b0625eac
Author: Michael Camilleri <[email protected]>
Date: Sat, 30 May 2026 13:46:29 +0900
Wire up undo/redo controls in the puzzle grid
The move journal already exposed undo/redo (canUndo/canRedo/undo/redo on
GameMutator), but nothing surfaced it to the player. This commit adds the
controls across the three input surfaces: entry menu, built-in keyboard
and external keyboard.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Diffstat:
3 files changed, 68 insertions(+), 16 deletions(-)
diff --git a/Crossmate/Views/HardwareKeyboardInputView.swift b/Crossmate/Views/HardwareKeyboardInputView.swift
@@ -35,7 +35,14 @@ struct HardwareKeyboardInputView: UIViewRepresentable {
)
}
+ let undo = UIKeyCommand(input: "z", modifierFlags: .command, action: #selector(handleKeyCommand(_:)))
+ undo.discoverabilityTitle = "Undo"
+ let redo = UIKeyCommand(input: "z", modifierFlags: [.command, .shift], action: #selector(handleKeyCommand(_:)))
+ redo.discoverabilityTitle = "Redo"
+
return letters + [
+ undo,
+ redo,
UIKeyCommand(input: UIKeyCommand.inputLeftArrow, modifierFlags: [], action: #selector(handleKeyCommand(_:))),
UIKeyCommand(input: UIKeyCommand.inputRightArrow, modifierFlags: [], action: #selector(handleKeyCommand(_:))),
UIKeyCommand(input: UIKeyCommand.inputLeftArrow, modifierFlags: .command, action: #selector(handleKeyCommand(_:))),
diff --git a/Crossmate/Views/KeyboardView.swift b/Crossmate/Views/KeyboardView.swift
@@ -164,27 +164,23 @@ struct KeyboardView: View {
.keyWidthMultiplier(metaKeyWidthMultiplier)
.popover(isPresented: $showingOverflow) {
VStack(alignment: .leading, spacing: 0) {
- Button {
- showingOverflow = false
+ overflowItem("Undo", systemImage: "arrow.uturn.backward", isEnabled: session.mutator.canUndo) {
+ session.mutator.undo()
+ }
+
+ overflowItem("Redo", systemImage: "arrow.uturn.forward", isEnabled: session.mutator.canRedo) {
+ session.mutator.redo()
+ }
+
+ Divider()
+
+ overflowItem("Toggle Draft", systemImage: "pencil") {
session.togglePencil()
- } label: {
- Text("Toggle Draft")
- .frame(maxWidth: .infinity, alignment: .leading)
- .padding(.horizontal, 16)
- .padding(.vertical, 12)
}
- .buttonStyle(.plain)
- Button {
- showingOverflow = false
+ overflowItem("Enter Rebus", systemImage: "text.cursor") {
session.startRebus()
- } label: {
- Text("Enter Rebus")
- .frame(maxWidth: .infinity, alignment: .leading)
- .padding(.horizontal, 16)
- .padding(.vertical, 12)
}
- .buttonStyle(.plain)
}
.frame(minWidth: 160)
.presentationCompactAdaptation(.popover)
@@ -192,6 +188,27 @@ struct KeyboardView: View {
}
}
+ /// One row in the overflow popover. Dismisses the popover, then runs the
+ /// action — disabled rows (e.g. Undo with nothing to undo) are greyed out.
+ private func overflowItem(
+ _ title: String,
+ systemImage: String,
+ isEnabled: Bool = true,
+ action: @escaping () -> Void
+ ) -> some View {
+ Button {
+ showingOverflow = false
+ action()
+ } label: {
+ Label(title, systemImage: systemImage)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .padding(.horizontal, 16)
+ .padding(.vertical, 12)
+ }
+ .buttonStyle(.plain)
+ .disabled(!isEnabled)
+ }
+
private func actionKey(
systemImage: String,
accessibilityLabel: String? = nil,
diff --git a/Crossmate/Views/PuzzleView.swift b/Crossmate/Views/PuzzleView.swift
@@ -365,6 +365,18 @@ struct PuzzleView: View {
private func handleHardwareKeyboardEvent(_ event: HardwareKeyboardEvent) -> Bool {
guard !isSolved, !isInputBlocked else { return false }
+ // Cmd+Z undoes, Shift-Cmd-Z redoes. Caught before the letter switch so
+ // the modified press isn't read as typing a "Z".
+ if event.keyCode == .keyboardZ, event.modifierFlags.contains(.command) {
+ guard !session.isRebusActive else { return false }
+ if event.modifierFlags.contains(.shift) {
+ session.mutator.redo()
+ } else {
+ session.mutator.undo()
+ }
+ return true
+ }
+
switch event.keyCode {
case .keyboardA, .keyboardB, .keyboardC, .keyboardD, .keyboardE,
.keyboardF, .keyboardG, .keyboardH, .keyboardI, .keyboardJ,
@@ -815,6 +827,22 @@ private struct PuzzleToolbarModifier: ViewModifier {
private var entryMenu: some View {
Menu {
Section {
+ Button {
+ session.mutator.undo()
+ } label: {
+ Label("Undo", systemImage: "arrow.uturn.backward")
+ }
+ .disabled(!session.mutator.canUndo)
+
+ Button {
+ session.mutator.redo()
+ } label: {
+ Label("Redo", systemImage: "arrow.uturn.forward")
+ }
+ .disabled(!session.mutator.canRedo)
+ }
+
+ Section {
Button("Enter Rebus") { session.startRebus() }
Button("Toggle Direction") { session.toggleDirection() }
}