[#_lesson_20_menus_and_screens]
== Lección 20: Menús y pantallas
image::learn/20-gui.png[La lección terminada, 480]
Puedes construir un menú con lo que ya sabes. Una caja, algo de texto, un
cursor que se mueve cuando se mueve el stick, un `if` para cada opción, y
aritmética para que todo siga alineado cuando añadas una cuarta opción.
Funciona. También son unas cien líneas para una página con tres botones, y cada
una de esas líneas te toca mantenerla a ti.
Singe lleva dentro todo un motor de documentos -- RmlUi -- y un menú escrito
para él es una página, como una página web, con una hoja de estilo. Reparte él
solo su texto en líneas. Se vuelve a maquetar solo cuando añades una opción.
Sabe qué es un botón, qué es un deslizador y a qué control está apuntando el
jugador. Esta lección construye con él un menú de pausa, sobre un juego que
sigue funcionando por detrás.
[#_two_files]
=== Dos archivos
Una página de GUI son dos cosas: un *documento* que dice qué hay en la página,
y *Lua* que dice qué hace la página. Viven en archivos separados, y esa es toda
la gracia del ejercicio -- la maquetación deja de ser código.
Empieza con un script llamado `menu.singe` y un juego tan pequeño que cabe en
un párrafo: una nave que cruza la pantalla y una puntuación que sube cada vez
que llega al otro lado. Llevas escribiendo este tipo de cosas desde la lección
siete.
[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
----
Eso funciona. Ahora el documento. Junto a tu script, crea un archivo llamado
`pause.rml`:
[source,html]
----
Pause
Paused
Score 000000
Arrow keys move, Return chooses.
----
Si alguna vez has visto el código fuente de una página web, esto te sonará. Si
no, aquí tienes todo lo que necesitas para leerlo.
Un documento está hecho de *elementos*. Un elemento empieza con un nombre entre
corchetes angulares, `
`, termina con el mismo nombre y una barra, `
`, y
lo que haya entre los dos está dentro de él. Algunos elementos no tienen nada
dentro y se cierran solos con una barra al final, como el `` de
arriba. Los elementos se anidan, y la sangría muestra el anidamiento igual que
lo muestra en Lua.
Las palabras que hay dentro de la etiqueta de apertura son *atributos*, y se
escriben `name="value"`. Dos de ellos te importan más que el resto. `id` le da a
un elemento un nombre por el que tu script puede encontrarlo, que es como se
hablan Lua y el documento. `class` mete el elemento en un grupo al que puede
apuntar la hoja de estilo; `panel`, `muted` y `list` son grupos que la hoja de
estilo del propio motor ya conoce.
El lenguaje de marcado se llama RML y el lenguaje de estilos se llama RCSS. Son
de RmlUi, no de Singe. El capítulo sobre la GUI de la referencia te remite a la
documentación de RmlUi para la lista completa de lo que puedes escribir, y
tarde o temprano la querrás. Todo lo de esta lección funciona sin ella.
[#_show_it]
=== Muéstrala
De vuelta al script. Encima de `onOverlayUpdate`, añade:
[source,lua]
----
local gui = guiNew(overlayGetWidth(), overlayGetHeight())
local page = guiLoad(gui, DIR .. "pause.rml")
----
`guiNew` crea una *GUI*: un rectángulo de tamaño fijo en el que se maquetan y se
dibujan los documentos. Devuelve un handle, igual que `fontLoad` y
`spriteLoad`. Hacerla del tamaño del overlay significa que un píxel del
documento es un píxel de tu overlay, y así las cuentas que haces de cabeza
siguen siendo sencillas.
`guiLoad` lee un documento en esa GUI y devuelve un segundo handle, para el
propio documento. Casi todas las demás llamadas reciben los dos: la GUI y el
documento que hay en ella. Una GUI puede contener hasta treinta y dos
documentos -- una página de título, una de opciones y una de fin de partida
pueden compartir una -- y pueden existir hasta dieciséis GUI a la vez.
Todavía no hay nada en pantalla, porque cargar un documento no lo dibuja. Añade
una línea a `onOverlayUpdate`, justo antes del `return`:
[source,lua]
----
guiDraw(gui)
----
Ejecútalo. Ahí está el panel, encima de la nave, con su título, sus tres
botones y su deslizador, y no has escrito nada de código de dibujo.
`guiDraw` compone la GUI sobre el overlay *solo para este fotograma*, como
todas las demás llamadas de dibujo, así que va en `onOverlayUpdate` y hay que
volver a llamarla en el fotograma siguiente. Dónde la pongas decide qué queda
encima: todo lo que dibujaste antes queda debajo, y todo lo que dibujes después
va por encima. Un punto de mira dibujado después de `guiDraw` nunca queda tapado
por el menú.
[#_let_the_player_use_it]
=== Deja que el jugador la use
Haz clic en un botón. No pasa nada, y el botón ni siquiera se ilumina.
Una GUI nueva es un dibujo, no un panel de control. Es a propósito: un HUD o un
cartel no deberían tragarse el botón de disparo. Para que una página reciba
entrada:
[source,lua]
----
guiSetInput(gui, true)
----
Ahora el ratón funciona, los botones se iluminan bajo el puntero y las teclas de
flecha saltan de un control a otro, porque el documento lo pidió con
`nav: auto`. En un mando o en un stick, las cuatro direcciones llegan como las
teclas de flecha, el primer botón de acción llega como Intro y el segundo llega
como Esc. Una recreativa con un joystick y dos botones maneja esta página sin
saber que es un documento.
Del control sobre el que actúan las teclas se dice que tiene el *foco*, y un
botón con el foco lleva un borde dorado. El foco empieza en Resume porque ese
botón dice `autofocus`: cada vez que se muestra la página, el foco va al control
marcado así. Si lo quitas, las teclas de flecha no tienen desde dónde moverse
hasta que el jugador hace clic en algo, lo que en una recreativa sin ratón no
pasa nunca.
Pero hacer clic sigue sin hacer nada, porque no hay nada escuchando.
[#_talk_to_it]
=== Háblale
Tres llamadas unen el documento a tu script, y entre las tres lo hacen casi
todo.
[source,lua]
----
guiSetHandler(gui, page, "resume", "click", function()
closeMenu()
end)
----
`guiSetHandler` dice: cuando el elemento con este `id` haga esto, llama a esta
función. El `id` es el del documento. El nombre del evento es de RmlUi:
`"click"` para los botones, `"change"` para cualquier cosa que el jugador
ajuste, y una larga lista de otros que puedes consultar cuando los necesites. Se
guarda una función por elemento y por evento, así que poner otra sustituye a la
anterior, y pasar `nil` en lugar de una función la quita.
Tu función se llama con cinco argumentos -- la GUI, el documento, el id, el
evento y el valor actual del elemento como cadena. Recoge los que quieras e
ignora el resto, igual que ya ignoras argumentos en Lua.
[source,lua]
----
guiSetHandler(gui, page, "speed", "change", function(g, d, id, event, value)
speed = math.floor(tonumber(value))
end)
----
El deslizador entrega su valor como cadena, y RmlUi escribe los números a su
manera, así que `4` llega como `"4.000000"`. `tonumber` convierte la cadena en
un número y `math.floor` tira los decimales. Saltarte ese paso y usar la cadena
como si fuera un número es un error que cometerás una vez.
[source,lua]
----
guiSetValue(gui, page, "score", string.format("%06d", score))
----
`guiSetValue` escribe en un elemento. En un control de formulario pone el valor
del control; en cualquier otra cosa -- un párrafo, un `div`, el
`` de tu documento -- sustituye lo que hay dentro del elemento,
y la página se vuelve a maquetar en el fotograma siguiente. `guiGetValue` es la
misma llamada al revés, y responde `nil` para un id que no tiene ningún
elemento, así que puedes preguntar por un elemento que quizá no exista sin
morir en el intento.
Esa es toda la API del día a día: `guiSetHandler`, `guiSetValue`, `guiGetValue`.
Hay muchísimo más a tu alcance a través de los propios objetos Lua de RmlUi, a
los que se llega desde la variable global `rmlui`, y el capítulo sobre la GUI de
la referencia explica cómo. Llegarás muy lejos antes de necesitarlo.
[#_hide_it_again]
=== Vuelve a esconderla
Un menú de pausa no está a la vista todo el tiempo. `guiHide` quita un
documento de la pantalla y `guiShow` lo vuelve a poner, conservando todo lo que
el jugador haya escrito o elegido.
La pausa en sí es `singeSetPauseFlag(true)`, que detiene el disco, los vídeos y
los sonidos pero *sigue llamando a tus callbacks*, así que todavía puedes
dibujar. El motor tiene su propia tecla de pausa, que congela el script por
completo, y las dos se pelearían; `singeSetPauseKeyEnabled(false)` le quita esa
tecla al motor para que `SWITCH_PAUSE` llegue a `onInputPressed` como cualquier
otro interruptor. En un teclado, esa tecla es la *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
----
Apagar la entrada junto con la página no es decoración. Una página escondida
que sigue recibiendo entrada se come las teclas de flecha del jugador durante
el resto de la partida.
[#_what_the_page_does_not_use_you_still_get]
=== Lo que la página no usa te sigue llegando
Esta es la regla que hace que sea seguro dejar una GUI a la vista durante la
partida, y vale la pena decirla claramente: *un evento que ha usado un elemento
nunca llega a tus callbacks.* Un Intro que ha pulsado el botón que tiene el
foco no llega como `onInputPressed`. Una flecha que ha movido el foco no llega
como `SWITCH_LEFT`. Un clic en un botón no es un interruptor del ratón.
Todo lo que la página no ha usado la atraviesa y te llega como siempre. Por eso
`P` cierra este menú: ningún elemento del documento usa `P`, así que la tecla
llega a `onInputPressed` exactamente igual que cuando el menú no está. También
es por eso que un HUD que recibe entrada sigue dejando pasar el botón de
disparo.
Hay una tecla que no sigue la regla. RmlUi usa la tecla Tab para mover el foco y
nunca la devuelve, así que Singe solo le pasa Tab a una GUI mientras alguien
está escribiendo de verdad en un campo de texto. Un juego que asigna Tab a algo
la conserva.
[#_style_it]
=== Dale estilo
Vuelve a mirar el `` del documento. Una sola línea hace casi todo el
trabajo:
[source,html]
----
----
Ese es el tema que trae el motor, y es la razón de que tu panel tenga un borde,
un degradado y un título dorado sin que tú hayas elegido nada de eso. Sin una
hoja de estilo, RmlUi casi no dibuja nada, porque todos los elementos empiezan
en línea y del tamaño de su contenido, así que enlazar esta primero y
sobrescribir lo que no te guste es el camino más rápido hacia una página que
parezca algo.
El tema te da `.panel` (una caja con borde y esquinas redondeadas), `.list`
(una caja con desplazamiento cuyas filas `div` hijas se resaltan y reciben el
foco), `.muted` (texto más tenue para las pistas), títulos, y todos los
controles de formulario con sus estados al pasar el ratón, con el foco y
pulsados. El capítulo sobre la GUI de la referencia los enumera.
Todo lo que va después del enlace es tuyo. El bloque `