diff --git a/CHANGELOG.md b/CHANGELOG.md
index f0671c51943..a8dda6ec07b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,12 @@
# Changelog
All notable changes to this project are documented in this file.
+## [1.18.0] - Unreleased
+
+### General
+
+- Added support for physical keys to `keys` and `KeyBinding`
+
## [1.17.1] - 2026-07-07
- Fixed a panic/crash on startup when a global reads `Palette.color-scheme` (or accent-color) during its initialization.
@@ -2489,4 +2495,4 @@ as well as the [Rust migration guide for the `sixtyfps` crate](api/rs/slint/migr
[1.15.1]: https://github.com/slint-ui/slint/releases/tag/v1.15.1
[1.16.0]: https://github.com/slint-ui/slint/releases/tag/v1.16.0
[1.16.1]: https://github.com/slint-ui/slint/releases/tag/v1.16.1
-[1.17.0]: https://github.com/slint-ui/slint/releases/tag/v1.17.0
\ No newline at end of file
+[1.17.0]: https://github.com/slint-ui/slint/releases/tag/v1.17.0
diff --git a/api/cpp/cbindgen.rs b/api/cpp/cbindgen.rs
index 35f4f5f0fb3..e35c8b7dbdd 100644
--- a/api/cpp/cbindgen.rs
+++ b/api/cpp/cbindgen.rs
@@ -469,6 +469,8 @@ fn gen_corelib(
"PointerEventButton",
"PointerEvent",
"PointerScrollEvent",
+ "WindowKeyEventType",
+ "WindowKeyEvent",
"Rect",
"BitmapFont",
"DataTransferOpaque",
diff --git a/api/cpp/include/private/slint_keys.h b/api/cpp/include/private/slint_keys.h
index 9863d3dda3e..bae108a102b 100644
--- a/api/cpp/include/private/slint_keys.h
+++ b/api/cpp/include/private/slint_keys.h
@@ -18,7 +18,7 @@ class Keys;
namespace private_api {
void make_keys(Keys &out, const slint::SharedString &key, bool alt, bool control, bool shift,
- bool meta, bool ignoreShift, bool ignoreAlt);
+ bool meta, bool ignoreShift, bool ignoreAlt, bool isPhysical);
} // namespace private_api
@@ -103,7 +103,7 @@ class Keys
Keys(cbindgen_private::types::Keys &&data) : data(std::move(data)) { }
friend void private_api::make_keys(Keys &out, const slint::SharedString &key, bool alt,
bool control, bool shift, bool meta, bool ignoreShift,
- bool ignoreAlt);
+ bool ignoreAlt, bool isPhysical);
};
namespace private_api {
@@ -111,10 +111,10 @@ namespace private_api {
// We need to use Keys& out so that we can forward-declare this and then use it as a friend
// Otherwise the size of `Keys` is not yet known.
inline void make_keys(Keys &out, const slint::SharedString &key, bool alt, bool control, bool shift,
- bool meta, bool ignoreShift, bool ignoreAlt)
+ bool meta, bool ignoreShift, bool ignoreAlt, bool isPhysical)
{
::slint::cbindgen_private::types::slint_keys(&key, alt, control, shift, meta, ignoreShift,
- ignoreAlt, &out.data);
+ ignoreAlt, isPhysical, &out.data);
}
} // namespace private_api
diff --git a/api/python/slint/tests/test_named_tuples.py b/api/python/slint/tests/test_named_tuples.py
index 05f58507840..9fac9b8e3e8 100644
--- a/api/python/slint/tests/test_named_tuples.py
+++ b/api/python/slint/tests/test_named_tuples.py
@@ -7,7 +7,7 @@
NAMED_TUPLES = [
(StandardListViewItem, {"text": ""}),
- (KeyEvent, {"text": "", "modifiers": None, "repeat": False}),
+ (KeyEvent, {"text": "", "physical_key": "", "modifiers": None, "repeat": False}),
(
KeyboardModifiers,
{"shift": False, "control": False, "alt": False, "meta": False},
diff --git a/cspell.json b/cspell.json
index 2e45deb780f..02a876a62f9 100644
--- a/cspell.json
+++ b/cspell.json
@@ -5,6 +5,7 @@
{
"languageId": "rust",
"words": [
+ "Backquote",
"bbox",
"bindgen",
"boxshadowcache",
@@ -13,6 +14,7 @@
"codemap",
"concat",
"Consts",
+ "evdev", // Linux input event device key codes
"evenodd",
"FBOs", // plural of FBO (frame buffer object)
"fontdb",
diff --git a/docs/astro/src/content/docs/reference/keyboard-input/overview.mdx b/docs/astro/src/content/docs/reference/keyboard-input/overview.mdx
index 525bda9dbbe..48b95314337 100644
--- a/docs/astro/src/content/docs/reference/keyboard-input/overview.mdx
+++ b/docs/astro/src/content/docs/reference/keyboard-input/overview.mdx
@@ -2,7 +2,7 @@
title: Key Handling Overview
description: Key Handling Overview
---
-{/* cSpell: ignore Backtab */}
+{/* cSpell: ignore Backtab WASD */}
import Link from '@slint/common-files/src/components/Link.astro';
import SpecialKeys from "/src/content/docs/reference/generated/enums/_keys.md"
@@ -21,6 +21,7 @@ Slint's elements and the built-in <
This structure is generated and passed to the key press and release callbacks of the `FocusScope` element.
- **`text`** (_string_): The unicode representation of the key pressed.
+- **`physical-key`** (_string_): The physical key that was pressed, if the backend reports it. This uses the same names as `@physical-keys(...)`, for example `A`, `Digit1`, or `LeftArrow`.
- **`modifiers`** (_KeyboardModifiers_): The keyboard modifiers active at the time of the key press event.
- **`repeat`** (_bool_): This field is set to true for key press events that are repeated, i.e. the key is held down. It's always false for key release events.
@@ -29,7 +30,7 @@ Slint's elements and the built-in <
Use the inside a `FocusScope` to declare key bindings (or keyboard shortcuts).
The `KeyBinding`'s `keys` property takes an instance of the and invokes the `activated` callback when the key combination is detected.
-The `keys` type represents a key, combined with modifiers and is constructed with the `@keys` macro.
+The `keys` type represents a key, combined with modifiers and is constructed with either the `@keys` macro for logical keys or the `@physical-keys` macro for physical key positions.
Key bindings in Slint are based on **logical keys** — the character a keypress produces on the current keyboard layout — not the physical position of a key.
This means, for example, that `@keys(Control + Z)` activates when the user presses the key that produces `z` on their layout, regardless of where that key sits on the keyboard.
@@ -101,6 +102,27 @@ This will allow your users to reach the binding on almost all keyboard layouts t
As another example, `@keys(Control + Plus)` is equivalent to `@keys(Control + Shift? + "+")`.
+### Physical Key Bindings
+
+Use `@physical-keys(..)` to match a physical key position instead of the character produced on the current keyboard layout.
+For example, WASD bindings in a game should use `@physical-keys(..)` so the movement keys are always in the same position on all layouts.
+
+Physical key names reuse familiar `Key` names such as `A`, `Digit1`, `BackQuote`, or `LeftArrow`, but they refer to a physical key location on a U.S. keyboard layout, instead of logical text.
+Unlike `@keys(..)`, `@physical-keys(..)` does not currently accept string literals.
+
+```slint
+export component Example inherits Window {
+ forward-focus: scope;
+
+ scope := FocusScope {
+ KeyBinding {
+ keys: @physical-keys(Control + A);
+ activated => { debug("The physical A key was pressed"); }
+ }
+ }
+}
+```
+
## Key Namespace
The `Key` namespace contains a list of well-known logical key codes (including any non-printable characters).
diff --git a/docs/astro/src/content/docs/reference/primitive-types.mdx b/docs/astro/src/content/docs/reference/primitive-types.mdx
index b46fc805a11..81df14ae4ff 100644
--- a/docs/astro/src/content/docs/reference/primitive-types.mdx
+++ b/docs/astro/src/content/docs/reference/primitive-types.mdx
@@ -328,7 +328,7 @@ In Python, properties or struct fields of the color type are mapped to for details.
diff --git a/editors/tree-sitter-slint/grammar.js b/editors/tree-sitter-slint/grammar.js
index 949b0790ba5..2ad32997044 100644
--- a/editors/tree-sitter-slint/grammar.js
+++ b/editors/tree-sitter-slint/grammar.js
@@ -412,6 +412,7 @@ module.exports = grammar({
prec.right(
choice(
$.keys,
+ $.physical_keys,
$.parens_op,
$.index_op,
$.tr,
@@ -488,6 +489,23 @@ module.exports = grammar({
")",
),
+ // @physical-keys(...)
+ _physical_keys_entry: ($) => seq($.simple_identifier, optional("?")),
+ physical_keys: ($) =>
+ seq(
+ "@physical-keys",
+ "(",
+ optional(
+ seq(
+ $._physical_keys_entry,
+ repeat(seq(
+ "+",
+ $._physical_keys_entry
+ ))
+ )),
+ ")",
+ ),
+
member_access: ($) =>
prec.left(
17,
diff --git a/internal/backends/android-activity/androidwindowadapter.rs b/internal/backends/android-activity/androidwindowadapter.rs
index f65ce4c6ba7..8d77ef7dc41 100644
--- a/internal/backends/android-activity/androidwindowadapter.rs
+++ b/internal/backends/android-activity/androidwindowadapter.rs
@@ -17,7 +17,8 @@ use i_slint_core::api::{
use i_slint_core::input::{InternalKeyEvent, KeyEvent, KeyEventType, TouchPhase};
use i_slint_core::lengths::PhysicalEdges;
use i_slint_core::platform::{
- Key, PointerEventButton, WindowAdapter, WindowEvent, WindowProperties,
+ Key, PointerEventButton, WindowAdapter, WindowEvent, WindowKeyEvent, WindowKeyEventType,
+ WindowProperties,
};
use i_slint_core::timers::{Timer, TimerMode};
use i_slint_core::window::{InputMethodRequest, WindowInner};
@@ -680,16 +681,27 @@ fn button_for_event(
}
fn map_key_event(key_event: &android_activity::input::KeyEvent) -> Option {
- let text = map_key_code(key_event.key_code())?;
- let repeat = key_event.repeat_count() > 0;
- match key_event.action() {
- KeyAction::Down if repeat => Some(WindowEvent::KeyPressRepeated { text }),
- KeyAction::Down => Some(WindowEvent::KeyPressed { text }),
- KeyAction::Up => Some(WindowEvent::KeyReleased { text }),
- KeyAction::Multiple if repeat => Some(WindowEvent::KeyPressRepeated { text }),
- KeyAction::Multiple => Some(WindowEvent::KeyPressed { text }),
- _ => None,
+ let mut slint_event = KeyEvent::default();
+ slint_event.text = map_key_code(key_event.key_code())?;
+ slint_event.physical_key = physical_key_name(key_event.scan_code());
+ slint_event.repeat = key_event.repeat_count() > 0;
+ let event_type = match key_event.action() {
+ KeyAction::Down | KeyAction::Multiple => WindowKeyEventType::Pressed,
+ KeyAction::Up => WindowKeyEventType::Released,
+ _ => return None,
+ };
+ let slint_event = WindowKeyEvent::new(event_type, slint_event);
+ Some(WindowEvent::Key(slint_event))
+}
+
+// The Android scan code is the evdev key code, which maps to XKB with a fixed offset of 8.
+fn physical_key_name(scan_code: i32) -> SharedString {
+ if scan_code <= 0 {
+ return SharedString::default();
}
+ i_slint_common::physical_key_codes::physical_key_name_from_xkb(scan_code as u32 + 8)
+ .unwrap_or_default()
+ .into()
}
fn map_key_code(code: android_activity::input::Keycode) -> Option {
diff --git a/internal/backends/linuxkms/calloop_backend/input.rs b/internal/backends/linuxkms/calloop_backend/input.rs
index 41fd79e3c0f..5c2f6272a9f 100644
--- a/internal/backends/linuxkms/calloop_backend/input.rs
+++ b/internal/backends/linuxkms/calloop_backend/input.rs
@@ -18,7 +18,9 @@ use std::path::Path;
use std::pin::Pin;
use std::rc::Rc;
+use i_slint_common::physical_key_codes::physical_key_name_from_xkb;
use i_slint_core::api::LogicalPosition;
+use i_slint_core::input::KeyEvent;
use i_slint_core::lengths::logical_point_from_api;
use i_slint_core::platform::{PlatformError, PointerEventButton, WindowEvent};
use i_slint_core::window::{WindowAdapter, WindowInner};
@@ -318,7 +320,8 @@ impl<'a> calloop::EventSource for LibInputHandler<'a> {
},
input::Event::Keyboard(input::event::KeyboardEvent::Key(key_event)) => {
// On Linux key codes have a fixed offset of 8: https://docs.rs/xkbcommon/0.6.0/xkbcommon/xkb/struct.Keycode.html
- let key_code = xkb::Keycode::new(key_event.key() + 8);
+ let xkb_keycode = key_event.key() + 8;
+ let key_code = xkb::Keycode::new(xkb_keycode);
let state = key_event.key_state();
let xkb_key_state = self.keystate.get_or_insert_with(|| {
@@ -365,11 +368,23 @@ impl<'a> calloop::EventSource for LibInputHandler<'a> {
}
if let Some(text) = map_key_sym(sym) {
- let event = match state {
- KeyState::Pressed => WindowEvent::KeyPressed { text },
- KeyState::Released => WindowEvent::KeyReleased { text },
+ let mut key_event = KeyEvent::default();
+ key_event.text = text;
+ key_event.physical_key =
+ physical_key_name_from_xkb(xkb_keycode).unwrap_or_default().into();
+ let event_type = match state {
+ KeyState::Pressed => {
+ i_slint_core::platform::WindowKeyEventType::Pressed
+ }
+ KeyState::Released => {
+ i_slint_core::platform::WindowKeyEventType::Released
+ }
};
- window.dispatch_event_with_result(event).map_err(Self::Error::other)?;
+ let key_event =
+ i_slint_core::platform::WindowKeyEvent::new(event_type, key_event);
+ window
+ .dispatch_event_with_result(WindowEvent::Key(key_event))
+ .map_err(Self::Error::other)?;
}
}
_ => {}
diff --git a/internal/backends/qt/qt_window.rs b/internal/backends/qt/qt_window.rs
index 229c03dda73..abc42b24c93 100644
--- a/internal/backends/qt/qt_window.rs
+++ b/internal/backends/qt/qt_window.rs
@@ -32,7 +32,7 @@ use i_slint_core::lengths::{
LogicalBorderRadius, LogicalLength, LogicalPoint, LogicalRect, LogicalSize, LogicalVector,
PhysicalPx, ScaleFactor, logical_size_from_api,
};
-use i_slint_core::platform::{PlatformError, WindowEvent};
+use i_slint_core::platform::{PlatformError, WindowEvent, WindowKeyEvent, WindowKeyEventType};
use i_slint_core::string::ToSharedString;
use i_slint_core::textlayout::sharedparley::{self, GlyphRenderer, fontique, parley};
use i_slint_core::window::{
@@ -350,9 +350,11 @@ cpp! {{
return;
QString text = event->text();
int key = event->key();
+ quint32 scan_code = event->nativeScanCode();
+ quint32 virtual_key = event->nativeVirtualKey();
bool repeat = event->isAutoRepeat();
- rust!(Slint_keyPress [rust_window: &QtWindow as "void*", key: i32 as "int", text: qttypes::QString as "QString", repeat: bool as "bool"] {
- rust_window.key_event(key, text.clone(), false, repeat);
+ rust!(Slint_keyPress [rust_window: &QtWindow as "void*", key: i32 as "int", text: qttypes::QString as "QString", scan_code: u32 as "quint32", virtual_key: u32 as "quint32", repeat: bool as "bool"] {
+ rust_window.key_event(key, text.clone(), scan_code, virtual_key, false, repeat);
});
}
void keyReleaseEvent(QKeyEvent *event) override {
@@ -365,8 +367,10 @@ cpp! {{
QString text = event->text();
int key = event->key();
- rust!(Slint_keyRelease [rust_window: &QtWindow as "void*", key: i32 as "int", text: qttypes::QString as "QString"] {
- rust_window.key_event(key, text.clone(), true, false);
+ quint32 scan_code = event->nativeScanCode();
+ quint32 virtual_key = event->nativeVirtualKey();
+ rust!(Slint_keyRelease [rust_window: &QtWindow as "void*", key: i32 as "int", text: qttypes::QString as "QString", scan_code: u32 as "quint32", virtual_key: u32 as "quint32"] {
+ rust_window.key_event(key, text.clone(), scan_code, virtual_key, true, false);
});
}
@@ -2201,20 +2205,28 @@ impl QtWindow {
.unwrap_or(key_generated::Qt_DropAction_IgnoreAction)
}
- fn key_event(&self, key: i32, text: qttypes::QString, released: bool, repeat: bool) {
+ fn key_event(
+ &self,
+ key: i32,
+ text: qttypes::QString,
+ scan_code: u32,
+ virtual_key: u32,
+ released: bool,
+ repeat: bool,
+ ) {
i_slint_core::animations::update_animations();
let text: String = text.into();
- let text = qt_key_to_string(key as key_generated::Qt_Key, text);
+ let mut key_event = KeyEvent::default();
+ key_event.text = qt_key_to_string(key as key_generated::Qt_Key, text);
+ key_event.physical_key = native_code_to_physical_key(scan_code, virtual_key);
+ key_event.repeat = repeat;
- let event = if released {
- WindowEvent::KeyReleased { text }
- } else if repeat {
- WindowEvent::KeyPressRepeated { text }
- } else {
- WindowEvent::KeyPressed { text }
- };
- self.window.dispatch_event(event);
+ let event_type =
+ if released { WindowKeyEventType::Released } else { WindowKeyEventType::Pressed };
+
+ let event = WindowKeyEvent::new(event_type, key_event);
+ self.window.dispatch_event(WindowEvent::Key(event));
timer_event();
}
@@ -2934,6 +2946,49 @@ mod key_codes {
i_slint_common::for_each_keys!(define_qt_key_to_string_fn);
}
+/// On X11 and Wayland the native scan code is the XKB keycode.
+#[cfg(target_os = "linux")]
+fn native_code_to_physical_key(scan_code: u32, _virtual_key: u32) -> SharedString {
+ i_slint_common::physical_key_codes::physical_key_name_from_xkb(scan_code)
+ .unwrap_or_default()
+ .into()
+}
+
+/// Qt reports extended keys with bit 8 set; Chromium's table uses 0xe0-prefixed scan codes.
+#[cfg(target_os = "windows")]
+fn native_code_to_physical_key(scan_code: u32, _virtual_key: u32) -> SharedString {
+ // Rewrite Qt's extended-key flag (bit 8) into the table's 0xe0 prefix, e.g. 0x14d -> 0xe04d.
+ let scan_code = if scan_code & 0x100 != 0 { (scan_code - 0x100) | 0xe000 } else { scan_code };
+ macro_rules! scan_code_to_physical_key {
+ ($($name:ident # $_w:ident # $_xkb:literal # $win:literal # $($_mac:literal)?;)*) => {
+ match scan_code {
+ $($win => stringify!($name).into(),)*
+ _ => Default::default(),
+ }
+ };
+ }
+ i_slint_common::for_each_physical_keys!(scan_code_to_physical_key)
+}
+
+/// Uses the macOS virtual key
+#[cfg(target_os = "macos")]
+fn native_code_to_physical_key(_scan_code: u32, virtual_key: u32) -> SharedString {
+ macro_rules! virtual_key_to_physical_key {
+ ($($name:ident # $_w:ident # $_xkb:literal # $_win:literal # $($mac:literal)?;)*) => {
+ match virtual_key {
+ $($($mac => stringify!($name).into(),)?)*
+ _ => Default::default(),
+ }
+ };
+ }
+ i_slint_common::for_each_physical_keys!(virtual_key_to_physical_key)
+}
+
+#[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
+fn native_code_to_physical_key(_scan_code: u32, _virtual_key: u32) -> SharedString {
+ SharedString::default()
+}
+
fn qt_key_to_string(key: key_generated::Qt_Key, event_text: String) -> SharedString {
// First try to see if we received one of the non-ascii keys that we have
// a special representation for. If that fails, try to use the provided
@@ -3050,3 +3105,42 @@ fn qt_password_character() -> char {
}} as u32)
.unwrap_or('●')
}
+
+#[cfg(all(test, target_os = "linux"))]
+mod tests {
+ use super::native_code_to_physical_key;
+
+ #[test]
+ fn xkb_scan_codes_map_to_physical_keys() {
+ assert_eq!(native_code_to_physical_key(38, 0), "A");
+ assert_eq!(native_code_to_physical_key(10, 0), "Digit1");
+ assert_eq!(native_code_to_physical_key(113, 0), "LeftArrow");
+ assert_eq!(native_code_to_physical_key(202, 0), "F24");
+ assert_eq!(native_code_to_physical_key(0, 0), "");
+ }
+}
+
+#[cfg(all(test, target_os = "windows"))]
+mod tests {
+ use super::native_code_to_physical_key;
+
+ #[test]
+ fn windows_scan_codes_map_to_physical_keys() {
+ assert_eq!(native_code_to_physical_key(0x001e, 0), "A");
+ assert_eq!(native_code_to_physical_key(0x0002, 0), "Digit1");
+ assert_eq!(native_code_to_physical_key(0x014d, 0), "RightArrow");
+ assert_eq!(native_code_to_physical_key(0, 0), "");
+ }
+}
+
+#[cfg(all(test, target_os = "macos"))]
+mod tests {
+ use super::native_code_to_physical_key;
+
+ #[test]
+ fn macos_virtual_keys_map_to_physical_keys() {
+ assert_eq!(native_code_to_physical_key(1, 0x00), "A");
+ assert_eq!(native_code_to_physical_key(1, 0x7b), "LeftArrow");
+ assert_eq!(native_code_to_physical_key(1, 0x99), "");
+ }
+}
diff --git a/internal/backends/testing/internal_tests.rs b/internal/backends/testing/internal_tests.rs
index e1404e03003..67cc4ffcb9b 100644
--- a/internal/backends/testing/internal_tests.rs
+++ b/internal/backends/testing/internal_tests.rs
@@ -75,6 +75,24 @@ pub fn send_key_combo_with_text<
}
}
+/// Simulate entering a keyboard shortcut where each key has text and an optional physical key string.
+///
+/// This is needed for tests that need to exercise physical keys (`@physical-keys(...)`).
+pub fn send_key_combo_with_text_and_physical_keys<
+ X: vtable::HasStaticVTable,
+ Component: Into> + ComponentHandle,
+>(
+ component: &Component,
+ keys: &[(SharedString, SharedString)],
+) {
+ for (text, physical_key) in keys {
+ send_keyboard_key_text_with_physical_key(component, text, physical_key, true);
+ }
+ for (text, physical_key) in keys.iter().rev() {
+ send_keyboard_key_text_with_physical_key(component, text, physical_key, false);
+ }
+}
+
/// Simulate a single key event with the given text (pressed or released).
///
/// Unlike [`send_keyboard_char`], the text is dispatched as a single event,
@@ -94,6 +112,29 @@ pub fn send_keyboard_key_text<
)
}
+/// Simulate a single key event with the given text and physical key string (pressed or released).
+pub fn send_keyboard_key_text_with_physical_key<
+ X: vtable::HasStaticVTable,
+ Component: Into> + ComponentHandle,
+>(
+ component: &Component,
+ text: &SharedString,
+ physical_key: &SharedString,
+ pressed: bool,
+) {
+ use i_slint_core::input::{InternalKeyEvent, KeyEventType};
+
+ let mut key_event = i_slint_core::items::KeyEvent::default();
+ key_event.text = text.clone();
+ key_event.physical_key = physical_key.clone();
+
+ WindowInner::from_pub(component.window()).process_key_input(InternalKeyEvent {
+ event_type: if pressed { KeyEventType::KeyPressed } else { KeyEventType::KeyReleased },
+ key_event,
+ ..Default::default()
+ });
+}
+
/// Simulate entering a sequence of ascii characters key by (pressed or released).
pub fn send_keyboard_char<
X: vtable::HasStaticVTable,
diff --git a/internal/backends/winit/event_loop.rs b/internal/backends/winit/event_loop.rs
index 11300e3f4c1..f5b52fae914 100644
--- a/internal/backends/winit/event_loop.rs
+++ b/internal/backends/winit/event_loop.rs
@@ -297,8 +297,26 @@ impl winit::application::ApplicationHandler for EventLoopState {
}
i_slint_common::for_each_keys!(winit_key_to_char)
}
+
+ fn to_slint_physical_key(
+ physical_key: &winit::keyboard::PhysicalKey,
+ ) -> SharedString {
+ macro_rules! winit_physical_key_to_name {
+ ($($name:ident # $code:ident # $_xkb:literal # $_win:literal # $($_mac:literal)?;)*) => {
+ #[cfg_attr(slint_nightly_test, allow(non_exhaustive_omitted_patterns))]
+ match physical_key {
+ $(winit::keyboard::PhysicalKey::Code(winit::keyboard::KeyCode::$code) => stringify!($name).into(),)*
+ _ => Default::default(),
+ }
+ };
+ }
+
+ i_slint_common::for_each_physical_keys!(winit_physical_key_to_name)
+ }
+
#[allow(unused_mut)]
let mut text = to_slint_key(&event, &key_code);
+ let physical_key = to_slint_physical_key(&event.physical_key);
#[cfg(target_os = "windows")]
let text_without_modifiers = {
@@ -346,6 +364,7 @@ impl winit::application::ApplicationHandler for EventLoopState {
};
let mut key_event = KeyEvent::default();
key_event.text = text;
+ key_event.physical_key = physical_key;
let event = corelib::input::InternalKeyEvent {
key_event,
diff --git a/internal/backends/winit/muda.rs b/internal/backends/winit/muda.rs
index 688ce1271df..b00fee437ad 100644
--- a/internal/backends/winit/muda.rs
+++ b/internal/backends/winit/muda.rs
@@ -338,6 +338,125 @@ fn key_string_to_key(string: &str) -> muda::accelerator::Key {
i_slint_common::for_each_keys!(key_string_to_code_impl)
}
+fn physical_key_string_to_code(string: &str) -> Option {
+ use muda::accelerator::Code;
+
+ static WARN_ONCE: std::sync::Once = std::sync::Once::new();
+ WARN_ONCE.call_once(|| {
+ i_slint_core::debug_log!(
+ "Warning: Physical keys used in menu shortcuts are interpreted as logical keys based on a US keyboard layout. Use @keys(...) for menu shortcuts."
+ );
+ });
+
+ macro_rules! key_string_to_code_impl {
+ ($($name:ident # $code:ident # $_xkb:literal # $_win:literal # $($_mac:literal)?;)*) => {
+ match string {
+ $(stringify!($name) => Some(Code::$code),)*
+ _ => None,
+ }
+ };
+ }
+
+ i_slint_common::for_each_physical_keys!(key_string_to_code_impl)
+}
+
+fn code_to_logical_key(code: muda::accelerator::Code) -> muda::accelerator::Key {
+ use muda::accelerator::{Code, Key};
+ match code {
+ Code::KeyA => Key::Character("a".into()),
+ Code::KeyB => Key::Character("b".into()),
+ Code::KeyC => Key::Character("c".into()),
+ Code::KeyD => Key::Character("d".into()),
+ Code::KeyE => Key::Character("e".into()),
+ Code::KeyF => Key::Character("f".into()),
+ Code::KeyG => Key::Character("g".into()),
+ Code::KeyH => Key::Character("h".into()),
+ Code::KeyI => Key::Character("i".into()),
+ Code::KeyJ => Key::Character("j".into()),
+ Code::KeyK => Key::Character("k".into()),
+ Code::KeyL => Key::Character("l".into()),
+ Code::KeyM => Key::Character("m".into()),
+ Code::KeyN => Key::Character("n".into()),
+ Code::KeyO => Key::Character("o".into()),
+ Code::KeyP => Key::Character("p".into()),
+ Code::KeyQ => Key::Character("q".into()),
+ Code::KeyR => Key::Character("r".into()),
+ Code::KeyS => Key::Character("s".into()),
+ Code::KeyT => Key::Character("t".into()),
+ Code::KeyU => Key::Character("u".into()),
+ Code::KeyV => Key::Character("v".into()),
+ Code::KeyW => Key::Character("w".into()),
+ Code::KeyX => Key::Character("x".into()),
+ Code::KeyY => Key::Character("y".into()),
+ Code::KeyZ => Key::Character("z".into()),
+ Code::Digit0 => Key::Character("0".into()),
+ Code::Digit1 => Key::Character("1".into()),
+ Code::Digit2 => Key::Character("2".into()),
+ Code::Digit3 => Key::Character("3".into()),
+ Code::Digit4 => Key::Character("4".into()),
+ Code::Digit5 => Key::Character("5".into()),
+ Code::Digit6 => Key::Character("6".into()),
+ Code::Digit7 => Key::Character("7".into()),
+ Code::Digit8 => Key::Character("8".into()),
+ Code::Digit9 => Key::Character("9".into()),
+ Code::Backquote => Key::Character("`".into()),
+ Code::Minus => Key::Character("-".into()),
+ Code::Equal => Key::Character("=".into()),
+ Code::BracketLeft => Key::Character("[".into()),
+ Code::BracketRight => Key::Character("]".into()),
+ Code::Backslash => Key::Character("\\".into()),
+ Code::Semicolon => Key::Character(";".into()),
+ Code::Quote => Key::Character("'".into()),
+ Code::Comma => Key::Character(",".into()),
+ Code::Period => Key::Character(".".into()),
+ Code::Slash => Key::Character("/".into()),
+ Code::Space => Key::Character(" ".into()),
+ Code::Escape => Key::Escape,
+ Code::Tab => Key::Tab,
+ Code::Enter => Key::Enter,
+ Code::Backspace => Key::Backspace,
+ Code::Delete => Key::Delete,
+ Code::Insert => Key::Insert,
+ Code::Home => Key::Home,
+ Code::End => Key::End,
+ Code::PageUp => Key::PageUp,
+ Code::PageDown => Key::PageDown,
+ Code::ArrowUp => Key::ArrowUp,
+ Code::ArrowDown => Key::ArrowDown,
+ Code::ArrowLeft => Key::ArrowLeft,
+ Code::ArrowRight => Key::ArrowRight,
+ Code::ContextMenu => Key::ContextMenu,
+ Code::CapsLock => Key::CapsLock,
+ Code::ScrollLock => Key::ScrollLock,
+ Code::Pause => Key::Pause,
+ Code::F1 => Key::F1,
+ Code::F2 => Key::F2,
+ Code::F3 => Key::F3,
+ Code::F4 => Key::F4,
+ Code::F5 => Key::F5,
+ Code::F6 => Key::F6,
+ Code::F7 => Key::F7,
+ Code::F8 => Key::F8,
+ Code::F9 => Key::F9,
+ Code::F10 => Key::F10,
+ Code::F11 => Key::F11,
+ Code::F12 => Key::F12,
+ Code::F13 => Key::F13,
+ Code::F14 => Key::F14,
+ Code::F15 => Key::F15,
+ Code::F16 => Key::F16,
+ Code::F17 => Key::F17,
+ Code::F18 => Key::F18,
+ Code::F19 => Key::F19,
+ Code::F20 => Key::F20,
+ Code::F21 => Key::F21,
+ Code::F22 => Key::F22,
+ Code::F23 => Key::F23,
+ Code::F24 => Key::F24,
+ _ => Key::Unidentified,
+ }
+}
+
fn keys_to_accelerator(
keys: &i_slint_core::input::Keys,
) -> Option {
@@ -370,9 +489,13 @@ fn keys_to_accelerator(
modifiers |= Modifiers::SUPER;
}
}
- let key = key_string_to_key(&shortcut.key);
-
- Some(KeyAccelerator::new(Some(modifiers), key))
+ if shortcut.is_physical {
+ let code = physical_key_string_to_code(&shortcut.key)?;
+ Some(KeyAccelerator::new(Some(modifiers), code_to_logical_key(code)))
+ } else {
+ let key = key_string_to_key(&shortcut.key);
+ Some(KeyAccelerator::new(Some(modifiers), key))
+ }
}
fn install_event_handler_if_necessary(proxy: EventLoopProxy) {
diff --git a/internal/backends/winit/wasm_input_helper.rs b/internal/backends/winit/wasm_input_helper.rs
index 0d5b2d8146c..ce1d88a5544 100644
--- a/internal/backends/winit/wasm_input_helper.rs
+++ b/internal/backends/winit/wasm_input_helper.rs
@@ -157,6 +157,24 @@ impl WasmInputHelper {
}
}
});
+
+ fn process_key_event(
+ window: &i_slint_core::api::Window,
+ event: &web_sys::KeyboardEvent,
+ text: SharedString,
+ event_type: KeyEventType,
+ ) {
+ let mut key_event = KeyEvent::default();
+ key_event.text = text;
+ key_event.physical_key = physical_key_name(&event.code());
+ key_event.repeat = event.repeat();
+ WindowInner::from_pub(window).process_key_input(InternalKeyEvent {
+ key_event,
+ event_type,
+ ..Default::default()
+ });
+ }
+
let win = window_adapter.clone();
let shared_state2 = shared_state.clone();
h.add_event_listener("keydown", move |e: web_sys::KeyboardEvent| {
@@ -177,12 +195,7 @@ impl WasmInputHelper {
}
shared_state2.borrow_mut().has_key_down = true;
- let win_event = if e.repeat() {
- WindowEvent::KeyPressRepeated { text }
- } else {
- WindowEvent::KeyPressed { text }
- };
- window_adapter.window().dispatch_event(win_event);
+ process_key_event(window_adapter.window(), &e, text, KeyEventType::KeyPressed);
}
});
@@ -194,7 +207,7 @@ impl WasmInputHelper {
{
e.prevent_default();
shared_state2.borrow_mut().has_key_down = false;
- window_adapter.window().dispatch_event(WindowEvent::KeyReleased { text });
+ process_key_event(window_adapter.window(), &e, text, KeyEventType::KeyReleased);
}
});
@@ -326,6 +339,19 @@ fn event_text(e: &web_sys::KeyboardEvent, is_apple: bool) -> Option SharedString {
+ macro_rules! code_to_name {
+ ($($name:ident # $code:ident # $_xkb:literal # $_win:literal # $($_mac:literal)?;)*) => {
+ match code {
+ $(stringify!($code) => stringify!($name).into(),)*
+ _ => Default::default(),
+ }
+ };
+ }
+ i_slint_common::for_each_physical_keys!(code_to_name)
+}
+
scoped_tls_hkt::scoped_thread_local!(static CURRENT_WASM_CLIPBOARD_DATA : for<'a> &'a RefCell);
pub(crate) fn set_clipboard_text(data: String, clipboard: i_slint_core::platform::Clipboard) {
diff --git a/internal/common/builtin_structs.rs b/internal/common/builtin_structs.rs
index 1e91c367fdc..99c8e196021 100644
--- a/internal/common/builtin_structs.rs
+++ b/internal/common/builtin_structs.rs
@@ -102,6 +102,12 @@ macro_rules! for_each_builtin_structs {
pub struct KeyEvent {
/// The unicode representation of the key pressed.
text: SharedString,
+ /// The physical key that was pressed, if the backend can report it.
+ ///
+ /// This uses the same names as `@physical-keys(...)`, for example `A`,
+ /// `Digit1`, or `LeftArrow`. It is empty when the backend doesn't provide
+ /// physical key information.
+ physical_key: SharedString,
/// The keyboard modifiers active at the time of the key press event.
modifiers: KeyboardModifiers,
/// This field is set to true for key press events that are repeated,
diff --git a/internal/common/lib.rs b/internal/common/lib.rs
index a5dbdcb6b63..9dcc4a7dac3 100644
--- a/internal/common/lib.rs
+++ b/internal/common/lib.rs
@@ -12,6 +12,7 @@ pub mod builtin_structs;
pub mod color_parsing;
pub mod enums;
pub mod key_codes;
+pub mod physical_key_codes;
#[cfg(feature = "shared-fontique")]
pub mod sharedfontique;
diff --git a/internal/common/physical_key_codes.rs b/internal/common/physical_key_codes.rs
new file mode 100644
index 00000000000..51f188b2875
--- /dev/null
+++ b/internal/common/physical_key_codes.rs
@@ -0,0 +1,136 @@
+// Copyright © SixtyFPS GmbH
+// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
+
+//! Shared physical key names used by `@physical-keys(...)`.
+//!
+//! Currently these names are based on the US keyboard key names.
+//!
+//! This is currently in the shape:
+//!
+//! Slint # Winit # Xkb # Win # Mac
+//!
+//! `Xkb`/`Win`/`Mac` are the native key codes for:
+//! Xkb: Linux X11/Wayland
+//! Win: PS/2 set-1 scan codes for Windows with 0xe0 prefix for extended keys
+//! Mac: The virtual_key code in the Qt backend
+//!
+//! See the comment in key_codes.rs for the usage of this macro.
+
+#[macro_export]
+macro_rules! for_each_physical_keys {
+ ($macro:ident) => {
+ $macro! {
+ A # KeyA # 38 # 0x001e # 0x0000;
+ B # KeyB # 56 # 0x0030 # 0x000b;
+ C # KeyC # 54 # 0x002e # 0x0008;
+ D # KeyD # 40 # 0x0020 # 0x0002;
+ E # KeyE # 26 # 0x0012 # 0x000e;
+ F # KeyF # 41 # 0x0021 # 0x0003;
+ G # KeyG # 42 # 0x0022 # 0x0005;
+ H # KeyH # 43 # 0x0023 # 0x0004;
+ I # KeyI # 31 # 0x0017 # 0x0022;
+ J # KeyJ # 44 # 0x0024 # 0x0026;
+ K # KeyK # 45 # 0x0025 # 0x0028;
+ L # KeyL # 46 # 0x0026 # 0x0025;
+ M # KeyM # 58 # 0x0032 # 0x002e;
+ N # KeyN # 57 # 0x0031 # 0x002d;
+ O # KeyO # 32 # 0x0018 # 0x001f;
+ P # KeyP # 33 # 0x0019 # 0x0023;
+ Q # KeyQ # 24 # 0x0010 # 0x000c;
+ R # KeyR # 27 # 0x0013 # 0x000f;
+ S # KeyS # 39 # 0x001f # 0x0001;
+ T # KeyT # 28 # 0x0014 # 0x0011;
+ U # KeyU # 30 # 0x0016 # 0x0020;
+ V # KeyV # 55 # 0x002f # 0x0009;
+ W # KeyW # 25 # 0x0011 # 0x000d;
+ X # KeyX # 53 # 0x002d # 0x0007;
+ Y # KeyY # 29 # 0x0015 # 0x0010;
+ Z # KeyZ # 52 # 0x002c # 0x0006;
+
+ Digit0 # Digit0 # 19 # 0x000b # 0x001d;
+ Digit1 # Digit1 # 10 # 0x0002 # 0x0012;
+ Digit2 # Digit2 # 11 # 0x0003 # 0x0013;
+ Digit3 # Digit3 # 12 # 0x0004 # 0x0014;
+ Digit4 # Digit4 # 13 # 0x0005 # 0x0015;
+ Digit5 # Digit5 # 14 # 0x0006 # 0x0017;
+ Digit6 # Digit6 # 15 # 0x0007 # 0x0016;
+ Digit7 # Digit7 # 16 # 0x0008 # 0x001a;
+ Digit8 # Digit8 # 17 # 0x0009 # 0x001c;
+ Digit9 # Digit9 # 18 # 0x000a # 0x0019;
+
+ BackQuote # Backquote # 49 # 0x0029 # 0x0032;
+ HyphenMinus # Minus # 20 # 0x000c # 0x001b;
+ Equals # Equal # 21 # 0x000d # 0x0018;
+ OpenBracket # BracketLeft # 34 # 0x001a # 0x0021;
+ CloseBracket # BracketRight # 35 # 0x001b # 0x001e;
+ BackSlash # Backslash # 51 # 0x002b # 0x002a;
+ Semicolon # Semicolon # 47 # 0x0027 # 0x0029;
+ Quote # Quote # 48 # 0x0028 # 0x0027;
+ Comma # Comma # 59 # 0x0033 # 0x002b;
+ Period # Period # 60 # 0x0034 # 0x002f;
+ Slash # Slash # 61 # 0x0035 # 0x002c;
+ Space # Space # 65 # 0x0039 # 0x0031;
+
+ Escape # Escape # 9 # 0x0001 # 0x0035;
+ Tab # Tab # 23 # 0x000f # 0x0030;
+ Return # Enter # 36 # 0x001c # 0x0024;
+ Backspace # Backspace # 22 # 0x000e # 0x0033;
+ Delete # Delete # 119 # 0xe053 # 0x0075;
+ Insert # Insert # 118 # 0xe052 # 0x0072;
+ Home # Home # 110 # 0xe047 # 0x0073;
+ End # End # 115 # 0xe04f # 0x0077;
+ PageUp # PageUp # 112 # 0xe049 # 0x0074;
+ PageDown # PageDown # 117 # 0xe051 # 0x0079;
+ UpArrow # ArrowUp # 111 # 0xe048 # 0x007e;
+ DownArrow # ArrowDown # 116 # 0xe050 # 0x007d;
+ LeftArrow # ArrowLeft # 113 # 0xe04b # 0x007b;
+ RightArrow # ArrowRight # 114 # 0xe04d # 0x007c;
+ Menu # ContextMenu # 135 # 0xe05d # 0x006e;
+ CapsLock # CapsLock # 66 # 0x003a # 0x0039;
+ ScrollLock # ScrollLock # 78 # 0x0046 # ;
+ Pause # Pause # 127 # 0x0045 # ;
+
+ F1 # F1 # 67 # 0x003b # 0x007a;
+ F2 # F2 # 68 # 0x003c # 0x0078;
+ F3 # F3 # 69 # 0x003d # 0x0063;
+ F4 # F4 # 70 # 0x003e # 0x0076;
+ F5 # F5 # 71 # 0x003f # 0x0060;
+ F6 # F6 # 72 # 0x0040 # 0x0061;
+ F7 # F7 # 73 # 0x0041 # 0x0062;
+ F8 # F8 # 74 # 0x0042 # 0x0064;
+ F9 # F9 # 75 # 0x0043 # 0x0065;
+ F10 # F10 # 76 # 0x0044 # 0x006d;
+ F11 # F11 # 95 # 0x0057 # 0x0067;
+ F12 # F12 # 96 # 0x0058 # 0x006f;
+ F13 # F13 # 191 # 0x0064 # 0x0069;
+ F14 # F14 # 192 # 0x0065 # 0x006b;
+ F15 # F15 # 193 # 0x0066 # 0x0071;
+ F16 # F16 # 194 # 0x0067 # 0x006a;
+ F17 # F17 # 195 # 0x0068 # 0x0040;
+ F18 # F18 # 196 # 0x0069 # 0x004f;
+ F19 # F19 # 197 # 0x006a # 0x0050;
+ F20 # F20 # 198 # 0x006b # 0x005a;
+ F21 # F21 # 199 # 0x006c # ;
+ F22 # F22 # 200 # 0x006d # ;
+ F23 # F23 # 201 # 0x006e # ;
+ F24 # F24 # 202 # 0x0076 # ;
+ }
+ };
+}
+
+/// Maps an XKB keycode (the evdev keycode plus 8) to a Slint physical key name.
+///
+/// This is what X11, Wayland, and libinput-based backends report as the native key code.
+/// On Android the evdev scan code (plus 8) matches this too.
+#[cfg(any(target_os = "linux", target_os = "android"))]
+pub fn physical_key_name_from_xkb(keycode: u32) -> Option<&'static str> {
+ macro_rules! xkb_to_name {
+ ($($name:ident # $_winit:ident # $xkb:literal # $_win:literal # $($_mac:literal)?;)*) => {
+ match keycode {
+ $($xkb => Some(stringify!($name)),)*
+ _ => None,
+ }
+ };
+ }
+ for_each_physical_keys!(xkb_to_name)
+}
diff --git a/internal/compiler/builtins.slint b/internal/compiler/builtins.slint
index 2665725806d..e4ddc956f97 100644
--- a/internal/compiler/builtins.slint
+++ b/internal/compiler/builtins.slint
@@ -1322,6 +1322,8 @@ component MenuItem {
/// The keyboard shortcut for this `MenuItem`.
///
/// This property can only be set in a `MenuItem` that is part of a .
+ ///
+ /// Only shortcuts created with @keys() are supported, @physical-keys() are not supported.
in property shortcut;
/// When true, a checkmark will be shown next to the title of the `MenuItem`.
/// \default false
diff --git a/internal/compiler/expression_tree.rs b/internal/compiler/expression_tree.rs
index 97fed60fddc..9332cc5eec6 100644
--- a/internal/compiler/expression_tree.rs
+++ b/internal/compiler/expression_tree.rs
@@ -2357,7 +2357,11 @@ pub fn pretty_print(f: &mut dyn std::fmt::Write, expression: &Expression) -> std
None => write!(f, "{}.{}", e.enumeration.name, e.value),
},
Expression::Keys(keys) => {
- write!(f, "@keys({keys})")
+ if keys.is_physical {
+ write!(f, "@physical-keys({keys})")
+ } else {
+ write!(f, "@keys({keys})")
+ }
}
Expression::ReturnStatement(e) => {
write!(f, "return ")?;
diff --git a/internal/compiler/generator/cpp.rs b/internal/compiler/generator/cpp.rs
index f4a7a577513..69d1efcbca5 100644
--- a/internal/compiler/generator/cpp.rs
+++ b/internal/compiler/generator/cpp.rs
@@ -3984,11 +3984,11 @@ fn compile_expression(expr: &llr::Expression, ctx: &EvaluationContext) -> String
Expression::BoolLiteral(b) => b.to_string(),
Expression::KeysLiteral(ks) => {
format!(
- "[&](const slint::SharedString &key, bool alt, bool control, bool shift, bool meta, bool ignoreShift, bool ignoreAlt) {{
+ "[&](const slint::SharedString &key, bool alt, bool control, bool shift, bool meta, bool ignoreShift, bool ignoreAlt, bool isPhysical) {{
slint::Keys out;
- slint::private_api::make_keys(out, key, alt, control, shift, meta, ignoreShift, ignoreAlt);
+ slint::private_api::make_keys(out, key, alt, control, shift, meta, ignoreShift, ignoreAlt, isPhysical);
return out;
- }}({}, {}, {}, {}, {}, {}, {})",
+ }}({}, {}, {}, {}, {}, {}, {}, {})",
shared_string_literal(&ks.key),
ks.modifiers.alt,
ks.modifiers.control,
@@ -3996,6 +3996,7 @@ fn compile_expression(expr: &llr::Expression, ctx: &EvaluationContext) -> String
ks.modifiers.meta,
ks.ignore_shift,
ks.ignore_alt,
+ ks.is_physical,
)
}
Expression::PropertyReference(nr) => access_member(nr, ctx).get_property(),
diff --git a/internal/compiler/generator/rust.rs b/internal/compiler/generator/rust.rs
index 6973c759fea..ecbc4f448b8 100644
--- a/internal/compiler/generator/rust.rs
+++ b/internal/compiler/generator/rust.rs
@@ -3294,7 +3294,7 @@ fn compile_expression(expr: &Expression, ctx: &EvaluationContext) -> TokenStream
let s = s.as_str();
quote!(sp::SharedString::from(#s))
}
- Expression::KeysLiteral(..) => compile_keys_literal(expr),
+ Expression::KeysLiteral(keys) => compile_keys_literal(&keys),
Expression::NumberLiteral(n) => {
if n.is_nan() {
quote!(f64::NAN)
@@ -3476,8 +3476,7 @@ fn compile_expression(expr: &Expression, ctx: &EvaluationContext) -> TokenStream
}
#[inline(never)]
-fn compile_keys_literal(expr: &Expression) -> TokenStream {
- let Expression::KeysLiteral(keys) = expr else { unreachable!() };
+fn compile_keys_literal(keys: &crate::langtype::Keys) -> TokenStream {
let key = &*keys.key;
let alt = keys.modifiers.alt;
let control = keys.modifiers.control;
@@ -3485,20 +3484,22 @@ fn compile_keys_literal(expr: &Expression) -> TokenStream {
let meta = keys.modifiers.meta;
let ignore_shift = keys.ignore_shift;
let ignore_alt = keys.ignore_alt;
+ let is_physical = keys.is_physical;
quote!(
sp::make_keys(
- #key.into(),
- {
- let mut modifiers = sp::KeyboardModifiers::default();
- modifiers.alt = #alt;
- modifiers.control = #control;
- modifiers.shift = #shift;
- modifiers.meta = #meta;
- modifiers
- },
- #ignore_shift,
- #ignore_alt))
+ #key.into(),
+ {
+ let mut modifiers = sp::KeyboardModifiers::default();
+ modifiers.alt = #alt;
+ modifiers.control = #control;
+ modifiers.shift = #shift;
+ modifiers.meta = #meta;
+ modifiers
+ },
+ #ignore_shift,
+ #ignore_alt,
+ #is_physical))
}
#[inline(never)]
diff --git a/internal/compiler/langtype.rs b/internal/compiler/langtype.rs
index 6a3b4839856..bc3a3ead73e 100644
--- a/internal/compiler/langtype.rs
+++ b/internal/compiler/langtype.rs
@@ -1180,6 +1180,7 @@ pub struct Keys {
pub modifiers: KeyboardModifiers,
pub ignore_shift: bool,
pub ignore_alt: bool,
+ pub is_physical: bool,
}
impl std::fmt::Display for Keys {
@@ -1200,20 +1201,24 @@ impl std::fmt::Display for Keys {
.then_some("Shift?+")
.or(self.modifiers.shift.then_some("Shift+"))
.unwrap_or_default();
- let keycode: String = self
- .key
- .chars()
- .flat_map(|character| {
- let mut escaped = vec![];
- if character.is_control() {
- escaped.extend(character.escape_unicode());
- } else {
- escaped.push(character);
- }
- escaped
- })
- .collect();
- write!(f, "{meta}{ctrl}{alt}{shift}\"{keycode}\"")
+ if self.is_physical {
+ write!(f, "{meta}{ctrl}{alt}{shift}{}", self.key)
+ } else {
+ let keycode: String = self
+ .key
+ .chars()
+ .flat_map(|character| {
+ let mut escaped = vec![];
+ if character.is_control() {
+ escaped.extend(character.escape_unicode());
+ } else {
+ escaped.push(character);
+ }
+ escaped
+ })
+ .collect();
+ write!(f, "{meta}{ctrl}{alt}{shift}\"{keycode}\"")
+ }
}
}
}
diff --git a/internal/compiler/llr/pretty_print.rs b/internal/compiler/llr/pretty_print.rs
index 5c826913fdc..e2c40be1b5a 100644
--- a/internal/compiler/llr/pretty_print.rs
+++ b/internal/compiler/llr/pretty_print.rs
@@ -480,7 +480,11 @@ impl<'a, T> Display for DisplayExpression<'a, T> {
Expression::NumberLiteral(x) => write!(f, "{x:?}"),
Expression::BoolLiteral(x) => write!(f, "{x:?}"),
Expression::KeysLiteral(keys) => {
- write!(f, "@keys({keys})",)
+ if keys.is_physical {
+ write!(f, "@physical-keys({keys})",)
+ } else {
+ write!(f, "@keys({keys})",)
+ }
}
Expression::PropertyReference(x) => write!(f, "{}", DisplayPropertyRef(x, ctx)),
Expression::FunctionParameterReference { index } => write!(f, "arg_{index}"),
diff --git a/internal/compiler/parser.rs b/internal/compiler/parser.rs
index a096454a055..3e2e7137b9a 100644
--- a/internal/compiler/parser.rs
+++ b/internal/compiler/parser.rs
@@ -398,7 +398,7 @@ declare_syntax! {
Expression-> [ ?Expression, ?FunctionCallExpression, ?IndexExpression, ?SelfAssignment,
?ConditionalExpression, ?QualifiedName, ?BinaryExpression, ?Array, ?ObjectLiteral,
?UnaryOpExpression, ?CodeBlock, ?StringTemplate, ?AtImageUrl, ?AtGradient, ?AtTr,
- ?MemberAccess, ?AtKeys ],
+ ?MemberAccess, ?AtKeys, ?AtPhysicalKeys ],
/// Concatenate the children Expressions and StringLiteral to make a string
StringTemplate -> [*Expression],
/// `@image-url("foo.png")`
@@ -414,6 +414,8 @@ declare_syntax! {
TrPlural -> [Expression],
/// `@keys(...)`
AtKeys -> [],
+ /// `@physical-keys(...)`
+ AtPhysicalKeys -> [],
/// expression()
FunctionCallExpression -> [*Expression],
/// `expression[index]`
diff --git a/internal/compiler/parser/expressions.rs b/internal/compiler/parser/expressions.rs
index f414a9e9d62..c4314da5709 100644
--- a/internal/compiler/parser/expressions.rs
+++ b/internal/compiler/parser/expressions.rs
@@ -261,10 +261,13 @@ fn parse_at_keyword(p: &mut impl Parser) {
"keys" => {
parse_keys(p);
}
+ "physical-keys" | "physical_keys" => {
+ parse_physical_keys(p);
+ }
_ => {
p.consume();
p.test(SyntaxKind::Identifier); // consume the identifier, so that autocomplete works
- p.error("Expected 'image-url', 'tr', 'keys', 'markdown' 'conic-gradient', 'linear-gradient', or 'radial-gradient' after '@'");
+ p.error("Expected 'image-url', 'tr', 'keys', 'physical-keys', 'markdown', 'conic-gradient', 'linear-gradient', or 'radial-gradient' after '@'");
}
}
}
@@ -487,10 +490,26 @@ fn parse_markdown(p: &mut impl Parser) {
/// @keys(Control +Shift + Alt?+Meta+Return)
/// ```
fn parse_keys(p: &mut impl Parser) {
- let mut p = p.start_node(SyntaxKind::AtKeys);
+ parse_key_like_macro(p, SyntaxKind::AtKeys, "keys");
+}
+
+#[cfg_attr(test, parser_test)]
+/// ```test,AtPhysicalKeys
+/// @physical-keys(A)
+/// @physical-keys(Control + Shift + Digit1)
+/// @physical-keys(Meta + LeftArrow)
+/// @physical-keys(Alt? + BackQuote)
+/// @physical-keys(Control + PageDown)
+/// ```
+fn parse_physical_keys(p: &mut impl Parser) {
+ parse_key_like_macro(p, SyntaxKind::AtPhysicalKeys, "physical-keys");
+}
+
+fn parse_key_like_macro(p: &mut impl Parser, syntax_kind: SyntaxKind, macro_name: &str) {
+ let mut p = p.start_node(syntax_kind);
p.expect(SyntaxKind::At);
- debug_assert_eq!(p.peek().as_str(), "keys");
- p.expect(SyntaxKind::Identifier); //"keys"
+ debug_assert_eq!(p.peek().as_str(), macro_name);
+ p.expect(SyntaxKind::Identifier);
p.expect(SyntaxKind::LParent);
// Parse custom syntax here...
diff --git a/internal/compiler/passes/lower_menus.rs b/internal/compiler/passes/lower_menus.rs
index d71c702f0af..a89bd1766d0 100644
--- a/internal/compiler/passes/lower_menus.rs
+++ b/internal/compiler/passes/lower_menus.rs
@@ -663,6 +663,16 @@ fn lower_menu_items(
&*binding,
);
}
+ if in_menubar
+ && let Some(binding) = element.borrow().binding("shortcut")
+ && matches!(&binding.expression.ignore_debug_hooks(), Expression::Keys(k) if k.is_physical)
+ {
+ diag.push_error(
+ "@physical-keys(..) in MenuItem shortcuts are not supported.\nUse @keys(...) for menu shortcuts instead"
+ .into(),
+ &*binding,
+ );
+ }
if element.borrow().base_type.type_name() == Some("MenuSeparator") {
element.borrow_mut().bindings.insert(
diff --git a/internal/compiler/passes/resolving.rs b/internal/compiler/passes/resolving.rs
index f4abe98e9a6..e3c9db9604a 100644
--- a/internal/compiler/passes/resolving.rs
+++ b/internal/compiler/passes/resolving.rs
@@ -19,9 +19,11 @@ use crate::parser::{NodeOrToken, SyntaxKind, SyntaxNode, identifier_text, syntax
use crate::symbol_counters::SymbolCounters;
use crate::typeregister::TypeRegister;
use core::num::IntErrorKind;
+use i_slint_common::for_each_physical_keys;
use smol_str::{SmolStr, ToSmolStr};
use std::collections::BTreeMap;
use std::rc::Rc;
+use std::sync::LazyLock;
use unicode_segmentation::UnicodeSegmentation;
mod remove_noop;
@@ -100,6 +102,9 @@ fn resolve_expression(
SyntaxKind::AtKeys => {
Expression::from_at_keys_node(node.clone().into(), &mut lookup_ctx)
}
+ SyntaxKind::AtPhysicalKeys => {
+ Expression::from_at_physical_keys_node(node.clone().into(), &mut lookup_ctx)
+ }
_ => {
debug_assert!(diag.has_errors());
Expression::Invalid
@@ -481,6 +486,11 @@ impl Expression {
ctx.diag.slint_sc_error("@keys() expressions are", &node);
return Self::from_at_keys_node(node.into(), ctx);
}
+ SyntaxKind::AtPhysicalKeys => {
+ #[cfg(feature = "slint-sc")]
+ ctx.diag.slint_sc_error("@physical-keys() expressions are", &node);
+ return Self::from_at_physical_keys_node(node.into(), ctx);
+ }
SyntaxKind::QualifiedName => {
#[cfg(feature = "slint-sc")]
ctx.diag.slint_sc_error("Identifier references are", &node);
@@ -1486,6 +1496,77 @@ impl Expression {
Expression::Keys(keys)
}
+ pub fn from_at_physical_keys_node(
+ node: syntax_nodes::AtPhysicalKeys,
+ ctx: &mut LookupCtx,
+ ) -> Self {
+ let mut keys = langtype::Keys { is_physical: true, ..Default::default() };
+
+ let idents_and_questions: Vec<_> = node
+ .children_with_tokens()
+ .filter(|n| matches!(n.kind(), SyntaxKind::Identifier | SyntaxKind::Question))
+ .skip(1)
+ .collect();
+
+ for (index, ident_or_question) in idents_and_questions.iter().enumerate() {
+ if ident_or_question.kind() == SyntaxKind::Question {
+ continue;
+ }
+ let identifier = ident_or_question;
+
+ let is_question = || -> bool {
+ matches!(
+ idents_and_questions.get(index + 1).map(NodeOrToken::kind),
+ Some(SyntaxKind::Question)
+ )
+ };
+
+ match identifier.as_token().unwrap().text() {
+ "Alt" => {
+ if is_question() {
+ keys.ignore_alt = true;
+ } else {
+ keys.modifiers.alt = true;
+ }
+ }
+ "Control" => keys.modifiers.control = true,
+ "Meta" => keys.modifiers.meta = true,
+ "Shift" => {
+ if is_question() {
+ keys.ignore_shift = true;
+ } else {
+ keys.modifiers.shift = true;
+ }
+ }
+ key_name => {
+ if let Some(key) = lookup_physical_key(key_name) {
+ keys.key = key.into();
+ } else {
+ ctx.diag.push_error(
+ format!(
+ "{key_name} is not supported by @physical-keys\n\
+ Use a physical key name such as A, Digit1, BackQuote, or LeftArrow"
+ ),
+ identifier,
+ );
+ keys.modifiers = KeyboardModifiers::default();
+ break;
+ }
+ }
+ }
+ }
+
+ if let Some(token) = node.child_token(SyntaxKind::StringLiteral) {
+ ctx.diag.push_error(
+ "String literals are not supported in @physical-keys (use a key name such as A or LeftArrow)"
+ .into(),
+ &token,
+ );
+ }
+
+ Expression::Keys(keys)
+ }
+
/// Perform the lookup
fn from_qualified_name_node(node: syntax_nodes::QualifiedName, ctx: &mut LookupCtx) -> Self {
Self::from_lookup_result(
@@ -2088,6 +2169,27 @@ impl Expression {
use i_slint_common::key_codes::{ShiftBehavior, lookup_key_name};
+fn with_physical_key_map(
+ fun: impl FnOnce(&std::collections::HashMap<&'static str, &'static str>) -> R,
+) -> R {
+ macro_rules! generate_physical_key_map {
+ [ $($name:ident # $code:ident # $_xkb:literal # $_win:literal # $($_mac:literal)?;)* ] => {
+ {
+ [$( (stringify!($name), stringify!($name)) ),*]
+ }
+ };
+ }
+
+ static PHYSICAL_KEY_MAP: LazyLock> =
+ LazyLock::new(|| for_each_physical_keys!(generate_physical_key_map).into_iter().collect());
+
+ fun(&PHYSICAL_KEY_MAP)
+}
+
+fn lookup_physical_key(keycode: &str) -> Option<&'static str> {
+ with_physical_key_map(|map| map.get(keycode).copied())
+}
+
/// Return the type that merge two times when they are used in two branch of a condition
///
/// Ideally this could just be Expression::common_target_type_for_type_list, but that function
diff --git a/internal/compiler/tests/syntax/elements/menu-shortcuts.slint b/internal/compiler/tests/syntax/elements/menu-shortcuts.slint
index 53bea0f9db5..8e0c58dd3f7 100644
--- a/internal/compiler/tests/syntax/elements/menu-shortcuts.slint
+++ b/internal/compiler/tests/syntax/elements/menu-shortcuts.slint
@@ -7,6 +7,10 @@ export component Test inherits Window {
MenuItem {
shortcut: @keys(Control + N);
}
+ MenuItem {
+ shortcut: @physical-keys(Control + N);
+// >
+// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
+
+export component SuperSimple {
+ property <[keys]> invalid_shortcuts: [
+ // Right-side modifier keys
+ @physical-keys(AltR + A),
+// >
+// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
+
+export component SuperSimple {
+ property <[keys]> unknown_physical_keys_list: [
+ @physical-keys(a),
+// ^error{a is not supported by @physical-keys↵Use a physical key name such as A, Digit1, BackQuote, or LeftArrow}
+ @physical-keys(FooBar),
+// >
+// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
+
+export component SuperSimple {
+ property <[keys]> shortcuts: [
+ @physical-keys(A),
+ @physical-keys(Control + Shift + Digit1),
+ @physical-keys(Meta + LeftArrow),
+ @physical-keys(Alt? + BackQuote),
+ @physical-keys(Control + PageDown),
+ ];
+}
diff --git a/internal/core/api.rs b/internal/core/api.rs
index d78eea96a68..8b4d018408b 100644
--- a/internal/core/api.rs
+++ b/internal/core/api.rs
@@ -736,6 +736,17 @@ impl Window {
self.0.process_mouse_input(MouseEvent::Exit);
WindowEventDispatchResult::Accepted
}
+ crate::platform::WindowEvent::Key(event) => self
+ .0
+ .process_key_input(InternalKeyEvent {
+ event_type: match event.event_type {
+ crate::platform::WindowKeyEventType::Pressed => KeyEventType::KeyPressed,
+ crate::platform::WindowKeyEventType::Released => KeyEventType::KeyReleased,
+ },
+ key_event: event.event,
+ ..Default::default()
+ })
+ .into(),
crate::platform::WindowEvent::KeyPressed { text } => self
.0
diff --git a/internal/core/input.rs b/internal/core/input.rs
index 7da0ab385e9..75452f59212 100644
--- a/internal/core/input.rs
+++ b/internal/core/input.rs
@@ -538,9 +538,16 @@ pub fn make_keys(
modifiers: KeyboardModifiers,
ignore_shift: bool,
ignore_alt: bool,
+ is_physical: bool,
) -> Keys {
Keys {
- inner: KeysInner { key: key.to_lowercase().into(), modifiers, ignore_shift, ignore_alt },
+ inner: KeysInner {
+ key: if is_physical { key } else { key.to_lowercase().into() },
+ modifiers,
+ ignore_shift,
+ ignore_alt,
+ is_physical,
+ },
}
}
@@ -560,6 +567,7 @@ pub(crate) mod ffi {
meta: bool,
ignore_shift: bool,
ignore_alt: bool,
+ is_physical: bool,
out: &mut Keys,
) {
*out = make_keys(
@@ -567,6 +575,7 @@ pub(crate) mod ffi {
KeyboardModifiers { alt, control, shift, meta },
ignore_shift,
ignore_alt,
+ is_physical,
);
}
@@ -696,7 +705,9 @@ fn keys_from_parts_inner<'a>(
}
// Key code literals in key_codes.rs are already NFC-normalized, just lowercase.
let key: SharedString = key_char.to_lowercase().collect::().into();
- return Ok(Keys { inner: KeysInner { key, modifiers, ignore_shift, ignore_alt } });
+ return Ok(Keys {
+ inner: KeysInner { key, modifiers, ignore_shift, ignore_alt, is_physical: false },
+ });
}
// Fallback: treat as a string literal (like @keys("€"))
@@ -713,7 +724,7 @@ fn keys_from_parts_inner<'a>(
}
let key = normalize_key(key_name);
- Ok(Keys { inner: KeysInner { key, modifiers, ignore_shift, ignore_alt } })
+ Ok(Keys { inner: KeysInner { key, modifiers, ignore_shift, ignore_alt, is_physical: false } })
}
/// Internal representation of the `Keys` type.
@@ -722,8 +733,6 @@ fn keys_from_parts_inner<'a>(
#[repr(C)]
pub struct KeysInner {
/// The `key` used to trigger the shortcut
- ///
- /// Note: This is currently converted to lowercase when the shortcut is created!
pub key: SharedString,
/// `KeyboardModifier`s that need to be pressed for the shortcut to fire
pub modifiers: KeyboardModifiers,
@@ -731,6 +740,8 @@ pub struct KeysInner {
pub ignore_shift: bool,
/// Whether to ignore alt state when matching the shortcut
pub ignore_alt: bool,
+ /// Whether this shortcut matches the physical key code instead of the logical key text.
+ pub is_physical: bool,
}
impl KeysInner {
@@ -776,20 +787,30 @@ impl Keys {
if inner.ignore_alt {
expected_modifiers.alt = key_event.modifiers.alt;
}
- // Note: The shortcut's key is already in lowercase and NFC-normalized
- // (by the compiler and backends respectively), so we only need to
- // lowercase the event text. Backends are expected to NFC-normalize
- // key event text before dispatching.
- //
- // This improves our handling of CapsLock and Shift, as the event text will be in uppercase
- // if caps lock is active, even if shift is not pressed.
- let event_text = key_event.text.chars().flat_map(|character| character.to_lowercase());
+ let key_matches = if inner.is_physical {
+ key_event.physical_key == inner.key
+ } else {
+ // Note: The shortcut's key is already in lowercase and NFC-normalized
+ // (by the compiler and backends respectively), so we only need to
+ // lowercase the event text. Backends are expected to NFC-normalize
+ // key event text before dispatching.
+ //
+ // This improves our handling of CapsLock and Shift, as the event text will be in uppercase
+ // if caps lock is active, even if shift is not pressed.
+ let event_text = key_event.text.chars().flat_map(|character| character.to_lowercase());
+ event_text.eq(inner.key.chars())
+ };
- event_text.eq(inner.key.chars()) && key_event.modifiers == expected_modifiers
+ key_matches && key_event.modifiers == expected_modifiers
}
fn format_key_for_display(&self) -> crate::SharedString {
let key_str = self.inner.key.as_str();
+
+ if self.inner.is_physical {
+ return key_str.into();
+ }
+
let first_char = key_str.chars().next();
if let Some(first_char) = first_char {
@@ -912,20 +933,24 @@ impl core::fmt::Debug for Keys {
.then_some("Shift?+")
.or(inner.modifiers.shift.then_some("Shift+"))
.unwrap_or_default();
- let keycode: SharedString = inner
- .key
- .chars()
- .flat_map(|character| {
- let mut escaped = alloc::vec![];
- if character.is_control() {
- escaped.extend(character.escape_unicode());
- } else {
- escaped.push(character);
- }
- escaped
- })
- .collect();
- write!(f, "{meta}{ctrl}{alt}{shift}\"{keycode}\"")
+ if inner.is_physical {
+ write!(f, "{meta}{ctrl}{alt}{shift}{}", inner.key)
+ } else {
+ let keycode: SharedString = inner
+ .key
+ .chars()
+ .flat_map(|character| {
+ let mut escaped = alloc::vec![];
+ if character.is_control() {
+ escaped.extend(character.escape_unicode());
+ } else {
+ escaped.push(character);
+ }
+ escaped
+ })
+ .collect();
+ write!(f, "{meta}{ctrl}{alt}{shift}\"{keycode}\"")
+ }
}
}
}
@@ -2840,7 +2865,7 @@ mod tests {
_expected_linux,
) in test_cases
{
- let shortcut = make_keys(key.into(), modifiers, ignore_shift, ignore_alt);
+ let shortcut = make_keys(key.into(), modifiers, ignore_shift, ignore_alt, false);
use crate::alloc::string::ToString;
let result = shortcut.to_string();
@@ -2959,7 +2984,7 @@ mod tests {
for (desc, parts, expected_key, mods, is, ia) in cases {
let result =
Keys::from_parts(parts.iter().copied()).unwrap_or_else(|e| panic!("{desc}: {e}"));
- assert_eq!(result, make_keys((*expected_key).into(), *mods, *is, *ia), "{desc}");
+ assert_eq!(result, make_keys((*expected_key).into(), *mods, *is, *ia, false), "{desc}");
}
}
diff --git a/internal/core/platform.rs b/internal/core/platform.rs
index 2db2e7d637f..9d20873c6c5 100644
--- a/internal/core/platform.rs
+++ b/internal/core/platform.rs
@@ -10,6 +10,7 @@ The backend is the abstraction for crates that need to do the actual drawing and
use crate::SharedString;
pub use crate::api::PlatformError;
use crate::api::{LogicalPosition, LogicalSize};
+use crate::input::KeyEvent;
pub use crate::renderer::Renderer;
#[cfg(all(not(feature = "std"), feature = "unsafe-single-threaded"))]
use crate::unsafe_single_threaded::OnceCell;
@@ -356,6 +357,8 @@ pub enum WindowEvent {
///
/// Always reported as [`Accepted`](crate::api::WindowEventDispatchResult::Accepted).
PointerExited,
+ /// A key was pressed or released.
+ Key(WindowKeyEvent),
/// A key was pressed.
KeyPressed {
/// The unicode representation of the key pressed.
@@ -421,6 +424,39 @@ pub enum WindowEvent {
WindowActiveChanged(bool),
}
+/// The kind of key event delivered through [`WindowEvent::Key`].
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+#[repr(u8)]
+#[non_exhaustive]
+pub enum WindowKeyEventType {
+ /// A key was pressed.
+ Pressed,
+ /// A key was released.
+ Released,
+}
+
+/// A platform key event delivered through [`WindowEvent::Key`].
+#[derive(Clone, Debug, PartialEq)]
+#[repr(C)]
+#[non_exhaustive]
+pub struct WindowKeyEvent {
+ /// Whether the key was pressed or released.
+ pub event_type: WindowKeyEventType,
+ /// The key event details.
+ pub event: KeyEvent,
+}
+
+impl WindowKeyEvent {
+ /// Construct a new WindowKeyEvent with the given type and key event
+ ///
+ /// Note: The KeyEvent::modifiers are ignored when processing the event and are overwritten with
+ /// the current modifier state tracked by the window!
+ pub fn new(event_type: WindowKeyEventType, event: KeyEvent) -> Self {
+ // The WindowKeyEvent struct needs a constructor function, as it is #[non_exhaustive].
+ Self { event_type, event }
+ }
+}
+
impl WindowEvent {
/// The position of the cursor for this event, if any
pub fn position(&self) -> Option {
diff --git a/internal/interpreter/eval.rs b/internal/interpreter/eval.rs
index b50a235aad2..981bce3999c 100644
--- a/internal/interpreter/eval.rs
+++ b/internal/interpreter/eval.rs
@@ -530,6 +530,7 @@ pub fn eval_expression(expression: &Expression, local_context: &mut EvalLocalCon
modifiers,
ks.ignore_shift,
ks.ignore_alt,
+ ks.is_physical,
))
}
Expression::ReturnStatement(x) => {
diff --git a/tests/cases/input/physical_keys.slint b/tests/cases/input/physical_keys.slint
new file mode 100644
index 00000000000..befadfd6caa
--- /dev/null
+++ b/tests/cases/input/physical_keys.slint
@@ -0,0 +1,55 @@
+// Copyright © SixtyFPS GmbH
+// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
+
+export component TestCase inherits Window {
+ in-out property matched: "";
+ forward-focus: scope;
+
+ init => {
+ scope.focus();
+ }
+
+ scope := FocusScope {
+ KeyBinding {
+ keys: @physical-keys(Control + A);
+ activated => {
+ matched = "physical-a";
+ }
+ }
+
+ KeyBinding {
+ keys: @keys(Control + A);
+ activated => {
+ matched = "logical-a";
+ }
+ }
+ }
+}
+
+/*
+```rust
+let instance = TestCase::new().unwrap();
+use i_slint_core::SharedString;
+use i_slint_core::input::key_codes::Key;
+
+slint_testing::send_key_combo_with_text_and_physical_keys(
+ &instance,
+ &[(Key::Control.into(), SharedString::default()), ("z".into(), "A".into())],
+);
+assert_eq!(instance.get_matched(), "physical-a");
+instance.set_matched("".into());
+
+slint_testing::send_key_combo_with_text_and_physical_keys(
+ &instance,
+ &[(Key::Control.into(), SharedString::default()), ("a".into(), "Q".into())],
+);
+assert_eq!(instance.get_matched(), "logical-a");
+instance.set_matched("".into());
+
+slint_testing::send_key_combo_with_text_and_physical_keys(
+ &instance,
+ &[(Key::Control.into(), SharedString::default()), ("a".into(), SharedString::default())],
+);
+assert_eq!(instance.get_matched(), "logical-a");
+```
+*/
diff --git a/tools/lsp/language/completion.rs b/tools/lsp/language/completion.rs
index a7548a9ceb8..d4e61451171 100644
--- a/tools/lsp/language/completion.rs
+++ b/tools/lsp/language/completion.rs
@@ -286,8 +286,10 @@ pub(crate) fn completion_at(
return with_lookup_ctx(document_cache, node, Some(offset), |ctx| {
resolve_expression_scope(ctx, document_cache, snippet_support)
})?;
- } else if node.kind() == SyntaxKind::AtKeys {
- return with_lookup_ctx(document_cache, node, Some(offset), at_keys_completions);
+ } else if matches!(node.kind(), SyntaxKind::AtKeys | SyntaxKind::AtPhysicalKeys) {
+ return with_lookup_ctx(document_cache, node.clone(), Some(offset), |ctx| {
+ at_key_like_completions(ctx, node.kind() == SyntaxKind::AtPhysicalKeys)
+ });
} else if let Some(q) = syntax_nodes::QualifiedName::new(node.clone()) {
match q.parent()?.kind() {
SyntaxKind::Element => {
@@ -1447,6 +1449,7 @@ fn macro_completions(token: SyntaxToken, snippet_support: bool) -> Option Option Vec {
- let keys = i_slint_compiler::lookup::KeysLookup;
-
+fn at_key_like_completions(ctx: &mut LookupCtx, physical: bool) -> Vec {
let mut completions = Vec::new();
- keys.for_each_entry(ctx, &mut |label, _expr| -> Option<()> {
+ let mut push_key = |label: &str| {
completions.push(
CompletionItem::new_simple(label.to_string(), "".into())
.with_kind(CompletionItemKind::ENUM_MEMBER),
);
- None
- });
+ };
+ if physical {
+ macro_rules! add_physical_key_completions {
+ ($($name:ident # $code:ident # $_xkb:literal # $_win:literal # $($_mac:literal)?;)*) => {
+ $(push_key(stringify!($name));)*
+ };
+ }
+ i_slint_common::for_each_physical_keys!(add_physical_key_completions);
+ for modifier in ["Control", "Alt", "Shift", "Meta"] {
+ push_key(modifier);
+ }
+ } else {
+ let keys = i_slint_compiler::lookup::KeysLookup;
+ keys.for_each_entry(ctx, &mut |label, _expr| -> Option<()> {
+ push_key(label);
+ None
+ });
+ }
for modifier in ["Shift", "Alt"] {
completions.push(
CompletionItem::new_simple(
@@ -2260,6 +2277,31 @@ mod tests {
assert_completions_found(expected, &res);
}
+ #[test]
+ fn physical_key_completion() {
+ let source = r#"
+ component Foo {
+ property shortcut: @physical-keys(🔺);
+ }
+ "#;
+ let res = get_completions(source).unwrap();
+ for label in [
+ "A",
+ "Digit1",
+ "BackQuote",
+ "LeftArrow",
+ "Shift?",
+ "Alt?",
+ "Control",
+ "Alt",
+ "Shift",
+ "Meta",
+ ] {
+ res.iter().find(|ci| ci.label == label).unwrap();
+ }
+ assert!(!res.iter().any(|ci| ci.label == "Plus"));
+ }
+
#[test]
fn changed_completion() {
let source1 = "{ property xyz; changed 🔺 => {} } ";