Skip to content

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

fonte: https://www.figma.com/community/file/1284819663700490015

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.

ts
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';
}
fonte: https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Flexible_box_layout/Basic_concepts

Nesta lição, vamos implementar layouts Flexbox e oferecer, nos nós, propriedades CSS com os mesmos nomes:

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

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.

orientação dos eixos

pixijs/layout ​

O pixijs/layout também é implementado com o Yoga. Implementações parecidas incluem: pixi-flex-layout

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

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

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.

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

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

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

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

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

Flex Basis, Grow, and 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 ​

Min/Max Width and Height

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.

ts
const layoutNodes = api.readLayoutFromECS(nodes);

(await serializeNodesToSVGElements(layoutNodes)).forEach((element) => {
    $namespace.appendChild(element);
});

Leitura adicional ​

Released under the MIT License.