← Blog

Layout de la consola con Yoga

Text.js ejecuta Yoga en WebAssembly con un factor de escala de puntos de 1, de modo que cada caja se ajusta a celdas enteras. El texto se segmenta con Intl.Segmenter y se mide en celdas.

Adam

Yoga, el motor flexbox que usa React Native, compilado a WebAssembly, calcula el layout de cada fotograma de Text.js. Las cajas y el texto comparten un único árbol de Yoga. La consola es una cuadrícula de celdas de caracteres, por lo que todas las posiciones y tamaños resultantes deben ser un número entero de celdas.

Un punto equivale a una celda

Yoga redondea sus resultados a una cuadrícula de píxeles definida por el factor de escala de puntos. Con un factor de 2, como en una pantalla Retina, los bordes pueden situarse en medios puntos. Text.js carga Yoga una vez y establece el factor en 1:

export function initYoga(): Promise<Yoga> {  if (yoga !== null) return Promise.resolve(yoga);  if (yogaPromise === null) {    yogaPromise = loadYoga().then((instance) => {      yoga = instance;      const config = instance.Config.create();      config.setPointScaleFactor(1);      config.setUseWebDefaults(false);      yogaConfig = config;      return instance;    });    yogaPromise.catch(() => {      yogaPromise = null;    });  }  return yogaPromise;}

Con un factor de 1, Yoga redondea cada borde a una unidad entera, y Text.js trata cada unidad como una celda. Yoga redondea la posición absoluta de cada borde y calcula los tamaños a partir de los bordes redondeados, de modo que las cajas adyacentes comparten un borde. Divide una fila de 81 columnas en dos cajas con width="50%" y una tendrá 40 columnas y la otra 41, sin huecos ni solapamientos.

Text.js aplica por su cuenta los valores predeterminados de CSS: flexDirection es row y flexShrink es 1, como en un navegador.

Medición del texto

Los nodos de texto son hojas de Yoga con una función de medición. Yoga la llama con el ancho disponible, y Text.js devuelve el tamaño del texto en celdas:

private measure(width: number, widthMode: MeasureMode, _height: number, _heightMode: MeasureMode): { width: number; height: number } {  const natural = this.naturalSize();  if (widthMode === MeasureMode.Undefined || Number.isNaN(width)) return natural;  const avail = Math.max(0, Math.floor(width));  if (natural.width <= avail) {    return { width: widthMode === MeasureMode.Exactly ? avail : natural.width, height: natural.height };  }  if ((this.props.wrap ?? "wrap") !== "wrap") return { width: avail, height: natural.height };  const lines = this.getLines(avail);  let maxLine = 0;  for (const l of lines) if (l.width > maxLine) maxLine = l.width;  return { width: widthMode === MeasureMode.Exactly ? avail : Math.min(avail, maxLine), height: lines.length };}

El texto que cabe se devuelve con su tamaño natural. El texto que no cabe se ajusta al ancho disponible, y su altura es el número de líneas. El nodo conserva su último ajuste de línea, por lo que volver a medir el mismo ancho no vuelve a ajustar el texto.

Medición de grupos de grafemas

La propiedad length de una cadena cuenta unidades de código UTF-16, lo que dice poco sobre cuántas celdas ocupa. Primero se divide el texto en grupos de grafemas con Intl.Segmenter, de modo que una e seguida de un acento combinante forma un grupo, al igual que una secuencia de emojis unidos mediante caracteres de unión de ancho cero. Después se asigna un ancho a cada grupo:

TextoCeldasMotivo
abc3Una celda por carácter
你好4Los caracteres anchos de Asia oriental ocupan dos
🚀2La presentación como emoji ocupa dos
é (e + U+0301)1Las marcas combinantes no ocupan ninguna
─│┌▼←⠋6Los caracteres de dibujo de cajas, las formas geométricas, las flechas y el braille tienen un ancho fijo de una celda

Los bordes, la marca ▼, las flechas y los fotogramas del indicador de carga siempre se miden como una celda, por lo que el borde de una caja nunca desalinea su contenido.

El ASCII imprimible omite la segmentación, y los demás anchos se guardan en caché por punto de código y por grupo.

Ajuste de línea

<Text> acepta un modo wrap:

  • wrap (el valor predeterminado) inserta saltos en los espacios y dentro de una palabra cuando esta es más ancha que la línea.
  • nowrap mantiene una sola línea y deja que la caja la recorte.
  • truncate-end, truncate-middle y truncate-start mantienen una sola línea y sustituyen lo que no cabe por ….

El truncamiento también cuenta celdas, por lo que una línea de texto en chino se corta entre caracteres, y los puntos suspensivos ocupan exactamente una celda.

En la práctica

Aquí, una barra lateral de 30 columnas se sitúa junto a un panel que ocupa el ancho restante:

<Box flexDirection="row" width="100%" height="100%">  <Box width={30} flexDirection="column" border="rounded" title="Sessions" overflow="hidden">    <Text wrap="truncate-end">session-03 · refactor the renderer pipeline</Text>  </Box>  <Box flexGrow={1} flexDirection="column" border="single" title="Conversation" paddingX={1}>    <Text>Message 0: 你好 🚀</Text>  </Box></Box>

En una consola de 100 columnas, la barra lateral ocupa 30 columnas incluido su borde, y el panel de conversación ocupa las otras 70. Un borde ocupa una celda a cada lado, por lo que el título de la sesión se trunca a las 28 celdas.