Saltar al contenido
zabloo

Button

El nodo que se pulsa. Convierte un toque, un Enter o un botón del mando en una named action que el juego escucha.

primitivedesde v1focusable

Un Button es el nodo que el jugador pulsa. Lo usas siempre que un gesto tiene que convertirse en una decisión sobre la que el juego actúa: empezar un nivel, confirmar una compra, cerrar un diálogo. Piensa en el menú principal: la fila Play es un Button que lleva la named action "play", y al pulsarla el juego oye esa palabra y nada más. Nunca se entera de qué aspecto tenía el botón, y el botón nunca se entera de qué hizo el juego con ella.

button-variants.viewIR v1
Three buttons in a row — a filled purple Play button, an outlined Options button, and a dimmed, disabled Quit button — over the caption primary, secondary, secondary disabled.
ESTADO
  • hover — inactivo
  • pressed — inactivo
  • focused — inactivo
  • selected — inactivo
  • disabled — inactivo

Se lee del último frame dibujado

Parado: pulsa Ejecutar para dibujarlo en la GPU.

import { Button, Column, Row, Text } from "@zabloo/react";

export default function ButtonVariants() {
  return (
    <Column
      layout={{
        grow: 1,
        justify: "center",
        align: "center",
        gap: "{space.5}",
        padding: "{space.6}",
      }}
      style={{ background: "{color.bg}" }}
    >
      <Row layout={{ gap: "{space.3}", align: "center" }}>
        <Button
          variant="primary"
          onClick="play"
          layout={{ padding: "{space.3}", width: 132, justify: "center", align: "center" }}
        >
          <Text style={{ color: "{color.on-brand}", fontSize: "{text.md}" }}>Play</Text>
        </Button>

        <Button
          variant="secondary"
          onClick="options"
          layout={{ padding: "{space.3}", width: 132, justify: "center", align: "center" }}
        >
          <Text style={{ color: "{color.text}", fontSize: "{text.md}" }}>Options</Text>
        </Button>

        <Button
          variant="secondary"
          disabled
          onClick="quit"
          layout={{ padding: "{space.3}", width: 132, justify: "center", align: "center" }}
        >
          <Text style={{ color: "{color.text}", fontSize: "{text.md}" }}>Quit</Text>
        </Button>
      </Row>

      <Text style={{ color: "{color.faint}", fontSize: "{text.xs}" }}>
        primary · secondary · secondary disabled
      </Text>
    </Column>
  );
}
Pulsa Run y pasa por encima de los tres botones sin quitar ojo a la fila de estado de debajo: nombra lo que lleva puesto el frame —hover, pressed— mientras el tercero, disabled, no reacciona a nada. Las pestañas llevan esos mismos botones tal y como los escribirías y tal y como se publican.

Las props vienen en dos tablas porque hay dos capas, y esa diferencia es el modelo mental entero del producto: variant="primary" es una palabra que escribes tú, y el theme la resuelve en un conjunto de estilos antes de exportar nada. El juego que recibe el envelope no tiene ni idea de que ahí hubiera una variant.

Props de autoría

Lo que escribes en @zabloo/react.

PropTipoPor defectoDescripción
onClickstringabsentNamed action a la que el juego se suscribe.
childrenReactNodeabsentContenido del botón.

Además de estas, todo componente acepta las props base del nodo —id, visible, disabled, layout, style, states, transition, autofocus, clip— más variant, que el theme resuelve al exportar y que nunca aparece en la IR.

Props de IR

Lo que llega al juego, cuando ya no queda capa de autoría.

PropTipoPor defectoDescripción
onClickstringabsentNamed action, se dispara al activarse.
childrenZNode[][]Hijos de flujo normales: una etiqueta, un icono, una fila con las dos cosas.
Un Button no tiene prop de label

Su contenido son sus hijos, colocados por el paso de flex de siempre. Un botón con un icono y un texto es un Button que contiene un Row, no un Button con una prop icon — por eso no hay límite a lo que un botón puede llevar dentro.

Comportamiento

Estados

El jugador se pone encima y lleva hover; lo mantiene pulsado y lleva pressed; llega hasta él con el teclado o con el mando y lleva focused. Como botón elegido de un grupo "exclusive-select" —una pestaña— lleva selected. Y disabled, propio o heredado de una sección que apaga un grupo entero de controles de golpe. El juego no oye nada de esto: un estado es el aspecto que tiene el botón, nunca un mensaje.

Focusable

Sí, para que el teclado y el mando puedan llegar hasta él. disabled lo saca de la navegación, y eso deja su onClick fuera de alcance también: un control al que el jugador no puede llegar no puede disparar nada.

Activación

El jugador lo toca, pulsa Enter mientras lo tiene con el focus, o pulsa A en el mando → el botón se activa → se dispara onClick. Una pulsación que termina fuera del control se cancela y no se envía nada: arrastrar el dedo fuera de un botón es como un jugador se echa atrás, y un mando que se desconecta a media pulsación se trata igual.

Actions

onClick, un solo nombre, se dispara después de que la activación termine. Dentro de un item de Repeat llega con un action context que dice desde qué elemento se ha disparado.

Degradación

En un SDK más antiguo

En un SDK más antiguo la etiqueta se sigue viendo, pero pulsarla no hace nada.

Un Button que el SDK no conoce degrada a un Container: el contenido sobrevive, la pulsación desaparece. Button existe desde la v1, así que en la práctica esto solo afecta a las props añadidas después — pero la regla es la misma que sigue cualquier nodo, y es la razón de que un contenido más nuevo que el SDK que lo recibe sea un caso normal y no un crash.

