Skip to content

feat(text-editor): add an outline to text layers and scale shadows with the text - #875

Merged
hm21 merged 2 commits into
stablefrom
feat/text-layer-outline
Sep 29, 2026
Merged

hm21 merged 2 commits into
stablefrom
feat/text-layer-outline

Conversation

@hm21

@hm21 hm21 commented Sep 29, 2026

Copy link
Copy Markdown
Owner

Description

Text over video has to stay readable over whatever is behind it. Today the only way to separate text from a busy or bright background is a background box, which covers the picture. This adds an outline to text layers and makes their shadows behave, so text reads well without a box.

Outline

  • TextLayer.outlineWidth and TextLayer.outlineColor (default: no outline), serialized in import/export with minified keys ow / oc.
  • RoundedBackgroundText draws the outline as a stroke pass under the glyphs. Half of the stroke lies inside the glyph, so outlineWidth is the visible thickness outside the glyph edge.
  • TextEditorState.setOutline({width, color}), initialised from the edited layer and written onto the layer done() returns. The input field previews it while typing.

Shadows

  • With an outline, the shadows are cast by the outlined glyphs. A text shadow on the stroke pass would only follow the bare glyph shape, so the painter draws an opaque silhouette (stroke + fill) into its own layer, tints it with the shadow color and blurs it. Merging both passes in one layer keeps a translucent shadow color from doubling where they overlap.
  • Behaviour change: shadows of textStyle (and the outline) are measured at a scale and fontScale of 1 and grow with the rendered font size. Before, pinching a text layer or raising its font scale kept the unscaled shadow offset and blur, so the shadow looked thinner the larger the text got. Unscaled layers look exactly as before.
  • With TextEditorStyle.inputShadows: null the input previews the selected style's shadows at the size the layer draws them.

Fixes the feature depends on

  • A text layer reserves room for its outline and shadows (reserveEffectSpace), symmetrically so its center stays put. A layer capture reads the repaint boundary's bounds, so a shadow below the glyphs was cut off in exports.
  • LayerCopyManager.createCopyTextLayer and LayerInteractionManager._copyLayer list every field by hand and dropped the outline, which lost it when a layer is edited, duplicated, grouped or transformed by crop/rotate.
  • TextLayer.toMapFromReference wrote shadows only while they were non-empty and even when unchanged. The import merges each history step over the previous one, so a shadow removed in a later step came back after export/import. It now writes shadows only when they changed, including an empty list.
  • RoundedBackgroundTextPainter.shouldRepaint also compares the text and the outline, so a style-only change repaints.

Related Issue: none (needed for readable text and captions in a video editor built on this package).

Type of Change

  • ✨ New feature (non-breaking change which adds functionality)
  • 🛠️ Bug fix (non-breaking change which fixes an issue)
  • ❌ Breaking change (fix or feature that would cause existing functionality to change)
  • 🧹 Code refactor
  • ✅ Build configuration change
  • 📝 Documentation
  • 🗑️ Chore

Tests

  • TextLayer: outline defaults, map round trip, copyWith, and the reference diff (unchanged shadows omitted, removed shadows written as an empty list, outline changes written).
  • Import/export: an outlined, shadowed layer whose shadows are removed in a later history step imports without them and keeps its outline. Fails against stable because the removed shadows come back.
  • RoundedBackgroundText: one paint pass without an outline, the outline pass under the text, the silhouette shadow layer, a transparent outline drawing nothing, and the reserved space growing by exactly the outline and shadow reach.
  • LayerWidgetTextItem: outline and shadows scale with scale * fontScale.
  • TextEditor: setOutline reaches the returned layer; editing a layer previews its outline at the current font scale.
  • Layer copies keep the outline (copy manager and grouping). Both fail without the fix.
  • flutter test (695 tests), flutter analyze and dart format pass.
  • Checked on a Samsung Galaxy S26 (Android) in a video editor using this branch: live preview while typing, font change, re-editing a layer, and exported videos with outline and shadow on titles and burned-in captions.

Text over video needs to stay readable over whatever is behind it, and a
background box covers the picture. TextLayer now carries outlineWidth and
outlineColor, drawn as a stroke pass under the glyphs, and the text editor
sets it via TextEditorState.setOutline and previews it while typing.

With an outline, the shadows are cast by the outlined glyphs through an
opaque silhouette in its own layer; a text shadow on the stroke pass would
only follow the bare glyphs. Outline and shadows are measured at a scale
and fontScale of 1 and grow with the rendered font size, and a text layer
reserves room for them so a layer capture no longer cuts them off.

Also fixes two ways the new values were lost: the layer copies made for
editing, duplicating, grouping and crop/rotate listed every field by hand
and dropped the outline, and a history diff never recorded removed
shadows, so an import merged the earlier ones back.
…erhang

- TextLayer.toMap wrote outlineColor only with a width, so a color set
  before the width fell back to black once a later history step turned
  the outline on and was imported as a diff.
- The input's background text, which draws the outline preview, ignored
  TextEditorStyle.textHeight while the editable text used it, so with a
  style height the outline sat a line fraction off the typed glyphs.
- The silhouette shadow layer was bounded by the line boxes and clipped
  the shadow of glyphs that paint beyond them.
@hm21
hm21 marked this pull request as ready for review September 29, 2026 08:40
@hm21
hm21 merged commit 8bfa2c4 into stable Sep 29, 2026
3 checks passed
hm21 added a commit to divinevideo/divine-mobile that referenced this pull request Sep 29, 2026
hm21/pro_image_editor#875 shipped as 14.5.0, so the git override pinning
its unreleased commit is replaced by the pub.dev release. The release also
carries its review fixes: the outline color is stored even without a width,
the outline shadow is no longer clipped at overhanging glyphs, and the
editing preview lays its outline out at the same line height as the text.
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