Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
Comment thread
relextm19 marked this conversation as resolved.
Resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package com.swmansion.enriched.common

import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Path
import android.graphics.RectF
import android.text.Layout
import android.text.Spanned
import com.swmansion.enriched.common.spans.EnrichedMentionSpan

// Mention backgrounds are drawn by the view instead of the span, because spans only get whole-line
// bounds and cannot round the corners of a single mention.
object MentionBackgroundDrawer {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val path = Path()
private val rect = RectF()
private val radii = FloatArray(8)

// Must be called with the canvas translated to the layout origin, before the text is drawn.
fun draw(
canvas: Canvas,
layout: Layout,
text: Spanned,
style: EnrichedStyle,
backgroundColorFor: (EnrichedMentionSpan, MentionStyle) -> Int = { _, mentionStyle ->
mentionStyle.backgroundColor
}, // lambda to handle press state bg color for enrichedText
) {
val spans = text.getSpans(0, text.length, EnrichedMentionSpan::class.java)
for (span in spans) {
val mentionStyle = style.mentionsStyle[span.getIndicator()] ?: continue
val color = backgroundColorFor(span, mentionStyle)
if (Color.alpha(color) == 0) continue

val start = text.getSpanStart(span)
val end = text.getSpanEnd(span)

paint.color = color
drawSpanBackground(canvas, layout, start, end, mentionStyle.borderRadius)
}
}

private fun drawSpanBackground(
canvas: Canvas,
layout: Layout,
start: Int,
end: Int,
radius: Float,
) {
val firstLine = layout.getLineForOffset(start)
val lastLine = layout.getLineForOffset(end - 1)

for (line in firstLine..lastLine) {
val isFirst = line == firstLine
val isLast = line == lastLine

val segmentStart =
if (isFirst) layout.getPrimaryHorizontal(start) else layout.getLineLeft(line)
Comment thread
relextm19 marked this conversation as resolved.
Resolved
val segmentEnd =
if (isLast) {
layout.getPrimaryHorizontal(end)
} else {
layout.getLineRight(line)
}

rect.set(
segmentStart,
layout.getLineTop(line).toFloat(),
segmentEnd,
layout.getLineBottom(line).toFloat(),
)

// Only round the outer ends of a wrapped mention.
val leftRadius = if (isFirst) radius else 0f
val rightRadius = if (isLast) radius else 0f
// top-left
radii[0] = leftRadius
radii[1] = leftRadius
// top-right
radii[2] = rightRadius
radii[3] = rightRadius
// bottom-right
radii[4] = rightRadius
radii[5] = rightRadius
// bottom-left
radii[6] = leftRadius
radii[7] = leftRadius

path.reset()
path.addRoundRect(rect, radii, Path.Direction.CW)
canvas.drawPath(path, paint)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ data class MentionStyle(
val color: Int,
val backgroundColor: Int,
val underline: Boolean,
val borderRadius: Float = 0f,
val pressColor: Int? = null,
val pressBackgroundColor: Int? = null,
)
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ open class EnrichedMentionSpan(

val mentionsStyle = enrichedStyle.mentionsStyle[indicator] ?: return
textPaint.color = mentionsStyle.color
textPaint.bgColor = mentionsStyle.backgroundColor
textPaint.isUnderlineText = mentionsStyle.underline
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ data class EnrichedTextStyle(
codeBlockRadius = parseFloat(codeblock, "borderRadius", allowFontScaling),
inlineCodeColor = parseColor(context, inlineCode, "color"),
inlineCodeBackgroundColor = parseColorWithOpacity(context, inlineCode, "backgroundColor"),
mentionsStyle = parseMentionsStyle(context, mentions),
mentionsStyle = parseMentionsStyle(context, mentions, allowFontScaling),
allowFontScaling = allowFontScaling,
)
}
Expand Down Expand Up @@ -183,6 +183,7 @@ data class EnrichedTextStyle(
private fun parseMentionsStyle(
context: ReactContext,
map: ReadableMap?,
allowFontScaling: Boolean,
): Map<String, MentionStyle> {
val result = mutableMapOf<String, MentionStyle>()
val iterator = map?.keySetIterator() ?: return result
Expand All @@ -195,6 +196,7 @@ data class EnrichedTextStyle(
color = parseColor(context, value, "color"),
backgroundColor = parseColorWithOpacity(context, value, "backgroundColor"),
underline = parseIsUnderline(value),
borderRadius = parseFloat(value, "borderRadius", allowFontScaling),
pressColor = parseColor(context, value, "pressColor"),
pressBackgroundColor = parseColorWithOpacity(context, value, "pressBackgroundColor"),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.swmansion.enriched.text
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.text.LineBreaker
import android.os.Build
Expand All @@ -25,6 +26,7 @@ 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.GumboNormalizer
import com.swmansion.enriched.common.MentionBackgroundDrawer
import com.swmansion.enriched.common.parser.EnrichedParser
import com.swmansion.enriched.common.pixelFromSpOrDp
import com.swmansion.enriched.text.spans.EnrichedTextImageSpan
Expand Down Expand Up @@ -92,6 +94,27 @@ class EnrichedTextView : AppCompatTextView {
setFontSize(EnrichedConstants.TEXT_DEFAULT_FONT_SIZE)
}

override fun onDraw(canvas: Canvas) {
val currentLayout = layout
val spanned = text as? Spanned
val style = enrichedStyle
if (currentLayout != null && spanned != null && style != null) {
canvas.save()
canvas.translate(totalPaddingLeft.toFloat(), totalPaddingTop.toFloat())
MentionBackgroundDrawer.draw(canvas, currentLayout, spanned, style) { span, mentionStyle ->
val isPressed = (span as? EnrichedTextClickableSpan)?.isPressed == true
if (isPressed && mentionStyle.pressBackgroundColor != null) {
mentionStyle.pressBackgroundColor
} else {
mentionStyle.backgroundColor
}
}
canvas.restore()
}

super.onDraw(canvas)
}

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 @@ -35,15 +35,7 @@ class EnrichedTextMentionSpan(
mentionsStyle.color
}

val bgColor =
if (isPressed && mentionsStyle.pressBackgroundColor != null) {
mentionsStyle.pressBackgroundColor
} else {
mentionsStyle.backgroundColor
}

textPaint.color = color
textPaint.bgColor = bgColor
}

override fun onClick(view: View) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import android.content.ClipboardManager
import android.content.Context
import android.graphics.BlendMode
import android.graphics.BlendModeColorFilter
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Rect
import android.graphics.text.LineBreaker
Expand Down Expand Up @@ -41,6 +42,7 @@ 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.GumboNormalizer
import com.swmansion.enriched.common.MentionBackgroundDrawer
import com.swmansion.enriched.common.parser.EnrichedParser
import com.swmansion.enriched.common.pixelFromSpOrDp
import com.swmansion.enriched.textinput.events.MentionHandler
Expand Down Expand Up @@ -293,6 +295,19 @@ class EnrichedTextInputView :
)
}

override fun onDraw(canvas: Canvas) {
Comment thread
relextm19 marked this conversation as resolved.
Resolved
val currentLayout = layout
val currentText = text
if (currentLayout != null && currentText != null) {
canvas.save()
canvas.translate(totalPaddingLeft.toFloat(), totalPaddingTop.toFloat())
MentionBackgroundDrawer.draw(canvas, currentLayout, currentText, htmlStyle)
Comment thread
relextm19 marked this conversation as resolved.
Resolved
canvas.restore()
}

super.onDraw(canvas)
}

// https://github.com/facebook/react-native/blob/36df97f500aa0aa8031098caf7526db358b6ddc1/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt#L295C1-L296C1
override fun onTouchEvent(ev: MotionEvent): Boolean {
when (ev.action) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,8 @@ class HtmlStyle : EnrichedStyle {
val color = parseColor(value, "color")
val backgroundColor = parseColorWithOpacity(value, "backgroundColor")
val isUnderline = parseIsUnderline(value)
val parsedStyle = MentionStyle(color, backgroundColor, isUnderline)
val borderRadius = parseFloat(value, "borderRadius")
val parsedStyle = MentionStyle(color, backgroundColor, isUnderline, borderRadius)
parsedMentionsStyle.put(key, parsedStyle)
}

Expand Down
1 change: 1 addition & 0 deletions apps/example-web/src/defaultHtmlStyle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export const WEB_DEFAULT_HTML_STYLE: HtmlStyle = {
backgroundColor: 'lightgreen',
color: 'green',
textDecorationLine: 'none',
borderRadius: 0,
},
'#': {
backgroundColor: 'lightblue',
Expand Down
1 change: 1 addition & 0 deletions apps/example/src/constants/editorConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export const htmlStyle = {
color: 'green',
backgroundColor: 'lightgreen',
textDecorationLine: 'none',
borderRadius: 0,
},
},
ol: {
Expand Down
1 change: 1 addition & 0 deletions apps/example/src/constants/htmlStyle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export const htmlStyle = {
color: 'green',
backgroundColor: 'lightgreen',
textDecorationLine: 'none',
borderRadius: 0,
},
},
ol: {
Expand Down
2 changes: 1 addition & 1 deletion ios/EnrichedTextInputView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -611,7 +611,7 @@ - (void)updateProps:(Props::Shared const &)props
bool newSingleProps = NO;

for (const auto &obj : newMentionStyle.items()) {
if (obj.second.isInt() || obj.second.isString()) {
if (obj.second.isNumber() || obj.second.isString()) {
// we are in just a single MentionStyleProps object
newSingleProps = YES;
break;
Expand Down
2 changes: 1 addition & 1 deletion ios/EnrichedTextView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -523,7 +523,7 @@ - (void)updateProps:(Props::Shared const &)props
bool newSingleProps = NO;

for (const auto &obj : newMentionStyle.items()) {
if (obj.second.isInt() || obj.second.isString()) {
if (obj.second.isNumber() || obj.second.isString()) {
newSingleProps = YES;
break;
} else if (obj.second.isObject()) {
Expand Down
87 changes: 87 additions & 0 deletions ios/extensions/LayoutManagerExtension.mm
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#import "LayoutManagerExtension.h"
#import "ColorExtension.h"
#import "interfaces/EnrichedViewHost.h"
#import "interfaces/MentionStyleProps.h"
#import "interfaces/StyleHeaders.h"
#import "utils/RangeUtils.h"
#import "utils/WeakBox.h"
Expand Down Expand Up @@ -60,6 +61,92 @@ - (void)my_drawBackgroundForGlyphRange:(NSRange)glyphRange
[self drawBlockQuotes:host origin:origin visibleCharRange:visibleCharRange];
[self drawLists:host origin:origin visibleCharRange:visibleCharRange];
[self drawCodeBlocks:host origin:origin visibleCharRange:visibleCharRange];
[self drawMentionBackgrounds:host
origin:origin
visibleCharRange:visibleCharRange];
}

- (void)drawMentionBackgrounds:(id<EnrichedViewHost>)host
origin:(CGPoint)origin
visibleCharRange:(NSRange)visibleCharRange {
NSTextStorage *textStorage = host.textView.textStorage;
NSRange wholeText = NSMakeRange(0, textStorage.length);
NSMutableArray<NSValue *> *ranges = [[NSMutableArray alloc] init];
[textStorage enumerateAttribute:@"EnrichedMention"
inRange:visibleCharRange
options:0
usingBlock:^(id _Nullable value, NSRange range,
BOOL *_Nonnull stop) {
if (value == nullptr || range.length == 0) {
return;
}
NSRange fullRange;
[textStorage attribute:@"EnrichedMention"
atIndex:range.location
longestEffectiveRange:&fullRange
inRange:wholeText];
[ranges addObject:[NSValue valueWithRange:fullRange]];
}];

for (NSValue *rangeValue in ranges) {
NSRange range = [rangeValue rangeValue];
UIColor *bgColor = [textStorage attribute:MentionBackgroundAttributeName
atIndex:range.location
effectiveRange:nullptr];
if (bgColor == nullptr || CGColorGetAlpha(bgColor.CGColor) == 0) {
continue;
}
MentionParams *params = [textStorage attribute:@"EnrichedMention"
atIndex:range.location
effectiveRange:nullptr];
MentionStyleProps *props =
[host.config mentionStylePropsForIndicator:params.indicator];
UIFont *font = [textStorage attribute:NSFontAttributeName
atIndex:range.location
effectiveRange:nullptr];
[bgColor setFill];

NSRange glyphRange = [self glyphRangeForCharacterRange:range
actualCharacterRange:nullptr];
// one rect per line in case the mention wraps
NSMutableArray<NSValue *> *lineRects = [[NSMutableArray alloc] init];
[self enumerateEnclosingRectsForGlyphRange:glyphRange
withinSelectedGlyphRange:NSMakeRange(NSNotFound, 0)
inTextContainer:host.textView.textContainer
usingBlock:^(CGRect rect, BOOL *stop) {
[lineRects
addObject:[NSValue
valueWithCGRect:rect]];
}];

// only the mention's start and end get rounded, so a wrapped mention
// still has 4 rounded corners in total
for (NSUInteger i = 0; i < lineRects.count; i++) {
CGRect bgRect = [self getTextAlignedUsedRect:[lineRects[i] CGRectValue]
font:font];
bgRect.origin.x += origin.x;
bgRect.origin.y += origin.y;

if (props.borderRadius <= 0) {
[[UIBezierPath bezierPathWithRect:bgRect] fill];
continue;
}

UIRectCorner corners = 0;
if (i == 0) {
corners = UIRectCornerTopLeft | UIRectCornerBottomLeft;
}
if (i == lineRects.count - 1) {
corners |= UIRectCornerTopRight | UIRectCornerBottomRight;
}

CGFloat radius = MIN(props.borderRadius, bgRect.size.height / 2);
[[UIBezierPath bezierPathWithRoundedRect:bgRect
byRoundingCorners:corners
cornerRadii:CGSizeMake(radius, radius)]
fill];
}
}
}

- (void)drawCodeBlocks:(id<EnrichedViewHost>)host
Expand Down
Loading
Loading