From bb6d4b09e66573bc61e1f1b5ab8788ac9e6b3a3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Tue, 26 May 2026 11:19:03 +0200 Subject: [PATCH 1/2] fix: inline styles reapply --- .../enriched/textinput/styles/InlineStyles.kt | 76 +++++++++++++++++++ .../textinput/watchers/EnrichedTextWatcher.kt | 2 +- 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/android/src/main/java/com/swmansion/enriched/textinput/styles/InlineStyles.kt b/android/src/main/java/com/swmansion/enriched/textinput/styles/InlineStyles.kt index 4e3b86601..fafd38ede 100644 --- a/android/src/main/java/com/swmansion/enriched/textinput/styles/InlineStyles.kt +++ b/android/src/main/java/com/swmansion/enriched/textinput/styles/InlineStyles.kt @@ -103,8 +103,16 @@ class InlineStyles( fun afterTextChanged( s: Editable, + startCursorPosition: Int, endCursorPosition: Int, ) { + if (endCursorPosition > startCursorPosition) { + for ((style, config) in EnrichedSpans.inlineSpans) { + if (view.spanState?.getStart(style) != null) continue + splitSpanOnInsertion(s, config.clazz, startCursorPosition, endCursorPosition) + } + } + for ((style, config) in EnrichedSpans.inlineSpans) { val start = view.spanState?.getStart(style) ?: continue var end = endCursorPosition @@ -117,6 +125,47 @@ class InlineStyles( setSpan(s, config.clazz, start, end) } + + // Collapse same-type inline spans that ended up adjacent + // Without this the HTML output would emit separate tags like ....... + for ((_, config) in EnrichedSpans.inlineSpans) { + mergeAdjacentSpansOfType(s, config.clazz) + } + } + + private fun mergeAdjacentSpansOfType( + spannable: Spannable, + type: Class, + ) { + var changed = true + while (changed) { + changed = false + val sortedSpans = + spannable + .getSpans(0, spannable.length, type) + .sortedBy { spannable.getSpanStart(it) } + + for (i in 0 until sortedSpans.size - 1) { + val leadingSpan = sortedSpans[i] + val trailingSpan = sortedSpans[i + 1] + val leadingStart = spannable.getSpanStart(leadingSpan) + val leadingEnd = spannable.getSpanEnd(leadingSpan) + val trailingStart = spannable.getSpanStart(trailingSpan) + val trailingEnd = spannable.getSpanEnd(trailingSpan) + + if (leadingStart < 0 || leadingEnd < 0 || trailingStart < 0 || trailingEnd < 0) continue + if (leadingEnd == trailingStart) { + spannable.removeSpan(leadingSpan) + spannable.removeSpan(trailingSpan) + val (safeStart, safeEnd) = spannable.getSafeSpanBoundaries(leadingStart, trailingEnd) + val mergedSpan = + type.getDeclaredConstructor(HtmlStyle::class.java).newInstance(view.htmlStyle) + spannable.setSpan(mergedSpan, safeStart, safeEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + changed = true + break + } + } + } } fun toggleStyle(name: String) { @@ -143,6 +192,33 @@ class InlineStyles( view.selection.validateStyles() } + private fun splitSpanOnInsertion( + spannable: Spannable, + type: Class, + insertStart: Int, + insertEnd: Int, + ) { + val spans = spannable.getSpans(insertStart, insertEnd, type) + for (span in spans) { + val spanStart = spannable.getSpanStart(span) + val spanEnd = spannable.getSpanEnd(span) + if (spanStart < 0 || spanEnd < 0) continue + + spannable.removeSpan(span) + + if (spanStart < insertStart) { + val (safeStart, safeEnd) = spannable.getSafeSpanBoundaries(spanStart, insertStart) + val left = type.getDeclaredConstructor(HtmlStyle::class.java).newInstance(view.htmlStyle) + spannable.setSpan(left, safeStart, safeEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } + if (spanEnd > insertEnd) { + val (safeStart, safeEnd) = spannable.getSafeSpanBoundaries(insertEnd, spanEnd) + val right = type.getDeclaredConstructor(HtmlStyle::class.java).newInstance(view.htmlStyle) + spannable.setSpan(right, safeStart, safeEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } + } + } + fun removeStyle( name: String, start: Int, diff --git a/android/src/main/java/com/swmansion/enriched/textinput/watchers/EnrichedTextWatcher.kt b/android/src/main/java/com/swmansion/enriched/textinput/watchers/EnrichedTextWatcher.kt index 028b41c15..cbc11fc36 100644 --- a/android/src/main/java/com/swmansion/enriched/textinput/watchers/EnrichedTextWatcher.kt +++ b/android/src/main/java/com/swmansion/enriched/textinput/watchers/EnrichedTextWatcher.kt @@ -44,7 +44,7 @@ class EnrichedTextWatcher( } private fun applyStyles(s: Editable) { - view.inlineStyles?.afterTextChanged(s, endCursorPosition) + view.inlineStyles?.afterTextChanged(s, startCursorPosition, endCursorPosition) view.paragraphStyles?.afterTextChanged(s, endCursorPosition, previousTextLength) view.listStyles?.afterTextChanged(s, endCursorPosition, previousTextLength) view.parametrizedStyles?.afterTextChanged(s, startCursorPosition, endCursorPosition) From f0f245712f0d77bdbd94700509b9a86c30e5a208 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Tue, 2 Jun 2026 10:37:16 +0200 Subject: [PATCH 2/2] fix: reuse setSpan logic --- .../enriched/textinput/styles/InlineStyles.kt | 47 +++++-------------- 1 file changed, 11 insertions(+), 36 deletions(-) diff --git a/android/src/main/java/com/swmansion/enriched/textinput/styles/InlineStyles.kt b/android/src/main/java/com/swmansion/enriched/textinput/styles/InlineStyles.kt index fafd38ede..af9df9327 100644 --- a/android/src/main/java/com/swmansion/enriched/textinput/styles/InlineStyles.kt +++ b/android/src/main/java/com/swmansion/enriched/textinput/styles/InlineStyles.kt @@ -126,44 +126,19 @@ class InlineStyles( setSpan(s, config.clazz, start, end) } - // Collapse same-type inline spans that ended up adjacent - // Without this the HTML output would emit separate tags like ....... - for ((_, config) in EnrichedSpans.inlineSpans) { - mergeAdjacentSpansOfType(s, config.clazz) + val isBackspace = endCursorPosition == startCursorPosition + if (!isBackspace) { + return } - } - private fun mergeAdjacentSpansOfType( - spannable: Spannable, - type: Class, - ) { - var changed = true - while (changed) { - changed = false - val sortedSpans = - spannable - .getSpans(0, spannable.length, type) - .sortedBy { spannable.getSpanStart(it) } - - for (i in 0 until sortedSpans.size - 1) { - val leadingSpan = sortedSpans[i] - val trailingSpan = sortedSpans[i + 1] - val leadingStart = spannable.getSpanStart(leadingSpan) - val leadingEnd = spannable.getSpanEnd(leadingSpan) - val trailingStart = spannable.getSpanStart(trailingSpan) - val trailingEnd = spannable.getSpanEnd(trailingSpan) - - if (leadingStart < 0 || leadingEnd < 0 || trailingStart < 0 || trailingEnd < 0) continue - if (leadingEnd == trailingStart) { - spannable.removeSpan(leadingSpan) - spannable.removeSpan(trailingSpan) - val (safeStart, safeEnd) = spannable.getSafeSpanBoundaries(leadingStart, trailingEnd) - val mergedSpan = - type.getDeclaredConstructor(HtmlStyle::class.java).newInstance(view.htmlStyle) - spannable.setSpan(mergedSpan, safeStart, safeEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - changed = true - break - } + // Collapse same-type inline spans that ended up adjacent to each other after deletion. + // Without this the HTML output would emit separate tags like ....... + for ((_, config) in EnrichedSpans.inlineSpans) { + for (span in s.getSpans(startCursorPosition, startCursorPosition, config.clazz)) { + val spanStart = s.getSpanStart(span) + val spanEnd = s.getSpanEnd(span) + if (spanStart < 0 || spanEnd < 0) continue + setSpan(s, config.clazz, spanStart, spanEnd) } } }