Play

Play

Cómo oye esto el juego

El botón dispara el nombre; el juego decide qué significa. Ese es el contrato entero: el juego nunca se entera de qué dibujó el botón, el botón nunca se entera de qué hizo el juego con ello, y no hay que rehacer ninguna de las dos partes cuando la otra cambia. Por eso una pantalla se puede volver a publicar sin tocar la build que el jugador ya tiene.

using UnityEngine;
using Zabloo;

[RequireComponent(typeof(ZablooDocument))]
public sealed class MainMenu : MonoBehaviour
{
  ZablooDocument _doc;

  // Start, not OnEnable: it runs after ZablooDocument has built the view.
  void Start()
  {
      _doc = GetComponent<ZablooDocument>();
      _doc.OnAction += OnZablooAction;
  }

  void OnDestroy()
  {
      if (_doc != null) _doc.OnAction -= OnZablooAction;
  }

  void OnZablooAction(string action)
  {
      if (action == "play") StartGame();
      if (action == "options") OpenOptions();
  }
}

Cuando la action se dispara desde dentro de un item de Repeat lleva un action context —el path, la key y el índice del item— para que el juego sepa qué fila se ha pulsado. El context es parte del formato y el renderer del navegador lo entrega; el OnAction de Unity es hoy Action<string> y entrega solo el nombre, así que un juego de Unity que necesite la fila la lee de su propio estado hasta que eso aterrice.

Composición

Como el contenido de un botón son sus hijos y nada más, todo el aspecto que puede tener un botón es una cuestión de layout en vez de una prop:

  • Un icono delante de una etiqueta — usa un Row dentro del botón cuando el icono refuerza la palabra en vez de sustituirla.
  • Un botón ancho al lado de uno estrecho — pon grow en el que deba quedarse el espacio sobrante, cuando el de confirmar tenga que pesar más que su pareja.
  • Un botón que es solo un icono — un Button cuyo único hijo es un Image, para una barra de herramientas donde el dibujo ya deja claro qué hace.
button-composition.viewIR v1
Four buttons showing what a button can hold: a filled Equip button with a shield icon before its label, a wide Confirm button sharing a line with a narrower Cancel, and two square icon-only buttons — a purple star and a gold lightning bolt.
ESTADO
  • hover — inactivo
  • pressed — inactivo
  • focused — inactivo
  • selected — inactivo
  • disabled — inactivo

Se lee del último frame dibujado

Parado: pulsa Ejecutar para dibujarlo en la GPU.

import { Button, Column, Image, Row, Text } from "@zabloo/react";

export default function ButtonComposition() {
  return (
    <Column
      layout={{
        grow: 1,
        justify: "center",
        align: "center",
        gap: "{space.4}",
        padding: "{space.6}",
      }}
      style={{ background: "{color.bg}" }}
    >
      {/* Icon + label: a Row inside the button. */}
      <Button variant="primary" onClick="equip" layout={{ padding: "{space.3}" }}>
        <Row layout={{ gap: "{space.2}", align: "center" }}>
          <Image
            src="icons/shield.png"
            layout={{ width: 18, height: 18 }}
            style={{ color: "{color.on-brand}" }}
          />
          <Text style={{ color: "{color.on-brand}", fontSize: "{text.md}" }}>Equip</Text>
        </Row>
      </Button>

      {/* A pair sharing a line: `grow` gives the primary the leftover space. */}
      <Row layout={{ gap: "{space.2}", width: 320 }}>
        <Button
          variant="primary"
          onClick="confirm"
          layout={{ grow: 1, padding: "{space.3}", justify: "center", align: "center" }}
        >
          <Text style={{ color: "{color.on-brand}", fontSize: "{text.md}" }}>Confirm</Text>
        </Button>
        <Button variant="secondary" onClick="cancel" layout={{ padding: "{space.3}" }}>
          <Text style={{ color: "{color.text}", fontSize: "{text.md}" }}>Cancel</Text>
        </Button>
      </Row>

      {/* Icon only: the accessible name is the game's business, not the IR's. */}
      <Row layout={{ gap: "{space.2}" }}>
        <Button
          variant="secondary"
          onClick="favorite"
          layout={{
            padding: "{space.2}",
            width: 40,
            height: 40,
            justify: "center",
            align: "center",
          }}
        >
          <Image
            src="icons/star.png"
            layout={{ width: 18, height: 18 }}
            style={{ color: "{color.brand}" }}
          />
        </Button>
        <Button
          variant="secondary"
          onClick="charge"
          layout={{
            padding: "{space.2}",
            width: 40,
            height: 40,
            justify: "center",
            align: "center",
          }}
        >
          <Image
            src="icons/bolt.png"
            layout={{ width: 18, height: 18 }}
            style={{ color: "{color.gold}" }}
          />
        </Button>
      </Row>
    </Column>
  );
}
Cuatro botones, y ninguno se diferencia por una prop: fíjate en cómo el icono y la etiqueta van dentro de un Row, en cómo Confirm se queda el ancho sobrante que Cancel no coge, y en que los dos cuadrados son el mismo nodo con un solo hijo. Todo es layout.

Un botón es además el anchor natural de un popover: un Overlay anclado a su id con trigger: "press" se abre con la misma pulsación que dispara onClick. Abrirse es comportamiento; nunca sustituye a la action declarada.

Dentro de un item de Repeat, el mismo botón aparece una vez por elemento del array enlazado y su action lleva consigo el item desde el que se disparó — así es como onClick: "buy" puede decir qué fila se ha comprado sin que la UI sepa nada de tiendas.

Relacionado