docs: spec clavier physique sur la version desktop

This commit is contained in:
herel committed 2026-08-31 09:56:28 +02:00
1 parent 691c98f862
commit b651864fad
1 file changed
+95
@@ -0,0 +1,95 @@
# 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).
## 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é)