← Blog

Disporre il layout della console con Yoga

Text.js esegue Yoga in WebAssembly con un fattore di scala dei punti di 1, così ogni riquadro occupa celle intere. Il testo è segmentato con Intl.Segmenter e misurato in celle.

Adam

Il layout di ogni frame di Text.js è calcolato da Yoga, il motore flexbox usato da React Native, compilato in WebAssembly. I riquadri e il testo condividono un unico albero Yoga. La console è una griglia di celle di caratteri, quindi ogni posizione e dimensione risultante deve corrispondere a un numero intero di celle.

Un punto corrisponde a una cella

Yoga arrotonda i risultati a una griglia di pixel definita dal fattore di scala dei punti. Con un fattore di 2, come su uno schermo Retina, i bordi possono cadere su mezzi punti. Text.js carica Yoga una sola volta e imposta il fattore a 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 fattore di 1, Yoga arrotonda ogni bordo a un’unità intera e Text.js considera ogni unità come una cella. Yoga arrotonda la posizione assoluta di ogni bordo e ricava le dimensioni dai bordi arrotondati, così i riquadri adiacenti condividono un bordo. Dividi una riga di 81 colonne in due riquadri con width="50%": uno avrà 40 colonne, l’altro 41, senza spazi né sovrapposizioni.

Text.js applica direttamente i valori predefiniti CSS: flexDirection è row e flexShrink è 1, come in un browser.

Misurazione del testo

I nodi di testo sono foglie di Yoga con una funzione di misurazione. Yoga la chiama con la larghezza disponibile e Text.js restituisce le dimensioni del testo in celle:

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 };}

Il testo che rientra nello spazio viene restituito con le sue dimensioni naturali. Il testo che non rientra va a capo alla larghezza disponibile e la sua altezza corrisponde al numero di righe. Il nodo conserva l’ultima suddivisione in righe, quindi una nuova misurazione con la stessa larghezza non suddivide di nuovo il testo.

Misurazione dei cluster di grafemi

La proprietà length di una stringa conta le unità di codice UTF-16, che dicono poco sul numero di celle occupate. Il testo viene prima suddiviso in cluster di grafemi con Intl.Segmenter, quindi e seguito da un accento combinante forma un solo cluster, così come una sequenza di emoji unita da caratteri di giunzione a larghezza zero. A ogni cluster viene poi assegnata una larghezza:

TestoCelleMotivo
abc3Una cella per carattere
你好4I caratteri larghi dell’Asia orientale ne occupano due
🚀2La rappresentazione come emoji ne occupa due
é (e + U+0301)1I segni combinanti non ne occupano nessuna
─│┌▼←⠋6I caratteri per disegnare riquadri, le forme geometriche, le frecce e il braille hanno una larghezza fissa di una cella

I bordi, il simbolo ▼, le frecce e i frame dell’indicatore di caricamento sono sempre misurati come una cella, quindi il bordo di un riquadro non disallinea mai il contenuto.

I caratteri ASCII stampabili saltano la segmentazione e le altre larghezze sono memorizzate nella cache per punto di codice e per cluster.

Ritorno a capo

<Text> accetta una modalità wrap:

  • wrap (predefinita) interrompe le righe in corrispondenza degli spazi e all’interno di una parola quando questa è più larga della riga.
  • nowrap mantiene una sola riga e lascia che il riquadro la ritagli.
  • truncate-end, truncate-middle e truncate-start mantengono una sola riga e sostituiscono ciò che non rientra con ….

Anche il troncamento conta le celle, quindi una riga di testo cinese viene tagliata tra un carattere e l’altro e i puntini di sospensione occupano esattamente una cella.

In pratica

Qui una barra laterale di 30 colonne affianca un pannello che occupa la larghezza rimanente:

<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>

In una console di 100 colonne, la barra laterale occupa 30 colonne incluso il bordo e il pannello della conversazione occupa le altre 70. Il bordo occupa una cella per lato, quindi il titolo della sessione viene troncato a 28 celle.