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 binding —bind="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.
Viewport: 960 × 340

- 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>
);
}{
"v": 1,
"tokens": {
"color.bg": "#0b0d13",
"color.surface": "#0e1016",
"color.raised": "#14141a",
"color.line": "#ffffff1f",
"color.line-strong": "#ffffff24",
"color.text": "#ffffff",
"color.muted": "#a1a1aa",
"color.faint": "#8a8a93",
"color.on-brand": "#ffffff",
"color.brand": "#8b5cf6",
"color.brand-strong": "#7c3aed",
"color.brand-hover": "#8b5cf6",
"color.brand-pressed": "#6d28d9",
"color.brand-soft": "#8b5cf61f",
"color.gold": "#fcd34d",
"color.slot": "#ffffff08",
"color.danger": "#f87171",
"radius.sm": 6,
"radius.md": 10,
"radius.lg": 14,
"radius.pill": 999,
"border.hairline": 1,
"border.focus": 2,
"space.1": 4,
"space.2": 8,
"space.3": 12,
"space.4": 16,
"space.5": 20,
"space.6": 24,
"text.xs": 11,
"text.sm": 13,
"text.md": 15,
"text.lg": 17,
"motion.fast": 120,
"motion.loop": 900
},
"views": {
"text-wrap": {
"type": "Container",
"layout": {
"direction": "column",
"grow": 1,
"justify": "center",
"align": "stretch",
"padding": "{space.6}"
},
"style": {
"background": "{color.bg}"
},
"children": [
{
"type": "Container",
"id": "panel",
"layout": {
"direction": "column",
"padding": "{space.5}",
"gap": "{space.4}",
"align": "stretch"
},
"style": {
"background": "{color.surface}",
"radius": "{radius.lg}",
"borderWidth": "{border.hairline}",
"borderColor": "{color.line}"
},
"children": [
{
"type": "Container",
"layout": {
"direction": "row",
"justify": "space-between",
"align": "center"
},
"children": [
{
"type": "Text",
"style": {
"color": "{color.text}",
"fontSize": "{text.lg}"
},
"text": "Quest log"
},
{
"type": "Container",
"layout": {
"direction": "row",
"gap": "{space.1}",
"align": "center"
},
"children": [
{
"type": "Text",
"style": {
"color": "{color.gold}",
"fontSize": "{text.sm}"
},
"text": {
"bind": "quest.count"
}
},
{
"type": "Text",
"style": {
"color": "{color.faint}",
"fontSize": "{text.sm}"
},
"text": "open"
}
]
}
]
},
{
"type": "Container",
"layout": {
"direction": "row",
"wrap": true,
"gap": "{space.4}",
"align": "start"
},
"children": [
{
"type": "Container",
"layout": {
"direction": "column",
"width": 236,
"gap": "{space.2}",
"align": "stretch"
},
"children": [
{
"type": "Text",
"style": {
"color": "{color.faint}",
"fontSize": "{text.xs}"
},
"text": "wraps"
},
{
"type": "Text",
"style": {
"color": "{color.muted}",
"fontSize": "{text.sm}",
"lineHeight": 19
},
"text": "The guild keeps its ledger in the back room, and the quartermaster reads it out loud once a week."
}
]
},
{
"type": "Container",
"layout": {
"direction": "column",
"width": 236,
"gap": "{space.2}",
"align": "stretch"
},
"children": [
{
"type": "Text",
"style": {
"color": "{color.faint}",
"fontSize": "{text.xs}"
},
"text": "maxLines 2 · ellipsis"
},
{
"type": "Text",
"style": {
"color": "{color.muted}",
"fontSize": "{text.sm}",
"lineHeight": 19,
"maxLines": 2,
"overflow": "ellipsis"
},
"text": "The guild keeps its ledger in the back room, and the quartermaster reads it out loud once a week."
}
]
}
]
},
{
"type": "Text",
"layout": {
"height": 40
},
"style": {
"color": "{color.text}",
"fontSize": "{text.md}",
"textAlign": "center",
"textAlignY": "center",
"background": "{color.slot}",
"radius": "{radius.md}"
},
"text": "textAlign · textAlignY"
}
]
}
]
}
}
}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.
| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| children | string | number | Array<string | number> | — | Contenido estático. Las cadenas y los números contiguos se unen al escribir. |
| bind | string | absent | Un 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.
| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| text | Bindable<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.
"" 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únstates.*más allá dedisabled, 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.
fontSizesalta (es la clave del atlas, así que una transition nunca lo interpola) y se acota a1..512.- Actions
Ninguna. Una palabra que el jugador puede pulsar es un
Textdentro de unButton.
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:
- El ancho disponible es lo que ofrece el padre menos el
padding. Unlayout.widthexplí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. UnScrollViewno ofrece nada en un eje con scroll, así que el texto dentro de un scroller horizontal no envuelve nunca. - Los cortes duros.
\r\ny\rse normalizan a\n, que corta siempre. Un párrafo vacío sigue produciendo una línea. - 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.
- 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.
- El truncado: las líneas que pasan de
maxLinesse tiran, y despuésoverflowcorta lo que siga siendo demasiado ancho — solo conwrap: false, ya que una línea envuelta ya cabe.overflow: "ellipsis"marca con…la última línea que se conserva. - 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 ellineHeightnunca saca de su centro a unTextde una sola línea.textAlignalinea cada línea por su propio ancho;textAlignYalinea 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 sí 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 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.
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
maxLinesyoverflowpara 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 unTextes 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.