Skip to content
21 changes: 21 additions & 0 deletions .maestro/enrichedInput/flows/image_width_clamping.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
appId: swmansion.enriched.example
---
# verifies the image width is clamped down to the total available space, and wraps properly
- launchApp

- tapOn:
id: "toggle-screen-button"

- runFlow:
file: "../subflows/set_editor_value.yaml"
env:
VALUE: >
<html><p><img src="https://picsum.photos/id/20/200/200" width="100" height="100"/><img src="https://picsum.photos/id/20/200/200" width="100" height="100"/><img src="https://picsum.photos/id/20/200/200" width="100" height="100"/><img src="https://picsum.photos/id/140/400/100" width="400" height="100"/></p></html>

- tapOn:
id: "size-max-button"

- runFlow:
file: "../subflows/capture_or_assert_screenshot.yaml"
env:
SCREENSHOT_NAME: "image_width_clamping"
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.
26 changes: 26 additions & 0 deletions .maestro/enrichedText/flows/image_wrap.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
appId: swmansion.enriched.example
---
# Validates that images wrap correctly
- launchApp

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

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

- runFlow:
file: '../subflows/set_enriched_text_value.yaml'
env:
VALUE: >
<html><p><img src="https://picsum.photos/id/1/300/100" width="300" height="100"/><img src="https://picsum.photos/id/1/500/100" width="500" height="100"/><img src="https://picsum.photos/id/1/500/100" width="500" height="100"/></p></html>

- swipe:
start: "5%, 35%"
end: "5%, 35%"
duration: 3000

