Layout do console com Yoga
Text.js executa Yoga em WebAssembly com um fator de escala de pontos de 1, para que cada caixa ocupe células inteiras. O texto é segmentado com Intl.Segmenter e medido em células.
O Yoga, o motor de flexbox usado pelo React Native e compilado para WebAssembly, calcula o layout de cada quadro do Text.js. Caixas e texto compartilham uma única árvore do Yoga. O console é uma grade de células de caracteres, então toda posição e todo tamanho resultantes precisam ser um número inteiro de células.
Um ponto é uma célula
O Yoga arredonda seus resultados para uma grade de pixels definida pelo fator de escala de pontos. Com o fator em 2, como em uma tela Retina, as bordas podem cair em meios pontos. O Text.js carrega o Yoga uma vez e define o fator como 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;}Com um fator de 1, o Yoga arredonda cada borda para uma unidade inteira, e o Text.js trata uma unidade como uma célula. O Yoga arredonda a posição absoluta de cada borda e deriva os tamanhos das bordas arredondadas, de modo que caixas vizinhas compartilhem uma borda. Divida uma linha de 81 colunas em duas caixas com width="50%" e uma ficará com 40 colunas, a outra com 41, sem espaço entre elas nem sobreposição.
O próprio Text.js aplica os padrões do CSS: flexDirection é row e flexShrink é 1, como em um navegador.
Medir texto
Os nós de texto são folhas do Yoga com uma função de medição. O Yoga a chama com a largura disponível, e o Text.js retorna o tamanho do texto em células:
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 };}O texto que cabe é retornado em seu tamanho natural. O texto que não cabe é quebrado na largura disponível, e sua altura é o número de linhas. O nó mantém sua última quebra de linhas, então medir a mesma largura novamente não repete a quebra do texto.
Medir agrupamentos de grafemas
O length de uma string conta unidades de código UTF-16, o que diz pouco sobre quantas células ela preenche. Primeiro, o texto é dividido em agrupamentos de grafemas com Intl.Segmenter, de modo que e seguido de um acento combinante seja um único agrupamento, assim como uma sequência de emojis unida por caracteres de junção de largura zero. Em seguida, cada agrupamento recebe uma largura:
| Texto | Células | Motivo |
|---|---|---|
abc | 3 | Uma célula por caractere |
你好 | 4 | Caracteres largos do leste asiático ocupam duas |
🚀 | 2 | A apresentação como emoji ocupa duas |
é (e + U+0301) | 1 | Marcas combinantes não ocupam nenhuma |
─│┌▼←⠋ | 6 | Caracteres de desenho de caixas, formas geométricas, setas e braille têm largura fixada em uma |
Bordas, a marca ▼, setas e quadros de indicadores de carregamento são sempre medidos como uma célula, então a borda de uma caixa nunca desalinha seu conteúdo.
O ASCII imprimível dispensa a segmentação, e as demais larguras são armazenadas em cache por ponto de código e por agrupamento.
Quebra de linhas
<Text> aceita um modo wrap:
wrap(o padrão) quebra nos espaços e dentro de uma palavra quando ela é mais larga que a linha.nowrapmantém uma linha e deixa a caixa recortá-la.truncate-end,truncate-middleetruncate-startmantêm uma linha e substituem o que não cabe por….
O truncamento também conta células, então uma linha de texto em chinês é cortada no limite de um caractere, e as reticências ocupam exatamente uma célula.
Na prática
Aqui, uma barra lateral de 30 colunas fica ao lado de um painel que ocupa a largura 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>Em um console de 100 colunas, a barra lateral ocupa 30 colunas incluindo sua borda, e o painel de conversa ocupa as outras 70. Uma borda ocupa uma célula de cada lado, então o título da sessão é truncado em 28 células.