Skip to content

avm2: Implement the Flash Text Engine (flash.text.engine) - #24153

Open
falpi wants to merge 1 commit into
ruffle-rs:masterfrom
falpi:add-font-text-engine-implementation
Open

avm2: Implement the Flash Text Engine (flash.text.engine)#24153
falpi wants to merge 1 commit into
ruffle-rs:masterfrom
falpi:add-font-text-engine-implementation

Conversation

@falpi

@falpi falpi commented Jul 7, 2026

Copy link
Copy Markdown

Summary

flash.text.engine shipped as a type-complete but inert package: the classes and constant enums existed, and TextBlock's storage was recently nativized, but TextBlock.createTextLine was a stub and TextLine's metrics and hit-testing were stub getters (there was no native text_line.rs module at all). Anything built on FTE — the whole flashx.textLayout (Text Layout Framework) stack that Spark RichText, Label, TextInput and TextArea compose their text with — rendered nothing usable.

This PR adds the composition engine on top of the native TextBlock object. Spark text controls now compose, wrap, measure, justify and edit real text, with a working caret and word selection.

What this adds over upstream

Area Before (upstream) After (this PR)
createTextLine / recreateTextLine stub — fakes a single line real line breaking over the content, reusing the core layout engine
TextLine metrics — ascent/descent/textWidth/textHeight stub getters measured from the backing field
Hit-testing — getAtomIndexAtPoint/getAtomBounds/getAtomIndexAtCharIndex/getAtomWordBoundaryOnLeft stubs (return −1 / placeholder) real caret placement & word selection
Line chain — firstLine/lastLine/firstInvalidLine, nextLine/previousLine lastLine stub, no walkable chain real chain with native endpoints
releaseLines single-line only full — ranges, #2004 validation, relink
content= / bidiLevel= line invalidation none invalidates the line chain
Justification (SpaceJustifier) none width-justify via the core Justify pass
findNext/PreviousAtomBoundary, findNext/PreviousWordBoundary absent present
TextLine.validity state machine native get/set, no rules full restricted transitions (#2008)

The enum/constant classes, ElementFormat, FontDescription, FontMetrics, the justifiers, TabStop and the content-element types are untouched — they were already present. The contribution is ~1,000 lines concentrated in TextBlock and TextLine.

How it works. break_text_line flattens the content-element tree into text + format runs, lays the remaining text out at the requested width with the existing layout engine, and takes the break at the start of the second laid-out line; each TextLine is backed by an EditText holding just that one line. A line's origin is its baseline start, so the backing field is re-homed by (-gutter, -(gutter + ascent)) and display coordinates + hit-testing match Flash Player. (Cost: laying out the remainder to take one break is O(N²) in the block's text length — fine for typical FTE content, a TODO for very large blocks.)

Known limitations / residual gaps

The composition path is implemented; the following remain stubbed or approximated (a member-level audit against the Adobe surface: of ~228 public members, ~165 real / ~31 partial / ~18 stub / ~14 missing — though most "real" members are the pre-existing enums and native storage, not this PR):

  • Inline graphic elementsGraphicElement is stubbed; hasGraphicElement/getAtomGraphic inert; totalAscent/Descent collapse to ascent/descent.
  • Event mirror regionsTextLineMirrorRegion is an empty shell; mirrorRegions absent.
  • Tab stops — data holders complete, but tabs are never laid out (hasTabs hardcoded false, lineOffset ignored).
  • Bidi / RTLbidiLevel stored + invalidates, but no reordering pass; visual order always equals logical order.
  • Vertical / rotated textlineRotation / textRotation stored, no rotated layout.
  • Advanced ElementFormat typographyalpha, baselineShift, typographicCase, digitCase/Width, ligatureLevel, dominant/alignmentBaseline, per-run textRotation, locale are stored/validated but not applied to the glyph; only font/size/color/weight/kerning/tracking reach the layout.
  • Font metrics / compatibilityElementFormat.getFontMetrics fabricates from fontSize; FontDescription.isFontCompatible always false; isDeviceFontCompatible missing.
  • Justification tuning — width-justify works, but the SpaceJustifier spacing envelope and EastAsianJustifier styles are inert.
  • Atom model — 1 char = 1 atom throughout (combining marks / surrogate pairs / graphic atoms miscounted).
  • Line recyclingrecreateTextLine reuses geometry but doesn't reset the recycled display object's state; releaseLineCreationData / getTextLineAtCharIndex absent.

A left-to-right business application rarely reaches these; rich, multilingual or vertical layouts do.

Testing

