Skip to content

Latest commit

 

History

History
268 lines (220 loc) · 11.8 KB

tables.md

File metadata and controls

268 lines (220 loc) · 11.8 KB

Tables: start with rows, add behaviour as needed

A table is a normal flow block. Define its columns, add a header and rows, and place it directly in pageFlow or inside a section/module.

Quick start: the smallest useful table

document.pageFlow(page -> page
        .addTable(table -> table
                .autoColumns(3)
                .headerRow("Item", "Qty", "Amount")
                .row("API support", "2", "£240")
                .row("Hosting", "1", "£80")
                .totalRow("Total", "", "£320")));

Start there. Add style or pagination behaviour only when the document needs it.

You want to… Add
Control fixed or automatic column widths .columns(DocumentTableColumn...)
Style every cell .defaultCellStyle(...)
Add several lines to one cell DocumentTableCell.lines(String...)
Put any document node in a cell DocumentTableCell.node(DocumentNode)
Merge cells across columns or rows DocumentTableCell.text(...).colSpan(...) / .rowSpan(...)
Colour alternating rows .zebra(odd, even)
Give the total a branded style .totalRow(style, values...)
Repeat headings after a page break .repeatHeader()

The rest of this page builds on that basic table. The features compose: one table can contain structured cells, spans, zebra rows, a branded total, and a header repeated on every continuation page.

Cell content — one line, several lines, or a node

Three factories, three shapes. Pick by what the cell holds, not by what the text looks like:

// One line. The argument is the line — a "\n" inside it is not a
// line break, it is just whitespace between two words.
DocumentTableCell.text("Design review");

// Several lines. One argument per line.
DocumentTableCell.lines("Design review", "2 h 30 m");

// Any registered node: paragraph, list, section, container, row,
// layer stack, or a nested table.
DocumentTableCell.node(new ParagraphNode(
        "Note", "**Blocked** — waiting on legal",
        DocumentTextStyle.DEFAULT, TextAlign.LEFT, 0.0,
        DocumentInsets.zero(), DocumentInsets.zero()));

text("Design review\n2 h 30 m") renders as the single line Design review 2 h 30 m. That is the plain-text cell's contract: a cell's line list is what the author passed, and text(...) passes exactly one. Use lines(...) when the break is part of the content, or node(...) when the cell needs styling, markdown, or structure around it.

Paragraph content is a different story: ParagraphNode does treat \n as a hard line break, inside a cell exactly as it does anywhere else on the page. So a composed cell built from a paragraph whose text contains \n breaks where the author wrote it.

A composed cell reserves the child's measured height and renders the child's whole sub-tree, so a SectionNode of three paragraphs is three paragraphs in the cell, not just the section's background. The row stays atomic: a composed cell does not split across a page break, so keep the child shorter than one page's content area.

Row span — merge a cell vertically

Spanning cells declare how many rows they cover via rowSpan(int). The layout layer skips occupied grid positions when interpreting subsequent source rows, so authors only specify the cells that are not yet covered by a prior spanning cell.

import com.demcha.compose.document.table.DocumentTableCell;
import com.demcha.compose.document.table.DocumentTableColumn;

addTable(table -> table
        .columns(
                DocumentTableColumn.auto(),
                DocumentTableColumn.auto(),
                DocumentTableColumn.auto())
        .defaultCellStyle(bordered)
        // Row 0: Tall middle cell spans BOTH rows below it.
        .rowCells(
                DocumentTableCell.text("A0"),
                DocumentTableCell.lines("Tall middle", "(spans 3 rows)").rowSpan(3),
                DocumentTableCell.text("C0"))
        // Row 1: only A1 + C1 — middle is occupied by the spanning cell.
        .rowCells(
                DocumentTableCell.text("A1"),
                DocumentTableCell.text("C1"))
        .rowCells(
                DocumentTableCell.text("A2"),
                DocumentTableCell.text("C2")));

Row span composes with colSpan — a single cell can be both colSpan(2).rowSpan(3) to merge a 2x3 block. The layout layer raises a precise diagnostic if a span overlaps another cell, exceeds the remaining rows / columns, or leaves a gap.

Zebra — alternating row fills

zebra(odd, even) paints odd-indexed rows (0, 2, 4 — first, third, fifth visually) in one fill and even-indexed rows (1, 3, 5) in another. Either argument may be null to skip painting that parity.

import com.demcha.compose.document.style.DocumentColor;

DocumentColor zebraOdd = DocumentColor.rgb(244, 247, 252);
DocumentColor zebraEven = DocumentColor.WHITE;

