From 69b3ba1f234521f9d773b3d49117dbae7edaa4fb 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 6a14d4523..195a1a75a 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 faf34c4757850286ea590a0536ed169595819c30 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 0334b2f75..557064dff 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 459f3880930dbf737012b45e8d578b604b0f86ec 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 f3b0d61c3..e48e38379 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 d9d69a99e179b87b272f274117f2e14b0366a36c 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 ebb6189a6..5415f119d 100644 --- a/glue/crumble/crumble.html +++ b/glue/crumble/crumble.html @@ -118,8 +118,8 @@
- - + +
@@ -134,8 +134,8 @@
- - + +
From 3870a7527861b6ad0cf8d2148d8c1d313b596fa2 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 195a1a75a..fd0ee18ce 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 372beba1180a7f39b6f2eacc59eae332f9b68dcf 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 0d11b3075..4b29923e8 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 317970226807ed79bd548d9052229fe78dc22f6a 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 000000000..03ff20529 --- /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 e48e38379..867d2067c 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 fd0ee18ce..d3afadb80 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 4b29923e8..9839287e9 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 dd7e99743a8f246827c8ebc2f2827aa1d9cbe615 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 03ff20529..000000000 --- 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 1301f743e6aa12bf66d86f43975543b7ec2150f4 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 867d2067c..e48e38379 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 d3afadb80..dcf28bac1 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 9839287e9..022e505cc 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 69c53deca505a38c0c8e7ec6bd9dff7866f29527 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 e48e38379..a194bbe5b 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 101571c39ce706555d63768b16064137b81cab32 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 a194bbe5b..7fa4b1fb7 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 dcf28bac1..896469059 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 022e505cc..0d93536d6 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 5186c9a4bbe8d501c083c3ef54740663b5c89ce9 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 896469059..6c0cd979c 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 fcd8546dc2c7b2f20d97a94ad6931d6e6cb139d3 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 0d93536d6..9ebe87ca0 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");