Skip to content
Open
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
56 changes: 56 additions & 0 deletions .maestro/enrichedInput/flows/dynamic_ol_markers.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
appId: swmansion.enriched.example
---
# Validates that ordered list margins dynamically adjust when transitioning
# between single-digit (9) and double-digit (10) indexes.
- launchApp

- tapOn:
id: 'toggle-screen-button'

- runFlow:
file: '../subflows/set_editor_value.yaml'
env:
VALUE: >
<html>
<ol>
<li>A</li>
<li>B</li>
<li>C</li>
<li>D</li>
<li>E</li>
<li>F</li>
<li>G</li>
<li>H</li>
<li>I</li>
</ol>
</html>

- tapOn:
id: 'size-max-button'

- runFlow:
file: '../subflows/capture_or_assert_screenshot.yaml'
env:
SCREENSHOT_NAME: 'dynamic_ol_markers_before'

- tapOn:
id: "editor-input"
point: "50%, 95%"
- pressKey: Enter
- inputText: "J"

- runFlow:
file: '../subflows/capture_or_assert_screenshot.yaml'
env:
SCREENSHOT_NAME: 'dynamic_ol_markers_after'

- tapOn:
id: "editor-input"
point: "50%, 95%"
- pressKey: Backspace
- pressKey: Backspace

- runFlow:
file: '../subflows/capture_or_assert_screenshot.yaml'
env:
SCREENSHOT_NAME: 'dynamic_ol_markers_revert'
Binary file modified .maestro/enrichedInput/screenshots/android/checkbox_toggle.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .maestro/enrichedInput/screenshots/android/font_scaling.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .maestro/enrichedInput/screenshots/android/inline_styles.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .maestro/enrichedInput/screenshots/android/link_not_extended.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .maestro/enrichedInput/screenshots/android/links_visual_auto.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .maestro/enrichedInput/screenshots/android/links_visual_manual.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .maestro/enrichedInput/screenshots/ios/empty_element_parsing.png
Binary file modified .maestro/enrichedInput/screenshots/ios/empty_lists_parsing.png
Binary file modified .maestro/enrichedInput/screenshots/ios/paragraph_styles_lists.png
41 changes: 41 additions & 0 deletions .maestro/enrichedText/flows/dynamic_ol_markers.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
appId: swmansion.enriched.example
---
# Validates that ordered list margins dynamically adjust when transitioning
# between single-digit (9) and double-digit (10) indexes.
- launchApp

- tapOn:
id: 'toggle-screen-button'

- tapOn:
id: 'toggle-enriched-text-screen-button'

- runFlow:
file: '../subflows/set_enriched_text_value.yaml'
env:
VALUE: >
<html>
<ol>
<li>A</li>
<li>B</li>
<li>C</li>
</ol>
<br>
<ol>
<li>A</li>
<li>B</li>
<li>C</li>
<li>D</li>
<li>E</li>
<li>F</li>
<li>G</li>
<li>H</li>
<li>I</li>
<li>J</li>
</ol>
</html>

- runFlow:
file: '../subflows/capture_or_assert_screenshot.yaml'
env:
SCREENSHOT_NAME: 'dynamic_ol_markers'
Binary file modified .maestro/enrichedText/screenshots/android/alignment_visual.png
Binary file modified .maestro/enrichedText/screenshots/android/font_scaling.png
Binary file modified .maestro/enrichedText/screenshots/ios/alignment_visual.png
Binary file modified .maestro/enrichedText/screenshots/ios/font_scaling.png
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.swmansion.enriched.common

import android.graphics.Paint
import android.text.Spannable
import android.text.style.ParagraphStyle
import com.swmansion.enriched.common.spans.EnrichedOrderedListSpan
import com.swmansion.enriched.textinput.utils.getSafeSpanBoundaries