Automated.

  • avm2/textline_validity — the validity state-machine test — is taken off the known-failure list (real transitions + #2008).
  • Existing FTE regression tests pass: textblock_createline_fte, textblock_properties, text_engine_fontdescription, text_engine_groupelement, textline_inapplicable_properties.

Manual — Spark component showcase. A single-page Spark showcase (the main Spark controls, plus a few mx-legacy ones, instantiated with sample data in labelled panels; a status label driven by interaction handlers also verifies event routing through the Spark skins). Because all Spark text is composed by flashx.textLayout on top of FTE, every labelled control exercises this PR. All of the following render correctly and match Flash Player (glyph placement, line-box heights and baselines, caret geometry), and interactions behave correctly:

  • Text controls (the FTE surface): s:Label, s:RichText (justified paragraph, word wrap), s:TextInput (text entry, click-to-place caret, keyboard navigation, drag and double-click word selection), s:TextArea (multi-line editing with embedded newlines, selection across wrapped lines, scrolling).
  • Controls whose labels / prompts / item renderers compose through the same path: s:Button, s:ToggleButton, s:CheckBox, s:RadioButton, s:ButtonBar, s:TabBar; s:DropDownList, s:ComboBox (editable), s:List; s:DataGrid (Spark) column headers and cell renderers; s:NumericStepper; and s:Panel titles, s:BorderContainer and s:TileGroup labels.
  • Non-text Spark controls and FXG graphics (regression check across the stack): s:Spinner, s:HSlider, s:VSlider, s:HScrollBar; the s:Rect / s:Ellipse / s:Group primitives; and the s:Scroller / VGroup / HGroup layout containers.
  • Classic mx components (the TextField path — unaffected by this PR, verified to still render alongside): mx:ProgressBar, mx:ColorPicker, mx:DateField, mx:DataGrid.

Interaction handlers (button clicks, toggle/checkbox/radio changes, list and dropdown selection, stepper/slider/date changes) update a live status label, confirming event routing through the Spark skins works — not just static rendering.

visual/definefont4 — expected divergence

This PR changes how TLF content is laid out and rendered: TextLines are now
individually positioned by TLF using the real font metrics (previously the
stub values ascent = 12.0 / descent = 3.0), and each line renders with its
origin on the text baseline, like in Flash Player. As a result the output of
visual/definefont4 no longer matches its reference image, which was captured
with the previous behavior (whole paragraph laid out as a single word-wrapped
EditText): the text is shifted by roughly one EditText gutter (2 px), plus
a ~1 px difference in line spacing.

The rendering is otherwise identical (same glyphs, same rasterization — the
first line matches the reference pixel-perfectly once the shift is accounted
for). Since the divergence is a consequence of the more accurate line
placement rather than a rendering bug, the test is marked with the existing
known_failure = true convention on its image comparison, with the current
output pinned as output.ruffle.png so any future regression is still caught.
output.expected.png is left untouched as the reference to converge to.

Checklist

  • I, a human, have self-reviewed this PR and fully understand the changes within.
  • I have made or updated tests where possible.
  • All of my commits are properly scoped, compile successfully, and pass all tests.
  • This PR does not make sense to split up into smaller PRs.
  • An LLM was involved in the authoring of this code.

@falpi
falpi force-pushed the add-font-text-engine-implementation branch 2 times, most recently from 9fe104a to 46782d5 Compare July 7, 2026 23:32
Replace the stubbed flash.text.engine classes with a working implementation
of line breaking, metrics, line management, navigation, and justification.

Line breaking. TextBlock.createTextLine and recreateTextLine break the
block's content into TextLines by reusing the core text layout engine. The
content element tree is flattened into text plus format runs -- GroupElement
recurses over its children and GraphicElement contributes a U+FDEF
placeholder -- the remaining text is laid out at the requested width, and the
break point for the new line is taken from the start of the second laid-out
line. Each TextLine delegates its own layout and rendering to a backing
EditText that holds just that one line.

Metrics. TextLine exposes real ascent, descent, textWidth and textHeight
extracted from the backing field's layout, with the line origin placed on
the baseline as in Flash Player.

Line management. Lines are wired into a first/previous/next/last chain, and
textLineCreationResult reports success, complete, insufficientWidth or
emergency. Assigning TextBlock.content or bidiLevel invalidates the
previously broken lines; firstInvalidLine walks the chain to the first
non-valid line; lastLine returns the last broken line; and releaseLines
detaches an inclusive range of lines, relinks the surrounding chain, and
invalidates every line that followed the released range. The
TextLine.validity state machine matches Flash Player, including the
restricted transitions that raise ArgumentError ruffle-rs#2008.

Navigation and hit-testing. TextBlock.findNextAtomBoundary,
findPreviousAtomBoundary, findNextWordBoundary and findPreviousWordBoundary
locate atom and word boundaries in the content text. TextLine implements
getAtomIndexAtPoint, getAtomIndexAtCharIndex and getAtomBounds (including the
trailing paragraph terminator), which the Text Layout Framework relies on to
place and move the caret.

Justification. A paragraph whose block carries a SpaceJustifier requesting
justification is rendered by switching the backing field to Justify alignment
and spreading the line to the requested width.
@falpi
falpi force-pushed the add-font-text-engine-implementation branch from 46782d5 to 1610468 Compare July 8, 2026 09:26
@torokati44 torokati44 added the fte/tlf Issues relating to the FTE (Flash Text Engine) AVM2 API or the TLF (Text Layout Framework) library label Jul 8, 2026
@torokati44

Copy link
Copy Markdown
Member

See: #23879

@torokati44 torokati44 added the llm The PR contains mostly LLM-generated code label Jul 8, 2026
@falpi

falpi commented Jul 8, 2026

Copy link
Copy Markdown
Author

See: #23879

I guess a lot of people are working on this... let's hope that sooner or later we manage to get decent support for FTE in the mainstream.

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

Labels

fte/tlf Issues relating to the FTE (Flash Text Engine) AVM2 API or the TLF (Text Layout Framework) library llm The PR contains mostly LLM-generated code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants