diff --git a/richeditor-compose/build.gradle.kts b/richeditor-compose/build.gradle.kts index f6ba33f4..fb306253 100644 --- a/richeditor-compose/build.gradle.kts +++ b/richeditor-compose/build.gradle.kts @@ -50,11 +50,19 @@ kotlin { } android { - namespace = "com.mohamedrejeb.richeditor" + namespace = "com.mohamedrejeb.richeditor.compose" compileSdk = libs.versions.android.compileSdk.get().toInt() defaultConfig { minSdk = libs.versions.android.minSdk.get().toInt() consumerProguardFile("proguard-rules.pro") } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlin { + jvmToolchain(8) + } } \ No newline at end of file diff --git a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/model/RichParagraph.kt b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/model/RichParagraph.kt index 135788f8..73e96f84 100644 --- a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/model/RichParagraph.kt +++ b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/model/RichParagraph.kt @@ -3,6 +3,7 @@ package com.mohamedrejeb.richeditor.model import androidx.compose.ui.text.ParagraphStyle import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.style.* +import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.sp import com.mohamedrejeb.richeditor.model.RichParagraph.Type.Companion.startText import com.mohamedrejeb.richeditor.ui.test.getRichTextStyleTreeRepresentation @@ -16,6 +17,9 @@ internal class RichParagraph( var type: Type = Type.Default, ) { interface Type { + var startTextWidth: TextUnit + get() = 0.sp + set(_) {} val style: ParagraphStyle get() = ParagraphStyle() val startRichSpan: RichSpan get() = RichSpan(paragraph = RichParagraph(type = this)) @@ -26,26 +30,47 @@ internal class RichParagraph( object Default : Type class UnorderedList : Type { - override val style: ParagraphStyle = ParagraphStyle( - textIndent = TextIndent(firstLine = 20.sp, restLine = 38.sp), + + override var startTextWidth: TextUnit = 0.sp + set(value) { + field = value + style = getParagraphStyle() + } + + override var style: ParagraphStyle = getParagraphStyle() + + private fun getParagraphStyle() = ParagraphStyle( + textIndent = TextIndent(firstLine = (38 - startTextWidth.value).sp, restLine = 38.sp), lineHeight = 20.sp, ) - override val startRichSpan: RichSpan = RichSpan( + + override var startRichSpan: RichSpan = RichSpan( paragraph = RichParagraph(type = this), text = "• ", ) override val nextParagraphType: Type get() = UnorderedList() override fun copy(): Type = UnorderedList() + + } - data class OrderedList( + class OrderedList( val number: Int, ) : Type { - override val style: ParagraphStyle = ParagraphStyle( - textIndent = TextIndent(firstLine = 20.sp, restLine = 42.sp), + override var startTextWidth: TextUnit = 0.sp + set(value) { + field = value + style = getParagraphStyle() + } + + override var style: ParagraphStyle = getParagraphStyle() + + private fun getParagraphStyle() = ParagraphStyle( + textIndent = TextIndent(firstLine = (38 - startTextWidth.value).sp, restLine = 38.sp), lineHeight = 20.sp, ) + override val startRichSpan: RichSpan = RichSpan( paragraph = RichParagraph(type = this), text = "$number. ", diff --git a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/model/RichTextState.kt b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/model/RichTextState.kt index e7460943..2b07a456 100644 --- a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/model/RichTextState.kt +++ b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/model/RichTextState.kt @@ -13,6 +13,9 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TransformedText import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.style.TextIndent +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.sp import com.mohamedrejeb.richeditor.annotation.ExperimentalRichTextApi import com.mohamedrejeb.richeditor.model.RichParagraph.Type.Companion.startText import com.mohamedrejeb.richeditor.parser.html.RichTextStateHtmlParser @@ -64,8 +67,6 @@ class RichTextState internal constructor( internal var singleParagraphMode by mutableStateOf(false) - internal var readOnly by mutableStateOf(false) - internal var textLayoutResult: TextLayoutResult? by mutableStateOf(null) private set @@ -147,7 +148,7 @@ class RichTextState internal constructor( ) { richTextConfig = RichTextConfig( linkColor = if (linkColor.isSpecified) linkColor else richTextConfig.linkColor, - linkTextDecoration = if (linkTextDecoration != null) linkTextDecoration else richTextConfig.linkTextDecoration, + linkTextDecoration = linkTextDecoration ?: richTextConfig.linkTextDecoration, codeColor = if (codeColor.isSpecified) codeColor else richTextConfig.codeColor, codeBackgroundColor = if (codeBackgroundColor.isSpecified) codeBackgroundColor else richTextConfig.codeBackgroundColor, codeStrokeColor = if (codeStrokeColor.isSpecified) codeStrokeColor else richTextConfig.codeStrokeColor, @@ -517,8 +518,6 @@ class RichTextState internal constructor( * @param newTextFieldValue the new text field value. */ internal fun onTextFieldValueChange(newTextFieldValue: TextFieldValue) { - if (readOnly) return - tempTextFieldValue = newTextFieldValue if (tempTextFieldValue.text.length > textFieldValue.text.length) @@ -1795,8 +1794,41 @@ class RichTextState internal constructor( } } - internal fun onTextLayout(textLayoutResult: TextLayoutResult) { + internal fun onTextLayout( + textLayoutResult: TextLayoutResult, + density: Density, + ) { this.textLayoutResult = textLayoutResult + adjustRichParagraphLayout( + density = density, + ) + } + + private fun adjustRichParagraphLayout( + density: Density, + ) { + var isParagraphUpdated = false + + richParagraphList.toList().forEach { richParagraph -> + val type = richParagraph.type + if (!type.startRichSpan.textRange.collapsed) { + textLayoutResult?.let { textLayoutResult -> + val start = textLayoutResult.getHorizontalPosition(type.startRichSpan.textRange.min, true) + val end = textLayoutResult.getHorizontalPosition(type.startRichSpan.textRange.max, true) + val distanceSp = with(density) { + (end - start).toSp() + } + + if (type.startTextWidth != distanceSp) { + type.startTextWidth = distanceSp + isParagraphUpdated = true + } + } + } + } + + if (isParagraphUpdated) + updateTextFieldValue(textFieldValue) } internal fun getLinkByOffset(offset: Offset): String? { @@ -2019,6 +2051,7 @@ class RichTextState internal constructor( * * @return A copy of this [RichTextState]. */ + @OptIn(ExperimentalRichTextApi::class) fun copy(): RichTextState { val richParagraphList = richParagraphList.map { it.copy() } val richTextState = RichTextState(richParagraphList) diff --git a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/ui/BasicRichText.kt b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/ui/BasicRichText.kt index 0c871824..7c8fe057 100644 --- a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/ui/BasicRichText.kt +++ b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/ui/BasicRichText.kt @@ -11,6 +11,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.PointerIcon import androidx.compose.ui.input.pointer.pointerHoverIcon import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.text.TextStyle @@ -29,6 +30,7 @@ fun BasicRichText( minLines: Int = 1, inlineContent: Map = mapOf() ) { + val density = LocalDensity.current val uriHandler = LocalUriHandler.current val pointerIcon = remember { mutableStateOf(PointerIcon.Default) @@ -66,7 +68,10 @@ fun BasicRichText( }, style = style, onTextLayout = { - state.onTextLayout(it) + state.onTextLayout( + textLayoutResult = it, + density = density, + ) onTextLayout(it) }, overflow = overflow, diff --git a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/ui/BasicRichTextEditor.kt b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/ui/BasicRichTextEditor.kt index 05e32cc2..2c36628c 100644 --- a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/ui/BasicRichTextEditor.kt +++ b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/ui/BasicRichTextEditor.kt @@ -1,6 +1,5 @@ package com.mohamedrejeb.richeditor.ui -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.interaction.Interaction import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.PressInteraction @@ -12,24 +11,24 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.LocalTextStyle import androidx.compose.runtime.* import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.platform.ClipboardManager import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.text.* import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextIndent import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.sp +import com.mohamedrejeb.richeditor.model.RichParagraph import com.mohamedrejeb.richeditor.model.RichTextState import kotlinx.coroutines.CoroutineScope -import kotlin.time.ExperimentalTime /** @@ -213,10 +212,6 @@ internal fun BasicRichTextEditor( state.singleParagraphMode = singleParagraph } - LaunchedEffect(readOnly) { - state.readOnly = readOnly - } - if (!singleParagraph) { // Workaround for Android to fix a bug in BasicTextField where it doesn't select the correct text // when the text contains multiple paragraphs. @@ -242,6 +237,7 @@ internal fun BasicRichTextEditor( BasicTextField( value = state.textFieldValue, onValueChange = { + if (readOnly) return@BasicTextField if (it.text.length > maxLength) return@BasicTextField state.onTextFieldValueChange(it) }, @@ -282,7 +278,10 @@ internal fun BasicRichTextEditor( minLines = minLines, visualTransformation = state.visualTransformation, onTextLayout = { - state.onTextLayout(it) + state.onTextLayout( + textLayoutResult = it, + density = density, + ) onTextLayout(it) }, interactionSource = interactionSource, diff --git a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/utils/MutableListExt.kt b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/utils/MutableListExt.kt index a63912b3..587725db 100644 --- a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/utils/MutableListExt.kt +++ b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/utils/MutableListExt.kt @@ -1,6 +1,6 @@ package com.mohamedrejeb.richeditor.utils -internal inline fun MutableList.removeRange(start: Int, end: Int) { +internal fun MutableList.removeRange(start: Int, end: Int) { for (i in (end - 1) downTo start) { removeAt(i) } diff --git a/sample/common/src/commonMain/kotlin/com/mohamedrejeb/richeditor/sample/common/slack/SlackDemoContent.kt b/sample/common/src/commonMain/kotlin/com/mohamedrejeb/richeditor/sample/common/slack/SlackDemoContent.kt index 92a1c101..7517b6df 100644 --- a/sample/common/src/commonMain/kotlin/com/mohamedrejeb/richeditor/sample/common/slack/SlackDemoContent.kt +++ b/sample/common/src/commonMain/kotlin/com/mohamedrejeb/richeditor/sample/common/slack/SlackDemoContent.kt @@ -185,7 +185,6 @@ fun SlackDemoContent() { unfocusedIndicatorColor = Color.Transparent, placeholderColor = Color.White.copy(alpha = .6f), ), - maxLength = 10, modifier = Modifier .fillMaxWidth() .padding(8.dp)