Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 70 additions & 26 deletions android/src/main/java/com/swmansion/enriched/styles/ListStyles.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import com.swmansion.enriched.utils.ParagraphUtils.applyParagraphSpan
import com.swmansion.enriched.utils.ParagraphUtils.findPreviousAlignmentSpan
import com.swmansion.enriched.utils.ParagraphUtils.getPreviousParagraphSpan
import com.swmansion.enriched.utils.getParagraphBounds
import com.swmansion.enriched.utils.getParagraphRanges
import com.swmansion.enriched.utils.getSafeSpanBoundaries
import com.swmansion.enriched.utils.removeSpans
import com.swmansion.enriched.utils.removeZWS
Expand All @@ -26,6 +27,12 @@ import com.swmansion.enriched.watchers.TextChangedEvent
class ListStyles(
private val view: EnrichedTextInputView,
) {
private data class StyledListReplacement(
val text: SpannableStringBuilder,
val insertedCharacters: Int,
val styleStart: Int,
)

private fun setSpan(
editable: Editable,
name: TextStyle,
Expand All @@ -37,17 +44,17 @@ class ListStyles(
when (name) {
TextStyle.UNORDERED_LIST -> {
val span = EnrichedUnorderedListSpan(view.htmlStyle)
editable.setSpan(span, safeStart, safeEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
applyParagraphSpan(editable, span, safeStart, safeEnd)
}

TextStyle.ORDERED_LIST -> {
val span = EnrichedOrderedListSpan(view.htmlStyle)
editable.setSpan(span, safeStart, safeEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
applyParagraphSpan(editable, span, safeStart, safeEnd)
}

TextStyle.CHECK_LIST -> {
val span = EnrichedChecklistSpan(view.htmlStyle)
editable.setSpan(span, safeStart, safeEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
applyParagraphSpan(editable, span, safeStart, safeEnd)
}

else -> {}
Expand All @@ -61,7 +68,9 @@ class ListStyles(
clazz: Class<out EnrichedSpan>,
removeZWS: Boolean = true,
): Boolean {
val spans = editable.getSpans(start, end, clazz)
val safeStart = start.coerceAtMost(end).coerceAtLeast(0).coerceAtMost(editable.length)
val safeEnd = end.coerceAtLeast(start).coerceAtLeast(safeStart).coerceAtMost(editable.length)
val spans = editable.getSpans(safeStart, safeEnd, clazz)
if (spans.isEmpty()) return false

val flag = Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
Expand All @@ -72,32 +81,81 @@ class ListStyles(

editable.removeSpan(span)

if (spanStart < start) {
if (spanStart < safeStart) {
editable.setSpan(
span.copy(),
spanStart,
start,
safeStart,
flag,
)
}

if (spanEnd > end) {
if (spanEnd > safeEnd) {
editable.setSpan(
span.copy(),
end,
safeEnd,
spanEnd,
flag,
)
}
}

if (removeZWS) {
editable.removeZWS(start, end)
val replacement = SpannableStringBuilder(editable.subSequence(safeStart, safeEnd))
replacement.removeZWS(0, replacement.length)
val removedCharacters = safeEnd - safeStart - replacement.length

if (removedCharacters > 0) {
editable.replace(safeStart, safeEnd, replacement)
}
}

return true
}

private fun buildStyledListReplacement(
editable: Editable,
start: Int,
end: Int,
name: TextStyle,
): StyledListReplacement {
val replacement = SpannableStringBuilder(editable.subSequence(start, end))
var insertedCharacters = 0
var styleStart = 0
val paragraphs = replacement.getParagraphRanges()

for (paragraph in paragraphs) {
val currentStart = paragraph.first + insertedCharacters
var currentEnd = paragraph.last + insertedCharacters

if (!replacement.containsZWS(currentStart, currentEnd)) {
replacement.insert(currentStart, Strings.ZERO_WIDTH_SPACE_STRING)
currentEnd += 1
insertedCharacters += 1
}

setSpan(replacement, name, currentStart, currentEnd)
reapplyAlignment(replacement, currentStart, currentEnd)
styleStart = currentEnd + 1
}

return StyledListReplacement(replacement, insertedCharacters, styleStart)
}

private fun CharSequence.containsZWS(
start: Int,
end: Int,
): Boolean {
val safeStart = start.coerceIn(0, length)
val safeEnd = end.coerceIn(safeStart, length)

for (index in safeStart until safeEnd) {
if (this[index] == Strings.ZERO_WIDTH_SPACE_CHAR) return true
}

return false
}

fun toggleStyle(name: TextStyle) {
val config = EnrichedSpans.listSpans[name] ?: return
val editable = view.editableText
Expand All @@ -124,23 +182,9 @@ class ListStyles(
return
}

var currentStart = start
val paragraphs = editable.substring(start, end).split(Strings.NEWLINE_STRING)
removeSpansForRange(editable, start, end, config.clazz, false)

for (paragraph in paragraphs) {
var currentEnd = currentStart + paragraph.length

if (!paragraph.contains(Strings.ZERO_WIDTH_SPACE_CHAR)) {
editable.insert(currentStart, Strings.ZERO_WIDTH_SPACE_STRING)
currentEnd += 1
}
setSpan(editable, name, currentStart, currentEnd)
reapplyAlignment(editable, currentStart, currentEnd)
currentStart = currentEnd + 1
}

spanState.setStartWithStateChangeEmitting(name, currentStart)
val replacement = buildStyledListReplacement(editable, start, end, name)
editable.replace(start, end, replacement.text)
spanState.setStartWithStateChangeEmitting(name, start + replacement.styleStart)
}

private fun handleAfterTextChanged(
Expand Down
134 changes: 89 additions & 45 deletions android/src/main/java/com/swmansion/enriched/styles/ParagraphStyles.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import com.swmansion.enriched.utils.ParagraphUtils.applyParagraphSpan
import com.swmansion.enriched.utils.ParagraphUtils.getPreviousParagraphSpan
import com.swmansion.enriched.utils.getListRange
import com.swmansion.enriched.utils.getParagraphBounds
import com.swmansion.enriched.utils.getParagraphRanges
import com.swmansion.enriched.utils.getParagraphsBounds
import com.swmansion.enriched.utils.isTheSameParagraphInSelection
import com.swmansion.enriched.utils.removeSpans
Expand All @@ -39,43 +40,63 @@ import com.swmansion.enriched.watchers.TextChangedEvent
class ParagraphStyles(
private val view: EnrichedTextInputView,
) {
private data class StyledParagraphReplacement(
val text: SpannableStringBuilder,
val insertedCharacters: Int,
)

private fun removeStyleForSelection(
editable: Editable,
start: Int,
end: Int,
clazz: Class<out EnrichedSpan>,
removeZWS: Boolean = true,
): Boolean {
val paragraphRanges = editable.getParagraphsBounds(start, end)
val (paragraphStart, paragraphEnd) = editable.getParagraphBounds(start, end)

val spans = editable.getSpans(paragraphStart, paragraphEnd, clazz).distinct()
var removedAny = false

for (range in paragraphRanges) {
val paragraphStart = range.first
val paragraphEnd = range.last
if (removeZWS) {
editable.removeZWS(paragraphStart, paragraphEnd)
for (span in spans) {
val spanStart = editable.getSpanStart(span)
val spanEnd = editable.getSpanEnd(span)

val intersects = spanStart < paragraphEnd && spanEnd > paragraphStart
if (!intersects) continue

editable.removeSpan(span)

if (spanStart < paragraphStart) {
editable.setSpan(
span.copy(),
spanStart,
paragraphStart,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE,
)
}

if (spanEnd > paragraphEnd) {
editable.setSpan(
span.copy(),
paragraphEnd,
spanEnd,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE,
)
}
Comment thread
IvanIhnatsiuk marked this conversation as resolved.
val spans = editable.getSpans(paragraphStart, paragraphEnd, clazz)
if (spans.isEmpty()) continue

for (span in spans) {
val spanStart = editable.getSpanStart(span)
val spanEnd = editable.getSpanEnd(span)
val intersects = spanStart <= paragraphEnd && spanEnd >= paragraphStart
if (!intersects) continue
removedAny = true
}

editable.removeSpan(span)
view.transactionManager.runSilently {
if (removedAny && removeZWS) {
val (paragraphStart, paragraphEnd) = editable.getParagraphBounds(start, end)
val replacement = SpannableStringBuilder(editable.subSequence(paragraphStart, paragraphEnd))
replacement.removeZWS(0, replacement.length)
val removedCharacters = paragraphEnd - paragraphStart - replacement.length

if (spanStart < paragraphStart) {
val leftSpan = span.copy()
editable.setSpan(leftSpan, spanStart, paragraphStart, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
if (spanEnd > paragraphEnd) {
val rightSpan = span.copy()
editable.setSpan(rightSpan, paragraphEnd + 1, spanEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
if (removedCharacters > 0) {
editable.replace(paragraphStart, paragraphEnd, replacement)
}

removedAny = true
}
}

Expand All @@ -87,6 +108,37 @@ class ParagraphStyles(
setSpan(span, 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}

private fun buildStyledParagraphReplacement(
editable: Editable,
start: Int,
end: Int,
name: TextStyle,
clazz: Class<out EnrichedSpan>,
): StyledParagraphReplacement? {
createSpan(name) ?: return null

val replacement = SpannableStringBuilder(editable.subSequence(start, end))

replacement.removeSpans(0, replacement.length, clazz)

var insertedCharacters = 0
val paragraphs = replacement.getParagraphRanges()
for (paragraph in paragraphs) {
val span = createSpan(name) ?: return null
val currentStart = paragraph.first + insertedCharacters
val currentEnd = paragraph.last + insertedCharacters

if (currentStart == currentEnd) {
replacement.insert(currentStart, buildZWSWithSpan(span))
insertedCharacters += 1
} else {
applyParagraphSpan(replacement, span, currentStart, currentEnd)
}
}

return StyledParagraphReplacement(replacement, insertedCharacters)
}

fun afterTextChanged(event: TextChangedEvent) {
val s = event.text
var endCursorPosition = event.endCursorPosition
Expand Down Expand Up @@ -183,21 +235,9 @@ class ParagraphStyles(
return
}

var currentStart = start
val paragraphs = ssb.substring(start, end).split(Strings.NEWLINE_STRING)
removeStyleForSelection(ssb, start, end, config.clazz, false)

for (paragraph in paragraphs) {
val span = createSpan(name) ?: return
var currentEnd = currentStart + paragraph.length

if (paragraph.isEmpty()) {
ssb.insert(currentStart, Strings.ZERO_WIDTH_SPACE_STRING)
currentEnd += 1
}
applyParagraphSpan(ssb, span, currentStart, currentEnd)
currentStart = currentEnd + 1
}
val replacement =
buildStyledParagraphReplacement(ssb, start, end, name, config.clazz) ?: return
ssb.replace(start, end, replacement.text)

selection.validateStyles()
}
Expand Down Expand Up @@ -275,7 +315,7 @@ class ParagraphStyles(
)
}

editable.replace(paragraphStart, paragraphEnd, zwsBuilder)
editable.insert(paragraphStart, zwsBuilder)
return
}

Expand All @@ -294,14 +334,18 @@ class ParagraphStyles(
val (listStart, listEnd) =
editable.getListRange(paragraphStart, paragraphEnd, listSpan)

val paragraphsBounds = editable.getParagraphsBounds(listStart, listEnd)

editable.removeSpans(listStart, listEnd, EnrichedAlignmentSpan::class.java)

editable.setSpan(
EnrichedAlignmentSpan(alignment),
listStart,
listEnd,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE,
)
for (paragraphBound in paragraphsBounds) {
editable.setSpan(
EnrichedAlignmentSpan(alignment),
paragraphBound.start,
paragraphBound.endInclusive,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE,
)
}
} else {
editable.removeSpans(paragraphStart, paragraphEnd, EnrichedAlignmentSpan::class.java)
editable.setSpan(EnrichedAlignmentSpan(alignment), paragraphStart, paragraphEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
Expand Down
Loading