From 4aec90b39e0660e42714814bf10ef4d50efb708f Mon Sep 17 00:00:00 2001 From: GautamNambiar Date: Wed, 13 May 2026 00:50:17 +0800 Subject: [PATCH 01/13] 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 1e15490f09f24bfdfe45747d63859d07b827bf3a Mon Sep 17 00:00:00 2001 From: GautamNambiar Date: Wed, 13 May 2026 00:53:42 +0800 Subject: [PATCH 02/13] 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 b57441589f58fd8958b020a4c70f9895dff26541 Mon Sep 17 00:00:00 2001 From: GautamNambiar Date: Wed, 13 May 2026 01:00:06 +0800 Subject: [PATCH 03/13] 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 0f3572252fe8e344a1dda8ed3891c7bcda619f45 Mon Sep 17 00:00:00 2001 From: GautamNambiar Date: Wed, 13 May 2026 01:03:06 +0800 Subject: [PATCH 04/13] 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 ff24cc89828d6726434fc0f481091a88fab68534 Mon Sep 17 00:00:00 2001 From: GautamNambiar Date: Wed, 13 May 2026 15:25:20 +0800 Subject: [PATCH 05/13] 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 1d7454dcf7dc93d208e509e4cf88b9811669c98d Mon Sep 17 00:00:00 2001 From: GautamNambiar Date: Wed, 13 May 2026 15:44:40 +0800 Subject: [PATCH 06/13] 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 4b02bb1d80d172d091fad40e2cefe09c586c2819 Mon Sep 17 00:00:00 2001 From: GautamNambiar Date: Wed, 13 May 2026 19:50:52 +0800 Subject: [PATCH 07/13] 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 294959b5af904234b95d02f2807a74855992ce9e Mon Sep 17 00:00:00 2001 From: GautamNambiar Date: Thu, 14 May 2026 19:28:06 +0800 Subject: [PATCH 08/13] 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 34ae69c8bd1491a3bedfd09c444d23909021c01a Mon Sep 17 00:00:00 2001 From: GautamNambiar Date: Thu, 14 May 2026 20:42:43 +0800 Subject: [PATCH 09/13] 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 fb13e1b1203e681027ff7e513553685f47eff4d0 Mon Sep 17 00:00:00 2001 From: Gautam Nambiar Date: Sat, 16 May 2026 12:49:24 +0800 Subject: [PATCH 10/13] 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 a0bb7b90be67de8dfc4213fe16ed6ce4f0851c8b Mon Sep 17 00:00:00 2001 From: GautamNambiar Date: Sun, 17 May 2026 21:30:18 +0800 Subject: [PATCH 11/13] 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 9ab9beee6c219bbe3c2d58b67391a3dad1c5e51f Mon Sep 17 00:00:00 2001 From: GautamNambiar Date: Mon, 18 May 2026 08:23:51 +0800 Subject: [PATCH 12/13] 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 */ From b83b1942d3938b52fadbce7b81da0adc2e37b725 Mon Sep 17 00:00:00 2001 From: Gautam Nambiar Date: Sun, 7 Jun 2026 00:38:09 +0800 Subject: [PATCH 13/13] Regenerate Crumble embedded resource --- src/stim/diagram/crumble_data.cc | 182 +++++++++++++++---------------- 1 file changed, 91 insertions(+), 91 deletions(-) diff --git a/src/stim/diagram/crumble_data.cc b/src/stim/diagram/crumble_data.cc index 0d93536d..9ebe87ca 100644 --- a/src/stim/diagram/crumble_data.cc +++ b/src/stim/diagram/crumble_data.cc @@ -708,57 +708,57 @@ std::string stim_draw_internal::make_crumble_html() { )CRUMBLE_PART"); result.append(R"CRUMBLE_PART( )CRUMBLE_PART");