Skip to content
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
8d0f5ab
input: add @physical-keys for physical shortcuts
amirHdev Apr 21, 2026
ff56fbc
platform: avoid key event type name collisions
amirHdev Apr 21, 2026
95d8887
winit: warn and map physical keys in menu accelerators
amirHdev May 4, 2026
3b62989
[autofix.ci] apply automated fixes
autofix-ci[bot] May 4, 2026
addbb72
platform: reuse KeyEvent in WindowEvent::Key
amirHdev May 6, 2026
cbd3def
input: address physical key shortcut review feedback
amirHdev Jun 2, 2026
c19cbf5
input: adapt physical keys to current runtime
amirHdev Jun 2, 2026
367a9ba
ci: allow Backquote key spelling
amirHdev Jun 2, 2026
2d76209
input: mark parsed shortcut tests as logical keys
amirHdev Jun 2, 2026
5aab4cc
lsp: complete physical key modifiers
amirHdev Jun 3, 2026
29733f5
Fix conflicts after rebasing onto 20f2e2ae
LeonMatthes Jul 20, 2026
8bd1424
Add Changelog entry
LeonMatthes Jul 20, 2026
f4683a4
Qt backend: Add physical keys support on Linux
LeonMatthes Jul 20, 2026
77cca09
Qt: Add physical key codes for macOS/Windows
LeonMatthes Jul 20, 2026
3f98e62
Align physical key codes table
LeonMatthes Jul 20, 2026
6907839
LinuxKMS: Add physical keys support
LeonMatthes Jul 21, 2026
73d2a6d
Winit WASM: Add physical key support
LeonMatthes Jul 21, 2026
d24694d
Android: Add physical keys support
LeonMatthes Jul 21, 2026
140985a
Fix comment being picked up as doc test.
LeonMatthes Jul 22, 2026
6502f4e
Move Physical keys docs above code example
LeonMatthes Jul 22, 2026
9e85f40
Incorporate review feedback by @tronical
LeonMatthes Jul 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
[1.17.0]: https://github.com/slint-ui/slint/releases/tag/v1.17.0
2 changes: 2 additions & 0 deletions api/cpp/cbindgen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,8 @@ fn gen_corelib(
"PointerEventButton",
"PointerEvent",
"PointerScrollEvent",
"WindowKeyEventType",
"WindowKeyEvent",
"Rect",
"BitmapFont",
"DataTransferOpaque",
Expand Down
8 changes: 4 additions & 4 deletions api/cpp/include/private/slint_keys.h
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -103,18 +103,18 @@ 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 {

// 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
Expand Down
2 changes: 1 addition & 1 deletion api/python/slint/tests/test_named_tuples.py
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down
2 changes: 2 additions & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
{
"languageId": "rust",
"words": [
"Backquote",
"bbox",
"bindgen",
"boxshadowcache",
Expand All @@ -13,6 +14,7 @@
"codemap",
"concat",
"Consts",
"evdev", // Linux input event device key codes
"evenodd",
"FBOs", // plural of FBO (frame buffer object)
"fontdb",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Slint's <Link type="KeyBinding" label="KeyBinding"/> 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.

Expand All @@ -29,7 +30,7 @@ Slint's <Link type="KeyBinding" label="KeyBinding"/> elements and the built-in <
Use the <Link type="KeyBinding" label="KeyBinding element"/> inside a `FocusScope` to declare key bindings (or keyboard shortcuts).
The `KeyBinding`'s `keys` property takes an instance of the <Link label="keys type" type="keys"/> 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.
Expand Down Expand Up @@ -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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps here it would also be worthwhile to mention the menu limitation? (or is that overdoing it?)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added that onto the docs of the MenuItem.shortcut property now. I think it's unnecessary here, but definitely makes sense there.


If you want to match a physical key position instead of the character produced on the current keyboard layout, use `@physical-keys(..)`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest to flip it around: "Use @physical-keys(..) to match ..." . Also saves you the "want" :)

Could you include an example (use-case, not code) as well, perhaps? (games?)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done (Added WASD as example)


Physical key names reuse familiar `Key` names such as `A`, `Digit1`, `BackQuote`, or `LeftArrow`, but they refer to a physical key location instead of logical text.
Today these names refer to positions on a U.S. keyboard layout.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After yesterday's discussion I feel even more that "Today" should be omitted.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, and reworded it to make it shorter.

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).
Expand Down
2 changes: 1 addition & 1 deletion docs/astro/src/content/docs/reference/primitive-types.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ In Python, properties or struct fields of the color type are mapped to <LangRefL
A keys represents a key combined with a list of modifiers.
This is the primitive type used to detect if a KeyEvent should trigger a given key binding.

Key bindings in Slint are based on **logical keys** — the character a key produces on the current keyboard layout — not the physical position of a key on the keyboard.
Key bindings in Slint can use **logical keys** via `@keys(...)` or **physical keys** via `@physical-keys(...)`.
See <Link type="KeyBindingOverview" label="Key Bindings" /> for details.

<Tabs syncKey="dev-language">
Expand Down
18 changes: 18 additions & 0 deletions editors/tree-sitter-slint/grammar.js
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,7 @@ module.exports = grammar({
prec.right(
choice(
$.keys,
$.physical_keys,
$.parens_op,
$.index_op,
$.tr,
Expand Down Expand Up @@ -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,
Expand Down
32 changes: 22 additions & 10 deletions internal/backends/android-activity/androidwindowadapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -680,16 +681,27 @@ fn button_for_event(
}

fn map_key_event(key_event: &android_activity::input::KeyEvent) -> Option<WindowEvent> {
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<SharedString> {
Expand Down
25 changes: 20 additions & 5 deletions internal/backends/linuxkms/calloop_backend/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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(|| {
Expand Down Expand Up @@ -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)?;
}
}
_ => {}
Expand Down
Loading
Loading