Skip to content

Commit

Permalink
Merge pull request #222 from MohamedRejeb/1.x
Browse files Browse the repository at this point in the history
Fix Crash when setting maxLines in Text
  • Loading branch information
MohamedRejeb authored Mar 30, 2024
2 parents 964839a + 468d09a commit 3a825eb
Show file tree
Hide file tree
Showing 9 changed files with 105 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,10 @@ package com.mohamedrejeb.richeditor.annotation
" the future.",
level = RequiresOptIn.Level.WARNING
)
annotation class ExperimentalRichTextApi()
@Target(
AnnotationTarget.CLASS,
AnnotationTarget.FUNCTION,
AnnotationTarget.PROPERTY
)
@Retention(AnnotationRetention.BINARY)
annotation class ExperimentalRichTextApi
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.mohamedrejeb.richeditor.annotation

@RequiresOptIn(
level = RequiresOptIn.Level.ERROR,
message = "This is internal API that may change frequently and without warning."
)
@Target(
AnnotationTarget.CLASS,
AnnotationTarget.FUNCTION,
AnnotationTarget.PROPERTY
)
@Retention(AnnotationRetention.BINARY)
annotation class InternalRichTextApi
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.sp
import com.mohamedrejeb.richeditor.annotation.ExperimentalRichTextApi
import com.mohamedrejeb.richeditor.utils.fastForEachIndexed
import com.mohamedrejeb.richeditor.utils.getBoundingBoxes

@ExperimentalRichTextApi
interface RichSpanStyle {
val spanStyle: (RichTextConfig) -> SpanStyle

Expand Down Expand Up @@ -48,7 +50,7 @@ interface RichSpanStyle {
textRange: TextRange,
richTextConfig: RichTextConfig,
topPadding: Float,
startPadding: Float
startPadding: Float,
) = Unit

override val acceptNewTextInTheEdges: Boolean =
Expand Down Expand Up @@ -157,7 +159,7 @@ interface RichSpanStyle {
textRange: TextRange,
richTextConfig: RichTextConfig,
topPadding: Float,
startPadding: Float
startPadding: Float,
) = Unit

override val acceptNewTextInTheEdges: Boolean =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.isSpecified
import androidx.compose.ui.unit.isUnspecified
import com.mohamedrejeb.richeditor.parser.utils.MARK_BACKGROUND_COLOR
import com.mohamedrejeb.richeditor.parser.utils.SMALL_FONT_SIZE
import com.mohamedrejeb.richeditor.parser.utils.MarkBackgroundColor
import com.mohamedrejeb.richeditor.parser.utils.SmallFontSize
import com.mohamedrejeb.richeditor.utils.maxDecimals
import kotlin.math.roundToInt

Expand Down Expand Up @@ -43,51 +43,52 @@ internal object CssDecoder {
val cssStyleMap = mutableMapOf<String, String>()
val htmlTags = mutableListOf<String>()

if (spanStyle.color.isSpecified) {
if (spanStyle.color.isSpecified)
cssStyleMap["color"] = decodeColorToCss(spanStyle.color)
}

if (spanStyle.fontSize.isSpecified) {
if (spanStyle.fontSize == SMALL_FONT_SIZE) {
if (spanStyle.fontSize == SmallFontSize)
htmlTags.add("small")
} else {
else
decodeTextUnitToCss(spanStyle.fontSize)?.let { fontSize ->
cssStyleMap["font-size"] = fontSize
}
}
}

spanStyle.fontWeight?.let { fontWeight ->
if (fontWeight == FontWeight.Bold) {
if (fontWeight == FontWeight.Bold)
htmlTags.add("b")
} else {
else
cssStyleMap["font-weight"] = decodeFontWeightToCss(fontWeight)
}
}

spanStyle.fontStyle?.let { fontStyle ->
if (fontStyle == FontStyle.Italic) {
if (fontStyle == FontStyle.Italic)
htmlTags.add("i")
} else {
else
cssStyleMap["font-style"] = decodeFontStyleToCss(fontStyle)
}
}
if (spanStyle.letterSpacing.isSpecified) {

if (spanStyle.letterSpacing.isSpecified)
decodeTextUnitToCss(spanStyle.letterSpacing)?.let { letterSpacing ->
cssStyleMap["letter-spacing"] = letterSpacing
}
}

spanStyle.baselineShift?.let { baselineShift ->
when (baselineShift) {
BaselineShift.Subscript -> htmlTags.add("sub")
BaselineShift.Superscript -> htmlTags.add("sup")
else -> cssStyleMap["baseline-shift"] = decodeBaselineShiftToCss(baselineShift)
}
}

if (spanStyle.background.isSpecified) {
if (spanStyle.background == MARK_BACKGROUND_COLOR) {
if (spanStyle.background == MarkBackgroundColor)
htmlTags.add("mark")
} else {
else
cssStyleMap["background"] = decodeColorToCss(spanStyle.background)
}
}

spanStyle.textDecoration?.let { textDecoration ->
when (textDecoration) {
TextDecoration.Underline -> htmlTags.add("u")
Expand All @@ -99,8 +100,8 @@ internal object CssDecoder {

else -> cssStyleMap["text-decoration"] = decodeTextDecorationToCss(textDecoration)
}

}

spanStyle.shadow?.let { shadow ->
cssStyleMap["text-shadow"] = decodeTextShadowToCss(shadow)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@ import androidx.compose.ui.text.style.BaselineShift
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.em

internal val MARK_BACKGROUND_COLOR = Color.Yellow
internal val SMALL_FONT_SIZE = 0.8f.em
internal val MarkBackgroundColor = Color.Yellow
internal val SmallFontSize = 0.8f.em

internal val BoldSpanStyle = SpanStyle(fontWeight = FontWeight.Bold)
internal val ItalicSpanStyle = SpanStyle(fontStyle = FontStyle.Italic)
internal val UnderlineSpanStyle = SpanStyle(textDecoration = TextDecoration.Underline)
internal val StrikethroughSpanStyle = SpanStyle(textDecoration = TextDecoration.LineThrough)
internal val SubscriptSpanStyle = SpanStyle(baselineShift = BaselineShift.Subscript)
internal val SuperscriptSpanStyle = SpanStyle(baselineShift = BaselineShift.Superscript)
internal val MarkSpanStyle = SpanStyle(background = MARK_BACKGROUND_COLOR)
internal val SmallSpanStyle = SpanStyle(fontSize = SMALL_FONT_SIZE)
internal val MarkSpanStyle = SpanStyle(background = MarkBackgroundColor)
internal val SmallSpanStyle = SpanStyle(fontSize = SmallFontSize)
internal val H1SPanStyle = SpanStyle(fontSize = 2.em, fontWeight = FontWeight.Bold)
internal val H2SPanStyle = SpanStyle(fontSize = 1.5.em, fontWeight = FontWeight.Bold)
internal val H3SPanStyle = SpanStyle(fontSize = 1.17.em, fontWeight = FontWeight.Bold)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ package com.mohamedrejeb.richeditor.ui
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.text.TextRange
import com.mohamedrejeb.richeditor.annotation.ExperimentalRichTextApi
import com.mohamedrejeb.richeditor.model.RichSpanStyle
import com.mohamedrejeb.richeditor.model.RichTextState
import com.mohamedrejeb.richeditor.utils.fastForEach

@OptIn(ExperimentalRichTextApi::class)
internal fun Modifier.drawRichSpanStyle(
richTextState: RichTextState,
topPadding: Float = 0f,
Expand Down Expand Up @@ -42,9 +44,9 @@ internal fun Modifier.drawRichSpanStyle(
drawCustomStyle(
layoutResult = textLayoutResult,
textRange = textRange,
topPadding = topPadding,
startPadding = startPadding,
richTextConfig = richTextState.richTextConfig,
topPadding = topPadding,
startPadding = startPadding
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package com.mohamedrejeb.richeditor.utils

import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.style.ResolvedTextDirection
import com.mohamedrejeb.richeditor.annotation.ExperimentalRichTextApi
import com.mohamedrejeb.richeditor.annotation.InternalRichTextApi
import kotlin.math.max
import kotlin.math.min

Expand All @@ -19,24 +22,42 @@ import kotlin.math.min
* @param flattenForFullParagraphs whether to return bounds for entire paragraphs instead of separate lines.
* @return the list of bounds for the given range.
*/
@ExperimentalRichTextApi
fun TextLayoutResult.getBoundingBoxes(
startOffset: Int,
endOffset: Int,
flattenForFullParagraphs: Boolean = false
): List<Rect> {
if (multiParagraph.lineCount == 0)
return emptyList()

val lastLinePosition =
Offset(
x = multiParagraph.getLineRight(multiParagraph.lineCount - 1),
y = multiParagraph.getLineTop(multiParagraph.lineCount - 1)
)

val lastOffset = multiParagraph.getOffsetForPosition(lastLinePosition)

if (startOffset >= lastOffset)
return emptyList()

if (startOffset == endOffset)
return emptyList()

if (startOffset < 0 || endOffset > layoutInput.text.length)
if (startOffset < 0 || endOffset < 0 || endOffset > layoutInput.text.length)
return emptyList()

val startLineNum = getLineForOffset(min(startOffset, endOffset))
val endLineNum = getLineForOffset(max(startOffset, endOffset))
val start = min(startOffset, endOffset)
val end = min(max(start, endOffset), lastOffset)

val startLineNum = getLineForOffset(min(start, end))
val endLineNum = getLineForOffset(max(start, end))

if (flattenForFullParagraphs) {
val isFullParagraph = (startLineNum != endLineNum)
&& getLineStart(startLineNum) == startOffset
&& multiParagraph.getLineEnd(endLineNum, visibleEnd = true) == endOffset
&& getLineStart(startLineNum) == start
&& multiParagraph.getLineEnd(endLineNum, visibleEnd = true) == end

if (isFullParagraph) {
return listOf(
Expand All @@ -53,22 +74,36 @@ fun TextLayoutResult.getBoundingBoxes(
// Compose UI does not offer any API for reading paragraph direction for an entire line.
// So this code assumes that all paragraphs in the text will have the same direction.
// It also assumes that this paragraph does not contain bi-directional text.
val isLtr = multiParagraph.getParagraphDirection(offset = layoutInput.text.lastIndex) == ResolvedTextDirection.Ltr
val isLtr = multiParagraph.getParagraphDirection(offset = start) == ResolvedTextDirection.Ltr

return fastMapRange(startLineNum, endLineNum) { lineNum ->
val left =
if (lineNum == startLineNum)
getHorizontalPosition(
offset = start,
usePrimaryDirection = isLtr
)
else
getLineLeft(
lineIndex = lineNum
)

val right =
if (lineNum == endLineNum)
getHorizontalPosition(
offset = end,
usePrimaryDirection = isLtr
)
else
getLineRight(
lineIndex = lineNum
)

Rect(
top = getLineTop(lineNum),
bottom = getLineBottom(lineNum),
left = if (lineNum == startLineNum) {
getHorizontalPosition(startOffset, usePrimaryDirection = isLtr)
} else {
getLineLeft(lineNum)
},
right = if (lineNum == endLineNum) {
getHorizontalPosition(endOffset, usePrimaryDirection = isLtr)
} else {
getLineRight(lineNum)
}
left = left,
right = right,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import androidx.compose.ui.unit.dp
import com.mohamedrejeb.richeditor.model.rememberRichTextState
import com.mohamedrejeb.richeditor.ui.material3.RichText

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HtmlToRichText(
html: TextFieldValue,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.unit.dp
import com.mohamedrejeb.richeditor.annotation.ExperimentalRichTextApi
import com.mohamedrejeb.richeditor.model.RichSpanStyle
import com.mohamedrejeb.richeditor.model.RichTextConfig
import com.mohamedrejeb.richeditor.utils.fastForEachIndexed
import com.mohamedrejeb.richeditor.utils.getBoundingBoxes

@OptIn(ExperimentalRichTextApi::class)
object SpellCheck: RichSpanStyle {
override val spanStyle: (RichTextConfig) -> SpanStyle = {
SpanStyle()
Expand Down

0 comments on commit 3a825eb

Please # to comment.