Skip to content

feat(text): let text declare that it must fit its box - #170

Merged
LeadcodeDev merged 1 commit into
mainfrom
feat/text-autofit
Aug 11, 2026
Merged

feat(text): let text declare that it must fit its box#170
LeadcodeDev merged 1 commit into
mainfrom
feat/text-autofit

Conversation

@LeadcodeDev

Copy link
Copy Markdown
Owner

Closes the second Critical gap from the re-scored Remotion differential — and the one the original audit called the most profitable reliability lever on this project. With #169 merged, both Critical gaps are now closed.

Why this is worth more than its size

Until now, a text that overflowed its box was reported and nothing more. apply_fixes deliberately refuses ContentOverflowsBox, and its comment says why:

growing the box/card, shrinking the font, or shortening the copy are all legitimate fixes … not ours to pick

That reasoning is correct only while the engine has no way to shrink text at all. Given one, the arbitration disappears: the author declares the intent ("this text must fit here"), and a whole class of generation failure stops existing. The value is not the measuring function — it is the removal of a failure class.

What this adds

style.text-autofit makes text and gradient_text reduce their font size until they fit the resolved width and, where taffy defines one, the content-box height.

$ rustmotion validate -f overflowing.json
ERROR: text (wrapped content exceeds its own box)
  hint: text wraps to 343px tall at this width but its box is only 80px tall

# same file, with "text-autofit": true
$ rustmotion validate -f overflowing.json
Valid scenario: 1 scene(s) in 1 view(s)

white-space: nowrap still decides whether the text wraps; autofit decides at what size — they compose rather than compete. auto_scroll never interacts: codeblock and terminal do not read the field, and a test pins that a declared text-autofit stays inert on components whose painters ignore it, so it cannot become a measure/paint divergence somewhere else.

rustmotion info also reports each text's natural size, through the same measurer the engine and the validator use.

The three hazards, and how each is answered

Measure and paint must agree. This repository spent a whole chantier repairing divergences where TextIntrinsic measured one thing and the painter drew another, blinding the geometry pass to real overflow. One pure resolve_text_autofit is called with identical arguments from both sides — the agreement is structural, not coincidental. The tests assert it directly rather than checking the render merely looks right.

The size must not drift. Paint runs per frame. The resolution is fed the complete content, never the typewriter-truncated view, so a reveal cannot make the size oscillate mid-read. Asserted by rendering the same content at two instants and comparing pixel grids byte for byte.

Shrinking needs a floor, or a visible defect is traded for a discreet one. The floor reuses MIN_LEGIBLE_FONT_RATIO — already calibrated by real visual inspection — relocated into rustmotion-core so both sides share one constant instead of inventing a second. At the floor, the text stops shrinking and the geometry violation is still raised: never a silence.

The floor's known limit, and what I did about it

The floor is pinned to a 1080-tall reference, because IntrinsicMeasure::measure receives no frame height and using the real one on the paint side alone would reintroduce exactly the divergence above.

On a taller canvas the pinned floor therefore sits below the legibility threshold, which is relative to the real frame. Verified: a declared 120px shrinking toward ~13px on a 2160-tall frame passed check_legibility in complete silence, because that check reads the declared size.

That is the failure mode this feature exists to remove, not relocate. check_legibility now warns in precisely that case:

Warning: views[0]…: text-autofit may shrink this text to ~13px, below the 26px
legibility floor for a 2160px-tall frame. Give it a wider or taller box so it
settles above that, or check the rendered frame.

Conditional by design — it fires only where the two constants genuinely diverge (canvases taller than 1080) and stays quiet at 1080, so it is not noise on every autofitting text in the common canvas. It says "may" because resolving the actual shrunk size needs layout, which that pass does not run.

The precise fix is a canvas-relative floor on both sides, which needs the frame height plumbed into TextIntrinsic and through box_builder. Out of scope here; worth its own change.

Recommendation on --fix, not applied

validate.rs was kept read-only on purpose: wiring an auto-fix that rewrites the user's file is a decision to take explicitly, not a side effect.

With autofit available, apply_fixes could answer ContentOverflowsBox on text/gradient_text by inserting "text-autofit": true. It is non-destructive and reversible — it alters neither declared dimensions nor content — putting it in the same risk category as the two fixes already accepted (white-space removed, auto_scroll forced), both of which also change the render. I recommend wiring it; say the word and it is a small change.

Verification

Not covered, deliberately

  • caption / rich_text / codeblock / terminal do not get autofit — their painters do not consume it, and adding the field without the painter is how measure/paint divergences start.
  • letter-spacing / line-height declared in absolute units scale with the shrink, where CSS would keep them fixed. Documented approximation; the common cases (unitless, %, em, or the 1.3x default) are exact.
  • The height axis only engages when the box has a height taffy actually resolves.

