From 0b920aae423fb8822a88fe4290177a01a76a634e Mon Sep 17 00:00:00 2001 From: Sylvain <67423638+Le-Syl21@users.noreply.github.com> Date: Wed, 10 Jun 2026 09:53:48 +0200 Subject: [PATCH] Add modifier keys to `egui::Key` (#8127) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem winit has always delivered distinct physical variants for every keyboard key — \`KeyCode::ShiftLeft\` vs \`KeyCode::ShiftRight\`, \`KeyCode::ControlLeft\`/\`ControlRight\`, \`AltLeft\`/\`AltRight\`, \`SuperLeft\`/\`SuperRight\`, plus the ISO 102nd key \`KeyCode::IntlBackslash\` (the one between LShift and Z, labelled \`<>|\` on French AZERTY and \`\\|\` on UK QWERTY). Today none of these reach egui: - Pressing Shift / Ctrl / Alt alone produces *no* \`Event::Key\` at all. \`key_from_key_code\` and \`key_from_named_key\` both return \`None\` for modifiers, so the \`if let Some(active_key)\` branch in \`on_keyboard_input\` is skipped. The collapsed \`Modifiers\` bools are the only trace of the press, and they don't distinguish left vs right. - \`KeyCode::IntlBackslash\` has no arm in \`key_from_key_code\`, so on French / UK ISO keyboards the \`<>|\` key is completely invisible to egui apps — neither \`key\` nor \`physical_key\` is ever set. ## Who hits this - Games / kiosk frontends / pincab UIs that bind \`LeftFlipper = LShift\` vs \`RightFlipper = RShift\` (or \`LeftMagna = LCtrl\` vs \`RightMagna = RCtrl\`) — currently impossible inside egui without shelling out to platform APIs (\`device_query\`, raw X11, etc.). - Anyone on an ISO keyboard who wants to capture the 102nd key in an input-binding UI. Previously discussed: context in #2977 (closed by #3649 which added \`physical_key\`, but only for keys already in \`egui::Key\`). ## Change Two small additions, no behaviour change for existing code: **\`crates/egui/src/data/key.rs\`** — new variants at the end of \`Key\`: - \`ShiftLeft\`, \`ShiftRight\`, \`ControlLeft\`, \`ControlRight\`, \`AltLeft\`, \`AltRight\`, \`SuperLeft\`, \`SuperRight\` - \`IntlBackslash\` plus their entries in \`Key::ALL\`, \`Key::from_name\`, and \`Key::name\` (the \`key_from_name\` roundtrip test at the bottom of the file still passes). **\`crates/egui-winit/src/lib.rs\`** — new arms in \`key_from_key_code\`: \`\`\`rust KeyCode::ShiftLeft => Key::ShiftLeft, KeyCode::ShiftRight => Key::ShiftRight, // ...ControlLeft/Right, AltLeft/Right, SuperLeft/Right... KeyCode::IntlBackslash => Key::IntlBackslash, \`\`\` The existing \`Modifiers\` struct is untouched — shortcut matching (\"Ctrl+C\"), \`consume_shortcut\`, etc. still see the collapsed state. The new variants are purely additive and only surface as physical \`Event::Key\` presses when someone is specifically looking for them. ## Test - Existing \`test_key_from_name\` test still passes (updated the sentinel to \`Key::IntlBackslash as usize + 1\`). - Manual smoke test: pressing left vs right Shift, Ctrl, Alt each produces an \`Event::Key { key: Key::ShiftLeft/Right/..., physical_key: Some(Key::ShiftLeft/Right/...), ... }\`; pressing the AZERTY 102nd key yields \`Key::IntlBackslash\`. Character-key behaviour and \`Modifiers\` bools are unchanged. ## Not included - **Web backend** (\`eframe_web\`): \`PhysicalKey\` isn't fully exposed there yet per the existing \`physical_key\` docs, so these new variants are only emitted on native. Happy to extend to web in a follow-up if wanted. - \`ModifiersSymmetric\` / per-side sticky state: would be a bigger API change in \`Modifiers\`. This PR stays at the minimum: forward what winit already gives us for the event path. Closes no issue directly but addresses the underlying gap noted in the thread of #2977 (scancode forwarding) for the modifier / Intl-key subset. --- crates/egui-winit/src/lib.rs | 16 +++++++++ crates/egui/src/data/key.rs | 67 +++++++++++++++++++++++++++++++++++- 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index 59ab42222..5385eb948 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -1582,6 +1582,22 @@ fn key_from_key_code(key: winit::keyboard::KeyCode) -> Option { KeyCode::F34 => Key::F34, KeyCode::F35 => Key::F35, + // Modifier keys — egui now surfaces them as distinct physical + // variants so games / capture UIs can bind them independently. + // The collapsed `Modifiers.shift/ctrl/alt/command` booleans still + // track just the "any side is pressed" state for shortcut matching. + KeyCode::ShiftLeft => Key::ShiftLeft, + KeyCode::ShiftRight => Key::ShiftRight, + KeyCode::ControlLeft => Key::ControlLeft, + KeyCode::ControlRight => Key::ControlRight, + KeyCode::AltLeft => Key::AltLeft, + KeyCode::AltRight => Key::AltRight, + KeyCode::SuperLeft => Key::SuperLeft, + KeyCode::SuperRight => Key::SuperRight, + + // ISO 102nd key — `<>|` on French AZERTY, `\|` on UK QWERTY. + KeyCode::IntlBackslash => Key::IntlBackslash, + _ => { return None; } diff --git a/crates/egui/src/data/key.rs b/crates/egui/src/data/key.rs index 2a0f33fc3..edcc58f42 100644 --- a/crates/egui/src/data/key.rs +++ b/crates/egui/src/data/key.rs @@ -188,6 +188,38 @@ pub enum Key { /// Android sends this key on Back button press. /// Does not work on Web. BrowserBack, + + // ---------------------------------------------- + // Modifier keys (exposed as distinct left/right variants so that + // games and input-capture UIs can bind them independently). egui's + // `Modifiers` struct still collapses both sides for the common case + // (e.g. "Ctrl+C"); these variants are emitted only as physical + // `Event::Key` presses. + /// Left Shift key. + ShiftLeft, + /// Right Shift key. + ShiftRight, + /// Left Control key. + ControlLeft, + /// Right Control key. + ControlRight, + /// Left Alt / Option key. + AltLeft, + /// Right Alt / AltGr / Option key. + AltRight, + /// Left Super / Meta / Command / Windows key. + SuperLeft, + /// Right Super / Meta / Command / Windows key. + SuperRight, + + // ---------------------------------------------- + // International keys — physical positions that only exist on + // non-US keyboards. + /// ISO 102nd key: physically located between the left Shift and Z + /// on ISO layouts. On French AZERTY it produces `<>|`; on UK + /// QWERTY a secondary `\` / `|`. Missing from US ANSI keyboards. + IntlBackslash, + // When adding keys, remember to also update: // * crates/egui-winit/src/lib.rs // * Key::ALL @@ -314,6 +346,17 @@ impl Key { Self::F35, // Navigation keys: Self::BrowserBack, + // Modifier keys (physical L/R): + Self::ShiftLeft, + Self::ShiftRight, + Self::ControlLeft, + Self::ControlRight, + Self::AltLeft, + Self::AltRight, + Self::SuperLeft, + Self::SuperRight, + // International keys: + Self::IntlBackslash, ]; /// Converts `"A"` to `Key::A`, `Space` to `Key::Space`, etc. @@ -444,6 +487,17 @@ impl Key { "BrowserBack" => Self::BrowserBack, + "ShiftLeft" => Self::ShiftLeft, + "ShiftRight" => Self::ShiftRight, + "ControlLeft" => Self::ControlLeft, + "ControlRight" => Self::ControlRight, + "AltLeft" => Self::AltLeft, + "AltRight" => Self::AltRight, + "SuperLeft" => Self::SuperLeft, + "SuperRight" => Self::SuperRight, + + "IntlBackslash" => Self::IntlBackslash, + _ => return None, }) } @@ -599,6 +653,17 @@ impl Key { Self::F35 => "F35", Self::BrowserBack => "BrowserBack", + + Self::ShiftLeft => "ShiftLeft", + Self::ShiftRight => "ShiftRight", + Self::ControlLeft => "ControlLeft", + Self::ControlRight => "ControlRight", + Self::AltLeft => "AltLeft", + Self::AltRight => "AltRight", + Self::SuperLeft => "SuperLeft", + Self::SuperRight => "SuperRight", + + Self::IntlBackslash => "IntlBackslash", } } } @@ -607,7 +672,7 @@ impl Key { fn test_key_from_name() { assert_eq!( Key::ALL.len(), - Key::BrowserBack as usize + 1, + Key::IntlBackslash as usize + 1, "Some keys are missing in Key::ALL" );