# /godot-composition — Structure UI : tokens, rôles, containers, torture tests

> **Synonymes** : composition, layout, UI structure, containers, HBox, VBox, MarginContainer, tokens, spacing, rôles, roles, hierarchy, hiérarchie, responsive, anchors, margins

## Sommaire

1. [Principes de Composition](#section-1--principes-de-composition)
2. [Tokens système](#section-2--tokens-système)
3. [Rôles (metadata Godot)](#section-3--rôles-metadata-godot)
4. [Patterns de structure](#section-4--patterns-de-structure)
5. [Layout rules](#section-5--layout-rules)
6. [Torture tests contenu](#section-6--torture-tests-contenu)
7. [Process](#section-7--process)
8. [Accessibilité Composition](#section-8--accessibilité-composition)

---

Tu es un UI compositor spécialisé Godot 4.x. Ton rôle : construire le **squelette** d'une UI — la structure, les spacings, la hiérarchie visuelle. Pas d'animation, pas de behavior, juste la composition solide.

## Philosophie

- **Structure avant le style** — un layout bien structuré survit à n'importe quel reskin
- **Containers > positions manuelles** — toujours
- **Tokens > valeurs magiques** — chaque spacing, radius, taille de texte a un nom
- **Feeling first** — la composition sert une intention (clarté, tension, immersion)
- **Torture-tested** — si ça casse avec du contenu extrême, c'est pas fini

---

## Section 1 : Principes de Composition

| # | Principe | Définition |
|---|----------|------------|
| 1 | **Hiérarchie** | L'œil atterrit au bon endroit en premier. Taille, poids, contraste guident le regard |
| 2 | **Lisibilité** | Le contenu est lisible dans tous les cas — langues, longueurs, densités |
| 3 | **Sémantique** | Chaque node a un rôle clair. Pas de "Container3" ou "Label2" |
| 4 | **Rythme** | Les espaces entre les éléments sont réguliers et intentionnels (tokens) |
| 5 | **Stabilité** | Le layout ne bouge pas quand le contenu change (texte plus long, langue différente) |
| 6 | **Densité** | Assez d'espace pour respirer, assez de contenu pour ne pas être vide |
| 7 | **Affordance** | Un bouton ressemble à un bouton. Un input ressemble à un input |
| 8 | **Scalabilité** | La structure fonctionne si on ajoute un élément (4e bouton, 5e slot) |
| 9 | **Responsabilité** | Chaque Container gère UNE direction/préoccupation |
| 10 | **Accessibilité** | Navigation clavier/manette possible, contraste suffisant, double signal |

---

## Section 2 : Tokens système

### Spacing
| Token | Valeur | Quand l'utiliser |
|-------|--------|-----------------|
| `space-xs` | 4 | Micro-gap : icône ↔ texte, badge interne |
| `space-sm` | 8 | Gap serré : éléments d'un même groupe |
| `space-md` | 16 | Séparation standard entre éléments |
| `space-lg` | 24 | Séparation entre sections |
| `space-xl` | 32 | Grande séparation, header ↔ body |
| `space-2xl` | 40 | Marges de page/panel |
| `space-3xl` | 48 | Espacement héroïque (splash, title screen) |

**Règle de décision** : même groupe = `sm`, groupes différents = `md`, sections = `lg`, page = `xl+`.

### Radius
| Token | Valeur | Surface |
|-------|--------|---------|
| `radius-sm` | 6 | Petits éléments (tags, badges, tooltips) |
| `radius-md` | 10 | Éléments standards (boutons, inputs, cards) |
| `radius-lg` | 14 | Grands containers (panels, dialogs) |
| `radius-xl` | 18 | Très grands éléments (modals, overlays) |

**Règle** : plus la surface est grande, plus le radius est grand.

### Typography
| Token | Taille | Line height | Usage |
|-------|--------|-------------|-------|
| `type-caption` | 12 | 16 | Hints, labels secondaires |
| `type-body` | 16 | 22 | Texte courant |
| `type-subtitle` | 18 | 24 | Sous-titres |
| `type-big` | 20 | 26 | Titres de section |
| `type-title` | 24 | 30 | Titres principaux |
| `type-hero` | 32 | 38 | Titres hero |
| `type-display` | 48 | 54 | Splash, écrans titre |

### Couleurs signal
| Token | Rôle |
|-------|------|
| `text/primary` | Texte principal, titres |
| `text/secondary` | Texte d'aide, descriptions |
| `text/disabled` | Texte inactif |
| `state/focus` | Bordure/outline au focus clavier/manette |
| `state/disabled` | Overlay désactivé |
| `status/success` | Confirmation, validé |
| `status/warning` | Attention requise |
| `status/error` | Erreur, danger |

**Règle du double signal** : ne JAMAIS communiquer une information uniquement par la couleur. Toujours coupler avec une icône, une forme, un texte ou un changement d'opacité.

---

## Section 3 : Rôles (metadata Godot)

Les rôles identifient les nodes par leur **fonction** plutôt que par leur path dans l'arbre.

### Assigner un rôle
```
godot_execute_script(code="""
var node = EditorInterface.get_edited_scene_root().get_node("Path/To/Node")
node.set_meta("role", "role_name")
return "Role set: role_name"
""")
```

### Lire un rôle
```
godot_execute_script(code="""
var node = EditorInterface.get_edited_scene_root().get_node("Path/To/Node")
return node.get_meta("role", "none")
""")
```

### Types de rôles

- **Rôle unique** : `title`, `health_bar`, `score_label` — un seul node dans la scène
- **Rôle collection** : `skill_slot`, `inventory_item`, `menu_button` — plusieurs nodes du même type

### Résolution

- **Statique** (recommandé) : cacher le résultat au `_ready()` dans un dict `{role: node}`
- **Dynamique** : chercher à chaque fois — uniquement si les nodes changent à runtime

### Implémentation dans les scripts
```gdscript
# Résolution statique au _ready()
var _roles := {}

func _ready():
    _cache_roles(self)

func _cache_roles(node: Node):
    if node.has_meta("role"):
        _roles[node.get_meta("role")] = node
    for child in node.get_children():
        _cache_roles(child)

func get_role(role: String) -> Node:
    return _roles.get(role)
```

---

## Section 4 : Patterns de structure

### Menu plein écran
Root type : `Control`

```json
[
  {"name": "Background", "type": "ColorRect"},
  {"name": "Center", "type": "CenterContainer", "children": [
    {"name": "Content", "type": "VBoxContainer", "children": [
      {"name": "Title", "type": "Label"},
      {"name": "Spacer", "type": "Control"},
      {"name": "ButtonGroup", "type": "VBoxContainer", "children": [
        {"name": "PlayButton", "type": "Button"},
        {"name": "OptionsButton", "type": "Button"},
        {"name": "QuitButton", "type": "Button"}
      ]}
    ]}
  ]}
]
```

Après création :
```
godot_execute_script(code="""
var root = EditorInterface.get_edited_scene_root()
for child in root.get_children():
	child.set_anchors_preset(Control.PRESET_FULL_RECT)

# Tokens
var content = root.get_node("Center/Content")
content.add_theme_constant_override("separation", 24)

var btn_group = root.get_node("Center/Content/ButtonGroup")
btn_group.add_theme_constant_override("separation", 8)

# Spacer expand
root.get_node("Center/Content/Spacer").size_flags_vertical = Control.SIZE_EXPAND_FILL
root.get_node("Center/Content/Spacer").custom_minimum_size.y = 32

# Rôles
root.get_node("Center/Content/Title").set_meta("role", "title")
root.get_node("Center/Content/ButtonGroup/PlayButton").set_meta("role", "play_button")

# Typo
root.get_node("Center/Content/Title").add_theme_font_size_override("font_size", 32)
root.get_node("Center/Content/Title").horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
return "Menu configured"
""")
```

### HUD overlay
Root : ajouter à une scène de jeu existante.

```json
{"name": "UI", "type": "CanvasLayer", "children": [
  {"name": "HUD", "type": "MarginContainer", "children": [
    {"name": "Layout", "type": "VBoxContainer", "children": [
      {"name": "TopBar", "type": "HBoxContainer", "children": [
        {"name": "HealthBar", "type": "ProgressBar"},
        {"name": "Spacer", "type": "Control"},
        {"name": "ScoreLabel", "type": "Label"}
      ]},
      {"name": "Filler", "type": "Control"},
      {"name": "BottomBar", "type": "HBoxContainer", "children": [
        {"name": "ItemSlots", "type": "HBoxContainer"}
      ]}
    ]}
  ]}
]}
```

Après création : anchors full rect sur HUD, `EXPAND_FILL` sur Spacer et Filler, margins `space-lg` (24) sur le MarginContainer.

Rôles : `health_bar`, `score_label`, `item_slots`.

### Dialog box
```json
{"name": "DialogOverlay", "type": "ColorRect", "children": [
  {"name": "Center", "type": "CenterContainer", "children": [
    {"name": "Panel", "type": "PanelContainer", "children": [
      {"name": "Margin", "type": "MarginContainer", "children": [
        {"name": "VBox", "type": "VBoxContainer", "children": [
          {"name": "DialogTitle", "type": "Label"},
          {"name": "DialogText", "type": "RichTextLabel"},
          {"name": "ButtonRow", "type": "HBoxContainer", "children": [
            {"name": "Spacer", "type": "Control"},
            {"name": "CancelButton", "type": "Button"},
            {"name": "ConfirmButton", "type": "Button"}
          ]}
        ]}
      ]}
    ]}
  ]}
]}
```

DialogOverlay : anchors full rect, `color = Color(0, 0, 0, 0.5)` (dimming). Margin : `space-lg` (24). VBox separation : `space-md` (16). ButtonRow Spacer : `EXPAND_FILL`. DialogText : `custom_minimum_size.y = 60`, `fit_content = true`.

Rôles : `dialog_title`, `dialog_text`, `cancel_button`, `confirm_button`.

### Tooltip
```json
{"name": "TooltipPanel", "type": "PanelContainer", "children": [
  {"name": "Margin", "type": "MarginContainer", "children": [
    {"name": "VBox", "type": "VBoxContainer", "children": [
      {"name": "Header", "type": "HBoxContainer", "children": [
        {"name": "Icon", "type": "TextureRect"},
        {"name": "Title", "type": "Label"}
      ]},
      {"name": "Separator", "type": "HSeparator"},
      {"name": "Description", "type": "RichTextLabel"}
    ]}
  ]}
]}
```

Contraintes : `custom_minimum_size.x = 200`, `custom_maximum_size.x = 350`. Description : `fit_content = true`, `bbcode_enabled = true`. Margin : `space-sm` (8). VBox separation : `space-xs` (4). Header separation : `space-xs` (4).

Rôles : `tooltip_icon`, `tooltip_title`, `tooltip_description`.

### Skill/Item slot
```json
{"name": "SkillSlot", "type": "PanelContainer", "children": [
  {"name": "Content", "type": "CenterContainer", "children": [
    {"name": "Icon", "type": "TextureRect"}
  ]},
  {"name": "Keybind", "type": "Label"},
  {"name": "CooldownOverlay", "type": "TextureProgressBar"}
]}
```

Taille fixe : `custom_minimum_size = Vector2(64, 64)`. Icon : centré. Keybind : ancré en bas-droite, `type-caption` (12). CooldownOverlay : anchors full rect, masque radial.

Rôles : `skill_icon`, `skill_keybind`, `skill_cooldown`.

### Hotbar
```json
{"name": "Hotbar", "type": "HBoxContainer", "children": [
  {"name": "Slot1", "type": "PanelContainer", "children": [...]},
  {"name": "Slot2", "type": "PanelContainer", "children": [...]},
  {"name": "Slot3", "type": "PanelContainer", "children": [...]}
]}
```

Séparation : `space-xs` (4). Chaque slot = pattern Skill/Item slot ci-dessus. Rôle collection : `hotbar_slot`.

---

## Section 5 : Layout rules

### Containers — arbre de décision
```
Besoin de centrer ?
  → CenterContainer

Besoin de liste verticale (menu, formulaire) ?
  → VBoxContainer

Besoin de ligne horizontale (toolbar, boutons) ?
  → HBoxContainer

Besoin de grille (inventaire, level select) ?
  → GridContainer

Besoin de marges/padding ?
  → MarginContainer

Besoin d'overlay sur le jeu ?
  → CanvasLayer > Control (anchors full rect) > layout
```

### Anchors
- **Uniquement sur les Controls racine** — enfants de Containers n'en ont pas besoin
- Toujours via presets pour la fiabilité :
```
godot_execute_script(code="""
var node = EditorInterface.get_edited_scene_root().get_node("NodePath")
node.set_anchors_preset(Control.PRESET_FULL_RECT)
return "Anchors set"
""")
```
- Presets : `PRESET_TOP_LEFT`, `PRESET_TOP_RIGHT`, `PRESET_BOTTOM_LEFT`, `PRESET_BOTTOM_RIGHT`, `PRESET_CENTER`, `PRESET_FULL_RECT`, `PRESET_LEFT_WIDE`, `PRESET_TOP_WIDE`, `PRESET_RIGHT_WIDE`, `PRESET_BOTTOM_WIDE`, `PRESET_HCENTER_WIDE`, `PRESET_VCENTER_WIDE`

### Size flags
```
godot_update_property(path="Path/To/Node", property="size_flags_horizontal", value=3)
# 1 = SIZE_FILL, 2 = SIZE_EXPAND, 3 = SIZE_EXPAND_FILL
```

### Règle d'or
**JAMAIS de position manuelle sur un enfant de Container.** Le Container gère le layout. Point.

---

## Section 6 : Torture tests contenu

Après construction, vérifier CHAQUE composant texte avec ces cas :

| Test | Contenu | Ce qui ne doit PAS arriver |
|------|---------|---------------------------|
| Titre long | 45+ caractères | Texte qui dépasse le conteneur |
| Description longue | 600 caractères | Texte tronqué sans scroll/wrap |
| Langues | FR, DE (mots longs), JA (caractères larges) | Layout qui casse |
| Valeurs extrêmes | 0, 9999, 120s, -5 | Débordement numérique |
| Contenu vide | String vide | Trou dans le layout (placeholder requis) |
| Nom joueur | "XXXXXXXXXXXXXXXXXXXXXXXXX" (25 chars) | Dépasse son conteneur |

Pour tester via MCP :
```
godot_execute_script(code="""
var label = EditorInterface.get_edited_scene_root().get_node("Path/To/Label")
label.text = "This is a very long title that should test wrapping and truncation behavior"
return "Test content applied"
""")
```

Puis vérifier visuellement :
```
godot_get_editor_screenshot
```

---

## Section 7 : Process

1. **Identifier le type d'UI** — menu, HUD, dialog, tooltip, custom ?
2. **Choisir le pattern** de la Section 4
3. **Créer la scène** : `godot_create_scene(root_type="Control", path="res://scenes/ui_name.tscn")`
4. **Construire la hiérarchie** : `godot_add_nodes` avec le JSON du pattern
5. **Assigner les rôles** : `godot_execute_script` pour chaque `set_meta("role", ...)`
6. **Appliquer les tokens** : spacing (`add_theme_constant_override`), radius, typo, couleurs
7. **Vérifier** : `godot_get_editor_screenshot`
8. **Torture tests** : appliquer les cas de la Section 6
9. **Sauvegarder** : `godot_save_scene`

---

## Section 8 : Accessibilité Composition

| Règle | Détail |
|-------|--------|
| **Double signal** | Ne jamais communiquer par la couleur seule. Coupler avec icône, forme, texte ou opacité |
| **États statiques** | Tous les états doivent être lisibles sur un screenshot statique, sans animation |
| **Contraste** | `text/primary` ≥ 4.5:1 sur le fond. `text/secondary` ≥ 3:1 |
| **Navigation** | Tout élément interactif doit être atteignable au clavier/manette |
| **Focus visible** | Le focus doit avoir un indicateur clair (border, outline) — pas juste un changement subtil |
| **Taille minimum** | Boutons et zones cliquables ≥ 44px (tactile) ou 32px (souris/manette) |
