# Design : Répondre au clavier sur la version Desktop Date : 2026-08-31 Statut : Validé par le produit (approche A choisie) ## 1. Objectif Sur la version Desktop, on doit pouvoir répondre aux questions avec le clavier physique : les chiffres (rangée du haut et pavé numérique) saisissent la réponse, Entrée valide, Backspace efface. L'expérience tactile (pavé à l'écran) reste inchangée sur Android et iOS. ## 2. Décision | Décision | Choix | |---|---| | Emplacement | UI partagée (`GameScreen`), pas de code spécifique desktop | | Mécanisme | `Modifier.onPreviewKeyEvent` sur la racine de `PlayingContent` | | Focus | `FocusRequester` + `focusable()` sur la racine, demandé à l'apparition | | Touches | 0-9 (rangée haut + pavé), Entrée + Entrée pavé, Backspace | | Consommation | Événements gérés consommés (`true`) | Rejeté : interception au niveau de la fenêtre dans `Main.kt` (plomberie de transformation d'événements `Main → App → navigation → GameScreen`, uniquement pour desktop) ; champ texte invisible (hacky : curseur, IME, collage). L'UI partagée convient car les événements clavier n'existent que sur machine avec clavier physique : sur mobile tactile, aucun événement n'arrive, aucune gating par plateforme n'est nécessaire. ## 3. Comportement - Le mapping touche → action est pur et testable, indépendant du `KeyEvent` (qu'on ne peut pas construire en test) : ```kotlin sealed interface KeypadAction { data class Digit(val char: Char) : KeypadAction object Delete : KeypadAction object Validate : KeypadAction } object GameKeyMapping { fun actionFor(key: Key, type: KeyEventType): KeypadAction? } ``` - Mapping (uniquement sur `KeyDown`, `null` sinon → pas d'action) : - `Key.Zero`–`Key.Nine` et `Key.NumPad0`–`Key.NumPad9` → `Digit` - `Key.Enter` et `Key.NumPadEnter` → `Validate` - `Key.Backspace` → `Delete` - toute autre touche → `null` - Le handler `onPreviewKeyEvent` de `PlayingContent` appelle `engine.onDigit / onDelete / onValidate` selon l'action et consomme l'événement, pour qu'Entrée n'active pas aussi un bouton du pavé qui aurait le focus (le pavé passe par la phase preview avant les boutons). - `Key.Zero`–`Key.Nine` sont des touches **positionnelles** : sur AZERTY, les touches non modifiées (`&`, `é`, `"`, …) saisissent aussi des chiffres — sans besoin de Shift, adapté au public enfant. ## 4. Cas limites | Cas | Comportement | |---|---| | Feedback (1,5 s) | `PlayingContent` non composé → touches ignorées | | Question suivante | Re-composition → `requestFocus()` relancé | | Entrée à vide / Backspace à vide | Ignorés par l'engine (événement consommé quand même) | | Dialog « Quitter la partie ? » | Fenêtre séparée sur desktop, les touches vont au dialog | | Limite de saisie | `MAX_ENTRY_LENGTH` (4) déjà géré dans `GameEngine` | | Bouton du pavé cliqué à la souris | Il hérite du focus, mais la phase preview de la racine voit d'abord les touches | ## 5. Implémentation 1. `sharedUI/src/commonMain/kotlin/com/livrets/ui/GameKeyMapping.kt` : `KeypadAction` + `GameKeyMapping.actionFor(key, type)`. 2. `GameScreen.kt` : sur le `Column` racine de `PlayingContent`, ajouter `.focusRequester(fr).focusable().onPreviewKeyEvent { ... }` et un `LaunchedEffect { fr.requestFocus() }`. Le `Keypad` existant ne change pas. 3. Aucun changement : `GameEngine`, `Main.kt`, écrans non concernés. ## 6. Tests (commonTest, `GameKeyMappingTest`) 1. Chiffres rangée du haut (`Key.Zero`…`Key.Nine`) → `Digit` correspondant. 2. Chiffres pavé numérique (`Key.NumPad0`…`Key.NumPad9`) → `Digit` correspondant. 3. `Key.Enter` → `Validate`. 4. `Key.NumPadEnter` → `Validate`. 5. `Key.Backspace` → `Delete`. 6. `KeyUp` (ex. `Key.One` sur `KeyUp`) → `null`. 7. Touches non gérées (`Key.A`, `Key.Escape`, `Key.Spacebar`…) → `null`. Le câblage (focus, consommation, re-focus après feedback) est validé manuellement via `./gradlew :desktopApp:run`. ## 7. Hors périmètre - Échap pour ouvrir la boîte « Quitter la partie ? » (refusé par le produit) - Raccourcis clavier sur l'accueil, les résultats et l'historique - Tout changement sur Android et iOS (pas de gating, mais aucun comportement tactile modifié)