Lição 33 - Motor de layout
Os navegadores implementam vários sistemas de layout, como Flexbox, Grid e Block, e assim fica fácil obter efeitos como “centralizar” sem calcular à mão a posição dos nós.
Em aplicativos no estilo de uma tela que rodam fora do DOM, você precisa implementar a lógica do próprio motor de layout. O Figma implementou o Auto Layout, em que Grid está em beta, enquanto Vertical e Horizontal correspondem à propriedade flex-direction do CSS. Para detalhes, veja: Figma - Guide to auto layout

Ferramentas voltadas a design para código em geral oferecem essa capacidade. Para detalhes, veja: Layout in pencil.dev
Um objeto pai pode assumir o tamanho e o posicionamento dos filhos com um sistema de layout no estilo flexbox, por meio de propriedades como layout, justifyContent e alignItems.
export interface Layout {
/** Enable flex layout. None means all children are absolutely positioned and will not be affected by layout properties. Frames default to horizontal, groups default to none. */
layout?: 'none' | 'vertical' | 'horizontal';
/** The gap between children in the main axis direction. Defaults to 0. */
gap?: NumberOrVariable;
layoutIncludeStroke?: boolean;
/** The Inside padding along the edge of the container */
padding?:
| /** The inside padding to all sides */ NumberOrVariable
| /** The inside horizontal and vertical padding */ [
NumberOrVariable,
NumberOrVariable,
]
| /** Top, Right, Bottom, Left padding */ [
NumberOrVariable,
NumberOrVariable,
NumberOrVariable,
NumberOrVariable,
];
/** Control the justify alignment of the children along the main axis. Defaults to 'start'. */
justifyContent?:
| 'start'
| 'center'
| 'end'
| 'space_between'
| 'space_around';
/** Control the alignment of children along the cross axis. Defaults to 'start'. */
alignItems?: 'start' | 'center' | 'end';
}Nesta lição, vamos implementar layouts Flexbox e oferecer, nos nós, propriedades CSS com os mesmos nomes:
const parent = {
id: 'parent',
type: 'rect',
x: 100,
y: 100,
width: 200,
height: 200,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
};
const child = {
id: 'child',
parentId: 'parent',
type: 'rect',
width: 50,
height: 50,
};Yoga
Usar o motor de layout Yoga no frontend só é possível via WASM. Hoje existem várias implementações disponíveis:
- yoga-layout-prebuilt Faz bastante tempo que não recebe atualização.
- yoga-wasm-web As primeiras versões do satori usavam esse pacote para converter HTML em SVG e calcular layouts. O taffy também tem um exemplo relacionado
- yoga-layout Recomendamos usá-lo, e a nossa implementação também se baseia nele. O react-pdf/yoga também o usa.
Vale notar que o Yoga também se aplica ao espaço 3D, desde que um plano seja especificado. Para detalhes, veja: react-three-flex
Outra diferença importante em relação ao Flexbox do DOM é que você precisa especificar o plano do contêiner em 3D. Os elementos são posicionados no plano 2D definido pelos dois eixos, usando a largura e a altura calculadas ao longo desses eixos.

