KEY-CLIPBOARD: Ctrl+C / Ctrl+V copy and paste settings on Windows and Linux

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 <noreply@anthropic.com>
This commit is contained in:
Nathaniel Ledford
2026-10-09 13:23:10 -04:00
co-authored by Claude Opus 5.5
parent a92c38fdd4
commit f9a6b85a83
3 changed files with 42 additions and 19 deletions
+25 -18
View File
@@ -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
})
}
+16
View File
@@ -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);
}
+1 -1
View File
@@ -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` | |