[#_lesson_20_menus_and_screens]
== Leçon 20 : Menus et écrans
image::learn/20-gui.png[La leçon terminée, 480]
Vous pouvez construire un menu avec ce que vous savez déjà. Une boîte, un peu
de texte, un curseur qui bouge quand le stick bouge, un `if` pour chaque choix,
et de l'arithmétique pour garder tout ça aligné quand vous ajoutez une
quatrième option. Ça marche. C'est aussi une centaine de lignes pour une page
avec trois boutons, et chacune de ces lignes, c'est à vous de l'entretenir.
Singe contient tout un moteur de documents -- RmlUi -- et un menu écrit pour lui
est une page, comme une page web, avec une feuille de style. Elle fait passer
son texte à la ligne toute seule. Elle se met en page d'elle-même quand vous
ajoutez une option. Elle sait ce qu'est un bouton, ce qu'est un curseur, et
quel contrôle le joueur est en train de viser. Cette leçon s'en sert pour
construire un menu de pause, par-dessus un jeu qui continue de tourner
derrière.
[#_two_files]
=== Deux fichiers
Une page de GUI, c'est deux choses : un *document* qui dit ce qu'il y a sur la
page, et du *Lua* qui dit ce que fait la page. Ils vivent dans des fichiers
séparés, et c'est tout l'intérêt de l'exercice -- la mise en page cesse d'être
du code.
Commencez par un script appelé `menu.singe` et un jeu si petit qu'il tient en
un paragraphe : un vaisseau qui traverse l'écran, et un score qui augmente
chaque fois qu'il arrive de l'autre côté. Vous écrivez ce genre de chose depuis
la leçon sept.
[source,lua]
----
dofile("Singe/Framework.singe")
overlaySetResolution(discGetWidth(), discGetHeight())
local ship = spriteLoad(DIR .. "art/spaceship.png")
local shipX = 0
local speed = 2
local score = 0
function onOverlayUpdate()
overlayClear()
shipX = shipX + speed
if shipX > overlayGetWidth() then
shipX = -spriteGetWidth(ship)
score = score + 10
end
spriteDraw(ship, shipX, 300)
return OVERLAY_UPDATED
end
----
Ça tourne. Maintenant, le document. À côté de votre script, créez un fichier
appelé `pause.rml` :
[source,html]
----
Pause
Paused
Score 000000
Arrow keys move, Return chooses.
----
Si vous avez déjà vu le code source d'une page web, ça vous dira quelque chose.
Sinon, voici tout ce qu'il vous faut pour le lire.
Un document est fait d'*éléments*. Un élément commence par un nom entre
chevrons, `
`, finit par le même nom avec une barre oblique, `
`, et
tout ce qui se trouve entre les deux est à l'intérieur. Certains éléments n'ont
rien à l'intérieur et se ferment eux-mêmes avec une barre oblique à la fin,
comme le `` ci-dessus. Les éléments s'imbriquent, et l'indentation
montre l'imbrication de la même façon qu'en Lua.
Les mots à l'intérieur de la balise ouvrante sont des *attributs*, écrits
`name="value"`. Deux d'entre eux vous concernent plus que les autres. `id`
donne à un élément un nom grâce auquel votre script peut le trouver, et c'est
ainsi que Lua et le document se parlent. `class` met l'élément dans un groupe
que la feuille de style peut viser ; `panel`, `muted` et `list` sont des
groupes que la feuille de style du moteur connaît déjà.
Le langage de balisage s'appelle RML et le langage de style s'appelle RCSS. Ce
sont ceux de RmlUi, pas ceux de Singe. Le chapitre GUI de la Référence renvoie à
la documentation de RmlUi pour la liste complète de ce que vous pouvez écrire,
et vous en aurez besoin un jour. Tout ce qui est dans cette leçon fonctionne sans.
[#_show_it]
=== Affichez-la
Retour au script. Au-dessus de `onOverlayUpdate`, ajoutez :
[source,lua]
----
local gui = guiNew(overlayGetWidth(), overlayGetHeight())
local page = guiLoad(gui, DIR .. "pause.rml")
----
`guiNew` crée une *GUI* : un rectangle de taille fixe dans lequel des documents
sont mis en page et dessinés. L'appel renvoie un handle, comme le font
`fontLoad` et `spriteLoad`. La faire de la taille de l'overlay veut dire qu'un
pixel du document est un pixel de votre overlay, ce qui garde simple
l'arithmétique que vous faites de tête.
`guiLoad` lit un document dans cette GUI et renvoie un second handle, pour le
document lui-même. Presque tous les autres appels prennent les deux : la GUI et
le document qu'elle contient. Une GUI peut contenir jusqu'à trente-deux
documents -- une page de titre, une page d'options et une page de fin de partie
peuvent en partager une -- et il peut exister jusqu'à seize GUI à la fois.
Rien n'est encore à l'écran, parce que charger un document ne le dessine pas.
Ajoutez une ligne à `onOverlayUpdate`, juste avant le `return` :
[source,lua]
----
guiDraw(gui)
----
Lancez-le. Le panneau est là, par-dessus le vaisseau, avec son titre, ses trois
boutons et son curseur, et vous n'avez écrit aucun code de dessin.
`guiDraw` superpose la GUI à l'overlay *pour cette image seulement*, comme tous
les autres appels de dessin, donc sa place est dans `onOverlayUpdate` et il
faut l'appeler de nouveau à l'image suivante. L'endroit où vous la mettez
décide de ce qui est au-dessus : tout ce que vous avez dessiné avant est en
dessous, et tout ce que vous dessinez après passe par-dessus. Un réticule
dessiné après `guiDraw` n'est jamais caché par le menu.
[#_let_the_player_use_it]
=== Laissez le joueur s'en servir
Cliquez sur un bouton. Il ne se passe rien, et le bouton ne s'allume même pas.
Une nouvelle GUI est une image, pas un tableau de commandes. C'est voulu : un
HUD ou une pancarte ne doit pas avaler le bouton de tir. Pour qu'une page
accepte les entrées :
[source,lua]
----
guiSetInput(gui, true)
----
Maintenant la souris fonctionne, les boutons s'allument sous le pointeur, et
les touches fléchées passent d'un contrôle à l'autre, parce que le document l'a
demandé avec `nav: auto`. Sur une manette ou un stick, les quatre directions
arrivent comme les touches fléchées, le premier bouton d'action arrive comme
Entrée, et le second comme Échap. Une borne avec un joystick et deux boutons
pilote cette page sans savoir que c'est un document.
On dit que le contrôle sur lequel agissent les touches a le *focus*, et un
bouton qui a le focus porte une bordure dorée. Le focus démarre sur Resume parce
que ce bouton porte `autofocus` : chaque fois que la page s'affiche, le focus va
au contrôle marqué ainsi. Retirez-le et les touches fléchées n'ont aucun point
de départ tant que le joueur n'a pas cliqué quelque part, ce qui, sur une borne
sans souris, n'arrive jamais.
Pourtant, cliquer ne fait toujours rien, parce que personne n'écoute.
[#_talk_to_it]
=== Parlez-lui
Trois appels relient le document à votre script, et à eux trois ils font
presque tout.
[source,lua]
----
guiSetHandler(gui, page, "resume", "click", function()
closeMenu()
end)
----
`guiSetHandler` dit : quand l'élément qui porte cet `id` fait telle chose,
appelle cette fonction. Cet `id` est celui du document. Le nom de l'événement est
celui de RmlUi : `"click"` pour les boutons, `"change"` pour tout ce que le
joueur règle, et une longue liste d'autres que vous pourrez chercher quand vous
en aurez besoin. Une seule fonction est gardée par élément et par événement,
donc en régler une autre la remplace, et passer `nil` au lieu d'une fonction la
retire.
Votre fonction est appelée avec cinq arguments -- la GUI, le document, l'id,
l'événement, et la valeur actuelle de l'élément sous forme de chaîne.
Prenez-en autant que vous voulez et ignorez le reste, comme vous ignorez déjà
des arguments en Lua.
[source,lua]
----
guiSetHandler(gui, page, "speed", "change", function(g, d, id, event, value)
speed = math.floor(tonumber(value))
end)
----
Le curseur transmet sa valeur sous forme de chaîne, et RmlUi formate les
nombres à sa façon, donc `4` arrive sous la forme `"4.000000"`. `tonumber`
transforme la chaîne en nombre et `math.floor` jette la partie décimale. Sauter
cette étape et utiliser la chaîne comme un nombre est une erreur que vous ferez
une fois.
[source,lua]
----
guiSetValue(gui, page, "score", string.format("%06d", score))
----
`guiSetValue` écrit dans un élément. Pour un contrôle de formulaire, elle règle
la valeur du contrôle ; pour tout le reste -- un paragraphe, une `div`, le
`` de votre document -- elle remplace ce qu'il y a dans
l'élément, et la page refait sa mise en page à l'image suivante. `guiGetValue`
est le même appel à l'envers, et elle répond `nil` pour un id que rien ne
porte, si bien que vous pouvez demander des nouvelles d'un élément qui n'est
peut-être pas là sans que tout plante.
Voilà toute l'API de tous les jours : `guiSetHandler`, `guiSetValue`,
`guiGetValue`. Il y a bien plus à disposition à travers les objets Lua propres
à RmlUi, accessibles par la globale `rmlui`, et le chapitre GUI de la référence
montre comment. Vous irez loin avant d'en avoir besoin.
[#_hide_it_again]
=== Cachez-la de nouveau
Un menu de pause n'est pas affiché tout le temps. `guiHide` retire un document
de l'écran et `guiShow` le remet, en gardant tout ce que le joueur a tapé ou
choisi.
La pause elle-même, c'est `singeSetPauseFlag(true)`, qui arrête le disque, les
vidéos et les sons mais *continue d'appeler vos callbacks*, si bien que vous
pouvez encore dessiner. Le moteur a sa propre touche de pause, qui fige
complètement le script, et les deux se battraient ;
`singeSetPauseKeyEnabled(false)` retire cette touche au moteur pour que
`SWITCH_PAUSE` arrive à `onInputPressed` comme n'importe quel autre contact.
Sur un clavier, cette touche est *P*.
[source,lua]
----
local function closeMenu()
paused = false
singeSetPauseFlag(false)
guiHide(gui, page)
guiSetInput(gui, false)
end
local function openMenu()
paused = true
singeSetPauseFlag(true)
guiSetValue(gui, page, "score", string.format("%06d", score))
guiShow(gui, page)
guiSetInput(gui, true)
end
----
Couper les entrées en même temps que la page n'est pas de la décoration. Une
page cachée qui accepte encore les entrées mange les touches fléchées du joueur
pour le reste de la partie.
[#_what_the_page_does_not_use_you_still_get]
=== Ce que la page n'utilise pas, vous le recevez quand même
C'est la règle qui permet de laisser une GUI affichée pendant le jeu sans
danger, et elle mérite d'être dite clairement : *un événement qu'un élément a
utilisé n'atteint jamais vos callbacks.* Un appui sur Entrée qui a actionné le
bouton qui a le focus n'arrive pas comme `onInputPressed`. Une flèche qui a
déplacé le focus n'arrive pas comme `SWITCH_LEFT`. Un clic sur un bouton n'est
pas un contact de souris.
Tout ce que la page n'a pas utilisé passe tout droit, comme d'habitude. C'est
pour ça que `P` ferme ce menu : aucun élément du document n'utilise `P`, donc
la touche arrive à `onInputPressed` exactement comme quand le menu n'est pas
affiché. C'est aussi pour ça qu'un HUD qui accepte les entrées laisse quand
même passer le bouton de tir.
Une touche ne suit pas la règle. RmlUi utilise Tab pour déplacer le focus et ne
rend jamais la touche, donc Singe ne donne Tab à une GUI que pendant qu'on est
réellement en train de taper dans un champ de texte. Un jeu qui affecte Tab à
quelque chose le garde.
[#_style_it]
=== Mettez-la en forme
Revenez au `` du document. Une ligne fait l'essentiel du travail :
[source,html]
----
----
C'est le thème que le moteur fournit, et c'est pour ça que votre panneau a une
bordure, un dégradé et un titre doré sans que vous ayez rien choisi. Sans
feuille de style, RmlUi ne dessine presque rien, parce que chaque élément part
en ligne et à la taille de son contenu ; alors lier celle-ci en premier et
redéfinir ce qui vous déplaît est le chemin le plus rapide vers une page qui
ressemble à quelque chose.
Le thème vous donne `.panel` (une boîte arrondie avec une bordure), `.list`
(une boîte qui défile et dont les lignes `div` enfants sont mises en évidence
et prennent le focus), `.muted` (un texte plus pâle pour les indications), des
titres, et tous les contrôles de formulaire avec leurs états survol, focus et
enfoncé. Le chapitre GUI de la Référence les liste.
Tout ce qui vient après le lien est à vous. Le bloc `