Skip to content

Commit 4e60cff

Browse files
Fix shift remapping in KeyboardStateTracker to use xkb_state
1 parent 16bc38f commit 4e60cff

File tree

3 files changed

+154
-29
lines changed

3 files changed

+154
-29
lines changed

src/server/frontend_wayland/keyboard_state_tracker.cpp

Lines changed: 64 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,65 @@
1515
*/
1616

1717
#include "keyboard_state_tracker.h"
18+
#include "mir_toolkit/events/enums.h"
1819

1920
#include <mir/events/keyboard_event.h>
21+
#include <mir/fatal.h>
2022

21-
#include <xkbcommon/xkbcommon-keysyms.h>
23+
#include <xkbcommon/xkbcommon.h>
2224

2325
#include <algorithm>
2426

2527
namespace mf = mir::frontend;
2628

29+
namespace
30+
{
31+
// xkb scancodes are offset by 8 from evdev scancodes for compatibility with
32+
// the X protocol. This matches the same helper used in xkb_mapper.cpp.
33+
uint32_t constexpr to_xkb_scan_code(uint32_t evdev_scan_code)
34+
{
35+
return evdev_scan_code + 8;
36+
}
37+
} // namespace
38+
39+
void mf::KeyboardStateTracker::XkbKeyState::update_keymap(
40+
std::shared_ptr<mir::input::Keymap> const& new_keymap, xkb_context* context)
41+
{
42+
if (!new_keymap || (current_keymap && current_keymap->matches(*new_keymap)))
43+
return;
44+
45+
current_keymap = new_keymap;
46+
compiled_keymap = new_keymap->make_unique_xkb_keymap(context);
47+
state = {xkb_state_new(compiled_keymap.get()), xkb_state_unref};
48+
}
49+
50+
void mf::KeyboardStateTracker::XkbKeyState::update_key(uint32_t xkb_keycode, MirKeyboardAction action)
51+
{
52+
if (state)
53+
xkb_state_update_key(state.get(), xkb_keycode, action == mir_keyboard_action_down ? XKB_KEY_DOWN : XKB_KEY_UP);
54+
}
55+
56+
void mf::KeyboardStateTracker::XkbKeyState::rederive_keysyms_from_scancodes(
57+
std::unordered_map<uint32_t, xkb_keysym_t>& scancode_to_keysym) const
58+
{
59+
if (!state)
60+
return;
61+
62+
for (auto& [sc, ks] : scancode_to_keysym)
63+
{
64+
auto const derived = xkb_state_key_get_one_sym(state.get(), to_xkb_scan_code(sc));
65+
if (derived != XKB_KEY_NoSymbol)
66+
ks = derived;
67+
}
68+
}
69+
70+
mf::KeyboardStateTracker::KeyboardStateTracker()
71+
: context{xkb_context_new(XKB_CONTEXT_NO_FLAGS), xkb_context_unref}
72+
{
73+
if (!context)
74+
fatal_error("KeyboardStateTracker: failed to create XKB context");
75+
}
76+
2777
bool mf::KeyboardStateTracker::process(MirEvent const& event)
2878
{
2979
if (event.type() != mir_event_type_input)
@@ -40,7 +90,15 @@ bool mf::KeyboardStateTracker::process(MirEvent const& event)
4090
auto const action = key_event->action();
4191
auto const modifiers = key_event->modifiers();
4292

43-
auto& [scancode_to_keysym, shift_state] = device_states[input_event->device_id()];
93+
auto& [scancode_to_keysym, shift_state, xkb_key_state] =
94+
device_states[input_event->device_id()];
95+
96+
xkb_key_state.update_keymap(key_event->keymap(), context.get());
97+
98+
// Keep xkb_key_state in sync with every key event so that its modifier
99+
// tracking stays accurate for subsequent keysym queries.
100+
auto const xkb_keycode = to_xkb_scan_code(static_cast<uint32_t>(scancode));
101+
xkb_key_state.update_key(xkb_keycode, action);
44102

45103
auto const prev_shift_state = shift_state;
46104
shift_state = modifiers & (mir_input_event_modifier_shift | mir_input_event_modifier_shift_left |
@@ -60,18 +118,10 @@ bool mf::KeyboardStateTracker::process(MirEvent const& event)
60118
processed = true;
61119
}
62120

63-
// Transitioned from no shift to at least one shift
64-
if (prev_shift_state == 0 && shift_state != 0)
65-
{
66-
for (auto& [sc, ks] : scancode_to_keysym)
67-
ks = xkb_keysym_to_upper(ks);
68-
}
69-
else if (prev_shift_state != 0 && shift_state == 0)
70-
{
71-
// Transitioned from at least one shift to no shift
72-
for (auto& [sc, ks] : scancode_to_keysym)
73-
ks = xkb_keysym_to_lower(ks);
74-
}
121+
// When the shift state changes, re-derive every pressed keysym from its
122+
// scancode using the layout-aware XKB state.
123+
if (prev_shift_state != shift_state)
124+
xkb_key_state.rederive_keysyms_from_scancodes(scancode_to_keysym);
75125

76126
return processed;
77127
}

src/server/frontend_wayland/keyboard_state_tracker.h

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@
1717
#ifndef MIR_FRONTEND_WAYLAND_KEYBOARD_STATE_TRACKER_H_
1818
#define MIR_FRONTEND_WAYLAND_KEYBOARD_STATE_TRACKER_H_
1919

20+
#include "mir_toolkit/events/enums.h"
2021
#include <mir/events/event.h>
22+
#include <mir/input/keymap.h>
2123

24+
#include <memory>
2225
#include <unordered_map>
2326

2427
namespace mir
@@ -35,11 +38,10 @@ namespace frontend
3538
/// need to inspect it, so a single tracker instance can be shared across many
3639
/// objects without each one having to maintain its own pressed-key set.
3740
///
38-
/// Shift-key transitions are handled specially: when a Shift key is pressed
39-
/// all lowercase keysyms already in the pressed set are promoted to their
40-
/// uppercase equivalents, and when all Shift keys are released they are
41-
/// demoted back. This keeps the stored keysyms consistent with what the
42-
/// keyboard layer reports as the logical key for subsequent events.
41+
/// Shift-key transitions are handled specially: when the shift state changes,
42+
/// all currently-pressed keysyms are re-derived from their scancodes using the
43+
/// layout-aware XKB state. This keeps the stored keysyms consistent with what
44+
/// the keyboard layer reports as the logical key for subsequent events.
4345
///
4446
/// Keysyms are stored per scancode so that a key-up event always removes the
4547
/// keysym that was recorded at key-down, regardless of any modifier changes
@@ -48,7 +50,7 @@ namespace frontend
4850
class KeyboardStateTracker
4951
{
5052
public:
51-
KeyboardStateTracker() = default;
53+
KeyboardStateTracker();
5254

5355
// Returns true if the passed in event was an up or down keyboard event and
5456
// was processed, false otherwise.
@@ -59,14 +61,49 @@ class KeyboardStateTracker
5961
auto scancode_is_pressed(MirInputDeviceId device, int32_t scancode) const -> bool;
6062

6163
private:
64+
/// Owns the per-device XKB keymap and state used to resolve keysyms from
65+
/// scancodes in a layout- and modifier-aware way.
66+
struct XkbKeyState
67+
{
68+
/// Update the compiled keymap and XKB state when a new keymap arrives.
69+
/// \param new_keymap The keymap carried on the incoming event.
70+
/// \param context The shared XKB context owned by the tracker.
71+
void update_keymap(std::shared_ptr<mir::input::Keymap> const& new_keymap, xkb_context* context);
72+
73+
/// Feed a key press or release into the XKB state so that modifier
74+
/// tracking stays accurate for subsequent keysym queries.
75+
/// \param xkb_keycode The XKB keycode for the key.
76+
/// \param action The keyboard action.
77+
void update_key(uint32_t xkb_keycode, MirKeyboardAction action);
78+
79+
/// Re-derive every keysym in \a scancode_to_keysym from its scancode
80+
/// using the current modifier state of the XKB state machine.
81+
void rederive_keysyms_from_scancodes(
82+
std::unordered_map<uint32_t, xkb_keysym_t>& scancode_to_keysym) const;
83+
84+
private:
85+
/// The keymap currently in use for this device, used to detect keymap
86+
/// changes and rebuild compiled_keymap/state when they occur.
87+
std::shared_ptr<mir::input::Keymap> current_keymap;
88+
89+
/// The compiled keymap and live XKB state, kept in sync with every
90+
/// key event so that modifier state is accurate for keysym queries.
91+
std::unique_ptr<xkb_keymap, void(*)(xkb_keymap*)> compiled_keymap{nullptr, xkb_keymap_unref};
92+
std::unique_ptr<xkb_state, void(*)(xkb_state*)> state{nullptr, xkb_state_unref};
93+
};
94+
6295
struct DeviceState
6396
{
6497
/// Maps each currently-pressed scancode to the keysym that was recorded
6598
/// when it was pressed (updated on shift-state transitions).
66-
std::unordered_map<uint32_t, uint32_t> scancode_to_keysym;
99+
std::unordered_map<uint32_t, xkb_keysym_t> scancode_to_keysym;
67100
MirInputEventModifiers shift_state{0};
101+
XkbKeyState xkb_key_state;
68102
};
69103

104+
/// Shared XKB context — one per tracker, reused across all devices.
105+
std::unique_ptr<xkb_context, void(*)(xkb_context*)> context;
106+
70107
std::unordered_map<MirInputDeviceId, DeviceState> device_states;
71108
};
72109
}

