Skip to content

feat: allow inline text styles on dialogue content runs - #381

Merged
han4wluc merged 2 commits into
mainfrom
feat/inline-text-style-on-dialogue-runs
Oct 19, 2026
Merged

han4wluc merged 2 commits into
mainfrom
feat/inline-text-style-on-dialogue-runs

Conversation

@han4wluc

Copy link
Copy Markdown
Contributor

Summary

Layout elements reject any authored inline textStyle and must use textStyleId. That rule also rejected dialogue content runs, so a project containing

dialogue:
  content:
    - text: "Hello"
      textStyle:
        fontWeight: bold

failed while building the render state with Inline textStyle is not allowed in layout elements at "root[0].content[0]". Use textStyleId instead. Editors that let a writer emphasise selected dialogue text (bold, italic, underline, an accent color) save exactly this shape, so those projects could not be previewed or exported.

A run inside the content array of a text or text-revealing element is the writer's text rather than layout design, so it may now carry a small inline textStyle. Layout elements themselves keep the existing rule.

Behavior

A run's inline textStyle accepts only these fields:

Field Accepted values
fontWeight normal, bold, bolder, lighter, or a number from 1 to 1000 (a numeric string also works)
fontStyle normal, italic, oblique (the same values text style resources use)
textDecoration none, underline
fill an opaque hex color; a short #abc is expanded to six digits (reuses normalizeHexColor)

Any other field (for example fontSize, fontFamily, shadow) or an invalid value fails fast with the path of the run, in the same style as the existing errors:

  • Inline textStyle.fontSize is not allowed in text content runs at "root[0].content[1]". Only fontWeight, fontStyle, textDecoration, and fill may be authored inline; use textStyleId for other style changes
  • Inline textStyle.fill at "root[0].content[1]" must resolve to a hex color
  • Inline textStyle.fontWeight at "..." must be normal, bold, bolder, lighter, or a number from 1 to 1000
  • Inline textStyle.fontStyle at "..." must be normal, italic, or oblique
  • Inline textStyle.textDecoration at "..." must be none or underline
  • Inline textStyle at "..." must be an object

Run with both textStyleId and textStyle: textStyleId resolves through resources.textStyles first, then the inline fields replace the matching fields of the resolved style. An inline fill replaces the text style's fill together with its colorAlpha.

Run with only textStyle: the renderer receives only the authored fields. Route Graphics merges a run's textStyle over the element's own style (prepareRichTextSegments in both text and text-revealing), so a run with only fontWeight: bold keeps the element's font, size, and color.

Unchanged and still throwing the existing errors:

  • textStyle on a layout element, including with styled runs in its content
  • hover, click, and rightClick textStyle on an element
  • furigana with an inline textStyle (furigana keeps using textStyleId)
  • textStyle on a run of any element type other than text and text-revealing

Once templates are rendered the engine cannot distinguish dialogue content from a run written directly in a layout's content array, so both follow the same rule. Runs of legacy text.content visuals and history/NVL line content go through the same path.

Docs

docs/Concepts.md and docs/RouteEngine.md now carve out the exception where they state that inline textStyle is invalid, and docs/RouteEngine.md has a new "Inline Text Run Styles" section describing the allowlist and the textStyleId + textStyle semantics. docs/L10n.md notes that line.dialogue replacement entries contain only text.

Testing

  • bun run test: 101 files, 2456 tests pass (baseline on origin/main: 100 files, 2408 tests). bun run lint and bun run check:test-markers pass.
  • New and extended render-state cases: resolveTextStyleIds (allowlisted fields, both-fields overlay, nested elements, furigana, every disallowed key and invalid value, element/hover/click/rightClick/furigana/non-text element still throwing), addDialogue (ADV and NVL line content), addVisuals (text visual content), and addOverlayStack (history content). The existing throw cases are unchanged.
  • spec/RouteEngine.dialogueRunTextStyle.test.js runs the real engine: styled ADV runs, ADV append, the authored project data staying untouched, errors, and L10n (a package without a patch for the line keeps the canonical styled runs; a package replacement renders as authored; clearing the package restores the styled runs).
  • VT: new vt/specs/dialogue/inline-run-text-style.yaml with one new reference image (vt/reference/dialogue/inline-run-text-style-01.webp, no existing references changed). It renders bold, italic, accent-fill, and textStyleId + inline runs through both a text-revealing and a text element. Captured with the pinned VT Docker image, byte-identical across three runs, and the report passes. Re-running the dialogue, visual, and layout VT folders gives 79 of 81 images matching; the two mismatches (dialogue/character-persist--capture-03, visual/text-shadow-01) are identical on an unmodified origin/main.
  • The VT bundle uses route-graphics 1.43.0, which does not draw textDecoration, so underline is not part of the committed VT reference. Underline was checked separately with a Route Graphics 1.48.1 bundle (underline support shipped in 1.48.0): it renders in both text and text-revealing.

Notes for review

  • fill accepts a raw opaque hex rather than requiring a colorId, because the editor writes hex. Requiring a color resource would be a small change if preferred.
  • Transparent or semi-transparent run color is not supported inline; use a text style resource with colorAlpha through textStyleId.
  • line.dialogue L10n replacement entries still accept only text, so a translated line does not carry run styles. Lines that no package patches keep their styled runs. Allowing textStyle there would be a follow-up.
  • The action schema in src/schemas/presentationActions.yaml is not changed; it does not restrict extra properties on content items.

Layout elements still reject an authored inline textStyle and must use
textStyleId. A run inside the content array of a text or text-revealing
element (such as an item of dialogue content) is the writer's text rather
than layout design, so it may now carry a small inline textStyle instead of
throwing. This lets a bold, italic, underlined, or accented run reach the
renderer.

A run's textStyle accepts only these fields; any other field throws with the
path of the run:

- fontWeight: normal, bold, bolder, lighter, or a number from 1 to 1000
- fontStyle: normal, italic, or oblique
- textDecoration: none or underline
- fill: an opaque hex color

When a run has both textStyleId and textStyle, textStyleId resolves first and
the inline fields then replace the matching fields. A run with only inline
fields hands the renderer just those fields, and Route Graphics merges them
over the element's own text style, so the element keeps its font, size, and
color.

Inline textStyle on the element itself, on hover, click, or rightClick, on
furigana, and on elements of any other type still throws as before.

Tests: resolveTextStyleIds, addDialogue (adv and nvl), addVisuals, and
addOverlayStack render-state cases, an engine-level test that includes L10n
packages, and a VT spec with a new reference image for the dialogue render
path.
@han4wluc
han4wluc merged commit e90ad08 into main Oct 19, 2026
40 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant