Saltar al contenido
zabloo

Text

El nodo que enseña palabras. Lleva una cadena —escrita por ti, o leída en vivo de datos del juego— y se mide a partir de los glifos que dibuja.

primitivedesde v1sin focus

Un Text enseña una cadena, y no hace nada más. No acepta hijos: lo que se le da es una tirada de palabras, medida a partir de los propios glifos y colocada después como cualquier otra caja. Usas uno para cada palabra de una pantalla: la etiqueta de un botón, el título de una misión, el número de un contador de oro. Ese último merece imaginárselo, porque es donde el nodo se gana el sueldo: dale un bindingbind="player.gold"— y lee un número del que es dueño el juego, así que el contador sigue al oro sin código por medio.

text-wrap.viewIR v1
Viewport

Viewport: 960 × 340

A Quest log panel with a gold counter reading 3 open. Below it the same sentence is set twice at the same width: on the left it wraps over three lines in full, on the right it is capped at two lines and ends in an ellipsis. A last line reads textAlign · textAlignY, centred both ways inside its own box.
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 { Column, Row, Text } from "@zabloo/react";


const LORE =
  "The guild keeps its ledger in the back room, and the quartermaster reads it out loud once a week.";

const COLUMN_WIDTH = 236;

export default function TextWrap() {
  return (
    <Column
      // `align: "stretch"` on the ROOT is what hands the panel the view's own
      // width; centring it instead would pin the panel to its content and no
      // change of viewport could reach it (ZAB-153).
      layout={{ grow: 1, justify: "center", align: "stretch", padding: "{space.6}" }}
      style={{ background: "{color.bg}" }}
    >
      <Column
        id="panel"
        layout={{ padding: "{space.5}", gap: "{space.4}", align: "stretch" }}
        style={{
          background: "{color.surface}",
          radius: "{radius.lg}",
          borderWidth: "{border.hairline}",
          borderColor: "{color.line}",
        }}
      >
        {/* A bound Text shows the value as it is: there is no formatting at
            runtime, so the game composes the string before it calls SetData. */}
        <Row layout={{ justify: "space-between", align: "center" }}>
          <Text style={{ color: "{color.text}", fontSize: "{text.lg}" }}>Quest log</Text>
          <Row layout={{ gap: "{space.1}", align: "center" }}>
            <Text bind="quest.count" style={{ color: "{color.gold}", fontSize: "{text.sm}" }} />
            <Text style={{ color: "{color.faint}", fontSize: "{text.sm}" }}>open</Text>
          </Row>
        </Row>

        {/* The two columns keep their width at every viewport, and that is the
            point of THIS fixture: the comparison only holds while both are
            measured against the same width. What the viewport changes is
            whether they fit side by side — 236 + 16 + 236 clears a desktop and
            not a phone, so on a phone the second drops under the first
            (ZAB-153). Growing them instead would widen the paragraph until it
            no longer needed a third line, and the truncated twin would have
            nothing left to truncate. */}
        <Row layout={{ wrap: true, gap: "{space.4}", align: "start" }}>
          <Column layout={{ width: COLUMN_WIDTH, gap: "{space.2}", align: "stretch" }}>
            <Text style={{ color: "{color.faint}", fontSize: "{text.xs}" }}>wraps</Text>
            {/* No `maxLines`: the greedy first-fit pass breaks at spaces and
                uses as many lines as the width needs. */}
            <Text style={{ color: "{color.muted}", fontSize: "{text.sm}", lineHeight: 19 }}>
              {LORE}
            </Text>
          </Column>

          <Column layout={{ width: COLUMN_WIDTH, gap: "{space.2}", align: "stretch" }}>
            <Text style={{ color: "{color.faint}", fontSize: "{text.xs}" }}>
              maxLines 2 · ellipsis
            </Text>
            {/* Same string, same width. Lines past the cap are dropped and the
                last one keeps glyphs until the … fits. */}
            <Text
              style={{
                color: "{color.muted}",
                fontSize: "{text.sm}",
                lineHeight: 19,
                maxLines: 2,
                overflow: "ellipsis",
              }}
            >
              {LORE}
            </Text>
          </Column>
        </Row>

        {/* Centred on its own rect, and vertically centred inside it: both are
            style, so a theme or a state can override either. */}
        <Text
          layout={{ height: 40 }}
          style={{
            color: "{color.text}",
            fontSize: "{text.md}",
            textAlign: "center",
            textAlignY: "center",
            background: "{color.slot}",
            radius: "{radius.md}",
          }}
        >
          textAlign · textAlignY
        </Text>
      </Column>
    </Column>
  );
}
La misma frase al mismo ancho, dos veces. Fíjate en dónde para cada una: el bloque de la izquierda envuelve todo lo que haga falta, mientras que maxLines y overflow cortan el de la derecha a dos líneas y marcan el corte con puntos suspensivos. El contador que hay junto al título está enlazado — el panel de datos de la figura nombra el path que está leyendo.

