feat(text): let text declare that it must fit its box - #170
Merged
Conversation
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.
This was referenced Aug 11, 2026
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_fixesdeliberately refusesContentOverflowsBox, and its comment says why: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-autofitmakestextandgradient_textreduce their font size until they fit the resolved width and, where taffy defines one, the content-box height.white-space: nowrapstill decides whether the text wraps; autofit decides at what size — they compose rather than compete.auto_scrollnever interacts: codeblock and terminal do not read the field, and a test pins that a declaredtext-autofitstays inert on components whose painters ignore it, so it cannot become a measure/paint divergence somewhere else.rustmotion infoalso 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
TextIntrinsicmeasured one thing and the painter drew another, blinding the geometry pass to real overflow. One pureresolve_text_autofitis 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 intorustmotion-coreso 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::measurereceives 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_legibilityin complete silence, because that check reads the declared size.That is the failure mode this feature exists to remove, not relocate.
check_legibilitynow warns in precisely that case: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
TextIntrinsicand throughbox_builder. Out of scope here; worth its own change.Recommendation on
--fix, not appliedvalidate.rswas 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_fixescould answerContentOverflowsBoxontext/gradient_textby 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-spaceremoved,auto_scrollforced), both of which also change the render. I recommend wiring it; say the word and it is a small change.Verification
cargo test --workspace: 25 targets, 1031 tests, 0 failures (32 new)cargo fmt --all --checkandcargo clippy --workspace --all-targets -- -D warnings: cleanmain— checked that it declares notext-autofitand that the non-autofit path is unchangedNot covered, deliberately
caption/rich_text/codeblock/terminaldo 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-heightdeclared 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.