Skip to content

balanceSectionOnPage orders a balanced page by raw fragment x, reordering content when an origin is not its column's #3967

Description

@Nathaniel-260

When a multi-column section ends mid-page, balanceSectionOnPage redistributes its fragments and
writes the balanced x and y back onto them in the order it derived. That order is a sort on raw
x:

// packages/layout-engine/layout-engine/src/column-balancing.ts
const ordered = [...sectionFragments].sort((a, b) => {
  if (a.x !== b.x) return a.x - b.x;
  return a.y - b.y;
});

with the premise stated just above it: "During unbalanced layout the paginator fills column 0
top-to-bottom, then column 1, etc. — so (x, y) preserves the original sequence."

The premise is that every fragment in a column shares one origin. It does not hold:

  • a negative w:ind shifts a paragraph's origin left of its column,
  • a float offset shifts it too,
  • resolveTableFrame right-aligns or centres an over-wide table, which moves its origin outside the
    column entirely.

Because the derived order is written back rather than only read, getting it wrong reorders the
page
rather than merely laying it out oddly.

Impact

Content surfaces in the wrong place. A difference of 1e-7 in x is enough to swap two paragraphs,
so this is not limited to visibly outdented content — ordinary sub-pixel drift between fragments in
the same column reaches it.

Direction-independent, and reachable in plain LTR documents.

Reproduction

Two equal columns, 288px wide, 48px gutter, 96px left margin. Six paragraphs: 0-3 fill the left
column, 4-5 have spilled into the right. Paragraph 1 carries a 20px outdent and paragraph 2 differs
from the column origin by a hair.

placement       x        y
para 0          96       96
para 1          116      116     <- negative w:ind
para 2          96.0000001  136  <- sub-pixel drift
para 3          96       156
para 4          432      96
para 5          432      116

Sorting on x orders these 0, 2, 3, 1, 4, 5: paragraph 1 (x = 116) sorts after every other
left-column fragment. The balanced positions are then written back in that order, and paragraph 1
surfaces in the wrong place on the page.

Expected reading order after balancing is 0, 1, 2, 3, 4, 5.

Suggested direction

Sort by the column each fragment occupies rather than by its x. The column ordinal is fill order, so
it needs no RTL special case — where an x comparison does, because column 0 sits on the right and
document order descends in x.

One caveat worth stating for whoever picks this up: the resolved ordinal has to always yield a
number. A sort key that is sometimes absent leaves the comparator mixing two metrics, which is not a
total order, and Array.prototype.sort may then return different orders for the same input — it
does differ between engines, so the product and the test suite would not agree.

Where

balanceSectionOnPage in packages/layout-engine/layout-engine/src/column-balancing.ts.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions