diff --git a/src/components/common/MoveControls.tsx b/src/components/common/MoveControls.tsx index bfe1ffc9..b0ca03c8 100644 --- a/src/components/common/MoveControls.tsx +++ b/src/components/common/MoveControls.tsx @@ -23,6 +23,12 @@ function MoveControls({ const start = useStore(store, (s) => s.goToStart); const end = useStore(store, (s) => s.goToEnd); const deleteMove = useStore(store, (s) => s.deleteMove); + const startBranch = useStore(store, (s) => s.goToBranchStart); + const endBranch = useStore(store, (s) => s.goToBranchEnd); + const nextBranch = useStore(store, (s) => s.nextBranch); + const previousBranch = useStore(store, (s) => s.previousBranch); + const nextBranching = useStore(store, (s) => s.nextBranching); + const previousBranching = useStore(store, (s) => s.previousBranching); const keyMap = useAtomValue(keyMapAtom); useHotkeys(keyMap.PREVIOUS_MOVE.keys, previous); @@ -30,6 +36,13 @@ function MoveControls({ useHotkeys(keyMap.GO_TO_START.keys, start); useHotkeys(keyMap.GO_TO_END.keys, end); useHotkeys(keyMap.DELETE_MOVE.keys, readOnly ? () => {} : () => deleteMove()); + useHotkeys(keyMap.GO_TO_BRANCH_START.keys, startBranch); + useHotkeys(keyMap.GO_TO_BRANCH_END.keys, endBranch); + useHotkeys(keyMap.NEXT_BRANCH.keys, nextBranch); + useHotkeys(keyMap.PREVIOUS_BRANCH.keys, previousBranch); + useHotkeys(keyMap.NEXT_BRANCHING.keys, nextBranching); + useHotkeys(keyMap.PREVIOUS_BRANCHING.keys, previousBranching); + return ( diff --git a/src/state/keybinds.ts b/src/state/keybinds.ts index 102a8fab..2603b04a 100644 --- a/src/state/keybinds.ts +++ b/src/state/keybinds.ts @@ -13,8 +13,17 @@ const keys = { CLEAR_SHAPES: { name: "Clear shapes", keys: "ctrl+l" }, NEXT_MOVE: { name: "Next move", keys: "arrowright" }, PREVIOUS_MOVE: { name: "Previous move", keys: "arrowleft" }, - GO_TO_START: { name: "Go to start of game", keys: "arrowup" }, - GO_TO_END: { name: "Go to end of game", keys: "arrowdown" }, + GO_TO_BRANCH_START: { name: "Go to start of branch", keys: "arrowup" }, + GO_TO_BRANCH_END: { name: "Go to end of branch", keys: "arrowdown" }, + GO_TO_START: { name: "Go to start of game", keys: "shift+arrowup" }, + GO_TO_END: { name: "Go to end of game", keys: "shift+down" }, + NEXT_BRANCH: { name: "Next branch", keys: "c" }, + PREVIOUS_BRANCH: { name: "Previous branch", keys: "x" }, + NEXT_BRANCHING: { name: "Next branching point", keys: "shift+arrowright" }, + PREVIOUS_BRANCHING: { + name: "Previous branching point", + keys: "shift+arrowleft", + }, DELETE_MOVE: { name: "Delete move", keys: "delete" }, CYCLE_TABS: { name: "Cycle tabs", keys: "ctrl+tab" }, REVERSE_CYCLE_TABS: { name: "Reverse cycle tabs", keys: "ctrl+shift+tab" }, diff --git a/src/state/store.ts b/src/state/store.ts index d7a72759..04561889 100644 --- a/src/state/store.ts +++ b/src/state/store.ts @@ -34,6 +34,12 @@ export interface TreeStoreState { goToStart: () => void; goToEnd: () => void; goToMove: (move: number[]) => void; + goToBranchStart: () => void; + goToBranchEnd: () => void; + nextBranch: () => void; + previousBranch: () => void; + nextBranching: () => void; + previousBranching: () => void; goToAnnotation: (annotation: Annotation, color: "white" | "black") => void; @@ -245,6 +251,111 @@ export const createTreeStore = (id?: string, initialTree?: TreeState) => { ...state, position: move, })), + goToBranchStart: () => { + set( + produce((state) => { + if ( + state.position.length > 0 && + state.position[state.position.length - 1] !== 0 + ) { + state.position = state.position.slice(0, -1); + } + + while ( + state.position.length > 0 && + state.position[state.position.length - 1] === 0 + ) { + state.position = state.position.slice(0, -1); + } + }), + ); + }, + + goToBranchEnd: () => { + set( + produce((state) => { + let currentNode = getNodeAtPath(state.root, state.position); + while (currentNode.children.length > 0) { + state.position.push(0); + currentNode = currentNode.children[0]; + } + }), + ); + }, + + nextBranch: () => + set( + produce((state) => { + if (state.position.length === 0) return; + + const parent = getNodeAtPath(state.root, state.position.slice(0, -1)); + const branchIndex = state.position[state.position.length - 1]; + const node = parent.children[branchIndex]; + + // Makes the navigation more fluid and compatible with next/previous branching + if (node.children.length >= 2 && parent.children.length <= 1) { + state.position.push(0); + } + + state.position = [ + ...state.position.slice(0, -1), + (branchIndex + 1) % parent.children.length, + ]; + }), + ), + previousBranch: () => + set( + produce((state) => { + if (state.position.length === 0) return; + + const parent = getNodeAtPath(state.root, state.position.slice(0, -1)); + const branchIndex = state.position[state.position.length - 1]; + const node = parent.children[branchIndex]; + + // Makes the navigation more fluid and compatible with next/previous branching + if (node.children.length >= 2 && parent.children.length <= 1) { + state.position.push(0); + } + + state.position = [ + ...state.position.slice(0, -1), + (branchIndex + parent.children.length - 1) % parent.children.length, + ]; + }), + ), + + nextBranching: () => + set( + produce((state) => { + let node = getNodeAtPath(state.root, state.position); + let branchCount = node.children.length; + + if (branchCount === 0) return; + + do { + state.position.push(0); + node = node.children[0]; + branchCount = node.children.length; + } while (branchCount === 1); + }), + ), + + previousBranching: () => + set( + produce((state) => { + let node = getNodeAtPath(state.root, state.position); + let branchCount = node.children.length; + + if (state.position.length === 0) return; + + do { + state.position = state.position.slice(0, -1); + node = getNodeAtPath(state.root, state.position); + branchCount = node.children.length; + } while (branchCount === 1 && state.position.length > 0); + }), + ), + deleteMove: (path) => set( produce((state) => { diff --git a/src/utils/tests/store.test.ts b/src/utils/tests/store.test.ts index 553be727..48d28c5d 100644 --- a/src/utils/tests/store.test.ts +++ b/src/utils/tests/store.test.ts @@ -11,6 +11,7 @@ beforeEach(() => { const e4 = parseUci("e2e4")!; const d5 = parseUci("d7d5")!; +const e5 = parseUci("e7e5")!; const treeE4D5: () => TreeState = () => ({ ...defaultTree(), position: [0, 0], @@ -248,6 +249,26 @@ test("should handle goToPrevious", () => { }); }); +test("should handle goToBranchEnd", () => { + store.setState({ ...treeE4D5(), position: [] }); + store.getState().goToBranchEnd(); + + expect(getNewState()).toStrictEqual({ + ...treeE4D5(), + position: [0, 0], + }); +}); + +test("should handle goToBranchStart", () => { + store.setState({ ...treeE4D5(), position: [0, 0] }); + store.getState().goToBranchStart(); + + expect(getNewState()).toStrictEqual({ + ...treeE4D5(), + position: [], + }); +}); + test("should handle goToMove", () => { store.setState({ ...treeE4D5(), position: [] }); store.getState().goToMove([0]);