// Recomputes the shared marker column width for every ordered-list item.
fun updateOrderedListColumnMargins(
text: Spannable,
paint: Paint,
) {
val spans = text.getSpans(0, text.length, EnrichedOrderedListSpan::class.java)
val sortedSpans = spans.sortedBy { text.getSpanStart(it) }

val changedLists = mutableSetOf<MutableList<EnrichedOrderedListSpan>>()
var currentList = mutableListOf<EnrichedOrderedListSpan>()
var currentListChanged = false
var previousIndex = 0
var highestIndex = 0

for (span in sortedSpans.reversed()) {
if (span.index >= previousIndex) {
if (currentListChanged) {
// we'll re-layout that list
changedLists.add(currentList)
}

// we entered a new distinct list
currentList = mutableListOf()
currentListChanged = false
highestIndex = span.index
}

currentList.add(span)
if (span.updateColumnMargin(paint, highestIndex)) {
currentListChanged = true
}

previousIndex = span.index
}

if (currentListChanged) {
changedLists.add(currentList)
}

// A single empty ParagraphStyle over the whole list forces one re-layout of it.
// Uses the same trick as EnrichedSpanWatcher.updateNextLineLayout.
for (list in changedLists) {
forceOrderedListRelayout(text, list)
}
}

private fun forceOrderedListRelayout(
text: Spannable,
listSpans: List<EnrichedOrderedListSpan>,
) {
if (listSpans.isEmpty()) return

// Because the list was populated during a reversed() loop, the elements
// are in descending order. last() is the start of the list, first() is the end.
val start = text.getSpanStart(listSpans.last())
val end = text.getSpanEnd(listSpans.first())

if (start < 0 || end < 0 || start > end) return

class EmptySpan : ParagraphStyle

val (safeStart, safeEnd) = text.getSafeSpanBoundaries(start, end)
text.getSpans(safeStart, safeEnd, EmptySpan::class.java).forEach { text.removeSpan(it) }
text.setSpan(EmptySpan(), safeStart, safeEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,36 @@ import android.text.style.LeadingMarginSpan
import android.text.style.MetricAffectingSpan
import com.swmansion.enriched.common.EnrichedStyle
import com.swmansion.enriched.common.spans.interfaces.EnrichedParagraphSpan
import kotlin.math.ceil
import kotlin.math.max

open class EnrichedOrderedListSpan(
var index: Int,
private val enrichedStyle: EnrichedStyle,
) : MetricAffectingSpan(),
LeadingMarginSpan,
EnrichedParagraphSpan {
var columnMargin = enrichedStyle.olMarginLeft

// Computes the marker column width from the widest marker in the list ("<highestIndex>.").
// Returns true when the column width actually changed, so callers can force a relayout.
fun updateColumnMargin(
paint: Paint,
highestIndex: Int,
): Boolean {
val highestIndexText = "$highestIndex."

val originalTypeface = paint.typeface
paint.typeface = getTypeface(enrichedStyle.olMarkerFontWeight, originalTypeface)
val highestIndexWidth = ceil(paint.measureText(highestIndexText)).toInt()
paint.typeface = originalTypeface

val newColumnMargin = max(enrichedStyle.olMarginLeft, highestIndexWidth)
if (newColumnMargin == columnMargin) return false
columnMargin = newColumnMargin
return true
}

override fun updateMeasureState(p0: TextPaint) {
// Do nothing, but inform layout that this span affects text metrics
}
Expand All @@ -25,7 +48,7 @@ open class EnrichedOrderedListSpan(
// Do nothing, but inform layout that this span affects text metrics
}

override fun getLeadingMargin(first: Boolean): Int = enrichedStyle.olMarginLeft + enrichedStyle.olGapWidth
override fun getLeadingMargin(first: Boolean): Int = columnMargin + enrichedStyle.olGapWidth

override fun drawLeadingMargin(
canvas: Canvas,
Expand All @@ -42,17 +65,17 @@ open class EnrichedOrderedListSpan(
layout: Layout?,
) {
if (first) {
val originalColor = paint.color
val originalTypeface = paint.typeface
paint.color = enrichedStyle.olMarkerColor ?: originalColor
paint.typeface = getTypeface(enrichedStyle.olMarkerFontWeight, originalTypeface)

val text = "$index."
val width = paint.measureText(text)

val yPosition = baseline.toFloat()
val xPosition = (enrichedStyle.olMarginLeft + x - width / 2) * dir

val originalColor = paint.color
val originalTypeface = paint.typeface
val xPosition = (columnMargin + x - width) * dir

paint.color = enrichedStyle.olMarkerColor ?: originalColor
paint.typeface = getTypeface(enrichedStyle.olMarkerFontWeight, originalTypeface)
canvas.drawText(text, xPosition, yPosition, paint)

paint.color = originalColor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ data class EnrichedTextStyle(
blockquoteStripeWidth = parseFloat(blockquote, "borderWidth", allowFontScaling).toInt(),
blockquoteGapWidth = parseFloat(blockquote, "gapWidth", allowFontScaling).toInt(),
olGapWidth = parseFloat(orderedList, "gapWidth", allowFontScaling).toInt(),
olMarginLeft = calculateOlMarginLeft(fontSize, parseFloat(orderedList, "marginLeft", allowFontScaling).toInt()),
olMarginLeft = parseFloat(orderedList, "marginLeft", allowFontScaling).toInt(),
olMarkerFontWeight = parseOptionalFontWeight(orderedList, "markerFontWeight"),
olMarkerColor = parseOptionalColor(context, orderedList, "markerColor"),
ulGapWidth = parseFloat(unorderedList, "gapWidth", allowFontScaling).toInt(),
Expand Down Expand Up @@ -169,14 +169,6 @@ data class EnrichedTextStyle(
return parseFontWeight(weight)
}

private fun calculateOlMarginLeft(
fontSize: Int,
userMargin: Int,
): Int {
val leadMargin = fontSize / 2
return leadMargin + userMargin
}

private fun parseMentionsStyle(
context: ReactContext,
map: ReadableMap?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import com.swmansion.enriched.common.EnrichedSpanFlags
import com.swmansion.enriched.common.GumboNormalizer
import com.swmansion.enriched.common.parser.EnrichedParser
import com.swmansion.enriched.common.pixelFromSpOrDp
import com.swmansion.enriched.common.updateOrderedListColumnMargins
import com.swmansion.enriched.text.spans.EnrichedTextImageSpan
import com.swmansion.enriched.text.spans.interfaces.EnrichedTextClickableSpan
import com.swmansion.enriched.text.spans.interfaces.EnrichedTextSpan
Expand Down Expand Up @@ -179,6 +180,7 @@ class EnrichedTextView : AppCompatTextView {
val parsed = parseText(text, style)
if (parsed != null) {
parsedText = parsed
(parsed as? Spannable)?.let { updateOrderedListColumnMargins(it, paint) }
setText(parsed, BufferType.NORMAL)
observeAsyncImages()
} else {
Expand Down Expand Up @@ -283,6 +285,7 @@ class EnrichedTextView : AppCompatTextView {
}

if (modified) {
updateOrderedListColumnMargins(spannable, paint)
this.text = spannable
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import com.swmansion.enriched.common.EnrichedSpanFlags
import com.swmansion.enriched.common.GumboNormalizer
import com.swmansion.enriched.common.parser.EnrichedParser
import com.swmansion.enriched.common.pixelFromSpOrDp
import com.swmansion.enriched.common.updateOrderedListColumnMargins
import com.swmansion.enriched.textinput.events.MentionHandler
import com.swmansion.enriched.textinput.events.OnContextMenuItemPressEvent
import com.swmansion.enriched.textinput.events.OnInputBlurEvent
Expand Down Expand Up @@ -448,6 +449,9 @@ class EnrichedTextInputView :

runAsATransaction {
val newText = if (shouldParseHtml) parseText(value) else value
// Compute ordered-list marker column widths before setText so the first layout reserves
// the correct leading margin (getLeadingMargin runs during layout, before drawing).
(newText as? Spannable)?.let { updateOrderedListColumnMargins(it, paint) }
setText(newText)
applyLineSpacing()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,7 @@ class HtmlStyle : EnrichedStyle {
blockquoteStripeWidth = parseFloat(blockquoteStyle, "borderWidth").toInt()

val olStyle = style.getMap("ol")
val userDefinedMarginLeft = parseFloat(olStyle, "marginLeft").toInt()
val calculatedMarginLeft = calculateOlMarginLeft(view, userDefinedMarginLeft)
olMarginLeft = calculatedMarginLeft
olMarginLeft = parseFloat(olStyle, "marginLeft").toInt()
olGapWidth = parseFloat(olStyle, "gapWidth").toInt()
olMarkerColor = parseOptionalColor(olStyle, "markerColor")
olMarkerFontWeight = parseOptionalFontWeight(olStyle, "markerFontWeight")
Expand Down Expand Up @@ -213,16 +211,6 @@ class HtmlStyle : EnrichedStyle {
throw Error("Specified textDecorationLine value is not supported: $underline. Supported values are 'underline' and 'none'.")
}

private fun calculateOlMarginLeft(
view: EnrichedTextInputView?,
userMargin: Int,
): Int {
val fontSize = view?.fontSize?.toInt() ?: 0
val leadMargin = fontSize / 2

return leadMargin + userMargin
}

private fun ensureValueIsSet(
map: ReadableMap?,
key: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import android.text.SpannableStringBuilder
import android.text.Spanned
import com.swmansion.enriched.common.EnrichedConstants
import com.swmansion.enriched.common.EnrichedSpanFlags
import com.swmansion.enriched.common.updateOrderedListColumnMargins
import com.swmansion.enriched.textinput.EnrichedTextInputView
import com.swmansion.enriched.textinput.spans.EnrichedInputCheckboxListSpan
import com.swmansion.enriched.textinput.spans.EnrichedInputOrderedListSpan
Expand Down Expand Up @@ -115,6 +116,8 @@ class ListStyles(
val index = getOrderedListIndex(text, spanStart)
span.setListIndex(index)
}

updateOrderedListColumnMargins(text, view.paint)
}

private fun toggleStyle(
Expand Down
2 changes: 1 addition & 1 deletion apps/example/src/constants/editorConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export const htmlStyle = {
},
ol: {
gapWidth: 16,
marginLeft: 24,
marginLeft: 16,
markerColor: 'navy',
markerFontWeight: 'bold',
},
Expand Down
2 changes: 1 addition & 1 deletion ios/extensions/LayoutManagerExtension.mm
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,7 @@ - (void)drawDecimal:(id<EnrichedViewHost>)host
indent:(CGFloat)indent {
CGFloat gapWidth = [host.config orderedListGapWidth];
CGSize markerSize = [marker sizeWithAttributes:markerAttributes];
CGFloat markerX = origin.x + indent - gapWidth - markerSize.width / 2;
CGFloat markerX = origin.x + indent - gapWidth - markerSize.width;
UIFont *markerFont = markerAttributes[NSFontAttributeName];

// drawAtPoint draws from the top-left bounding box of the string.
Expand Down
14 changes: 14 additions & 0 deletions ios/inputAttributesManager/InputAttributesManager.mm
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ - (void)clearRemovedTypingAttributes {
}

- (void)handleDirtyRangesStyling {
// snapshot edited ranges (including 0-length ones) before filtering, so
// we can use them when recalculating adjacent ordered lists
NSArray<NSValue *> *editedRangesForListRecalc = [_dirtyRanges copy];

// Filter out 0 length ranges for styling.
NSPredicate *predicate = [NSPredicate
predicateWithBlock:^BOOL(NSValue *evaluatedObject, NSDictionary *_) {
Expand Down Expand Up @@ -122,6 +126,16 @@ - (void)handleDirtyRangesStyling {
}
}
}
// refresh ordered lists adjacent to any edit
OrderedListStyle *orderedListStyle =
(OrderedListStyle *)_input->stylesDict[@([OrderedListStyle getType])];
if (orderedListStyle != nil) {
for (NSValue *rangeObj in editedRangesForListRecalc) {
[orderedListStyle
recalculateListsAroundEditedRange:[rangeObj rangeValue]];
}
}

// do the typing attributes management, with no selection
[self manageTypingAttributesWithOnlySelection:NO];

Expand Down
Loading
Loading