- runFlow:
file: '../subflows/capture_or_assert_screenshot.yaml'
env:
SCREENSHOT_NAME: 'image_wrap'
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.
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import java.io.File
open class EnrichedImageSpan :
ImageSpan,
EnrichedInlineSpan {
var containerWidth: Int? = null
private var width: Int = 0
private var height: Int = 0

Expand Down Expand Up @@ -53,8 +54,19 @@ open class EnrichedImageSpan :
override fun getDrawable(): Drawable {
val drawable = super.getDrawable()
val scale = Resources.getSystem().displayMetrics.density
val maxWidth = this.containerWidth

drawable.setBounds(0, 0, (width * scale).toInt(), (height * scale).toInt())
var targetWidth = (width * scale).toInt()
var targetHeight = (height * scale).toInt()

// clamp down the width to the maximum available container space
if (maxWidth != null && targetWidth > maxWidth && maxWidth > 0) {
val aspectRatio = targetHeight.toFloat() / targetWidth.toFloat()
targetWidth = maxWidth
targetHeight = (targetWidth * aspectRatio).toInt()
}

drawable.setBounds(0, 0, targetWidth, targetHeight)
return drawable
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import com.facebook.react.views.text.ReactTypefaceUtils.parseFontStyle
import com.facebook.react.views.text.ReactTypefaceUtils.parseFontWeight
import com.swmansion.enriched.common.EnrichedConstants
import com.swmansion.enriched.common.EnrichedSpanFlags
import com.swmansion.enriched.common.ForceRedrawSpan
import com.swmansion.enriched.common.GumboNormalizer
import com.swmansion.enriched.common.parser.EnrichedParser
import com.swmansion.enriched.common.pixelFromSpOrDp
Expand Down Expand Up @@ -87,6 +88,31 @@ class EnrichedTextView : AppCompatTextView {
setFontSize(EnrichedConstants.TEXT_DEFAULT_FONT_SIZE)
}

override fun onMeasure(
widthMeasureSpec: Int,
heightMeasureSpec: Int,
) {
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)

if (widthMode != MeasureSpec.UNSPECIFIED) {
val availableWidth = widthSize - totalPaddingLeft - totalPaddingRight

if (availableWidth > 0) {
val spanned = text as? Spanned
val spans = spanned?.getSpans(0, spanned.length, EnrichedTextImageSpan::class.java)

// we need to update the image bounds, potentially clamping
// the width to the available one
spans?.forEach {
it.containerWidth = availableWidth
}
}
}

super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}

override fun onTouchEvent(event: MotionEvent): Boolean {
val spanned = text as? Spanned
val action = event.action
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import com.swmansion.enriched.common.GumboNormalizer
import com.swmansion.enriched.common.allowFontScalingFromProps
import com.swmansion.enriched.common.parser.EnrichedParser
import com.swmansion.enriched.common.pixelFromSpOrDp
import com.swmansion.enriched.text.spans.EnrichedTextImageSpan
import com.swmansion.enriched.textinput.spans.EnrichedLineHeightSpan
import kotlin.math.ceil

Expand Down Expand Up @@ -165,6 +166,13 @@ object MeasurementStore {
val lineHeight = lineHeightFromProps(props)
val allowFontScaling = allowFontScalingFromProps(props)

// we need to update the image bounds, potentially clamping
// the width to the available one
(rawText as? Spannable)?.let { spannable ->
val imageSpans = spannable.getSpans(0, spannable.length, EnrichedTextImageSpan::class.java)
imageSpans.forEach { it.containerWidth = width.toInt() }
}

val measuredText: CharSequence =
if (lineHeight > 0f) {
val spannable = SpannableString(rawText)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,31 @@ class EnrichedTextInputView :
super.scrollTo(0, y)
}

override fun onMeasure(
widthMeasureSpec: Int,
heightMeasureSpec: Int,
) {
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)

if (widthMode != MeasureSpec.UNSPECIFIED) {
val availableWidth = widthSize - totalPaddingLeft - totalPaddingRight

if (availableWidth > 0) {
val spannable = text as? Spannable
val spans = spannable?.getSpans(0, spannable.length, EnrichedInputImageSpan::class.java)

// we need to update the image bounds, potentially clamping
// the width to the available one
spans?.forEach {
it.containerWidth = availableWidth
Comment thread
hejsztynx marked this conversation as resolved.
}
}
}

super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}

override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection? {
var inputConnection = super.onCreateInputConnection(outAttrs)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import com.facebook.yoga.YogaMeasureOutput
import com.swmansion.enriched.common.allowFontScalingFromProps
import com.swmansion.enriched.common.parser.EnrichedParser
import com.swmansion.enriched.common.pixelFromSpOrDp
import com.swmansion.enriched.textinput.spans.EnrichedInputImageSpan
import com.swmansion.enriched.textinput.spans.EnrichedLineHeightSpan
import com.swmansion.enriched.textinput.styles.HtmlStyle
import java.util.concurrent.ConcurrentHashMap
Expand Down Expand Up @@ -170,6 +171,13 @@ object MeasurementStore {
rawText
}

// we need to update the image bounds, potentially clamping
// the width to the available one
(text as? Spannable)?.let { spannable ->
val imageSpans = spannable.getSpans(0, spannable.length, EnrichedInputImageSpan::class.java)
imageSpans.forEach { it.containerWidth = width.toInt() }
}

val typeface = applyStyles(defaultView.typeface, fontStyle, fontWeight, fontFamily, context.assets)
val paintParams = PaintParams(typeface, fontSize)
val size = measure(width, text, paintParams)
Expand Down
5 changes: 5 additions & 0 deletions ios/interfaces/ImageAttachment.mm
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#import "ImageAttachment.h"
#import "extensions/ImageExtension.h"
#import "utils/AttachmentLayoutUtils.h"

// NSTextStorage frequently recreates NSTextAttachment objects during attribute
// invalidation (e.g. on every keystroke). Without this cache each recreation
Expand Down Expand Up @@ -49,6 +50,10 @@ - (CGRect)attachmentBoundsForTextContainer:(NSTextContainer *)textContainer
characterIndex:(NSUInteger)charIndex {
CGRect baseBounds = self.bounds;

baseBounds.size =
[AttachmentLayoutUtils clampedSize:baseBounds.size
toMaxWidth:textContainer.size.width];

if (!textContainer.layoutManager.textStorage ||
charIndex >= textContainer.layoutManager.textStorage.length) {
return baseBounds;
Expand Down
2 changes: 2 additions & 0 deletions ios/utils/AttachmentLayoutUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,6 @@
textView:(UITextView *)textView
config:(EnrichedConfig *)config;

+ (CGSize)clampedSize:(CGSize)size toMaxWidth:(CGFloat)maxWidth;

@end
11 changes: 10 additions & 1 deletion ios/utils/AttachmentLayoutUtils.mm
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,8 @@ + (CGRect)frameForAttachment:(ImageAttachment *)attachment
CGRect lineRect =
[layoutManager lineFragmentRectForGlyphAtIndex:glyphRange.location
effectiveRange:NULL];
CGSize attachmentSize = attachment.bounds.size;
CGSize attachmentSize = [self clampedSize:attachment.bounds.size
toMaxWidth:textContainer.size.width];

UIFont *font = [storage attribute:NSFontAttributeName
atIndex:range.location
Expand All @@ -141,4 +142,12 @@ + (CGRect)frameForAttachment:(ImageAttachment *)attachment
return CGRectIntegral(rect);
}

+ (CGSize)clampedSize:(CGSize)size toMaxWidth:(CGFloat)maxWidth {
if (size.width > maxWidth && maxWidth > 0) {
CGFloat scale = maxWidth / size.width;
return CGSizeMake(maxWidth, size.height * scale);
}
return size;
}

@end
Loading