Closes the second Critical gap from the re-scored Remotion differential, and
the one the original audit called the most profitable reliability lever here.

Until now, a text that overflowed its box was reported and nothing more.
`apply_fixes` deliberately refuses `ContentOverflowsBox`, and its comment says
why: growing the box, shrinking the font and shortening the copy are all
legitimate, and picking one is not the validator's call. That reasoning holds
only while the engine has no way to shrink text at all. Given one, the
arbitration disappears — the author declares the intent, and a whole class of
generation failure stops existing.

`style.text-autofit` makes `text` and `gradient_text` reduce their font size
until they fit the resolved width and, where taffy defines one, the content
box height. `white-space: nowrap` still decides *whether* the text wraps;
autofit decides *at what size* — they compose rather than compete.
`auto_scroll` never interacts: codeblock and terminal do not read the field,
by construction rather than by convention.

Three hazards drove the design:

- **Measure and paint must agree.** This repository spent a whole chantier
  repairing divergences where `TextIntrinsic` measured one thing and the
  painter drew another, blinding the geometry pass to real overflow. One pure
  `resolve_text_autofit` is called with identical arguments from both sides,
  so the agreement is structural rather than coincidental — and the tests
  assert it rather than merely checking the render looks right.
- **The size must not drift.** Paint runs per frame. The resolution is fed
  the complete content, never the typewriter-truncated view, so a reveal
  cannot make the size oscillate mid-read. Asserted by rendering the same
  content at two instants and comparing pixels byte for byte.
- **Shrinking needs a floor**, or a visible defect is traded for a discreet
  one. The floor reuses `MIN_LEGIBLE_FONT_RATIO`, already calibrated by
  visual inspection, relocated into `rustmotion-core` so both sides share the
  one constant instead of inventing a second.

`rustmotion info` now reports each text's natural size, through the same
measurer the engine and the validator use.

The floor is pinned to a 1080-tall reference, because `IntrinsicMeasure`
cannot see the frame height and using the real one on the paint side alone
would reintroduce exactly the divergence above. On a taller canvas that floor
therefore sits below the legibility threshold, and a declared 120px shrinking
to ~13px on a 2160-tall frame would have passed the legibility check in
silence — the failure mode this feature exists to remove, not relocate.
`check_legibility` now warns in precisely that case, naming both numbers, and
stays quiet at 1080 where the two agree. The precise fix is a canvas-relative
floor on both sides; that needs the frame height plumbed into `TextIntrinsic`
and is tracked separately.
@LeadcodeDev LeadcodeDev added the enhancement New feature or request label Aug 11, 2026
@LeadcodeDev LeadcodeDev self-assigned this Aug 11, 2026
@LeadcodeDev
LeadcodeDev merged commit 14b7766 into main Aug 11, 2026
3 checks passed
@LeadcodeDev
LeadcodeDev deleted the feat/text-autofit branch August 11, 2026 08:33
LeadcodeDev added a commit that referenced this pull request Aug 11, 2026
…ver render (#172)

Two follow-ups the previous changes made possible but deliberately left out
of their own scope, since both alter what `validate` does to a user's file.

**`--fix` answers `ContentOverflowsBox` on text.** That arm did nothing
because growing the box, shrinking the font and shortening the copy are all
legitimate and produce different results — picking one was not the
validator's call. `style.text-autofit` (#170) removed the dilemma: it states
the author's intent without touching the declared box or the content, so
nothing written by hand is overwritten. Same risk category as the two fixes
already accepted, both of which also change the render.

Scoped to `text`/`gradient_text`, the two components whose painters
implement the field. Writing it anywhere else would be a no-op an author
could reasonably read as a fix, which is worse than leaving the violation
visible — a test pins that a `table` is left alone and nothing is claimed as
applied.

**`--strict-anim` stops at `freeze_at`.** Since #164 every render path
clamps there, so samples beyond it evaluate transforms at instants the video
cannot contain. It was reporting violations that cannot happen, which blocks
a correct scenario and sends a generator "fixing" what was never wrong.
Bounding the sample list rather than clamping each timestamp afterwards also
avoids generating a run of identical post-freeze samples.

`scene_duration` still reaches `BuildAnimationCtx` unchanged, so
duration-relative effects keep their real window (contract from PR #27) —
only the sampling ceiling moves. The mirror test matters as much as the new
one: the same fixture without a freeze must still be caught, or the bound
would be silencing real overflow instead of removing an unreachable sample.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant