Skip to content

Commit f37ddfd

Browse files
Add keyboard state tracker (#4746)
Related #4410 ## What's new? - Adds `KeyboardStateTracker` to provide global keyboard state tracking. This is meant as a utility class to be used with input triggers to provide a consistent global view of keyboard states: https://github.com/canonical/mir/blob/594cfcf01add05d4f46c1d9a1dffbf3fab4d0440/src/server/frontend_wayland/wl_seat.cpp#L163 https://github.com/canonical/mir/blob/84f77ea995ad6ce5760359b371b99db779dcde13/src/server/frontend_wayland/input_trigger_registration_v1.cpp#L192 https://github.com/canonical/mir/blob/84f77ea995ad6ce5760359b371b99db779dcde13/src/server/frontend_wayland/input_trigger_registration_v1.cpp#L217 ## How to test - Run mir unit tests with `--gtest_filter=KeyboardStateTracker*` ## Checklist - [x] Tests added and pass - [x] Adequate documentation added - [ ] ~(optional) Added Screenshots or videos~
2 parents 7d52b9b + 81b8690 commit f37ddfd

File tree

5 files changed

+631
-0
lines changed

5 files changed

+631
-0
lines changed

src/server/frontend_wayland/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ set(
6767
linux_drm_syncobj.cpp linux_drm_syncobj.h
6868
data_control_v1.cpp data_control_v1.h
6969
surface_registry.cpp surface_registry.h
70+
keyboard_state_tracker.cpp keyboard_state_tracker.h
7071
)
7172

7273
if (${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang" AND ${CMAKE_CXX_COMPILER_VERSION} VERSION_LESS 18.2)
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/*
2+
* Copyright © Canonical Ltd.
3+
*
4+
* This program is free software: you can redistribute it and/or modify it
5+
* under the terms of the GNU General Public License version 2 or 3,
6+
* as published by the Free Software Foundation.
7+
*
8+
* This program is distributed in the hope that it will be useful,
9+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
* GNU General Public License for more details.
12+
*
13+
* You should have received a copy of the GNU General Public License
14+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
15+
*/
16+
17+
#include "keyboard_state_tracker.h"
18+
#include "mir_toolkit/events/enums.h"
19+
20+
#include <mir/events/keyboard_event.h>
21+
#include <mir/fatal.h>
22+
23+
#include <xkbcommon/xkbcommon.h>
24+
25+
#include <algorithm>
26+
27+
namespace mf = mir::frontend;
28+
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)
43+
mir::fatal_error("KeyboardStateTracker: received null keymap");
44+
45+
if (current_keymap && current_keymap->matches(*new_keymap))
46+
return;
47+
48+
current_keymap = new_keymap;
49+
compiled_keymap = new_keymap->make_unique_xkb_keymap(context);
50+
state = {xkb_state_new(compiled_keymap.get()), xkb_state_unref};
51+
}
52+
53+
void mf::KeyboardStateTracker::XkbKeyState::update_key(uint32_t xkb_keycode, MirKeyboardAction action)
54+
{
55+
xkb_state_update_key(state.get(), xkb_keycode, action == mir_keyboard_action_down ? XKB_KEY_DOWN : XKB_KEY_UP);
56+
}
57+
58+
void mf::KeyboardStateTracker::XkbKeyState::rederive_keysyms_from_scancodes(
59+
std::unordered_map<uint32_t, xkb_keysym_t>& scancode_to_keysym) const
60+
{
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+
77+
bool mf::KeyboardStateTracker::process(MirEvent const& event)
78+
{
79+
if (event.type() != mir_event_type_input)
80+
return false;
81+
82+
auto const& input_event = event.to_input();
83+
84+
if (input_event->input_type() != mir_input_event_type_key)
85+
return false;
86+
87+
auto const* key_event = input_event->to_keyboard();
88+
auto const keysym = key_event->keysym();
89+
auto const scancode = key_event->scan_code();
90+
auto const action = key_event->action();
91+
auto const modifiers = key_event->modifiers();
92+
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);
102+
103+
auto const prev_shift_state = shift_state;
104+
shift_state = modifiers & (mir_input_event_modifier_shift | mir_input_event_modifier_shift_left |
105+
mir_input_event_modifier_shift_right);
106+
107+
auto processed = false;
108+
if (action == mir_keyboard_action_down)
109+
{
110+
scancode_to_keysym[scancode] = keysym;
111+
processed = true;
112+
}
113+
else if (action == mir_keyboard_action_up)
114+
{
115+
// Remove by scancode so that a mismatched key-up keysym (caused by a
116+
// modifier change while the key was held) does not leave stale entries.
117+
scancode_to_keysym.erase(scancode);
118+
processed = true;
119+
}
120+
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);
125+
126+
return processed;
127+
}
128+
129+
auto mf::KeyboardStateTracker::keysym_is_pressed(MirInputDeviceId device, xkb_keysym_t keysym) const -> bool
130+
{
131+
if (!device_states.contains(device))
132+
return false;
133+
134+
return std::ranges::any_of(
135+
device_states.at(device).scancode_to_keysym, [keysym](auto const& pair) { return pair.second == keysym; });
136+
}
137+
138+
auto mf::KeyboardStateTracker::scancode_is_pressed(MirInputDeviceId device, int32_t scancode) const -> bool
139+
{
140+
if (!device_states.contains(device))
141+
return false;
142+
143+
return device_states.at(device).scancode_to_keysym.contains(scancode);
144+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*
2+
* Copyright © Canonical Ltd.
3+
*
4+
* This program is free software: you can redistribute it and/or modify it
5+
* under the terms of the GNU General Public License version 2 or 3,
6+
* as published by the Free Software Foundation.
7+
*
8+
* This program is distributed in the hope that it will be useful,
9+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
* GNU General Public License for more details.
12+
*
13+
* You should have received a copy of the GNU General Public License
14+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
15+
*/
16+
17+
#ifndef MIR_FRONTEND_WAYLAND_KEYBOARD_STATE_TRACKER_H_
18+
#define MIR_FRONTEND_WAYLAND_KEYBOARD_STATE_TRACKER_H_
19+
20+
#include "mir_toolkit/events/enums.h"
21+
#include <mir/events/event.h>
22+
#include <mir/input/keymap.h>
23+
24+
#include <memory>
25+
#include <unordered_map>
26+
27+
namespace mir
28+
{
29+
namespace frontend
30+
{
31+
/// Maintains the set of currently-pressed keysyms and scancodes by processing
32+
/// keyboard \c MirEvents via \c process().
33+
///
34+
/// Callers feed every keyboard event into \c process() as it arrives, then
35+
/// query the resulting state with \c keysym_is_pressed() or \c
36+
/// scancode_is_pressed() to test whether a particular key is currently held
37+
/// down. This decouples state accumulation from the individual objects that
38+
/// need to inspect it, so a single tracker instance can be shared across many
39+
/// objects without each one having to maintain its own pressed-key set.
40+
///
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.
45+
///
46+
/// Keysyms are stored per scancode so that a key-up event always removes the
47+
/// keysym that was recorded at key-down, regardless of any modifier changes
48+
/// that occurred while the key was held (e.g. pressing Shift while holding
49+
/// a digit key).
50+
class KeyboardStateTracker
51+
{
52+
public:
53+
KeyboardStateTracker();
54+
55+
// Returns true if the passed in event was an up or down keyboard event and
56+
// was processed, false otherwise.
57+
bool process(MirEvent const& event);
58+
59+
auto keysym_is_pressed(MirInputDeviceId device, xkb_keysym_t keysym) const -> bool;
60+
61+
auto scancode_is_pressed(MirInputDeviceId device, int32_t scancode) const -> bool;
62+
63+
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. Must not be null.
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+
95+
struct DeviceState
96+
{
97+
/// Maps each currently-pressed scancode to the keysym that was recorded
98+
/// when it was pressed (updated on shift-state transitions).
99+
std::unordered_map<uint32_t, xkb_keysym_t> scancode_to_keysym;
100+
MirInputEventModifiers shift_state{0};
101+
XkbKeyState xkb_key_state;
102+
};
103+
104+
/// Shared XKB context — one per tracker, reused across all devices.
105+
std::unique_ptr<xkb_context, void(*)(xkb_context*)> context;
106+
107+
std::unordered_map<MirInputDeviceId, DeviceState> device_states;
108+
};
109+
}
110+
}
111+
112+
113+
#endif

tests/unit-tests/frontend_wayland/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ list(
55
${CMAKE_CURRENT_SOURCE_DIR}/test_desktop_file_manager.cpp
66
${CMAKE_CURRENT_SOURCE_DIR}/test_g_desktop_file_cache.cpp
77
${CMAKE_CURRENT_SOURCE_DIR}/test_output_manager.cpp
8+
${CMAKE_CURRENT_SOURCE_DIR}/test_keyboard_state_tracker.cpp
89
)
910

1011
set(UNIT_TEST_SOURCES ${UNIT_TEST_SOURCES} PARENT_SCOPE)

0 commit comments

Comments
 (0)