Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
31 changes: 31 additions & 0 deletions internal/backends/qt/qt_window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2448,6 +2448,37 @@ fn qt_key_to_string(key: key_generated::Qt_Key, event_text: String) -> SharedStr
key_generated::Qt_Key_Key_Bar => "|",
key_generated::Qt_Key_Key_BraceRight => "}",
key_generated::Qt_Key_Key_AsciiTilde => "~",
// Dead keys: map to their corresponding accent/diacritical characters so that
// FocusScope and Shortcut can match them. See #10862.
key_generated::Qt_Key_Key_Dead_Grave => "`",
key_generated::Qt_Key_Key_Dead_Acute => "\u{00b4}",
key_generated::Qt_Key_Key_Dead_Circumflex => "^",
key_generated::Qt_Key_Key_Dead_Tilde => "~",
key_generated::Qt_Key_Key_Dead_Macron => "\u{00af}",
key_generated::Qt_Key_Key_Dead_Breve => "\u{02d8}",
key_generated::Qt_Key_Key_Dead_Abovedot => "\u{02d9}",
key_generated::Qt_Key_Key_Dead_Diaeresis => "\u{00a8}",
key_generated::Qt_Key_Key_Dead_Abovering => "\u{02da}",
key_generated::Qt_Key_Key_Dead_Doubleacute => "\u{02dd}",
key_generated::Qt_Key_Key_Dead_Caron => "\u{02c7}",
key_generated::Qt_Key_Key_Dead_Cedilla => "\u{00b8}",
key_generated::Qt_Key_Key_Dead_Ogonek => "\u{02db}",
key_generated::Qt_Key_Key_Dead_Iota => "\u{0269}",
key_generated::Qt_Key_Key_Dead_Belowdot => "\u{0323}",
key_generated::Qt_Key_Key_Dead_Hook => "\u{0309}",
key_generated::Qt_Key_Key_Dead_Horn => "\u{031b}",
key_generated::Qt_Key_Key_Dead_Stroke => "\u{0338}",
key_generated::Qt_Key_Key_Dead_Lowline => "_",
key_generated::Qt_Key_Key_Dead_a => "a",
key_generated::Qt_Key_Key_Dead_A => "A",
key_generated::Qt_Key_Key_Dead_e => "e",
key_generated::Qt_Key_Key_Dead_E => "E",
key_generated::Qt_Key_Key_Dead_i => "i",
key_generated::Qt_Key_Key_Dead_I => "I",
key_generated::Qt_Key_Key_Dead_o => "o",
key_generated::Qt_Key_Key_Dead_O => "O",
key_generated::Qt_Key_Key_Dead_u => "u",
key_generated::Qt_Key_Key_Dead_U => "U",
_ => "",
}
.into()
Expand Down
5 changes: 5 additions & 0 deletions internal/backends/winit/event_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,11 @@ impl winit::application::ApplicationHandler<SlintEvent> for EventLoopState {
=> $char.into(),
)* )? )*
winit::keyboard::Key::Character(str) => str.as_str().into(),
winit::keyboard::Key::Dead(Some(c)) => {
let mut buf = [0u8; 4];
let s: &str = c.encode_utf8(&mut buf);
s.into()
}
_ => {
if let Some(text) = &event.text {
text.as_str().into()
Expand Down
31 changes: 20 additions & 11 deletions internal/core/items/input_items.rs
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,21 @@ impl FocusScope {

None
}

/// Processes any shortcuts for this key event - returns whether any Shortcut handled the key
/// event.
fn handle_shortcuts(self: Pin<&Self>, self_rc: &ItemRc, event: &KeyEvent) -> bool {
let shortcut = self.visit_shortcuts(self_rc, |shortcut| {
let keys = Shortcut::FIELD_OFFSETS.keys.apply_pin(shortcut.as_pin_ref()).get();
if keys.matches(&event) { Some(VRcMapped::clone(shortcut)) } else { None }
});
if let Some(shortcut) = shortcut {
Shortcut::FIELD_OFFSETS.activated.apply_pin(shortcut.as_pin_ref()).call(&());
true
} else {
false
}
}
}

impl Item for FocusScope {
Expand Down Expand Up @@ -555,21 +570,15 @@ impl Item for FocusScope {
) -> KeyEventResult {
let r = match event.event_type {
KeyEventType::KeyPressed => {
Self::FIELD_OFFSETS.key_pressed.apply_pin(self).call(&(event.clone(),))
}
KeyEventType::KeyReleased => {
let shortcut = self.visit_shortcuts(self_rc, |shortcut| {
let keys = Shortcut::FIELD_OFFSETS.keys.apply_pin(shortcut.as_pin_ref()).get();
if keys.matches(&event) { Some(VRcMapped::clone(shortcut)) } else { None }
});

if let Some(shortcut) = shortcut {
Shortcut::FIELD_OFFSETS.activated.apply_pin(shortcut.as_pin_ref()).call(&());
if self.handle_shortcuts(self_rc, event) {
EventResult::Accept
} else {
Self::FIELD_OFFSETS.key_released.apply_pin(self).call(&(event.clone(),))
Self::FIELD_OFFSETS.key_pressed.apply_pin(self).call(&(event.clone(),))
}
}
KeyEventType::KeyReleased => {
Self::FIELD_OFFSETS.key_released.apply_pin(self).call(&(event.clone(),))
}
KeyEventType::UpdateComposition | KeyEventType::CommitComposition => {
EventResult::Reject
}
Expand Down
60 changes: 60 additions & 0 deletions tests/cases/elements/dead_key_focus_scope.slint
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0

// Test that dead key characters (e.g. ^ on a German layout) are received
// by FocusScope's key-pressed callback and can trigger Shortcuts.
// Dead keys in winit only produce a KeyPressed event (no KeyReleased),
// so shortcuts must be able to fire on press-only sequences.
// Related: https://github.com/slint-ui/slint/issues/10862

export component TestCase inherits Window {
in-out property <int> shortcut-matches: 0;
in-out property <string> last-key-pressed: "";
out property has-focus <=> scope.has-focus;

forward-focus: scope;

scope := FocusScope {
Shortcut {
keys: @keys(Circumflex);
activated => { shortcut-matches += 1; }
}

key-pressed(event) => {
last-key-pressed = event.text;
EventResult.reject
}
}
}

/*

```rust
let instance = TestCase::new().unwrap();
assert!(instance.get_has_focus());

use slint::platform::WindowEvent;

// Simulate dead key behavior: only send KeyPressed, no KeyReleased.
// This is what winit produces for dead keys when IME is disabled
// (FocusScope does not enable IME).
instance.window().dispatch_event(WindowEvent::KeyPressed { text: "^".into() });

// The key event should be consumed by the shortcut
assert_eq!(instance.get_last_key_pressed(), "");

assert_eq!(instance.get_shortcut_matches(), 1, "shortcut should fire on dead key press without KeyReleased");
```

```cpp
auto handle = TestCase::create();
const TestCase &instance = *handle;
assert(instance.get_has_focus());

instance.window().dispatch_event(slint::platform::WindowEvent::KeyPressed { "^" });

assert_eq(instance.get_last_key_pressed(), "");

assert_eq(instance.get_shortcut_matches(), 1);
```
*/
Loading