From 9fe9158fc7baedc0c433dd40ca4437fc006282b5 Mon Sep 17 00:00:00 2001 From: GautamNambiar Date: Wed, 13 May 2026 00:50:17 +0800 Subject: [PATCH 01/12] Add support for meta key in keyboard shortcuts for Crumble Updated keyboard shortcuts to support the use of macOS command key to substitute for all instances of the control key. --- glue/crumble/main.js | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/glue/crumble/main.js b/glue/crumble/main.js index 6a14d452..195a1a75 100644 --- a/glue/crumble/main.js +++ b/glue/crumble/main.js @@ -197,7 +197,7 @@ editorState.canvas.addEventListener('mouseup', ev => { editorState.mouseDownY = undefined; editorState.curMouseX = ev.offsetX + OFFSET_X; editorState.curMouseY = ev.offsetY + OFFSET_Y; - editorState.changeFocus(highlightedArea, ev.shiftKey, ev.ctrlKey); + editorState.changeFocus(highlightedArea, ev.shiftKey, ev.ctrlKey || ev.metaKey); if (ev.buttons === 1) { isInScrubber = false; } @@ -233,6 +233,27 @@ function makeChordHandlers() { editorState.deleteAtFocus(preview); } }); + // Below, we allow for mac users to use the "command" key (meta) instead of "control" + res.set('meta+delete', preview => editorState.deleteCurLayer(preview)); + res.set('meta+backspace', preview => editorState.deleteCurLayer(preview)); + res.set('meta+enter', preview => editorState.insertLayer(preview)); + res.set('meta+z', preview => { if (!preview) editorState.undo() }); + res.set('meta+y', preview => { if (!preview) editorState.redo() }); + res.set('meta+shift+z', preview => { if (!preview) editorState.redo() }); + res.set('meta+c', async preview => { await copyToClipboard(); }); + res.set('meta+v', pasteFromClipboard); + res.set('meta+x', async preview => { + await copyToClipboard(); + if (editorState.focusedSet.size === 0) { + let c = editorState.copyOfCurCircuit(); + c.layers[editorState.curLayer].id_ops.clear(); + c.layers[editorState.curLayer].markers.length = 0; + editorState.commit_or_preview(c, preview); + } else { + editorState.deleteAtFocus(preview); + } + }); + res.set('l', preview => { if (!preview) { editorState.timelineSet = new Map(editorState.focusedSet.entries()); @@ -532,7 +553,7 @@ window.addEventListener('blur', () => { for (let anchor of document.getElementById('examples-div').querySelectorAll('a')) { anchor.onclick = ev => { // Don't stop the user from e.g. opening the example in a new tab using ctrl+click. - if (ev.shiftKey || ev.ctrlKey || ev.altKey || ev.button !== 0) { + if (ev.shiftKey || ev.ctrlKey || ev.metaKey || ev.altKey || ev.button !== 0) { return undefined; } let circuitText = anchor.href.split('#circuit=')[1]; From deef92c3247db89f04b40e227c28607f4f04bac4 Mon Sep 17 00:00:00 2001 From: GautamNambiar Date: Wed, 13 May 2026 00:53:42 +0800 Subject: [PATCH 02/12] Allow metaKey in getFocusedRow and getFocusedCol --- glue/crumble/keyboard/toolbox.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/glue/crumble/keyboard/toolbox.js b/glue/crumble/keyboard/toolbox.js index 0334b2f7..557064df 100644 --- a/glue/crumble/keyboard/toolbox.js +++ b/glue/crumble/keyboard/toolbox.js @@ -14,7 +14,7 @@ let DEF_ROW = [1, 2, 2, 2, 2, 0, 2, 2, 2, -1, -1, -1]; * @returns {undefined|!{row: !int, strength: !number}} */ function getFocusedRow(ev) { - if (ev.ctrlKey) { + if (ev.ctrlKey || ev.metaKey) { return undefined; } let hasX = +ev.chord.has('x'); @@ -36,7 +36,7 @@ function getFocusedRow(ev) { * @returns {undefined|!{col: !int, strength: !number}} */ function getFocusedCol(ev) { - if (ev.ctrlKey) { + if (ev.ctrlKey || ev.metaKey) { return undefined; } let best = undefined; From 041d9db6c1cbe0e168ac0ee4b22c916f0f3687b1 Mon Sep 17 00:00:00 2001 From: GautamNambiar Date: Wed, 13 May 2026 01:00:06 +0800 Subject: [PATCH 03/12] Update keyboard shortcuts in README.md to allow command key mac users can use command key instead of control key. For Windows keyboard users, since the meta key is the Windows key, this means that the control key can be replaced by the Windows key. (This is a harmless side effect.) --- glue/crumble/README.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/glue/crumble/README.md b/glue/crumble/README.md index f3b0d61c..e48e3837 100644 --- a/glue/crumble/README.md +++ b/glue/crumble/README.md @@ -116,15 +116,16 @@ button (now labelled "Hide Import/Export") again. - `escape`: Unselect. Set current selection to the empty set. - `delete`: Delete gates at current selection. - `backspace`: Delete gates at current selection. -- `ctrl+delete`: Delete current circuit layer. +- `ctrl+delete` or `cmd+delete`: Delete current circuit layer. - `ctrl+backspace`: Delete current circuit layer. - `ctrl+insert`: Insert empty layer at current circuit layer, pushing current circuit layer ahead in time. -- `ctrl+z`: Undo -- `ctrl+y`: Redo -- `ctrl+shift+z`: Redo -- `ctrl+c`: Copy selection to clipboard (or entire layer if nothing selected). -- `ctrl+v`: Past clipboard contents at current selection (or entire layer if nothing selected). -- `ctrl+x`: Cut selection to clipboard (or entire layer if nothing selected). +- `cmd+enter`: Insert empty layer at current circuit layer, pushing current circuit layer ahead in time. +- `ctrl+z` or `cmd+z`: Undo +- `ctrl+y` or `cmd+y`: Redo +- `ctrl+shift+z` or `cmd+shift+z`: Redo +- `ctrl+c` or `cmd+c`: Copy selection to clipboard (or entire layer if nothing selected). +- `ctrl+v` or `cmd+v`: Paste clipboard contents at current selection (or entire layer if nothing selected). +- `ctrl+x` or `cmd+x`: Cut selection to clipboard (or entire layer if nothing selected). - `f`: Reverse direction of selected two qubit gates (e.g. exchange the controls and targets of a CNOT). - `g`: Reverse order of circuit layers, from the current layer to the next empty layer. - `home`: Jump to the first layer of the circuit. From d3440dcd548b53623a0f652e31d115a943efb907 Mon Sep 17 00:00:00 2001 From: GautamNambiar Date: Wed, 13 May 2026 01:03:06 +0800 Subject: [PATCH 04/12] Update keyboard shortcuts for buttons in crumble.html cmd can be used instead of ctrl --- glue/crumble/crumble.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/glue/crumble/crumble.html b/glue/crumble/crumble.html index ebb6189a..5415f119 100644 --- a/glue/crumble/crumble.html +++ b/glue/crumble/crumble.html @@ -118,8 +118,8 @@
- - + +
@@ -134,8 +134,8 @@
- - + +
From 888d6e8d784796fa4c8b9725eb444268d84c215f Mon Sep 17 00:00:00 2001 From: GautamNambiar Date: Wed, 13 May 2026 15:25:20 +0800 Subject: [PATCH 05/12] Refactor keyboard shortcuts for cut and paste, support for cmd Fixing issues with keydown when pressing cmd --- glue/crumble/main.js | 100 +++++++++++++++++++++++++++++-------------- 1 file changed, 68 insertions(+), 32 deletions(-) diff --git a/glue/crumble/main.js b/glue/crumble/main.js index 195a1a75..fd0ee18c 100644 --- a/glue/crumble/main.js +++ b/glue/crumble/main.js @@ -222,37 +222,7 @@ function makeChordHandlers() { res.set('ctrl+shift+z', preview => { if (!preview) editorState.redo() }); res.set('ctrl+c', async preview => { await copyToClipboard(); }); res.set('ctrl+v', pasteFromClipboard); - res.set('ctrl+x', async preview => { - await copyToClipboard(); - if (editorState.focusedSet.size === 0) { - let c = editorState.copyOfCurCircuit(); - c.layers[editorState.curLayer].id_ops.clear(); - c.layers[editorState.curLayer].markers.length = 0; - editorState.commit_or_preview(c, preview); - } else { - editorState.deleteAtFocus(preview); - } - }); - // Below, we allow for mac users to use the "command" key (meta) instead of "control" - res.set('meta+delete', preview => editorState.deleteCurLayer(preview)); - res.set('meta+backspace', preview => editorState.deleteCurLayer(preview)); - res.set('meta+enter', preview => editorState.insertLayer(preview)); - res.set('meta+z', preview => { if (!preview) editorState.undo() }); - res.set('meta+y', preview => { if (!preview) editorState.redo() }); - res.set('meta+shift+z', preview => { if (!preview) editorState.redo() }); - res.set('meta+c', async preview => { await copyToClipboard(); }); - res.set('meta+v', pasteFromClipboard); - res.set('meta+x', async preview => { - await copyToClipboard(); - if (editorState.focusedSet.size === 0) { - let c = editorState.copyOfCurCircuit(); - c.layers[editorState.curLayer].id_ops.clear(); - c.layers[editorState.curLayer].markers.length = 0; - editorState.commit_or_preview(c, preview); - } else { - editorState.deleteAtFocus(preview); - } - }); + res.set('ctrl+x', cutToClipboard); res.set('l', preview => { if (!preview) { @@ -463,12 +433,78 @@ async function pasteFromClipboard(preview) { editorState.commit_or_preview(newCircuit, preview); } +async function cutToClipboard(preview) { + await copyToClipboard(); + if (editorState.focusedSet.size === 0) { + let c = editorState.copyOfCurCircuit(); + c.layers[editorState.curLayer].id_ops.clear(); + c.layers[editorState.curLayer].markers.length = 0; + editorState.commit_or_preview(c, preview); + } else { + editorState.deleteAtFocus(preview); + } +} + const CHORD_HANDLERS = makeChordHandlers(); /** * @param {!KeyboardEvent} ev */ -function handleKeyboardEvent(ev) { +async function handleKeyboardEvent(ev) { + if (ev.type === 'keydown' && ev.metaKey) { + if (ev.repeat) { + ev.preventDefault(); + editorState.chorder.handleFocusChanged(); + return; + } + + let key = ev.key.toLowerCase(); + + if (key === 'z' && !ev.shiftKey) { + ev.preventDefault(); + editorState.chorder.handleFocusChanged(); + editorState.undo(); + return; + } + if ((key === 'z' && ev.shiftKey) || key === 'y') { + ev.preventDefault(); + editorState.chorder.handleFocusChanged(); + editorState.redo(); + return; + } + if (key === 'c') { + ev.preventDefault(); + editorState.chorder.handleFocusChanged(); + await copyToClipboard(); + return; + } + if (key === 'v') { + ev.preventDefault(); + editorState.chorder.handleFocusChanged(); + await pasteFromClipboard(false); + return; + } + if (key === 'x') { + ev.preventDefault(); + editorState.chorder.handleFocusChanged(); + await cutToClipboard(false); + return; + } + if (key === 'backspace' || key === 'delete') { + ev.preventDefault(); + editorState.chorder.handleFocusChanged(); + editorState.deleteCurLayer(false); + return; + } + if (key === 'enter') { + ev.preventDefault(); + editorState.chorder.handleFocusChanged(); + editorState.insertLayer(false); + return; + } + } + editorState.chorder.handleKeyEvent(ev); + if (ev.type === 'keydown') { if (ev.key.toLowerCase() === 'q') { let d = ev.shiftKey ? 5 : 1; From 08a942d6ecc687c0af4c1b734c4efc6feb194d8b Mon Sep 17 00:00:00 2001 From: GautamNambiar Date: Wed, 13 May 2026 15:44:40 +0800 Subject: [PATCH 06/12] Regenerate Crumble embedded resource --- src/stim/diagram/crumble_data.cc | 195 ++++++++++++++++--------------- 1 file changed, 98 insertions(+), 97 deletions(-) diff --git a/src/stim/diagram/crumble_data.cc b/src/stim/diagram/crumble_data.cc index 0d11b307..4b29923e 100644 --- a/src/stim/diagram/crumble_data.cc +++ b/src/stim/diagram/crumble_data.cc @@ -243,9 +243,9 @@ std::string stim_draw_internal::make_crumble_html() { )CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); - result.append(R"CRUMBLE_PART( + result.append(R"CRUMBLE_PART( )CRUMBLE_PART"); - result.append(R"CRUMBLE_PART( + result.append(R"CRUMBLE_PART( )CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); @@ -275,9 +275,9 @@ std::string stim_draw_internal::make_crumble_html() { )CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); - result.append(R"CRUMBLE_PART( + result.append(R"CRUMBLE_PART( )CRUMBLE_PART"); - result.append(R"CRUMBLE_PART( + result.append(R"CRUMBLE_PART( )CRUMBLE_PART"); result.append(R"CRUMBLE_PART(
)CRUMBLE_PART"); @@ -708,57 +708,57 @@ std::string stim_draw_internal::make_crumble_html() { )CRUMBLE_PART"); result.append(R"CRUMBLE_PART( )CRUMBLE_PART"); From 41cf451f63fa921eb75082cad3ca2b06ad3fe2e4 Mon Sep 17 00:00:00 2001 From: GautamNambiar Date: Wed, 13 May 2026 19:50:52 +0800 Subject: [PATCH 07/12] Removed the cmd+v feature because it created bugs --- crumble-mac-shortcuts.html | 317 +++++++++++++++++++++++++++++++ glue/crumble/README.md | 2 +- glue/crumble/main.js | 6 - src/stim/diagram/crumble_data.cc | 12 +- 4 files changed, 324 insertions(+), 13 deletions(-) create mode 100644 crumble-mac-shortcuts.html diff --git a/crumble-mac-shortcuts.html b/crumble-mac-shortcuts.html new file mode 100644 index 00000000..03ff2052 --- /dev/null +++ b/crumble-mac-shortcuts.html @@ -0,0 +1,317 @@ + + + + + Crumble + + + + +
+
+
+ Crumble is a prototype stabilizer circuit editor.
+
+ Read the manual +
+
+
+
+
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + + + +
x
+
+ Example Circuits + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CodeStyleTaskSizeLink
Bacon-Shor CodeInterleaved (XZXZ)Memory (H)5x5x3 + Open Circuit +
Color CodeSuperdenseMemory (Z)5x7x3 + Open Circuit +
Honeycomb CodeYXZ-YZXMemory (H)7x9x3 + Open Circuit +
Honeycomb CodeYXZ-YZXMemory (V)7x9x3 + Open Circuit +
Surface CodeStandard (ИZ)Memory (V)5x5x3 + Open Circuit +
Surface Code3-CouplerMemory (V)5x5x4 + Open Circuit +
Surface CodeBiased (XZZX)Memory (V)5x5x3 + Open Circuit +
Toric CodeStandard (ZZ)Memory (ZH+ZV)6x6x3 + Open Circuit +
Color CodeSuperdenseStability (X+Z)6x4x4 + Open Circuit +
Surface CodeStandard (ИZ)Stability (Z)4x4x5 + Open Circuit +
Surface CodeStandard (ИZ)Prepare (RY)5x5x4 + Open Circuit +
Surface CodeStandard (ИZ)Surgery (MZZ)7x3x5 + Open Circuit +
+
+
+ + + diff --git a/glue/crumble/README.md b/glue/crumble/README.md index e48e3837..867d2067 100644 --- a/glue/crumble/README.md +++ b/glue/crumble/README.md @@ -124,7 +124,7 @@ button (now labelled "Hide Import/Export") again. - `ctrl+y` or `cmd+y`: Redo - `ctrl+shift+z` or `cmd+shift+z`: Redo - `ctrl+c` or `cmd+c`: Copy selection to clipboard (or entire layer if nothing selected). -- `ctrl+v` or `cmd+v`: Paste clipboard contents at current selection (or entire layer if nothing selected). +- `ctrl+v`: Paste clipboard contents at current selection (or entire layer if nothing selected). - `ctrl+x` or `cmd+x`: Cut selection to clipboard (or entire layer if nothing selected). - `f`: Reverse direction of selected two qubit gates (e.g. exchange the controls and targets of a CNOT). - `g`: Reverse order of circuit layers, from the current layer to the next empty layer. diff --git a/glue/crumble/main.js b/glue/crumble/main.js index fd0ee18c..d3afadb8 100644 --- a/glue/crumble/main.js +++ b/glue/crumble/main.js @@ -477,12 +477,6 @@ async function handleKeyboardEvent(ev) { await copyToClipboard(); return; } - if (key === 'v') { - ev.preventDefault(); - editorState.chorder.handleFocusChanged(); - await pasteFromClipboard(false); - return; - } if (key === 'x') { ev.preventDefault(); editorState.chorder.handleFocusChanged(); diff --git a/src/stim/diagram/crumble_data.cc b/src/stim/diagram/crumble_data.cc index 4b29923e..9839287e 100644 --- a/src/stim/diagram/crumble_data.cc +++ b/src/stim/diagram/crumble_data.cc @@ -808,12 +808,12 @@ std::string stim_draw_internal::make_crumble_html() { result.append(R"CRUMBLE_PART(w Map;o.set("shift+t",t=>y.De(-1,t)),o.set("t",t=>y.De(1,t)),o.set("escape",()=>{v.open?v.close():y._e()}),o.set("delete",t=>y.Ee(t)),o.set("backspace",t=>y.Ee(t)),o.set("ctrl+delete",t=>y.ke(t)),o.set("ctrl+insert",t=>y.ge(t)),o.set("ctrl+backspace",t=>y.ke(t)),o.set("ctrl+z",t=>{t||y.Nr()}),o.set("ctrl+y",t=>{t||y.Dr()}),o.set("ctrl+shift+z",t=>{t||y.Dr()}),o.set("ctrl+c",async t=>{await Ht()}),o.set("ctrl+v",Kt),o.set("ctrl+x",Qt),o.set("l",t=>{t||(y.de=new Map(y.we.entries()),y.Ae())}),o.set(" ",t=>y.Ge(t));for(let[t,r]of[["1",0],["2",1],["3",2],["4",3],["5",4],["6",5],["7",6],["8",7],["9",8],["0",9],["-",10],["=",11],["\\",12],["`",13]])o.set(""+t,t=>y.Ue(t,r)),o.set(t+"+x",t=>y.Qe(t,B.get("MARKX").M(r))),o.set(t+"+y",t=>y.Qe(t,B.get("MARKY").M(r))),o.set(t+"+z",t=>y.Qe(t,B.get("MARKZ").M(r))),o.set(t+"+d",t=>y.qe(t,r)),o.set(t+"+o",t=>y.$e(t,r)),o.set(t+"+j",t=>y.Ve(t,r)),o.set(t+"+k",t=>y.We(t,r));let r=.25;function a(t,r,e=void 0){for(var i of t){if(o.has(i))throw new Error("Chord collision: "+i);o.se)CRUMBLE_PART"); result.append(R"CRUMBLE_PART(t(i,t=>y.Qe(t,B.get(r)))}void 0!==e&&a(t.map(t=>"shift+"+t),e)}return o.set("p",t=>y.Qe(t,B.get("POLYGON"),[1,0,0,r])),o.set("alt+p",t=>y.Qe(t,B.get("POLYGON"),[0,1,0,r])),o.set("shift+p",t=>y.Qe(t,B.get("POLYGON"),[0,0,1,r])),o.set("p+x",t=>y.Qe(t,B.get("POLYGON"),[1,0,0,r])),o.set("p+y",t=>y.Qe(t,B.get("POLYGON"),[0,1,0,r])),o.set("p+z",t=>y.Qe(t,B.get("POLYGON"),[0,0,1,r])),o.set("p+x+y",t=>y.Qe(t,B.get("POLYGON"),[1,1,0,r])),o.set("p+x+z",t=>y.Qe(t,B.get("POLYGON"),[1,0,1,r])),o.set("p+y+z",t=>y.Qe(t,B.get("POLYGON"),[0,1,1,r])),o.set("p+x+y+z",t=>y.Qe(t,B.get("POLYGON"),[1,1,1,r])),o.set("m+p+x",t=>y.Qe(t,G("X".repeat(y.we.size)),[])),o.set("m+p+y",t=>y.Qe(t,G("Y".repeat(y.we.size)),[])),o.set("m+p+z",t=>y.Qe(t,G("Z".repeat(y.we.size)),[])),o.set("f",t=>y.me(t)),o.set("g",t=>y.be(t)),o.set("shift+>",t=>y.Te((t,r)=>[t+1,r],t,!1)),o.set("shift+<",t=>y.Te((t,r)=>[t-1,r],t,!1)),o.set("shift+v",t=>y.Te((t,r)=>[t,r+1],t,!1)),o.set("shift+^",t=>y.Te((t,r)=>[t,r-1],t,!1)),o.set(">",t=>y.Te((t,r)=>[t+1,r],t,!1)),)CRUMBLE_PART"); result.append(R"CRUMBLE_PART(o.set("<",t=>y.Te((t,r)=>[t-1,r],t,!1)),o.set("v",t=>y.Te((t,r)=>[t,r+1],t,!1)),o.set("^",t=>y.Te((t,r)=>[t,r-1],t,!1)),o.set(".",t=>y.Te((t,r)=>[t+.5,r+.5],t,!1)),o.set("b",t=>y.Ne(t)),a(["h","h+y","h+x+z"],"H","H"),a(["h+z","h+x+y"],"H_XY","H_XY"),a(["h+x","h+y+z"],"H_YZ","H_YZ"),a(["s+x","s+y+z"],"SQRT_X","SQRT_X_DAG"),a(["s+y","s+x+z"],"SQRT_Y","SQRT_Y_DAG"),a(["s","s+z","s+x+y"],"S","S_DAG"),a(["r+x","r+y+z"],"RX"),a(["r+y","r+x+z"],"RY"),a(["r","r+z","r+x+y"],"R"),a(["m+x","m+y+z"],"MX"),a(["m+y","m+x+z"],"MY"),a(["m","m+z","m+x+y"],"M"),a(["m+r+x","m+r+y+z"],"MRX"),a(["m+r+y","m+r+x+z"],"MRY"),a(["m+r","m+r+z","m+r+x+y"],"MR"),a(["c"],"CX","CX"),a(["c+x"],"CX","CX"),a(["c+y"],"CY","CY"),a(["c+z"],"CZ","CZ"),a(["j+x"],"X","X"),a(["j+y"],"Y","Y"),a(["j+z"],"Z","Z"),a(["c+x+y"],"XCY","XCY"),a(["alt+c+x"],"XCX","XCX"),a(["alt+c+y"],"YCY","YCY"),a(["w"],"SWAP","SWAP"),a(["w+x"],"CXSWAP",void 0),a(["c+w+x"],"CXSWAP",void 0),a(["i+w"],"ISWAP","ISWAP_DAG"),a(["w+z"],"CZSWAP",void 0),a(["c+w+z"],"CZSWAP",void 0)CRUMBLE_PART"); - result.append(R"CRUMBLE_PART(),a(["c+w"],"CZSWAP",void 0),a(["c+t"],"C_XYZ","C_ZYX"),a(["c+s+x"],"SQRT_XX","SQRT_XX_DAG"),a(["c+s+y"],"SQRT_YY","SQRT_YY_DAG"),a(["c+s+z"],"SQRT_ZZ","SQRT_ZZ_DAG"),a(["c+s"],"SQRT_ZZ","SQRT_ZZ_DAG"),a(["c+m+x"],"MXX","MXX"),a(["c+m+y"],"MYY","MYY"),a(["c+m+z"],"MZZ","MZZ"),a(["c+m"],"MZZ","MZZ"),o})();async function Bt(r){if("keydown"===r.type&&r.metaKey){if(r.repeat)return r.preventDefault(),void y.Ye.Vt();var e=r.key.toLowerCase();if("z"===e&&!r.shiftKey)return r.preventDefault(),y.Ye.Vt(),void y.Nr();if("z"===e&&r.shiftKey||"y"===e)return r.preventDefault(),y.Ye.Vt(),void y.Dr();if("c"===e)return r.preventDefault(),y.Ye.Vt(),void await Ht();if("v"===e)return r.preventDefault(),y.Ye.Vt(),void await Kt(!1);if("x"===e)return r.preventDefault(),y.Ye.Vt(),void await Qt(!1);if("backspace"===e||"delete"===e)return r.preventDefault(),y.Ye.Vt(),void y.ke(!1);if("enter"===e)return r.preventDefault(),y.Ye.Vt(),void y.ge(!1)}if(y.Ye.jt(r),"keydown"===r.type){if("q"===r.key.toLowerCase())return e=r.shiftKey?5:1,void)CRUMBLE_PART"); - result.append(R"CRUMBLE_PART( y.xe(y.Ur-e);if("e"===r.key.toLowerCase())return e=r.shiftKey?5:1,void y.xe(y.Ur+e);if("Home"===r.key)return y.xe(0),void r.preventDefault();if("End"===r.key)return y.xe(y.pe().yt.length-1),void r.preventDefault()}var t=y.Ye.Bt;if(0!==t.length){for(e=t[t.length-1];0{y.Re.set(y.ye(void 0));var t=y.Ye.qt(!1),r=window.devicePixelRatio||1,a=(o.width=o.scrollWidth*r,o.height=o.scrollHeight*r,o.getContext("2d"));a.save(),a.scale(r,r),a.clearRect(0,0,o.scrollWidth,o.scrollHeight),a.textAlign="right",a.textBaseline="middle",a.fillText("X",7.5,24.5),a.fillText("Y",7.5,56.5),a.fillText("Z",7.5,88.5),a.textAlign="center",a.textBaseli)CRUMBLE_PART"); - result.append(R"CRUMBLE_PART(ne="bottom";for(let t=0;t{try{var t,r=(()=>{var t=document.location.hash.substring(1),r=new Map;if(""!==t)for(var e of t.split("&")){var i,o=e.indexOf("=");-1!==o&&(i=e.substring(0,o),e=decodeURIComponent(e.substring(o+1)),r.set(i,e))}return r})(),e=(r.has("circuit")||("[[[DEFAULT-CIRCUIT-CONTENT-LITERAL]]]"===(t=document.getElementById("txtDefaultCircuit")).value.replaceAll("_","-")?r.set("circuit",""):r.set("circuit",t.value)),u.It(r.get("circuit"))),i=e.xt();qt.clear(i),e.yt.every(t=>t.ut())&&1===r.size&&i===r.get("circuit")?o.ii():o.oi(i,dt(i))}catch(t){throw new Error(t)}},window.addEventListener("popstate",Ct),Ct(),qt.kr().Yr().Zr(1).subscribe(t=>{o.oi(t,dt(t))})}y.Re.mr().subscribe(t=>requestAnimationFrame(()=>ht(y.canvas.getContext("2d"),t))),window.addEventListener("focus",()=>{)CRUMBLE_PART"); - result.append(R"CRUMBLE_PART(y.Ye.Vt()}),window.addEventListener("blur",()=>{y.Ye.Vt()});for(let r of document.getElementById("examples-div").querySelectorAll("a"))r.onclick=t=>{if(!(t.shiftKey||t.ctrlKey||t.metaKey||t.altKey||0!==t.button))return t=r.href.split("#circuit=")[1],y.rev.commit(t),v.close(),!1}; + result.append(R"CRUMBLE_PART(),a(["c+w"],"CZSWAP",void 0),a(["c+t"],"C_XYZ","C_ZYX"),a(["c+s+x"],"SQRT_XX","SQRT_XX_DAG"),a(["c+s+y"],"SQRT_YY","SQRT_YY_DAG"),a(["c+s+z"],"SQRT_ZZ","SQRT_ZZ_DAG"),a(["c+s"],"SQRT_ZZ","SQRT_ZZ_DAG"),a(["c+m+x"],"MXX","MXX"),a(["c+m+y"],"MYY","MYY"),a(["c+m+z"],"MZZ","MZZ"),a(["c+m"],"MZZ","MZZ"),o})();async function Bt(r){if("keydown"===r.type&&r.metaKey){if(r.repeat)return r.preventDefault(),void y.Ye.Vt();var e=r.key.toLowerCase();if("z"===e&&!r.shiftKey)return r.preventDefault(),y.Ye.Vt(),void y.Nr();if("z"===e&&r.shiftKey||"y"===e)return r.preventDefault(),y.Ye.Vt(),void y.Dr();if("c"===e)return r.preventDefault(),y.Ye.Vt(),void await Ht();if("x"===e)return r.preventDefault(),y.Ye.Vt(),void await Qt(!1);if("backspace"===e||"delete"===e)return r.preventDefault(),y.Ye.Vt(),void y.ke(!1);if("enter"===e)return r.preventDefault(),y.Ye.Vt(),void y.ge(!1)}if(y.Ye.jt(r),"keydown"===r.type){if("q"===r.key.toLowerCase())return e=r.shiftKey?5:1,void y.xe(y.Ur-e);if("e"===r.key.toLowerCase())return e=r.shiftKey?5:)CRUMBLE_PART"); + result.append(R"CRUMBLE_PART(1,void y.xe(y.Ur+e);if("Home"===r.key)return y.xe(0),void r.preventDefault();if("End"===r.key)return y.xe(y.pe().yt.length-1),void r.preventDefault()}var t=y.Ye.Bt;if(0!==t.length){for(e=t[t.length-1];0{y.Re.set(y.ye(void 0));var t=y.Ye.qt(!1),r=window.devicePixelRatio||1,a=(o.width=o.scrollWidth*r,o.height=o.scrollHeight*r,o.getContext("2d"));a.save(),a.scale(r,r),a.clearRect(0,0,o.scrollWidth,o.scrollHeight),a.textAlign="right",a.textBaseline="middle",a.fillText("X",7.5,24.5),a.fillText("Y",7.5,56.5),a.fillText("Z",7.5,88.5),a.textAlign="center",a.textBaseline="bottom";for(let t=0;t{try{var t,r=(()=>{var t=document.location.hash.substring(1),r=new Map;if(""!==t)for(var e of t.split("&")){var i,o=e.indexOf("=");-1!==o&&(i=e.substring(0,o),e=decodeURIComponent(e.substring(o+1)),r.set(i,e))}return r})(),e=(r.has("circuit")||("[[[DEFAULT-CIRCUIT-CONTENT-LITERAL]]]"===(t=document.getElementById("txtDefaultCircuit")).value.replaceAll("_","-")?r.set("circuit",""):r.set("circuit",t.value)),u.It(r.get("circuit"))),i=e.xt();qt.clear(i),e.yt.every(t=>t.ut())&&1===r.size&&i===r.get("circuit")?o.ii():o.oi(i,dt(i))}catch(t){throw new Error(t)}},window.addEventListener("popstate",Ct),Ct(),qt.kr().Yr().Zr(1).subscribe(t=>{o.oi(t,dt(t))})}y.Re.mr().subscribe(t=>requestAnimationFrame(()=>ht(y.canvas.getContext("2d"),t))),window.addEventListener("focus",()=>{y.Ye.Vt()}),window.addEventListener("blur",()=>{y.Ye.Vt()});for(l)CRUMBLE_PART"); + result.append(R"CRUMBLE_PART(et r of document.getElementById("examples-div").querySelectorAll("a"))r.onclick=t=>{if(!(t.shiftKey||t.ctrlKey||t.metaKey||t.altKey||0!==t.button))return t=r.href.split("#circuit=")[1],y.rev.commit(t),v.close(),!1}; )CRUMBLE_PART"); result.append(R"CRUMBLE_PART( )CRUMBLE_PART"); From a4382dda28b22a318852755dab795d0b8afd46fd Mon Sep 17 00:00:00 2001 From: GautamNambiar Date: Thu, 14 May 2026 19:28:06 +0800 Subject: [PATCH 08/12] Remove standalone Crumble test artifact --- crumble-mac-shortcuts.html | 317 ------------------------------------- 1 file changed, 317 deletions(-) delete mode 100644 crumble-mac-shortcuts.html diff --git a/crumble-mac-shortcuts.html b/crumble-mac-shortcuts.html deleted file mode 100644 index 03ff2052..00000000 --- a/crumble-mac-shortcuts.html +++ /dev/null @@ -1,317 +0,0 @@ - - - - - Crumble - - - - -
-
-
- Crumble is a prototype stabilizer circuit editor.
-
- Read the manual -
-
-
-
-
-
- - -
-
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- -
- - - - -
x
-
- Example Circuits - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
CodeStyleTaskSizeLink
Bacon-Shor CodeInterleaved (XZXZ)Memory (H)5x5x3 - Open Circuit -
Color CodeSuperdenseMemory (Z)5x7x3 - Open Circuit -
Honeycomb CodeYXZ-YZXMemory (H)7x9x3 - Open Circuit -
Honeycomb CodeYXZ-YZXMemory (V)7x9x3 - Open Circuit -
Surface CodeStandard (ИZ)Memory (V)5x5x3 - Open Circuit -
Surface Code3-CouplerMemory (V)5x5x4 - Open Circuit -
Surface CodeBiased (XZZX)Memory (V)5x5x3 - Open Circuit -
Toric CodeStandard (ZZ)Memory (ZH+ZV)6x6x3 - Open Circuit -
Color CodeSuperdenseStability (X+Z)6x4x4 - Open Circuit -
Surface CodeStandard (ИZ)Stability (Z)4x4x5 - Open Circuit -
Surface CodeStandard (ИZ)Prepare (RY)5x5x4 - Open Circuit -
Surface CodeStandard (ИZ)Surgery (MZZ)7x3x5 - Open Circuit -
-
-
- - - From 620ef886ae1fabc46c082bce575cdc8923de2c44 Mon Sep 17 00:00:00 2001 From: GautamNambiar Date: Thu, 14 May 2026 20:42:43 +0800 Subject: [PATCH 09/12] Support Command paste in Crumble on macOS --- glue/crumble/README.md | 2 +- glue/crumble/main.js | 80 ++++++++++++++++++++++++++ src/stim/diagram/crumble_data.cc | 98 ++++++++++++++++---------------- 3 files changed, 130 insertions(+), 50 deletions(-) diff --git a/glue/crumble/README.md b/glue/crumble/README.md index 867d2067..e48e3837 100644 --- a/glue/crumble/README.md +++ b/glue/crumble/README.md @@ -124,7 +124,7 @@ button (now labelled "Hide Import/Export") again. - `ctrl+y` or `cmd+y`: Redo - `ctrl+shift+z` or `cmd+shift+z`: Redo - `ctrl+c` or `cmd+c`: Copy selection to clipboard (or entire layer if nothing selected). -- `ctrl+v`: Paste clipboard contents at current selection (or entire layer if nothing selected). +- `ctrl+v` or `cmd+v`: Paste clipboard contents at current selection (or entire layer if nothing selected). - `ctrl+x` or `cmd+x`: Cut selection to clipboard (or entire layer if nothing selected). - `f`: Reverse direction of selected two qubit gates (e.g. exchange the controls and targets of a CNOT). - `g`: Reverse order of circuit layers, from the current layer to the next empty layer. diff --git a/glue/crumble/main.js b/glue/crumble/main.js index d3afadb8..dcf28bac 100644 --- a/glue/crumble/main.js +++ b/glue/crumble/main.js @@ -351,6 +351,8 @@ function makeChordHandlers() { } let fallbackEmulatedClipboard = undefined; +let pendingMetaPaste = false; +let pendingMetaPasteTimeout = undefined; async function copyToClipboard() { let c = editorState.copyOfCurCircuit(); c.layers = [c.layers[editorState.curLayer]] @@ -433,6 +435,64 @@ async function pasteFromClipboard(preview) { editorState.commit_or_preview(newCircuit, preview); } +/** + * @param {!string} text + * @param {!boolean} preview + */ +function pasteTextFromClipboardEvent(text, preview) { + let pastedCircuit = Circuit.fromStimCircuit(text); + if (pastedCircuit.layers.length !== 1) { + throw new Error(text); + } + let newCircuit = editorState.copyOfCurCircuit(); + if (editorState.focusedSet.size > 0) { + let [x, y] = minXY(editorState.focusedSet.values()); + pastedCircuit = pastedCircuit.shifted(x, y); + } + + // Include new coordinates. + let usedCoords = []; + for (let q = 0; q < pastedCircuit.qubitCoordData.length; q += 2) { + usedCoords.push([pastedCircuit.qubitCoordData[q], pastedCircuit.qubitCoordData[q + 1]]); + } + newCircuit = newCircuit.withCoordsIncluded(usedCoords); + let c2q = newCircuit.coordToQubitMap(); + + // Remove existing content at paste location. + for (let key of editorState.focusedSet.keys()) { + let q = c2q.get(key); + if (q !== undefined) { + newCircuit.layers[editorState.curLayer].id_pop_at(q); + } + } + + // Add content to paste location. + for (let op of pastedCircuit.layers[0].iter_gates_and_markers()) { + let newTargets = []; + for (let q of op.id_targets) { + let x = pastedCircuit.qubitCoordData[2*q]; + let y = pastedCircuit.qubitCoordData[2*q+1]; + newTargets.push(c2q.get(`${x},${y}`)); + } + newCircuit.layers[editorState.curLayer].put(new Operation( + op.gate, + op.tag, + op.args, + new Uint32Array(newTargets), + )); + } + + editorState.commit_or_preview(newCircuit, preview); +} + +function clearPendingMetaPaste() { + pendingMetaPaste = false; + if (pendingMetaPasteTimeout !== undefined) { + clearTimeout(pendingMetaPasteTimeout); + pendingMetaPasteTimeout = undefined; + } +} + async function cutToClipboard(preview) { await copyToClipboard(); if (editorState.focusedSet.size === 0) { @@ -477,6 +537,12 @@ async function handleKeyboardEvent(ev) { await copyToClipboard(); return; } + if (key === 'v') { + editorState.chorder.handleFocusChanged(); + pendingMetaPaste = true; + pendingMetaPasteTimeout = setTimeout(clearPendingMetaPaste, 1000); + return; + } if (key === 'x') { ev.preventDefault(); editorState.chorder.handleFocusChanged(); @@ -562,6 +628,20 @@ async function handleKeyboardEvent(ev) { } } +document.addEventListener('paste', ev => { + if (!pendingMetaPaste) { + return; + } + clearPendingMetaPaste(); + + let text = ev.clipboardData.getData('text/plain'); + if (text === '') { + return; + } + + ev.preventDefault(); + pasteTextFromClipboardEvent(text, false); +}); document.addEventListener('keydown', handleKeyboardEvent); document.addEventListener('keyup', handleKeyboardEvent); diff --git a/src/stim/diagram/crumble_data.cc b/src/stim/diagram/crumble_data.cc index 9839287e..022e505c 100644 --- a/src/stim/diagram/crumble_data.cc +++ b/src/stim/diagram/crumble_data.cc @@ -708,15 +708,15 @@ std::string stim_draw_internal::make_crumble_html() { )CRUMBLE_PART"); result.append(R"CRUMBLE_PART( )CRUMBLE_PART"); From 2c610da6c7c9bc4304f0c90a299433e5069f0f17 Mon Sep 17 00:00:00 2001 From: Gautam Nambiar Date: Sat, 16 May 2026 12:49:24 +0800 Subject: [PATCH 10/12] Update glue/crumble/README.md Co-authored-by: Iftach Yakar --- glue/crumble/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/glue/crumble/README.md b/glue/crumble/README.md index e48e3837..a194bbe5 100644 --- a/glue/crumble/README.md +++ b/glue/crumble/README.md @@ -119,7 +119,7 @@ button (now labelled "Hide Import/Export") again. - `ctrl+delete` or `cmd+delete`: Delete current circuit layer. - `ctrl+backspace`: Delete current circuit layer. - `ctrl+insert`: Insert empty layer at current circuit layer, pushing current circuit layer ahead in time. -- `cmd+enter`: Insert empty layer at current circuit layer, pushing current circuit layer ahead in time. +- `ctrl+insert` or`cmd+enter`: Insert empty layer at current circuit layer, pushing current circuit layer ahead in time. - `ctrl+z` or `cmd+z`: Undo - `ctrl+y` or `cmd+y`: Redo - `ctrl+shift+z` or `cmd+shift+z`: Redo From fca85a1c7ce43aa9d76db804bd321ea4b87a222f Mon Sep 17 00:00:00 2001 From: GautamNambiar Date: Sun, 17 May 2026 21:30:18 +0800 Subject: [PATCH 11/12] Address part of Crumble shortcut review feedback by getting rid of duplication --- glue/crumble/README.md | 3 +- glue/crumble/main.js | 48 +------------- src/stim/diagram/crumble_data.cc | 104 +++++++++++++++---------------- 3 files changed, 56 insertions(+), 99 deletions(-) diff --git a/glue/crumble/README.md b/glue/crumble/README.md index a194bbe5..7fa4b1fb 100644 --- a/glue/crumble/README.md +++ b/glue/crumble/README.md @@ -118,8 +118,7 @@ button (now labelled "Hide Import/Export") again. - `backspace`: Delete gates at current selection. - `ctrl+delete` or `cmd+delete`: Delete current circuit layer. - `ctrl+backspace`: Delete current circuit layer. -- `ctrl+insert`: Insert empty layer at current circuit layer, pushing current circuit layer ahead in time. -- `ctrl+insert` or`cmd+enter`: Insert empty layer at current circuit layer, pushing current circuit layer ahead in time. +- `ctrl+insert` or `cmd+enter`: Insert empty layer at current circuit layer, pushing current circuit layer ahead in time. - `ctrl+z` or `cmd+z`: Undo - `ctrl+y` or `cmd+y`: Redo - `ctrl+shift+z` or `cmd+shift+z`: Redo diff --git a/glue/crumble/main.js b/glue/crumble/main.js index dcf28bac..89646905 100644 --- a/glue/crumble/main.js +++ b/glue/crumble/main.js @@ -390,56 +390,14 @@ async function pasteFromClipboard(preview) { return; } - let pastedCircuit = Circuit.fromStimCircuit(text); - if (pastedCircuit.layers.length !== 1) { - throw new Error(text); - } - let newCircuit = editorState.copyOfCurCircuit(); - if (editorState.focusedSet.size > 0) { - let [x, y] = minXY(editorState.focusedSet.values()); - pastedCircuit = pastedCircuit.shifted(x, y); - } - - // Include new coordinates. - let usedCoords = []; - for (let q = 0; q < pastedCircuit.qubitCoordData.length; q += 2) { - usedCoords.push([pastedCircuit.qubitCoordData[q], pastedCircuit.qubitCoordData[q + 1]]); - } - newCircuit = newCircuit.withCoordsIncluded(usedCoords); - let c2q = newCircuit.coordToQubitMap(); - - // Remove existing content at paste location. - for (let key of editorState.focusedSet.keys()) { - let q = c2q.get(key); - if (q !== undefined) { - newCircuit.layers[editorState.curLayer].id_pop_at(q); - } - } - - // Add content to paste location. - for (let op of pastedCircuit.layers[0].iter_gates_and_markers()) { - let newTargets = []; - for (let q of op.id_targets) { - let x = pastedCircuit.qubitCoordData[2*q]; - let y = pastedCircuit.qubitCoordData[2*q+1]; - newTargets.push(c2q.get(`${x},${y}`)); - } - newCircuit.layers[editorState.curLayer].put(new Operation( - op.gate, - op.tag, - op.args, - new Uint32Array(newTargets), - )); - } - - editorState.commit_or_preview(newCircuit, preview); + pasteTextAtFocus(text, preview); } /** * @param {!string} text * @param {!boolean} preview */ -function pasteTextFromClipboardEvent(text, preview) { +function pasteTextAtFocus(text, preview) { let pastedCircuit = Circuit.fromStimCircuit(text); if (pastedCircuit.layers.length !== 1) { throw new Error(text); @@ -640,7 +598,7 @@ document.addEventListener('paste', ev => { } ev.preventDefault(); - pasteTextFromClipboardEvent(text, false); + pasteTextAtFocus(text, false); }); document.addEventListener('keydown', handleKeyboardEvent); document.addEventListener('keyup', handleKeyboardEvent); diff --git a/src/stim/diagram/crumble_data.cc b/src/stim/diagram/crumble_data.cc index 022e505c..0d93536d 100644 --- a/src/stim/diagram/crumble_data.cc +++ b/src/stim/diagram/crumble_data.cc @@ -708,15 +708,15 @@ std::string stim_draw_internal::make_crumble_html() { )CRUMBLE_PART"); result.append(R"CRUMBLE_PART( )CRUMBLE_PART"); From 6d3ed8f838fb7922b3226b3024ef0fb091a08072 Mon Sep 17 00:00:00 2001 From: GautamNambiar Date: Mon, 18 May 2026 08:23:51 +0800 Subject: [PATCH 12/12] Clarify shared Crumble paste helper --- glue/crumble/main.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/glue/crumble/main.js b/glue/crumble/main.js index 89646905..6c0cd979 100644 --- a/glue/crumble/main.js +++ b/glue/crumble/main.js @@ -394,6 +394,11 @@ async function pasteFromClipboard(preview) { } /** + * Applies already-read clipboard text at the current focus. + * + * Text can come from navigator.clipboard for Ctrl+V, or from a browser paste + * event for Cmd+V. Keeping this shared avoids duplicating paste behavior. + * * @param {!string} text * @param {!boolean} preview */