From f9a6b85a83abaef7db75c70306dca7a726b44553 Mon Sep 17 00:00:00 2001 From: Nathaniel Ledford Date: Fri, 9 Oct 2026 12:44:30 -0400 Subject: [PATCH] KEY-CLIPBOARD: Ctrl+C / Ctrl+V copy and paste settings on Windows and Linux MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit egui-winit turns Ctrl+C / Ctrl+X / Ctrl+V (with any other modifiers) into clipboard events and drops the key press, so outside text fields Copy Settings, Paste Settings and ⇧Ctrl+V (Paste Selected Settings) never fired there; macOS was spared because its menu bar takes ⌘C / ⌘V first. The shortcuts now read those events as the keys they were, as the keymap editor already did when recording a shortcut. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/shortcuts.rs | 43 +++++++++++++++++------------- crates/ui-egui/src/tests_keymap.rs | 16 +++++++++++ docs/parity.md | 2 +- 3 files changed, 42 insertions(+), 19 deletions(-) diff --git a/crates/ui-egui/src/shortcuts.rs b/crates/ui-egui/src/shortcuts.rs index cd69b2daa..d86b61f98 100644 --- a/crates/ui-egui/src/shortcuts.rs +++ b/crates/ui-egui/src/shortcuts.rs @@ -260,24 +260,31 @@ pub fn parse(s: &str) -> Option<(Modifiers, Key)> { } fn matches(i: &egui::InputState, m: Modifiers, k: Key) -> bool { - i.events.iter().any(|e| match e { - egui::Event::Key { key, physical_key, pressed: true, modifiers, .. } => { - // `Ctrl` is the physical Control key (on macOS distinct from Cmd; elsewhere Cmd = Ctrl); - // "Delete" (⌫ = Backspace) also matches forward-delete - let ctrl_ok = if m.ctrl { modifiers.ctrl } else { !modifiers.ctrl || modifiers.command }; - let cmd_ok = m.ctrl || modifiers.command == m.command; - // winit can report shifted digits as punctuation (Shift+1 = `!`). Recognize the - // physical number key for culling, without changing layout-aware letter shortcuts. - let shifted_digit = m.shift - && *physical_key == Some(k) - && matches!(k, Key::Num0 | Key::Num1 | Key::Num2 | Key::Num3 | Key::Num4 | Key::Num5 | Key::Num6 | Key::Num7 | Key::Num8 | Key::Num9); - (*key == k || shifted_digit || (k == Key::Backspace && *key == Key::Delete)) - && ctrl_ok - && cmd_ok - && modifiers.shift == m.shift - && modifiers.alt == m.alt - } - _ => false, + // the windowing layer turns ⌘C / ⌘X / ⌘V (with any other modifiers held) and the keyboard's + // Copy / Cut / Paste keys into these on Windows and Linux (macOS's menu bar takes them first) + let clipboard = Modifiers { command: true, ..i.modifiers }; + i.events.iter().any(|e| { + let (key, physical_key, modifiers) = match e { + egui::Event::Key { key, physical_key, pressed: true, modifiers, .. } => (key, physical_key, modifiers), + egui::Event::Copy => (&Key::C, &None, &clipboard), + egui::Event::Cut => (&Key::X, &None, &clipboard), + egui::Event::Paste(_) => (&Key::V, &None, &clipboard), + _ => return false, + }; + // `Ctrl` is the physical Control key (on macOS distinct from Cmd; elsewhere Cmd = Ctrl); + // "Delete" (⌫ = Backspace) also matches forward-delete + let ctrl_ok = if m.ctrl { modifiers.ctrl } else { !modifiers.ctrl || modifiers.command }; + let cmd_ok = m.ctrl || modifiers.command == m.command; + // winit can report shifted digits as punctuation (Shift+1 = `!`). Recognize the + // physical number key for culling, without changing layout-aware letter shortcuts. + let shifted_digit = m.shift + && *physical_key == Some(k) + && matches!(k, Key::Num0 | Key::Num1 | Key::Num2 | Key::Num3 | Key::Num4 | Key::Num5 | Key::Num6 | Key::Num7 | Key::Num8 | Key::Num9); + (*key == k || shifted_digit || (k == Key::Backspace && *key == Key::Delete)) + && ctrl_ok + && cmd_ok + && modifiers.shift == m.shift + && modifiers.alt == m.alt }) } diff --git a/crates/ui-egui/src/tests_keymap.rs b/crates/ui-egui/src/tests_keymap.rs index a0a1e1d06..e59158b11 100644 --- a/crates/ui-egui/src/tests_keymap.rs +++ b/crates/ui-egui/src/tests_keymap.rs @@ -146,3 +146,19 @@ fn modifiers_wait_for_their_key_and_clipboard_keys_record() { // a saved modifier-only shortcut (from the bug) means no shortcut assert_eq!(crate::shortcuts::parse("Cmd+SuperLeft"), None); } + +/// On Windows and Linux the windowing layer turns Ctrl+C / Ctrl+V into clipboard events, not key +/// presses: outside a text field they still copy the edit settings, and with ⇧ open Paste Selected +/// Settings. +#[test] +fn clipboard_events_run_the_copy_and_paste_shortcuts() { + let mut h = demo(); + assert!(h.app.session.clipboard.is_none()); + h.events.push(egui::Event::Copy); + h.settle(SETTLE); + assert!(h.app.session.clipboard.is_some(), "Ctrl+C copied the edit settings"); + let shift_cmd = egui::Modifiers::SHIFT | egui::Modifiers::COMMAND; + h.events.extend([egui::Event::ModifiersChanged(shift_cmd), egui::Event::Paste("text".into())]); + h.settle(SETTLE); + assert!(matches!(h.app.ui.dialog, Some(Dialog::PasteSettings { .. })), "⇧Ctrl+V: {:?}", h.app.ui.dialog); +} diff --git a/docs/parity.md b/docs/parity.md index 4c27755b5..cee55869c 100644 --- a/docs/parity.md +++ b/docs/parity.md @@ -650,7 +650,7 @@ From `06-shortcuts.md` part 1. Evidence is our binding; conflicts are explained | KEY-GRID | Grid — G | P0 | ✅ | `cmd:view.gridToggle`, `cmd:view.photoGrid` | G toggles Photo Grid ↔ Square Grid | | KEY-INFO | Info — I | P0 | ✅ | `cmd:panel.info` | | | KEY-KEYWORDS | Keywords — K | P0 | ✅ | `cmd:panel.keywords` | | -| KEY-CLIPBOARD | Copy / paste edit settings — ⌘C / ⌘V | P0 | ✅ | `cmd:develop.copy`, `cmd:develop.paste` | ⌘X has nothing to cut outside text fields | +| KEY-CLIPBOARD | Copy / paste edit settings — ⌘C / ⌘V | P0 | ✅ | `cmd:develop.copy`, `cmd:develop.paste`, `crates/ui-egui/src/shortcuts.rs` (`matches`) | ⌘X has nothing to cut outside text fields; on Windows and Linux, where the windowing layer sends Ctrl+C / Ctrl+V as clipboard events rather than key presses, those run the shortcuts too | | KEY-UNDOREDO | Undo / redo — ⌘Z / ⇧⌘Z | P0 | ✅ | `cmd:edit.undo`, `cmd:edit.redo` | | | KEY-MINIMIZE | Minimize — ⌘M | P1 | ✅ | `apps/lightcraft/src/native_menu.rs` | native Window ▸ Minimize | | KEY-AUTO | Auto — ⇧A | P0 | ✅ | `cmd:develop.auto` | |