Skip to main content

sdl_keybridge/
resolve.rs

1//! The four public API functions — the Rosetta Stone's forward, reverse,
2//! modifier, and name-parsing lookups.
3
4use std::borrow::Cow;
5
6use crate::keycode::Keycode;
7use crate::keymod::KeyMod;
8use crate::layout::{self, Layout, LayoutKey};
9use crate::localizer::{self, KeyLocalizer, LabelStyle, Modifier, Platform};
10use crate::named_key::NamedKey;
11use crate::scancode::Scancode;
12
13/// Every parallel representation of a key press, computed in a single pass.
14///
15/// Returned by [`resolve`].
16#[derive(Clone, Debug)]
17pub struct Resolved {
18    pub scancode: Scancode,
19    pub keycode: Keycode,
20    /// Localized glyph in English (`"Esc"`, `"Up"`, `"a"`, …) — style-aware.
21    pub glyph_en: Cow<'static, str>,
22    /// Localized glyph in the requested locale — style-aware.
23    pub glyph_local: Cow<'static, str>,
24    /// Produced Unicode character, if the key produces one at the current
25    /// modifier level. `None` for named keys like Escape / arrows / F-keys.
26    pub character: Option<char>,
27    /// Named-key identity, if this key is non-printable. `None` for
28    /// character-producing keys.
29    pub named_key: Option<NamedKey>,
30    /// Layout id echoed back — lets callers correlate results.
31    pub layout: &'static str,
32}
33
34/// Forward lookup — return every parallel representation of the key press.
35///
36/// - `scancode`: the physical key.
37/// - `mods`: current modifier bitmask (including Caps Lock and Num Lock
38///   latches, which are handled correctly — Caps only flips letters,
39///   Num Lock toggles keypad digit vs navigation).
40/// - `layout`: the layout id — e.g. `"windows/fr-t-k0-windows"`. An
41///   unknown id falls back to `"windows/en-t-k0-windows"` (CLDR US
42///   QWERTY for Windows).
43/// - `locale`: BCP 47 tag of the UI language — e.g. `"fr"`, `"en-US"`,
44///   `"zh-Hans"`.
45/// - `style`: textual (`"Up"`, `"Haut"`) vs symbolic (`↑`).
46/// - `localizer`: override for per-key translations; usually
47///   [`MultiLocalizer`](crate::MultiLocalizer). Must not be consumed.
48pub fn resolve<L: KeyLocalizer>(
49    scancode: Scancode,
50    mods: KeyMod,
51    layout: &str,
52    locale: &str,
53    style: LabelStyle,
54    localizer: &L,
55) -> Resolved {
56    let layout_ref = layout::get_layout(layout)
57        .or_else(|| layout::get_layout("windows/en-t-k0-windows"))
58        .expect("windows/en-t-k0-windows always present in CLDR data");
59
60    resolve_in_layout(scancode, mods, layout_ref, locale, style, localizer)
61}
62
63fn resolve_in_layout<L: KeyLocalizer>(
64    scancode: Scancode,
65    mods: KeyMod,
66    layout: &'static Layout,
67    locale: &str,
68    style: LabelStyle,
69    localizer: &L,
70) -> Resolved {
71    let key = match layout.key(scancode) {
72        Some(k) => k,
73        None => {
74            return Resolved {
75                scancode,
76                keycode: Keycode::UNKNOWN,
77                glyph_en: Cow::Borrowed(""),
78                glyph_local: Cow::Borrowed(""),
79                character: None,
80                named_key: None,
81                layout: layout.id,
82            };
83        }
84    };
85
86    // NumLock handling — applies only to keypad keys.
87    if let Some(nk) = key.named {
88        if let Some((numlock_on_named, numlock_off_named, numlock_char)) = keypad_behavior(nk) {
89            if mods.num() {
90                // NumLock ON — keypad produces digit character.
91                let character = numlock_char;
92                return Resolved {
93                    scancode,
94                    keycode: layout::named_key_keycode(numlock_on_named),
95                    glyph_en: character
96                        .map(|c| Cow::Owned(c.to_string()))
97                        .unwrap_or_else(|| {
98                            named_key_label(numlock_on_named, "en", style, localizer)
99                        }),
100                    glyph_local: character.map(|c| Cow::Owned(c.to_string())).unwrap_or_else(
101                        || named_key_label(numlock_on_named, locale, style, localizer),
102                    ),
103                    character,
104                    named_key: Some(numlock_on_named),
105                    layout: layout.id,
106                };
107            } else {
108                // NumLock OFF — keypad produces secondary navigation key.
109                return Resolved {
110                    scancode,
111                    keycode: layout::named_key_keycode(numlock_off_named),
112                    glyph_en: named_key_label(numlock_off_named, "en", style, localizer),
113                    glyph_local: named_key_label(numlock_off_named, locale, style, localizer),
114                    character: None,
115                    named_key: Some(numlock_off_named),
116                    layout: layout.id,
117                };
118            }
119        }
120
121        // Regular named key (not keypad).
122        return Resolved {
123            scancode,
124            keycode: layout::named_key_keycode(nk),
125            glyph_en: named_key_label(nk, "en", style, localizer),
126            glyph_local: named_key_label(nk, locale, style, localizer),
127            character: None,
128            named_key: Some(nk),
129            layout: layout.id,
130        };
131    }
132
133    // Printable key. Pick the glyph at the current modifier level.
134    let effective_shift = effective_shift_state(key, mods);
135    let effective_altgr = mods.altgr();
136
137    let glyph = pick_glyph(key, effective_shift, effective_altgr);
138
139    // SDL keycode for a printable key is the *base-level* Unicode code
140    // point (lowercase for letters) regardless of current modifiers.
141    let base_keycode = layout::layout_key_base_keycode(key);
142
143    let character = glyph;
144    let glyph_str = glyph.map(|c| c.to_string()).unwrap_or_default();
145
146    Resolved {
147        scancode,
148        keycode: base_keycode,
149        glyph_en: Cow::Owned(glyph_str.clone()),
150        glyph_local: Cow::Owned(glyph_str),
151        character,
152        named_key: None,
153        layout: layout.id,
154    }
155}
156
157/// Pick the glyph from a [`LayoutKey`]'s 4-level modifier table.
158fn pick_glyph(key: &LayoutKey, shift: bool, altgr: bool) -> Option<char> {
159    match (shift, altgr) {
160        (false, false) => key.base,
161        (true, false) => key.shift.or(key.base),
162        (false, true) => key.altgr.or(key.base),
163        (true, true) => key.shift_altgr.or(key.shift).or(key.base),
164    }
165    .and_then(|c| if c == '\0' { None } else { Some(c) })
166}
167
168/// Determine whether Shift is effectively applied to *this* key, taking
169/// Caps Lock into account — Caps only flips letters.
170fn effective_shift_state(key: &LayoutKey, mods: KeyMod) -> bool {
171    let shift = mods.shift();
172    let caps_applies = mods.caps() && is_letter_key(key);
173    shift ^ caps_applies
174}
175
176/// True if the key's base glyph is an alphabetic character (a-z in any
177/// script for which `char::is_alphabetic` returns true).
178fn is_letter_key(key: &LayoutKey) -> bool {
179    key.base.map(|c| c.is_alphabetic()).unwrap_or(false)
180}
181
182/// For keypad named keys, return the NumLock-aware triple
183/// `(numlock_on_named, numlock_off_named, numlock_char)`.
184///
185/// The NumLock ON form keeps the keypad identity and carries the digit /
186/// period character; the NumLock OFF form switches to the secondary
187/// navigation role.
188fn keypad_behavior(nk: NamedKey) -> Option<(NamedKey, NamedKey, Option<char>)> {
189    use NamedKey::*;
190    Some(match nk {
191        Keypad0 => (Keypad0, Insert, Some('0')),
192        Keypad1 => (Keypad1, End, Some('1')),
193        Keypad2 => (Keypad2, ArrowDown, Some('2')),
194        Keypad3 => (Keypad3, PageDown, Some('3')),
195        Keypad4 => (Keypad4, ArrowLeft, Some('4')),
196        Keypad5 => (Keypad5, Keypad5, Some('5')), // 5 has no nav counterpart
197        Keypad6 => (Keypad6, ArrowRight, Some('6')),
198        Keypad7 => (Keypad7, Home, Some('7')),
199        Keypad8 => (Keypad8, ArrowUp, Some('8')),
200        Keypad9 => (Keypad9, PageUp, Some('9')),
201        KeypadPeriod => (KeypadPeriod, Delete, Some('.')),
202        _ => return None,
203    })
204}
205
206/// Look up the locale + style label for a named key, with fallback
207/// chain: user localizer → requested locale → English → raw id.
208fn named_key_label<L: KeyLocalizer>(
209    nk: NamedKey,
210    locale: &str,
211    style: LabelStyle,
212    localizer: &L,
213) -> Cow<'static, str> {
214    match localizer::translate_for(nk.key_id(), locale, style, localizer) {
215        Some(s) => s,
216        None => Cow::Borrowed(nk.key_id()),
217    }
218}
219
220/// Reverse lookup — find the scancode whose *base-level* keycode matches.
221///
222/// Useful for cross-layout bridging: `scancode_for(k, "windows/ru-t-k0-jcuken")`
223/// then `resolve(sc, NONE, "windows/fr-t-k0-azerty", …)` to re-render a
224/// Russian-layout binding on a French-layout keyboard.
225pub fn scancode_for(keycode: Keycode, layout: &str) -> Option<Scancode> {
226    let l = layout::get_layout(layout)?;
227    for k in l.printable_keys.iter() {
228        if layout::layout_key_base_keycode(k) == keycode {
229            return Some(k.scancode);
230        }
231    }
232    for k in l.named_keys.iter() {
233        if layout::layout_key_base_keycode(k) == keycode {
234            return Some(k.scancode);
235        }
236    }
237    None
238}
239
240/// Localize the label for a single held modifier — platform-aware.
241///
242/// ```text
243/// modifier_label(Gui, Mac,   "en", Symbolic, …) == "⌘"
244/// modifier_label(Gui, Win,   "fr", Textual,  …) == "Windows"
245/// modifier_label(Alt, Mac,   "fr", Textual,  …) == "Option"
246/// modifier_label(Gui, Linux, "en", Textual,  …) == "Super"
247/// ```
248pub fn modifier_label<L: KeyLocalizer>(
249    modifier: Modifier,
250    platform: Platform,
251    locale: &str,
252    style: LabelStyle,
253    localizer: &L,
254) -> Cow<'static, str> {
255    let key = format!("{}_{}", modifier.key_id_prefix(), platform.id());
256    if let Some(s) = localizer::translate_for(&key, locale, style, localizer) {
257        return s;
258    }
259    let generic = modifier.key_id_prefix();
260    localizer::translate_for(generic, locale, style, localizer).unwrap_or(Cow::Borrowed(generic))
261}
262
263/// Inverse of `SDL_GetKeyName` — parse a textual key name into its
264/// canonical [`Keycode`].
265///
266/// Accepts the names SDL emits (`"Escape"`, `"Left Shift"`, `"F5"`,
267/// `"a"`, `"1"`, …). Matching is case-insensitive for letters and
268/// the `"Keypad X"` prefix; other named keys are matched verbatim.
269pub fn keycode_from_name(name: &str) -> Option<Keycode> {
270    let trimmed = name.trim();
271    if trimmed.is_empty() {
272        return None;
273    }
274
275    // Single-character printable names map to their Unicode code point.
276    let mut chars = trimmed.chars();
277    if let (Some(c), None) = (chars.next(), chars.next()) {
278        if !c.is_control() {
279            let lower = c.to_lowercase().next().unwrap_or(c);
280            return Some(Keycode::from(lower));
281        }
282    }
283
284    // Case-insensitive match against a fixed table.
285    let upper = trimmed.to_ascii_uppercase();
286    match upper.as_str() {
287        "RETURN" | "ENTER" => Some(Keycode::RETURN),
288        "ESCAPE" | "ESC" => Some(Keycode::ESCAPE),
289        "BACKSPACE" => Some(Keycode::BACKSPACE),
290        "TAB" => Some(Keycode::TAB),
291        "SPACE" => Some(Keycode::SPACE),
292        "CAPSLOCK" | "CAPS LOCK" => Some(Keycode::CAPSLOCK),
293        "NUMLOCK" | "NUM LOCK" | "NUMLOCKCLEAR" => Some(Keycode::NUM_LOCK_CLEAR),
294        "SCROLLLOCK" | "SCROLL LOCK" => Some(Keycode::SCROLL_LOCK),
295        "PRINTSCREEN" | "PRINT SCREEN" => Some(Keycode::PRINT_SCREEN),
296        "PAUSE" => Some(Keycode::PAUSE),
297        "INSERT" => Some(Keycode::INSERT),
298        "HOME" => Some(Keycode::HOME),
299        "PAGEUP" | "PAGE UP" => Some(Keycode::PAGE_UP),
300        "DELETE" => Some(Keycode::DELETE),
301        "END" => Some(Keycode::END),
302        "PAGEDOWN" | "PAGE DOWN" => Some(Keycode::PAGE_DOWN),
303        "RIGHT" => Some(Keycode::RIGHT),
304        "LEFT" => Some(Keycode::LEFT),
305        "DOWN" => Some(Keycode::DOWN),
306        "UP" => Some(Keycode::UP),
307        "APPLICATION" => Some(Keycode::APPLICATION),
308        "MENU" => Some(Keycode::MENU),
309        "LEFT CTRL" | "LCTRL" => Some(Keycode::LCTRL),
310        "RIGHT CTRL" | "RCTRL" => Some(Keycode::RCTRL),
311        "LEFT SHIFT" | "LSHIFT" => Some(Keycode::LSHIFT),
312        "RIGHT SHIFT" | "RSHIFT" => Some(Keycode::RSHIFT),
313        "LEFT ALT" | "LALT" => Some(Keycode::LALT),
314        "RIGHT ALT" | "RALT" | "ALTGR" => Some(Keycode::RALT),
315        "LEFT GUI" | "LGUI" => Some(Keycode::LGUI),
316        "RIGHT GUI" | "RGUI" => Some(Keycode::RGUI),
317        "F1" => Some(Keycode::F1),
318        "F2" => Some(Keycode::F2),
319        "F3" => Some(Keycode::F3),
320        "F4" => Some(Keycode::F4),
321        "F5" => Some(Keycode::F5),
322        "F6" => Some(Keycode::F6),
323        "F7" => Some(Keycode::F7),
324        "F8" => Some(Keycode::F8),
325        "F9" => Some(Keycode::F9),
326        "F10" => Some(Keycode::F10),
327        "F11" => Some(Keycode::F11),
328        "F12" => Some(Keycode::F12),
329        "F13" => Some(Keycode::F13),
330        "F14" => Some(Keycode::F14),
331        "F15" => Some(Keycode::F15),
332        "F16" => Some(Keycode::F16),
333        "F17" => Some(Keycode::F17),
334        "F18" => Some(Keycode::F18),
335        "F19" => Some(Keycode::F19),
336        "F20" => Some(Keycode::F20),
337        "F21" => Some(Keycode::F21),
338        "F22" => Some(Keycode::F22),
339        "F23" => Some(Keycode::F23),
340        "F24" => Some(Keycode::F24),
341        "KEYPAD 0" | "KP_0" => Some(Keycode::KP_0),
342        "KEYPAD 1" | "KP_1" => Some(Keycode::KP_1),
343        "KEYPAD 2" | "KP_2" => Some(Keycode::KP_2),
344        "KEYPAD 3" | "KP_3" => Some(Keycode::KP_3),
345        "KEYPAD 4" | "KP_4" => Some(Keycode::KP_4),
346        "KEYPAD 5" | "KP_5" => Some(Keycode::KP_5),
347        "KEYPAD 6" | "KP_6" => Some(Keycode::KP_6),
348        "KEYPAD 7" | "KP_7" => Some(Keycode::KP_7),
349        "KEYPAD 8" | "KP_8" => Some(Keycode::KP_8),
350        "KEYPAD 9" | "KP_9" => Some(Keycode::KP_9),
351        "KEYPAD ." | "KP_PERIOD" => Some(Keycode::KP_PERIOD),
352        "KEYPAD =" | "KP_EQUALS" => Some(Keycode::KP_EQUALS),
353        "KEYPAD ENTER" | "KP_ENTER" => Some(Keycode::KP_ENTER),
354        "KEYPAD /" | "KP_DIVIDE" => Some(Keycode::KP_DIVIDE),
355        "KEYPAD *" | "KP_MULTIPLY" => Some(Keycode::KP_MULTIPLY),
356        "KEYPAD -" | "KP_MINUS" => Some(Keycode::KP_MINUS),
357        "KEYPAD +" | "KP_PLUS" => Some(Keycode::KP_PLUS),
358        _ => None,
359    }
360}