Skip to content

fix(android): preserve inline styles during IME composition - #737

Open
woosanggyu wants to merge 3 commits into
software-mansion:mainfrom
woosanggyu:fix/android-preserve-inline-styles-during-ime-composition
Open

fix(android): preserve inline styles during IME composition#737
woosanggyu wants to merge 3 commits into
software-mansion:mainfrom
woosanggyu:fix/android-preserve-inline-styles-during-ime-composition

Conversation

@woosanggyu

Copy link
Copy Markdown

Summary

Fixes #734.

On Android, predictive keyboards replace the entire active composing word on each setComposingText call. When an inline style was toggled off in the middle of that word, the replacement discarded enriched inline spans that belonged to the unchanged prefix.

This change:

  • enables inline-style preservation only when a style is toggled during an active IME composition;
  • snapshots enriched inline spans from the current composing range before setComposingText or commitText;
  • restores spans only over unchanged prefix and suffix ranges, keeping newly typed text consistent with the current style state;
  • clears preservation state when the composition is committed or finished; and
  • queries all enriched inline spans in one pass to avoid overhead during normal typing.

The change is limited to the Android text input implementation.

Test Plan

Manual verification in the example Android app with Gboard predictive text / suggestions enabled:

  1. Type abc.
  2. Toggle bold on and type def without a space.
  3. Toggle bold off and type ghi without a space.
  4. Verify that only def remains bold.
  5. Repeat with italic, underline, and strikethrough.
  6. Commit the word and verify that the earlier inline styles are still preserved.

Automated checks:

  • Android Kotlin compilation passed.
  • Android lint and formatting checks passed.
  • yarn lint passed.
  • yarn typecheck passed.
  • yarn test --maxWorkers=2 passed (415 tests).

Screenshots / Videos

See the reproduction video in #734. The fix does not change layout or visual design.

Compatibility

OS Implemented
iOS
Android
Web

Checklist

  • E2E tests are passing (not run; the regression requires an active predictive-keyboard composing region)
  • Required E2E tests have been added (not applicable to the current automated input environment)

Copilot AI review requested due to automatic review settings July 31, 2026 03:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes an Android-only IME composition edge case where predictive keyboards (via repeated setComposingText) can drop previously-applied enriched inline style spans when an inline style is toggled mid-word. The change adds targeted span snapshot/restore logic in the Android InputConnection wrapper so only unchanged parts of the composing word keep their prior inline styling.

Changes:

  • Adds an inline-style restoration helper to reapply a specific inline style span over a given range when missing.
  • Tracks when inline-style preservation should run (only after an inline style is toggled during an active composing region).
  • Snapshots inline spans inside the composing range before setComposingText / commitText, then restores them over the unchanged prefix/suffix, clearing state when composition is committed/finished.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
android/src/main/java/com/swmansion/enriched/textinput/styles/InlineStyles.kt Adds restoreStyleOnRange to reapply a specific inline style span when absent.
android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputView.kt Stores the wrapped InputConnection and triggers preservation when toggling inline styles.
android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputConnectionWrapper.kt Implements composing-range snapshot + restore around IME updates and clears preservation state appropriately.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@hejsztynx hejsztynx left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @woosanggyu !
First of all, thanks for taking your time to implement that fix.

Unfortunately, it doesn't work quite right - when following the initial test case you provided, it's fine, but when you move the cursor elsewhere and return to the end of that typed word, the inline styles are not preserved.

Screen.Recording.2026-08-04.at.12.59.47.mov

@hejsztynx hejsztynx added the bug Something isn't working label Aug 4, 2026
The previous approach only armed span preservation when an inline style was
toggled during an active composition. Moving the caret ends the composition and
clears that flag, so returning to the end of the word and typing again let the
IME replace the whole word with no snapshot taken, dropping the styled run.

Gate on the content being replaced instead of on the toggle event: snapshot
whenever the composing region carries inline spans, and restore afterwards.
captureComposingText() returns null when there is nothing to preserve, so plain
typing still costs a single span lookup.

This also removes the cached EnrichedTextInputConnectionWrapper on the view,
which silently no-opped whenever the cached instance differed from the one the
IME was driving.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@woosanggyu

woosanggyu commented Aug 5, 2026

Copy link
Copy Markdown
Author

Thanks for catching that — you're right, and the root cause was the gate I used.

The old version only armed preservation when an inline style was toggled during
an active composition
. Moving the caret ends the composition and clears the
flag, so when you return to the end of the word the IME re-establishes a composing
region and replaces the whole word with no snapshot taken — the styled run inside
it is dropped.

The trigger isn't the toggle, it's a composing replacement over text that
carries inline spans
, which happens with or without a toggle. So I moved the
gate from the event to the content: setComposingText/commitText now snapshot
whenever the composing region contains inline spans, and restore afterwards.
captureComposingText() returns null when there's nothing to preserve, so plain
typing still costs a single getSpans call over the composing range.

That removed the whole state machine along with the view-side hook, so
EnrichedTextInputView.kt is no longer touched by this PR.

Verified on an emulator with Microsoft SwiftKey (English):

  1. abcBdefBghidef stays bold (original case, no regression)
  2. Same, then move the caret away, return to the end of the word, keep typing —
    def stays bold (your case)
  3. Bold a word, unbold part of it via selection, then keep typing — the unbolded
    part stays unbolded (the snapshot is taken fresh before each replacement, so
    deliberate removals aren't resurrected)

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

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Android] Inline styles are stripped from previously-styled text when toggling a style OFF mid-word with predictive text enabled

3 participants