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
Comment thread
relextm19 marked this conversation as resolved.
Resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package com.swmansion.enriched.common

import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Path
import android.graphics.Rect
import android.graphics.RectF
import android.text.Layout
import android.text.Spanned
import android.widget.TextView
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)
private val clipBounds = Rect()

fun clipToTextBounds(
canvas: Canvas,
view: TextView,
layout: Layout,
) {
val scrollX = view.scrollX
val scrollY = view.scrollY
val verticalSpace = view.height - view.compoundPaddingTop - view.compoundPaddingBottom
val maxScrollY = layout.height - verticalSpace

val clipLeft = (view.compoundPaddingLeft + scrollX).toFloat()
val clipTop = if (scrollY == 0) 0f else (view.extendedPaddingTop + scrollY).toFloat()
val clipRight = (view.width - view.compoundPaddingRight + scrollX).toFloat()
val clipBottom =
(view.height + scrollY - if (scrollY == maxScrollY) 0 else view.extendedPaddingBottom)
.toFloat()

canvas.clipRect(clipLeft, clipTop, clipRight, clipBottom)
}

// Must be called with the canvas translated to the layout origin and clipped to the visible text
// area before the text is drawn. Only lines inside the clip are 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
) {
if (!canvas.getClipBounds(clipBounds)) return

val firstVisibleLine = layout.getLineForVertical(clipBounds.top)
val lastVisibleLine = layout.getLineForVertical(clipBounds.bottom)
val visibleStart = layout.getLineStart(firstVisibleLine)
val visibleEnd = layout.getLineEnd(lastVisibleLine)

val spans = text.getSpans(visibleStart, visibleEnd, 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,
firstVisibleLine,
lastVisibleLine,
)
}
}

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

for (line in maxOf(firstLine, firstVisibleLine)..minOf(lastLine, lastVisibleLine)) {
val isFirst = line == firstLine
val isLast = line == lastLine

val segmentStart =
if (isFirst) layout.getPrimaryHorizontal(start) else layout.getLineLeft(line)
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,28 @@ 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()
MentionBackgroundDrawer.clipToTextBounds(canvas, this, currentLayout)
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,20 @@ 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()
MentionBackgroundDrawer.clipToTextBounds(canvas, this, currentLayout)
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
4 changes: 4 additions & 0 deletions docs/docs/api-reference/enriched-text-input.md
Original file line number Diff line number Diff line change
Expand Up @@ -1223,6 +1223,7 @@ interface MentionStyleProperties {
color?: ColorValue;
backgroundColor?: ColorValue;
textDecorationLine?: 'underline' | 'none';
borderRadius?: number;
}
```

Expand Down Expand Up @@ -1278,6 +1279,9 @@ should be a record with indicators as keys and configs as values.
[color](https://reactnative.dev/docs/colors) value, defaults to `yellow`
- `textDecorationLine` - whether mentions are underlined; `'underline'` or
`'none'`, defaults to `'underline'`
- `borderRadius` - corner radius of the mention background; defaults to `0`.
When a mention wraps onto multiple lines, only its outer corners (at the
start of the first line and the end of the last line) are rounded

:::tip

Expand Down
1 change: 1 addition & 0 deletions docs/docs/api-reference/enriched-text.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ interface EnrichedTextMentionStyleProperties {
color?: ColorValue;
backgroundColor?: ColorValue;
textDecorationLine?: 'underline' | 'none';
borderRadius?: number;
pressColor?: ColorValue;
pressBackgroundColor?: ColorValue;
}
Expand Down
22 changes: 11 additions & 11 deletions docs/docs/core-functionalities/styling-the-input.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -62,17 +62,17 @@ to their React Native `TextStyle` counterparts. Some are platform-limited
`htmlStyle` maps each supported element to a small config object. Anything you
omit falls back to the built-in default. The available keys are:

| Key | Styles | Notable options |
| ------------ | -------------- | ----------------------------------------------------------- |
| `h1`–`h6` | Headings | `fontSize`, `bold` |
| `blockquote` | Blockquote | `borderColor`, `borderWidth`, `gapWidth`, `color` |
| `codeblock` | Code block | `color`, `backgroundColor`, `borderRadius` |
| `code` | Inline code | `color`, `backgroundColor` |
| `a` | Links | `color`, `textDecorationLine` |
| `mention` | Mentions | `color`, `backgroundColor`, `textDecorationLine` |
| `ol` | Ordered list | `markerColor`, `markerFontWeight`, `marginLeft`, `gapWidth` |
| `ul` | Unordered list | `bulletColor`, `bulletSize`, `marginLeft`, `gapWidth` |
| `ulCheckbox` | Checkbox list | `boxColor`, `boxSize`, `marginLeft`, `gapWidth` |
| Key | Styles | Notable options |
| ------------ | -------------- | ---------------------------------------------------------------- |
| `h1`–`h6` | Headings | `fontSize`, `bold` |
| `blockquote` | Blockquote | `borderColor`, `borderWidth`, `gapWidth`, `color` |
| `codeblock` | Code block | `color`, `backgroundColor`, `borderRadius` |
| `code` | Inline code | `color`, `backgroundColor` |
| `a` | Links | `color`, `textDecorationLine` |
| `mention` | Mentions | `color`, `backgroundColor`, `textDecorationLine`, `borderRadius` |
| `ol` | Ordered list | `markerColor`, `markerFontWeight`, `marginLeft`, `gapWidth` |
| `ul` | Unordered list | `bulletColor`, `bulletSize`, `marginLeft`, `gapWidth` |
| `ulCheckbox` | Checkbox list | `boxColor`, `boxSize`, `marginLeft`, `gapWidth` |

The full list of properties, defaults, and platform notes lives in the
[`EnrichedTextInput`](/api-reference/enriched-text-input#htmlstyle) reference.
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
Loading
Loading