tests/unit-tests/frontend_wayland/test_keyboard_state_tracker.cpp

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
#include "src/server/frontend_wayland/keyboard_state_tracker.h"
1818

1919
#include <mir/test/doubles/advanceable_clock.h>
20+
#include <mir/events/keyboard_event.h>
21+
#include <mir/input/parameter_keymap.h>
2022

2123
#include <xkbcommon/xkbcommon-keysyms.h>
2224

@@ -34,12 +36,13 @@ namespace
3436
/// Based on standard US QWERTY layout (PC keyboard / evdev scancodes)
3537

3638
constexpr uint32_t key_1_scancode = 2;
39+
constexpr uint32_t key_0_scancode = 11;
3740
constexpr uint32_t a_scancode = 30;
3841
constexpr uint32_t b_scancode = 48;
3942

40-
constexpr uint32_t ctrl_l_scancode = 37;
41-
constexpr uint32_t shift_l_scancode = 50;
42-
constexpr uint32_t shift_r_scancode = 62;
43+
constexpr uint32_t ctrl_l_scancode = 29;
44+
constexpr uint32_t shift_l_scancode = 42;
45+
constexpr uint32_t shift_r_scancode = 54;
4346

4447
class KeyboardStateTrackerTest : public Test
4548
{
@@ -52,13 +55,15 @@ class KeyboardStateTrackerTest : public Test
5255
if (keysym == XKB_KEY_Shift_R)
5356
mod |= mir_input_event_modifier_shift_right;
5457

55-
return mir::events::make_key_event(
58+
auto event = mir::events::make_key_event(
5659
id,
5760
clock.now().time_since_epoch(),
5861
mir_keyboard_action_down,
5962
keysym,
6063
scancode,
6164
mod);
65+
event->to_input()->to_keyboard()->set_keymap(default_keymap);
66+
return event;
6267
}
6368

6469
auto key_up(uint32_t keysym, uint32_t scancode, MirInputDeviceId id = device_id) -> mir::EventUPtr
@@ -69,13 +74,15 @@ class KeyboardStateTrackerTest : public Test
6974
if (keysym == XKB_KEY_Shift_R)
7075
mod &= ~mir_input_event_modifier_shift_right;
7176

72-
return mir::events::make_key_event(
77+
auto event = mir::events::make_key_event(
7378
id,
7479
clock.now().time_since_epoch(),
7580
mir_keyboard_action_up,
7681
keysym,
7782
scancode,
7883
mod);
84+
event->to_input()->to_keyboard()->set_keymap(default_keymap);
85+
return event;
7986
}
8087

8188
auto static inline const device_id = MirInputDeviceId{0};
@@ -84,6 +91,12 @@ class KeyboardStateTrackerTest : public Test
8491
mtd::AdvanceableClock clock;
8592
mf::KeyboardStateTracker tracker;
8693

94+
// Default US QWERTY keymap attached to every test event so that the
95+
// tracker's xkb_state is populated and shift-transition re-derivation
96+
// via xkb_state_key_get_one_sym() works correctly.
97+
std::shared_ptr<mir::input::Keymap> const default_keymap{
98+
std::make_shared<mir::input::ParameterKeymap>()};
99+
87100
// Track modifier state per device to emulate Mir's tracking of modifiers
88101
std::unordered_map<MirInputDeviceId, MirInputEventModifiers> modifier_states;
89102
};
@@ -308,6 +321,29 @@ TEST_F(KeyboardStateTrackerTest, shift_release_on_one_device_does_not_demote_key
308321
EXPECT_FALSE(tracker.keysym_is_pressed(other_device_id, XKB_KEY_a));
309322
}
310323