Abajo hay dos tablas, y aquí la corta es la segunda. Lo que escribes acepta o hijos estáticos o un bind; lo que se publica funde las dos cosas en un único campo, porque a esas alturas la diferencia ha dejado de importar: el nodo tiene contenido, y de dónde salió el contenido era asunto de quien lo escribió.

Props de autoría

Lo que escribes en @zabloo/react.

PropTipoPor defectoDescripción
childrenstring | number | Array<string | number>Contenido estático. Las cadenas y los números contiguos se unen al escribir.
bindstringabsentUn data path. Mutuamente excluyente con children.

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
textBindable<string>El contenido, literal o enlazado. "" es contenido válido.

Una sola prop, y todo lo demás de un Text es style: color, fontSize, textAlign, textAlignY, lineHeight, wrap, overflow, maxLines. No hay props de layout específicas de Text — que es exactamente lo que permite que un variant tematice una etiqueta y que un estado lo pise, igual que con cualquier otra entrada visual.

Un texto vacío es un Text; uno ausente no

"" es el aspecto que tiene una etiqueta que no tiene nada que decir —un desplegable antes de elegir valor, un contador a cero, un path que el juego todavía no ha rellenado—, así que el nodo se carga y se pinta como cualquier otro, y conserva su altura. Lo que hace el nodo imposible de cargar es que el campo esté ausente: el lector lo tira con invalid-node, porque un Text al que nunca se le dio contenido es un árbol que nadie quiso escribir.

Comportamiento

Estados

Solo disabled, heredado de quien lo haya declarado — que es lo que permite que la etiqueta de una sección apagada se apague junto a los controles que nombra.

Focusable

No. Nada de lo que haga el jugador llega a un Text: nunca recibe hover y no acepta input, así que no se le aplica ningún states.* más allá de disabled, y aquí no hay nada que el juego pueda oír.

Glifos

Se dibujan solos: el SDK rasteriza los glifos de un TTF en su propio atlas y los dibuja como quads, sin ningún elemento de texto del motor por medio. La fuente está fijada en la v1 — todos los targets empotran la misma tipografía, que es lo que hace verificable el algoritmo de corte de abajo en vez de delegado. fontSize salta (es la clave del atlas, así que una transition nunca lo interpola) y se acota a 1..512.

Actions

Ninguna. Una palabra que el jugador puede pulsar es un Text dentro de un Button.

Cómo se maqueta el texto