pixijs/layout
O pixijs/layout também é implementado com o Yoga. Implementações parecidas incluem: pixi-flex-layout
const container = new Container({
layout: {
width: '80%',
height: '80%',
gap: 4,
flexWrap: 'wrap',
justifyContent: 'center',
alignContent: 'center',
},
});troika-flex-layout
O troika-flex-layout calcula o layout num WebWorker usando o yoga-layout-prebuilt:
import { requestFlexLayout } from 'troika-flex-layout';
// Describe your layout style tree, with a unique id for each node:
const styleTree = {
id: 'root',
width: 100,
height: 100,
alignItems: 'center',
justifyContent: 'center',
children: [
{
id: 'child',
width: '50%',
height: '50%',
},
],
};
// Initiate a layout request with a callback function:
requestFlexLayout(styleTree, (results) => {
// The results are a mapping of node ids to layout boxes:
// {
// root: { left: 0, top: 0, width: 100, height: 100 },
// child: { left: 25, top: 25, width: 50, height: 50 }
// }
});Alternativas ao Yoga
Implementações em JS puro:
- Motion Canvas Layouts Implementação de Flexbox nativa do navegador, sem precisar reimplementar algoritmos complexos de layout
- Simplifying WebGL: Building an Effective Layout Engine
Implementações em Rust:
- stretch implementa Flexbox e oferece um binding WASM
stretch-layout, mas está sem manutenção há muito tempo. - taffy Biblioteca de layout de UI de alto desempenho escrita em Rust. Hoje implementa vários algoritmos de layout CSS, incluindo Flexbox, Grid e Block. Os bindings WASM, porém, ainda não estão disponíveis. Para detalhes, veja: taffy wasm bindings
Nossa implementação
Escolhemos o yoga-layout oficial. Além do Flexbox conhecido, ele também oferece propriedades básicas de layout, como Margin e Padding.
import { loadYoga } from 'yoga-layout/load';
class YogaSystem extends System {
async prepare() {
Yoga = await loadYoga();
}
}É preciso construir uma árvore de layout paralela ao grafo de cena.
Primeiro, construímos uma árvore de layout paralela ao grafo de cena. A estrutura de cada nó é a seguinte:
interface StyleTreeNode {
id: string;
top?: number | string;
left?: number | string;
width?: number | string;
height?: number | string;
children: StyleTreeNode[];
padding?: number | number[];
margin?: number | number[];
gap?: number;
rowGap?: number;
columnGap?: number;
}Depois, no momento certo, convertemos a árvore de layout numa árvore Yoga, executamos o cálculo do layout e aplicamos o resultado aos nós do grafo de cena:
const root = Yoga.Node.createWithConfig(yogaConfig);
populateNode(root, styleTree);
root.calculateLayout();
const results = Object.create(null);
walkStyleTree(styleTree, (styleNode) => {
const { id, yogaNode } = styleNode;
results[id] = {
x: yogaNode.getComputedLeft(),
y: yogaNode.getComputedTop(),
width: yogaNode.getComputedWidth(),
height: yogaNode.getComputedHeight(),
};
});
root.freeRecursive();Quando fazer o layout
Quando o tamanho do próprio contêiner Flexbox muda, o layout precisa ser recalculado e aplicado à posição e ao tamanho dos filhos. O mesmo vale quando novos filhos entram no contêiner.
class YogaSystem extends System {
private readonly bounds = this.query((q) =>
q.addedOrChanged.and.removed
.with(ComputedBounds)
.trackWrites.and.with(Flex),
);
execute() {
this.bounds.addedOrChanged.forEach((entity) => {
// relayout
});
}
}AlignItems e JustifyContent
O uso mais comum é centralizar o conteúdo com essas duas propriedades:
Para implementar botões com texto centralizado:
O rótulo usa a largura intrínseca (wordWrap: false), e o Yoga centraliza a caixa de texto medida dentro do botão. Um wordWrapWidth fixo com maxLines: 1 e textOverflow: 'ellipsis' pode truncar o rótulo mesmo quando o botão tem espaço de sobra. Reserve essas opções para rótulos que precisem de truncamento de propósito.
Gap
O exemplo a seguir mostra o efeito de padding e gap:
const parent = {
id: 'yoga-gap-parent',
type: 'rect',
x: 100,
y: 100,
width: 200,
height: 250,
fill: 'grey',
display: 'flex',
padding: 10,
flexWrap: 'wrap',
gap: 10,
zIndex: 0,
};Flex Basis, Grow e Shrink
O flex grow aceita qualquer valor de ponto flutuante >= 0, e 0 é o valor padrão. Um contêiner distribui o espaço restante entre os filhos, ponderado pelo valor de flex grow de cada filho.
Largura e altura mínimas e máximas
Exportar SVG
Como o Flexbox do CSS só aceita elementos HTML como contêineres, e não elementos SVG, ainda é preciso usar as posições absolutas depois do layout ao exportar para SVG.
const layoutNodes = api.readLayoutFromECS(nodes);
(await serializeNodesToSVGElements(layoutNodes)).forEach((element) => {
$namespace.appendChild(element);
});