Skip to content

Text runs can be measured narrower than their shaped line width, forcing mid-word breaks in flex rows #181

Closed

Description

@santhiprakash

Summary

A flex-row item holding a <Text> whose content is a nested inline <Text> run can be laid out narrower than the line its breaker actually produces. When the shaped width exceeds the measured box by even a fraction of a point, the breaker falls back to character-level breaks — e.g. Mumbai renders as Mumba / i, and Mumbai, renders as Mumbai / ,.

Root cause

Two measurement paths disagree:

  • Intrinsic sizing: measure_intrinsic_width (engine/src/layout/mod.rs) measures text via FontContext::measure_string — a per-character hmtx-width sum plus a 0.01 epsilon.
  • Line breaking for styled runs: break_runs_into_lines → measure_styled_chars (engine/src/text/mod.rs) resolves the concrete family per character and shapes contiguous runs with rustybuzz, then sums the shaped cluster widths.

For Mumbai in IBM Plex Serif 10pt the shaped width is ~39.13pt while measure_string reports ~39.03pt (+0.01 epsilon → ~39.04pt box). The styled breaker sees the final i exceed max_width and character-breaks it.

Plain string content does not hit this: break_into_lines → measure_chars takes the same per-character path measure_string uses whenever the font stack lists more than one family, so box and breaker agree. The discrepancy only surfaces for styled runs — any <Text> containing nested <Text> children — because runs always go through measure_styled_chars' shaping path even when the family list resolves to a single concrete font.

Minimal repro

import { Document, Page, View, Text, Font, renderToObject } from "@formepdf/react";

Font.register({ family: "IBM Plex Serif", src: /* IBM Plex Serif regular TTF/OTF bytes or URL */ });

renderToObject(
  <Document>
    <Page size="A4">
      <View style={{ flexDirection: "row", justifyContent: "space-between", width: 200 }}>
        <Text style={{ fontFamily: "IBM Plex Serif", fontSize: 10 }}>Acme</Text>
        <Text style={{ fontFamily: "IBM Plex Serif", fontSize: 10 }}>
          <Text>Mumbai</Text>
        </Text>
      </View>
    </Page>
  </Document>,
);

The trailing text lays out as Mumba / i. Emitting the string directly (no nested <Text>) keeps it on one line. Which strings split depends on the font — Bengaluru also splits under IBM Plex Serif, while Noto Serif is self-consistent for these strings.

Suggested fix direction

Measure run-structured text with the same measure_styled_chars shaping path the breaker uses (or clamp the breaker's available width to the same measure), so a content-measured box can never end up narrower than the shaped line it must hold. Short of that, the breaker's character-level fallback could refuse to split a word when no break opportunity exists within the line.

Found while investigating reactive-resume/reactive-resume#3586 — happy to provide the laid-out ElementInfo dumps (w=39.04 box vs the two forced lines) if useful.

Activity

  1. self-assigned this
    on Oct 6, 2026
  2. danmolitor commented on Oct 6, 2026

    @danmolitor
    Owner

    Thanks for the detailed report and the measurements, they made this quick to pin down. Fixed in #184, which is on main and ships in the next release.

    It turned out plain text hit it too, not just nested runs: with a single font family the plain path also shapes. Intrinsic width now uses the same shaped measurement as the line breaker, so a content-sized box always holds its line. "Mumbai", "Mumbai," and "Bengaluru" in IBM Plex Serif now stay on one line either way.

    It also fixed a second bug in the same code: text with a hard break (like <span>…</span><br><span>…</span>) was measured as all its lines end to end instead of its widest line.

    I'll close this once the release is out.

    Also, if you get a chance and wouldn't mind filling this out - https://www.formepdf.com/production - that'd be awesome. Thanks!

  3. danmolitor commented on Oct 6, 2026

    @danmolitor
    Owner

    Shipped in 0.28.0 (now on npm, PyPI and crates.io). See the 0.28.0 entry in CHANGELOG.md.

  4. santhiprakash commented on Oct 9, 2026

    @santhiprakash
    Author

    @/tmp/forme_reply.md

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions