Todas as publicações

Como aninhar um Row dentro de um Col no gpdf?

Não dá — ColBuilder não tem método Row no gpdf. A grade de 12 colunas é plana de propósito. Três idiomas substituem os rows aninhados.

A pergunta, em outras palavras

Você vem do Bootstrap ou do Tailwind, onde .row e .col se aninham livremente. Dá para colocar um .row dentro de um .col dentro de outro .row e a grade continua cascateando. Você senta com o gpdf, vê o mesmo idioma r.Col(span, fn), e vai procurar c.Row(...) dentro do callback da coluna. Não tem. Foi um esquecimento?

TL;DR

Não. A grade de 12 colunas do gpdf é plana de propósito. ColBuilder só aceita conteúdo — Text, Image, Table, Box, List, Spacer — e Row / AutoRow vivem em PageBuilder, não em ColBuilder. Se você veio aqui atrás da sintaxe, ela não existe. Siga lendo para as três coisas que a substituem.

A cara da API

Aqui está o que o conjunto de métodos de ColBuilder realmente contém (de gpdf/template/grid.go):

func (c *ColBuilder) Text(text string, opts ...TextOption)
func (c *ColBuilder) Image(src []byte, opts ...ImageOption)
func (c *ColBuilder) Box(fn func(c *ColBuilder), opts ...BoxOption)
func (c *ColBuilder) Table(header []string, rows [][]string, opts ...TableOption)
func (c *ColBuilder) Line(opts ...LineOption)
func (c *ColBuilder) List(items []string, opts ...ListOption)
func (c *ColBuilder) Spacer(height document.Value)
// …PageNumber, TotalPages, RichText, QRCode, Barcode

Sem Row. Sem AutoRow. Sem Col. O caminho Col → Row não existe como método, e c.Box(fn, ...) é o que mais se aproxima — mas Box aceita outro *ColBuilder, não um row. Você pode aninhar colunas dentro de colunas (mais ou menos, via Box), mas não pode abrir uma nova linha horizontal dentro de uma coluna. Essa é a restrição.

Idioma 1 — Rows irmãos no nível da página

É isso que 90% dos padrões "row aninhado" realmente querem.

package main

import (
    "log"
    "os"

    "github.com/gpdf-dev/gpdf"
    "github.com/gpdf-dev/gpdf/document"
    "github.com/gpdf-dev/gpdf/template"
)

func main() {
    doc := gpdf.NewDocument(
        gpdf.WithPageSize(document.A4),
        gpdf.WithMargins(document.UniformEdges(document.Mm(15))),
    )
    page := doc.AddPage()

    // O que você queria escrever (mas não dá):
    //
    //   page.AutoRow(func(r *template.RowBuilder) {
    //       r.Col(8, func(c *template.ColBuilder) {
    //           c.Row(...) ❌ não existe
    //       })
    //   })

    // O que você escreve no lugar:
    page.AutoRow(func(r *template.RowBuilder) {
        r.Col(8, func(c *template.ColBuilder) {
            c.Text("Título do artigo", template.FontSize(18), template.Bold())
        })
        r.Col(4, func(c *template.ColBuilder) {
            c.Text("2026-05-16")
        })
    })
    page.AutoRow(func(r *template.RowBuilder) {
        r.Col(8, func(c *template.ColBuilder) {
            c.Text("O parágrafo de abertura ocupa a mesma coluna de 8.")
        })
        r.Col(4, func(c *template.ColBuilder) {
            c.Text("por Taiki Noda")
        })
    })

    data, err := doc.Generate()
    if err != nil {
        log.Fatal(err)
    }
    _ = os.WriteFile("flat.pdf", data, 0o644)
}

Os dois AutoRow compartilham os mesmos spans 8+4, então as colunas se alinham visualmente. Não tem sub-grade; tem uma sequência plana de rows que por acaso usam a mesma divisão. A saída é idêntica à de um layout CSS que aninhasse .row dentro de .col-8 — porque a única coisa que a forma aninhada comprava era localidade sintática, e o gpdf prefere que você gaste esse orçamento em consistência de largura.

Idioma 2 — c.Box para agrupamento visual

Se a motivação de verdade era "quero um cartão com borda com dois elementos empilhados dentro desta coluna", você queria Box, não um sub-row:

page.AutoRow(func(r *template.RowBuilder) {
    r.Col(6, func(c *template.ColBuilder) {
        c.Box(func(c *template.ColBuilder) {
            c.Text("Faturar para", template.Bold())
            c.Text("Acme Ltda.")
            c.Text("São Paulo, Brasil")
        },
            template.WithBoxBorder(template.Border(
                template.BorderWidth(document.Pt(1)),
                template.BorderColor(pdf.RGBHex(0xBDBDBD)),
            )),
            template.WithBoxPadding(document.UniformEdges(document.Mm(4))),
        )
    })
    r.Col(6, func(c *template.ColBuilder) {
        c.Box(func(c *template.ColBuilder) {
            c.Text("Enviar para", template.Bold())
            c.Text("Mesmo do faturamento")
        },
            template.WithBoxPadding(document.UniformEdges(document.Mm(4))),
        )
    })
})

O *ColBuilder que Box recebe empilha o conteúdo verticalmente. Também não dá para dividir um Box horizontalmente — para isso, você volta ao Idioma 1. Mas para o padrão de "cartão" que a sintaxe de row aninhado costuma tentar, esta é a ferramenta certa. A linha c.Box em gpdf/template/grid.go:246 é o único aninhamento que a grade permite, e é deliberadamente unidimensional.

Idioma 3 — Planeje a sub-grade direto em 12 colunas

Às vezes você quer mesmo um layout de 2 colunas dentro do que parece uma seção de meia página: uma miniatura e uma legenda na metade esquerda, um parágrafo na direita. O instinto é Col(6) > Row > Col(6) + Col(6). O equivalente plano é só Col(3) + Col(3) + Col(6):

page.AutoRow(func(r *template.RowBuilder) {
    r.Col(3, func(c *template.ColBuilder) {
        c.Image(thumbBytes)
    })
    r.Col(3, func(c *template.ColBuilder) {
        c.Text("Foto por Ansel Adams", template.Italic())
        c.Text("1942")
    })
    r.Col(6, func(c *template.ColBuilder) {
        c.Text("O parágrafo do corpo ocupa a metade direita da página.")
    })
})

3 + 3 somados dão 6, então o par miniatura/legenda ocupa exatamente a metade esquerda. Doze fatora em 2, 3, 4 e 6, então uma grade aninhada quase sempre achata limpo. Se sua grade aninhada era Col(8) > Row > Col(7) + Col(5), isso não achata — mas esses números também não significam nada num documento de verdade. Escolha a versão plana que significa.

Por que sem aninhamento

Uma grade plana resolve larguras em uma passada. O row é uma porcentagem da largura da página menos margens. Cada Col(span) é span / 12 disso. Pronto. Sem recursão, sem largura-de-uma-largura-de-uma-largura, sem contexto pai costurado pelo motor de layout. A linha em grid.go que calcula a largura da coluna é literalmente uma linha:

Width: document.Pct(float64(col.span) / float64(gridColumns) * 100),

Adicione aninhamento e essa linha vira um percurso de árvore. De repente você precisa decidir o que Col(6) dentro de Col(8) dentro de Col(12) significa — 6 é 50% da coluna pai, 50% do row, ou 50% da página? Bootstrap escolheu "50% do pai" e adicionou breakpoints e gutters para tornar isso suportável. PDFs não têm breakpoints. PDFs não têm contêiner fluido. Pegar emprestado o idioma de aninhamento importaria três problemas que não temos, em troca de um atalho sintático que não precisamos.

"Mas eu quero localidade sintática"

Justo. A desvantagem de achatar é que duas chamadas AutoRow que pertencem conceitualmente juntas podem se afastar no código conforme você edita. Um pequeno helper fecha a lacuna:

func card(page *template.PageBuilder, title, body string) {
    page.AutoRow(func(r *template.RowBuilder) {
        r.Col(12, func(c *template.ColBuilder) {
            c.Text(title, template.Bold())
        })
    })
    page.AutoRow(func(r *template.RowBuilder) {
        r.Col(12, func(c *template.ColBuilder) {
            c.Text(body)
        })
    })
}

A localidade vive na sua função, não na API. O gpdf não embute card porque são três linhas e a sua versão vai se ajustar melhor ao seu documento do que a nossa.

Receitas relacionadas

Experimente o gpdf

O gpdf é uma biblioteca Go para gerar PDFs. Licença MIT, zero dependências externas, suporte CJK nativo.

go get github.com/gpdf-dev/gpdf

⭐ Star no GitHub · Leia a documentação