addTable(table -> table
        .columns(DocumentTableColumn.auto(), DocumentTableColumn.auto(), DocumentTableColumn.auto())
        .defaultCellStyle(bordered)
        .headerRow("Item", "Qty", "Amount")
        .row("Apples", "12", "$24.00")
        .row("Pears", "6", "$18.00")
        .row("Strawberries", "20", "$40.00")
        .row("Mangoes", "4", "$16.00")
        .zebra(zebraOdd, zebraEven));

A two-arg overload accepts full DocumentTableStyle values when the zebra row needs more than just a fill colour:

.zebra(
        DocumentTableStyle.builder().fillColor(zebraOdd).padding(DocumentInsets.of(8)).build(),
        DocumentTableStyle.builder().fillColor(zebraEven).padding(DocumentInsets.of(8)).build())

Zebra is applied lazily at build() time and only to rows that don't already have an explicit rowStyle(idx, ...) override, so headerStyle(...) and totalRow(...) always win.

Totals row — bold + subtle fill

totalRow(values...) appends the row at the end of the table and assigns a default totals style (bold text + a subtle gray-blue fill). A two-arg overload takes a custom DocumentTableStyle for branded totals rows.

DocumentTableStyle goldenTotal = DocumentTableStyle.builder()
        .fillColor(DocumentColor.rgb(232, 220, 180))
        .stroke(DocumentStroke.of(rule, 0.6))
        .padding(DocumentInsets.of(7))
        .textStyle(DocumentTextStyle.builder()
                .decoration(DocumentTextDecoration.BOLD)
                .color(ink)
                .build())
        .build();

addTable(table -> table
        .columns(DocumentTableColumn.auto(), DocumentTableColumn.auto(), DocumentTableColumn.auto())
        .defaultCellStyle(bordered)
        .headerRow("Item", "Qty", "Amount")
        .row("Apples",  "12", "$24.00")
        .row("Pears",   "6",  "$18.00")
        .row("Mangoes", "4",  "$16.00")
        .totalRow(goldenTotal, "Total", "22", "$58.00")
        .zebra(zebraOdd, zebraEven));

The totals row is registered with an explicit rowStyle entry at the last row index, so it wins over zebra alternation on that row regardless of parity.

Repeated header on page break

repeatHeader() repeats the first row at the top of every continuation page when the table is split across pages. repeatHeader(int) repeats N leading rows — useful when you have both a title row AND a column-header row that should both repeat.

addTable(table -> {
    TableBuilder t = table
            .columns(DocumentTableColumn.auto(), DocumentTableColumn.auto())
            .defaultCellStyle(bordered)
            .headerRow("Item", "Amount")
            .headerStyle(headerStyle)
            .repeatHeader();              // ← header re-emits on every page
    for (int i = 1; i <= 60; i++) {
        t.row("Row " + i, String.format("$%d.00", i));
    }
});

repeatHeader(0) is the default and behaves exactly like a v1.4 table — the second page starts directly with whatever data row the split landed on. Authors opting into the feature only pay for it when they explicitly call the method.

Put it all together

The runnable example examples/.../TableAdvancedExample.java combines the advanced table features on one PDF: a 3-column invoice with a row-spanning side note, zebra body rows, a totals row, and a repeating "Item / Qty / Amount" header on every continuation page. The output lands at examples/target/generated-pdfs/table-advanced.pdf on every full example sweep.

Layout invariants you can rely on

The table regression suite pins five invariants:

  1. Row span placement — TableBuilderRowSpanTest verifies the spanning cell's height equals the SUM of its covered row heights and that subsequent rows correctly skip occupied columns.
  2. Spanning cell render direction — TableResolvedCell.yOffset is negative for spanning cells, so they extend visually downward through the rows they merge instead of upward beyond the starting row.
  3. Zebra precedence — TableBuilderZebraAndTotalsTest verifies that explicit row styles (header, totals, manual rowStyle(idx)) always win over zebra alternation.
  4. Header repeats on every continuation page — TableBuilderRepeatHeaderTest walks all pages of a 60-row table and asserts every page's first row is the configured header.
  5. Default repeat-header = 0 — tables without a repeatHeader() call paginate exactly as they did in v1.4, so existing layouts stay byte-stable.

See also

  • TableBuilder — full builder API.
  • DocumentTableCell — cell payload with colSpan / rowSpan mutators.
  • DocumentTableStyle — style overrides for fill, stroke, text style, padding.
  • Runnable example: examples/src/main/java/com/demcha/examples/features/tables/TableAdvancedExample.java.