Los puntos de corte tienen que ser idénticos en todos los targets, así que el algoritmo está especificado, no delegado: un SDK implementa exactamente esto, no “lo que haga el motor de texto de la plataforma”. La versión corta, en el orden en que corre el paso:

  1. El ancho disponible es lo que ofrece el padre menos el padding. Un layout.width explícito sustituye a esa oferta para ese subárbol; una fila y una columna se comportan igual, porque la v1 no mide competencia entre hijos. Un ScrollView no ofrece nada en un eje con scroll, así que el texto dentro de un scroller horizontal no envuelve nunca.
  2. Los cortes duros. \r\n y \r se normalizan a \n, que corta siempre. Un párrafo vacío sigue produciendo una línea.
  3. El wrap por palabras es voraz, a primer encaje. Las únicas oportunidades de corte son las tiradas de espacio y tabulador — un espacio duro aguanta, y un guion también. Los espacios que caen en un corte se tiran; los que empiezan una línea cuentan, así que la sangría sobrevive. Toda medida incluye el kerning, y un corte termina la cadena.
  4. Las palabras largas que no caben ni en una línea para ellas solas se parten entre glifos, en el último que quepa, con un mínimo de un glifo por línea.
  5. El truncado: las líneas que pasan de maxLines se tiran, y después overflow corta lo que siga siendo demasiado ancho — solo con wrap: false, ya que una línea envuelta ya cabe. overflow: "ellipsis" marca con la última línea que se conserva.
  6. La colocación. El alto del bloque es líneas × lineHeight; la línea base de cada línea va a media interlínea, así que subir el lineHeight nunca saca de su centro a un Text de una sola línea. textAlign alinea cada línea por su propio ancho; textAlignY alinea el bloque entero.

Todas las propiedades de texto saltan igual que fontSize: un re-wrap no tiene valores intermedios con sentido, así que ninguna es animable.

El Text vacío

"" es una línea —la misma regla del párrafo vacío del paso 2, aplicada al contenido entero—, así que un Text vacío mide 0 × lineHeight y conserva su altura. No es un caso especial; es lo que el algoritmo ya decía.

Ese es el comportamiento que un layout necesita. Una fila con gap alrededor de un <Text> cuyo binding se queda en blanco conserva su hueco en vez de colapsar y desplazar a sus hermanos un gap a la izquierda. El ancho es cero: una línea vacía no pinta nada, así que nada reserva sitio horizontal. Un nodo que deba desaparecer del todo usa visible, que lo saca del layout, gap incluido.

Degradación

En un SDK más antiguo

En un SDK más antiguo la caja sigue ahí con su tamaño — las palabras de dentro no.

Un SDK que no conociera este tipo dibujaría un Container vacío: Text es una hoja, así que no hay contenido hijo que preservar. La caja, su fondo y su borde sobreviven; los glifos no. Text existe desde la v1, así que en la práctica esto es la forma de la regla más que un caso con el que te vayas a encontrar.

Master volume

Composición

Un Text enlazado enseña el valor tal cual. No hay formato ni interpolación en tiempo de ejecución: un juego que quiera 1,250 gold compone esa cadena antes de llamar a SetData, y un juego que quiera dos colores usa dos nodos.

  • Una etiqueta estática — para las palabras de las que es dueña la pantalla: Buy, Settings, el título de un panel.
  • Una etiqueta enlazada — para todo lo que el juego posee y cambia: una cuenta de oro, el nombre de un jugador, el paso actual de una misión.
  • Una etiqueta con tope — usa maxLines y overflow para texto de largo desconocido en un hueco fijo, como la descripción de un objeto en la fila de una tienda.
  • Dos etiquetas en un Row — cuando una línea necesite dos colores, porque un Text es un solo pintado.
// Static, bound, and truncated.
<Text>Buy</Text>
<Text bind="player.gold" />
<Text style={{ maxLines: 2, overflow: "ellipsis", textAlign: "center" }}>
A long description that will be cut after two lines
</Text>

// Two nodes, because one Text is one paint. The Row is the composition.
<Row layout={{ gap: 4, align: "center" }}>
<Text bind="player.gold" style={{ color: "{color.gold}" }} />
<Text style={{ color: "{color.faint}" }}>gold</Text>
</Row>

<Text></Text> emite text: "" — un nodo de verdad con un hueco de verdad, según la regla de arriba. Varios componentes se apoyan en ello: un Select antes de que se elija un valor, un Badge sin cuenta.

Relacionado