324+
TEST_F(KeyboardStateTrackerTest, pressing_shift_after_digit_promotes_to_symbol)
325+
{
326+
tracker.process(*key_down(XKB_KEY_0, key_0_scancode));
327+
EXPECT_TRUE(tracker.keysym_is_pressed(device_id, XKB_KEY_0));
328+
329+
tracker.process(*key_down(XKB_KEY_Shift_L, shift_l_scancode));
330+
331+
EXPECT_TRUE(tracker.keysym_is_pressed(device_id, XKB_KEY_parenright));
332+
EXPECT_FALSE(tracker.keysym_is_pressed(device_id, XKB_KEY_0));
333+
}
334+
335+
TEST_F(KeyboardStateTrackerTest, releasing_shift_after_digit_demotes_symbol_back_to_digit)
336+
{
337+
// Shift held, '0' pressed (which the layout reports as ')'), then Shift
338+
// released: the tracker should revert to '0'.
339+
tracker.process(*key_down(XKB_KEY_Shift_L, shift_l_scancode));
340+
tracker.process(*key_down(XKB_KEY_parenright, key_0_scancode));
341+
tracker.process(*key_up(XKB_KEY_Shift_L, shift_l_scancode));
342+
343+
EXPECT_TRUE(tracker.keysym_is_pressed(device_id, XKB_KEY_0));
344+
EXPECT_FALSE(tracker.keysym_is_pressed(device_id, XKB_KEY_parenright));
345+
}
346+
311347
TEST_F(KeyboardStateTrackerTest, key_up_clears_key_when_modifier_changed_while_held)
312348
{
313349
// Simulate: '1' pressed (keysym = XKB_KEY_1), then Shift pressed, then '1'
@@ -320,11 +356,13 @@ TEST_F(KeyboardStateTrackerTest, key_up_clears_key_when_modifier_changed_while_h
320356

321357
tracker.process(*key_down(XKB_KEY_Shift_L, shift_l_scancode));
322358

323-
// XKB_KEY_1 has no uppercase equivalent so it remains as XKB_KEY_1
324-
EXPECT_TRUE(tracker.keysym_is_pressed(device_id, XKB_KEY_1));
359+
// With a real xkb_state, pressing Shift re-derives '1' -> '!' correctly.
360+
EXPECT_TRUE(tracker.keysym_is_pressed(device_id, XKB_KEY_exclam));
361+
EXPECT_FALSE(tracker.keysym_is_pressed(device_id, XKB_KEY_1));
325362
EXPECT_TRUE(tracker.scancode_is_pressed(device_id, key_1_scancode));
326363

327-
// Key-up event reports XKB_KEY_exclam because Shift is still held
364+
// Key-up event reports XKB_KEY_exclam because Shift is still held.
365+
// The tracker must clear the entry by scancode, not by keysym.
328366
tracker.process(*key_up(XKB_KEY_exclam, key_1_scancode));
329367

330368
EXPECT_FALSE(tracker.keysym_is_pressed(device_id, XKB_KEY_1));

0 commit comments

Comments
 (0)