diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 972d219e..126d84f1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,11 +13,12 @@ nexus-publish = "2.0.0" # For sample androidx-appcompat = "1.7.0" activity-compose = "1.9.3" +symspellkt = "3.1.0" voyager = "1.1.0-beta03" richeditor = "1.0.0-rc10" coroutines = "1.9.0" ktor = "3.0.1" -android-minSdk = "21" +android-minSdk = "26" android-compileSdk = "34" [libraries] @@ -32,6 +33,8 @@ nexus-publish = { module = "io.github.gradle-nexus.publish-plugin:io.github.grad # For sample androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity-compose" } +symspellkt = { module = "com.darkrockstudios:symspellkt", version.ref = "symspellkt" } +symspellkt-fdic = { module = "com.darkrockstudios:symspellktfdic", version.ref = "symspellkt" } voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" } richeditor-compose = { module = "com.mohamedrejeb.richeditor:richeditor-compose", version.ref = "richeditor" } diff --git a/richeditor-compose-spellcheck/.gitignore b/richeditor-compose-spellcheck/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/richeditor-compose-spellcheck/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/richeditor-compose-spellcheck/api/android/richeditor-compose-spellcheck.api b/richeditor-compose-spellcheck/api/android/richeditor-compose-spellcheck.api new file mode 100644 index 00000000..a2f1a12b --- /dev/null +++ b/richeditor-compose-spellcheck/api/android/richeditor-compose-spellcheck.api @@ -0,0 +1,82 @@ +public final class com/mohamedrejeb/richeditor/compose/spellcheck/RememberSpellCheckStateKt { + public static final fun rememberSpellCheckState (Lcom/darkrockstudios/symspellkt/api/SpellChecker;Landroidx/compose/runtime/Composer;I)Lcom/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckState; +} + +public final class com/mohamedrejeb/richeditor/compose/spellcheck/SpellCheck : com/mohamedrejeb/richeditor/model/RichSpanStyle { + public static final field $stable I + public static final field INSTANCE Lcom/mohamedrejeb/richeditor/compose/spellcheck/SpellCheck; + public fun appendCustomContent (Landroidx/compose/ui/text/AnnotatedString$Builder;Lcom/mohamedrejeb/richeditor/model/RichTextState;)Landroidx/compose/ui/text/AnnotatedString$Builder; + public fun drawCustomStyle-zdrCDHg (Landroidx/compose/ui/graphics/drawscope/DrawScope;Landroidx/compose/ui/text/TextLayoutResult;JLcom/mohamedrejeb/richeditor/model/RichTextConfig;FF)V + public fun getAcceptNewTextInTheEdges ()Z + public fun getSpanStyle ()Lkotlin/jvm/functions/Function1; +} + +public final class com/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckMenuState { + public static final field $stable I + public fun (Lcom/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckState;)V + public final fun clearSpellCheck ()V + public final fun component1 ()Lcom/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckState; + public final fun copy (Lcom/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckState;)Lcom/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckMenuState; + public static synthetic fun copy$default (Lcom/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckMenuState;Lcom/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckState;ILjava/lang/Object;)Lcom/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckMenuState; + public fun equals (Ljava/lang/Object;)Z + public final fun getMissSpelling ()Landroidx/compose/runtime/MutableState; + public final fun getSpellCheckState ()Lcom/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckState; + public fun hashCode ()I + public final fun performCorrection (Lcom/mohamedrejeb/richeditor/utils/WordSegment;Ljava/lang/String;)V + public fun toString ()Ljava/lang/String; +} + +public final class com/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckMenuState$MissSpelling { + public static final field $stable I + public synthetic fun (Lcom/mohamedrejeb/richeditor/utils/WordSegment;JLkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lcom/mohamedrejeb/richeditor/utils/WordSegment; + public final fun component2-F1C5BW0 ()J + public final fun copy-Uv8p0NA (Lcom/mohamedrejeb/richeditor/utils/WordSegment;J)Lcom/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckMenuState$MissSpelling; + public static synthetic fun copy-Uv8p0NA$default (Lcom/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckMenuState$MissSpelling;Lcom/mohamedrejeb/richeditor/utils/WordSegment;JILjava/lang/Object;)Lcom/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckMenuState$MissSpelling; + public fun equals (Ljava/lang/Object;)Z + public final fun getMenuPosition-F1C5BW0 ()J + public final fun getWordSegment ()Lcom/mohamedrejeb/richeditor/utils/WordSegment; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckState { + public static final field $stable I + public fun (Lcom/mohamedrejeb/richeditor/model/RichTextState;Lcom/darkrockstudios/symspellkt/api/SpellChecker;)V + public final fun correctSpelling (Lcom/mohamedrejeb/richeditor/utils/WordSegment;Ljava/lang/String;)V + public final fun getRichTextState ()Lcom/mohamedrejeb/richeditor/model/RichTextState; + public final fun getSpellChecker ()Lcom/darkrockstudios/symspellkt/api/SpellChecker; + public final fun getSuggestions (Ljava/lang/String;)Ljava/util/List; + public final fun handleSpanClick-AXecNCo (Lcom/mohamedrejeb/richeditor/model/RichSpanStyle;JJ)Lcom/mohamedrejeb/richeditor/utils/WordSegment; + public final fun onTextChange (Lcom/mohamedrejeb/richeditor/model/RichTextState;)V + public final fun runSpellCheck ()V + public final fun setSpellChecker (Lcom/darkrockstudios/symspellkt/api/SpellChecker;)V +} + +public final class com/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckTextContextMenuProvider_androidKt { + public static final fun SpellCheckTextContextMenuProvider (Landroidx/compose/ui/Modifier;Lcom/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckMenuState;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;I)V +} + +public final class com/mohamedrejeb/richeditor/compose/spellcheck/ui/ComposableSingletons$SpellCheckDropdownKt { + public static final field INSTANCE Lcom/mohamedrejeb/richeditor/compose/spellcheck/ui/ComposableSingletons$SpellCheckDropdownKt; + public static field lambda-1 Lkotlin/jvm/functions/Function2; + public fun ()V + public final fun getLambda-1$richeditor_compose_spellcheck_release ()Lkotlin/jvm/functions/Function2; +} + +public final class com/mohamedrejeb/richeditor/compose/spellcheck/ui/ComposableSingletons$SpellCheckedRichTextEditorKt { + public static final field INSTANCE Lcom/mohamedrejeb/richeditor/compose/spellcheck/ui/ComposableSingletons$SpellCheckedRichTextEditorKt; + public static field lambda-1 Lkotlin/jvm/functions/Function3; + public fun ()V + public final fun getLambda-1$richeditor_compose_spellcheck_release ()Lkotlin/jvm/functions/Function3; +} + +public final class com/mohamedrejeb/richeditor/compose/spellcheck/ui/SpellCheckedRichTextEditorKt { + public static final fun SpellCheckedRichTextEditor (Landroidx/compose/ui/Modifier;ZLandroidx/compose/ui/text/TextStyle;Landroidx/compose/foundation/text/KeyboardOptions;Landroidx/compose/foundation/text/KeyboardActions;ZIIILkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function1;Landroidx/compose/foundation/interaction/MutableInteractionSource;Landroidx/compose/ui/graphics/Brush;Lcom/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckState;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;III)V +} + +public final class com/mohamedrejeb/richeditor/compose/spellcheck/utils/SpellCheckUtilsKt { + public static final fun isSpelledCorrectly (Ljava/lang/String;Ljava/util/List;)Z + public static final fun spellingIsCorrect (Ljava/util/List;Ljava/lang/String;)Z +} + diff --git a/richeditor-compose-spellcheck/api/desktop/richeditor-compose-spellcheck.api b/richeditor-compose-spellcheck/api/desktop/richeditor-compose-spellcheck.api new file mode 100644 index 00000000..3fe4521e --- /dev/null +++ b/richeditor-compose-spellcheck/api/desktop/richeditor-compose-spellcheck.api @@ -0,0 +1,82 @@ +public final class com/mohamedrejeb/richeditor/compose/spellcheck/RememberSpellCheckStateKt { + public static final fun rememberSpellCheckState (Lcom/darkrockstudios/symspellkt/api/SpellChecker;Landroidx/compose/runtime/Composer;I)Lcom/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckState; +} + +public final class com/mohamedrejeb/richeditor/compose/spellcheck/SpellCheck : com/mohamedrejeb/richeditor/model/RichSpanStyle { + public static final field $stable I + public static final field INSTANCE Lcom/mohamedrejeb/richeditor/compose/spellcheck/SpellCheck; + public fun appendCustomContent (Landroidx/compose/ui/text/AnnotatedString$Builder;Lcom/mohamedrejeb/richeditor/model/RichTextState;)Landroidx/compose/ui/text/AnnotatedString$Builder; + public fun drawCustomStyle-zdrCDHg (Landroidx/compose/ui/graphics/drawscope/DrawScope;Landroidx/compose/ui/text/TextLayoutResult;JLcom/mohamedrejeb/richeditor/model/RichTextConfig;FF)V + public fun getAcceptNewTextInTheEdges ()Z + public fun getSpanStyle ()Lkotlin/jvm/functions/Function1; +} + +public final class com/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckMenuState { + public static final field $stable I + public fun (Lcom/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckState;)V + public final fun clearSpellCheck ()V + public final fun component1 ()Lcom/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckState; + public final fun copy (Lcom/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckState;)Lcom/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckMenuState; + public static synthetic fun copy$default (Lcom/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckMenuState;Lcom/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckState;ILjava/lang/Object;)Lcom/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckMenuState; + public fun equals (Ljava/lang/Object;)Z + public final fun getMissSpelling ()Landroidx/compose/runtime/MutableState; + public final fun getSpellCheckState ()Lcom/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckState; + public fun hashCode ()I + public final fun performCorrection (Lcom/mohamedrejeb/richeditor/utils/WordSegment;Ljava/lang/String;)V + public fun toString ()Ljava/lang/String; +} + +public final class com/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckMenuState$MissSpelling { + public static final field $stable I + public synthetic fun (Lcom/mohamedrejeb/richeditor/utils/WordSegment;JLkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lcom/mohamedrejeb/richeditor/utils/WordSegment; + public final fun component2-F1C5BW0 ()J + public final fun copy-Uv8p0NA (Lcom/mohamedrejeb/richeditor/utils/WordSegment;J)Lcom/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckMenuState$MissSpelling; + public static synthetic fun copy-Uv8p0NA$default (Lcom/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckMenuState$MissSpelling;Lcom/mohamedrejeb/richeditor/utils/WordSegment;JILjava/lang/Object;)Lcom/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckMenuState$MissSpelling; + public fun equals (Ljava/lang/Object;)Z + public final fun getMenuPosition-F1C5BW0 ()J + public final fun getWordSegment ()Lcom/mohamedrejeb/richeditor/utils/WordSegment; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckState { + public static final field $stable I + public fun (Lcom/mohamedrejeb/richeditor/model/RichTextState;Lcom/darkrockstudios/symspellkt/api/SpellChecker;)V + public final fun correctSpelling (Lcom/mohamedrejeb/richeditor/utils/WordSegment;Ljava/lang/String;)V + public final fun getRichTextState ()Lcom/mohamedrejeb/richeditor/model/RichTextState; + public final fun getSpellChecker ()Lcom/darkrockstudios/symspellkt/api/SpellChecker; + public final fun getSuggestions (Ljava/lang/String;)Ljava/util/List; + public final fun handleSpanClick-AXecNCo (Lcom/mohamedrejeb/richeditor/model/RichSpanStyle;JJ)Lcom/mohamedrejeb/richeditor/utils/WordSegment; + public final fun onTextChange (Lcom/mohamedrejeb/richeditor/model/RichTextState;)V + public final fun runSpellCheck ()V + public final fun setSpellChecker (Lcom/darkrockstudios/symspellkt/api/SpellChecker;)V +} + +public final class com/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckTextContextMenuProvider_desktopKt { + public static final fun SpellCheckTextContextMenuProvider (Landroidx/compose/ui/Modifier;Lcom/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckMenuState;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;I)V +} + +public final class com/mohamedrejeb/richeditor/compose/spellcheck/ui/ComposableSingletons$SpellCheckDropdownKt { + public static final field INSTANCE Lcom/mohamedrejeb/richeditor/compose/spellcheck/ui/ComposableSingletons$SpellCheckDropdownKt; + public static field lambda-1 Lkotlin/jvm/functions/Function2; + public fun ()V + public final fun getLambda-1$richeditor_compose_spellcheck ()Lkotlin/jvm/functions/Function2; +} + +public final class com/mohamedrejeb/richeditor/compose/spellcheck/ui/ComposableSingletons$SpellCheckedRichTextEditorKt { + public static final field INSTANCE Lcom/mohamedrejeb/richeditor/compose/spellcheck/ui/ComposableSingletons$SpellCheckedRichTextEditorKt; + public static field lambda-1 Lkotlin/jvm/functions/Function3; + public fun ()V + public final fun getLambda-1$richeditor_compose_spellcheck ()Lkotlin/jvm/functions/Function3; +} + +public final class com/mohamedrejeb/richeditor/compose/spellcheck/ui/SpellCheckedRichTextEditorKt { + public static final fun SpellCheckedRichTextEditor (Landroidx/compose/ui/Modifier;ZLandroidx/compose/ui/text/TextStyle;Landroidx/compose/foundation/text/KeyboardOptions;Landroidx/compose/foundation/text/KeyboardActions;ZIIILkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function1;Landroidx/compose/foundation/interaction/MutableInteractionSource;Landroidx/compose/ui/graphics/Brush;Lcom/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckState;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;III)V +} + +public final class com/mohamedrejeb/richeditor/compose/spellcheck/utils/SpellCheckUtilsKt { + public static final fun isSpelledCorrectly (Ljava/lang/String;Ljava/util/List;)Z + public static final fun spellingIsCorrect (Ljava/util/List;Ljava/lang/String;)Z +} + diff --git a/richeditor-compose-spellcheck/api/richeditor-compose-spellcheck.klib.api b/richeditor-compose-spellcheck/api/richeditor-compose-spellcheck.klib.api new file mode 100644 index 00000000..b2e1931a --- /dev/null +++ b/richeditor-compose-spellcheck/api/richeditor-compose-spellcheck.klib.api @@ -0,0 +1,81 @@ +// Klib ABI Dump +// Targets: [iosArm64, iosSimulatorArm64, iosX64, js, wasmJs] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: true +// - Show declarations: true + +// Library unique name: +final class com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheckMenuState { // com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheckMenuState|null[0] + constructor (com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheckState) // com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheckMenuState.|(com.mohamedrejeb.richeditor.compose.spellcheck.SpellCheckState){}[0] + + final val missSpelling // com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheckMenuState.missSpelling|{}missSpelling[0] + final fun (): androidx.compose.runtime/MutableState // com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheckMenuState.missSpelling.|(){}[0] + final val spellCheckState // com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheckMenuState.spellCheckState|{}spellCheckState[0] + final fun (): com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheckState // com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheckMenuState.spellCheckState.|(){}[0] + + final fun clearSpellCheck() // com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheckMenuState.clearSpellCheck|clearSpellCheck(){}[0] + final fun component1(): com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheckState // com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheckMenuState.component1|component1(){}[0] + final fun copy(com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheckState = ...): com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheckMenuState // com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheckMenuState.copy|copy(com.mohamedrejeb.richeditor.compose.spellcheck.SpellCheckState){}[0] + final fun equals(kotlin/Any?): kotlin/Boolean // com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheckMenuState.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheckMenuState.hashCode|hashCode(){}[0] + final fun performCorrection(com.mohamedrejeb.richeditor.utils/WordSegment, kotlin/String) // com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheckMenuState.performCorrection|performCorrection(com.mohamedrejeb.richeditor.utils.WordSegment;kotlin.String){}[0] + final fun toString(): kotlin/String // com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheckMenuState.toString|toString(){}[0] + + final class MissSpelling { // com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheckMenuState.MissSpelling|null[0] + constructor (com.mohamedrejeb.richeditor.utils/WordSegment, androidx.compose.ui.geometry/Offset) // com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheckMenuState.MissSpelling.|(com.mohamedrejeb.richeditor.utils.WordSegment;androidx.compose.ui.geometry.Offset){}[0] + + final val menuPosition // com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheckMenuState.MissSpelling.menuPosition|{}menuPosition[0] + final fun (): androidx.compose.ui.geometry/Offset // com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheckMenuState.MissSpelling.menuPosition.|(){}[0] + final val wordSegment // com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheckMenuState.MissSpelling.wordSegment|{}wordSegment[0] + final fun (): com.mohamedrejeb.richeditor.utils/WordSegment // com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheckMenuState.MissSpelling.wordSegment.|(){}[0] + + final fun component1(): com.mohamedrejeb.richeditor.utils/WordSegment // com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheckMenuState.MissSpelling.component1|component1(){}[0] + final fun component2(): androidx.compose.ui.geometry/Offset // com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheckMenuState.MissSpelling.component2|component2(){}[0] + final fun copy(com.mohamedrejeb.richeditor.utils/WordSegment = ..., androidx.compose.ui.geometry/Offset = ...): com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheckMenuState.MissSpelling // com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheckMenuState.MissSpelling.copy|copy(com.mohamedrejeb.richeditor.utils.WordSegment;androidx.compose.ui.geometry.Offset){}[0] + final fun equals(kotlin/Any?): kotlin/Boolean // com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheckMenuState.MissSpelling.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheckMenuState.MissSpelling.hashCode|hashCode(){}[0] + final fun toString(): kotlin/String // com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheckMenuState.MissSpelling.toString|toString(){}[0] + } +} + +final class com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheckState { // com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheckState|null[0] + constructor (com.mohamedrejeb.richeditor.model/RichTextState, com.darkrockstudios.symspellkt.api/SpellChecker?) // com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheckState.|(com.mohamedrejeb.richeditor.model.RichTextState;com.darkrockstudios.symspellkt.api.SpellChecker?){}[0] + + final val richTextState // com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheckState.richTextState|{}richTextState[0] + final fun (): com.mohamedrejeb.richeditor.model/RichTextState // com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheckState.richTextState.|(){}[0] + + final var spellChecker // com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheckState.spellChecker|{}spellChecker[0] + final fun (): com.darkrockstudios.symspellkt.api/SpellChecker? // com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheckState.spellChecker.|(){}[0] + final fun (com.darkrockstudios.symspellkt.api/SpellChecker?) // com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheckState.spellChecker.|(com.darkrockstudios.symspellkt.api.SpellChecker?){}[0] + + final fun correctSpelling(com.mohamedrejeb.richeditor.utils/WordSegment, kotlin/String) // com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheckState.correctSpelling|correctSpelling(com.mohamedrejeb.richeditor.utils.WordSegment;kotlin.String){}[0] + final fun getSuggestions(kotlin/String): kotlin.collections/List // com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheckState.getSuggestions|getSuggestions(kotlin.String){}[0] + final fun handleSpanClick(com.mohamedrejeb.richeditor.model/RichSpanStyle, androidx.compose.ui.text/TextRange, androidx.compose.ui.geometry/Offset): com.mohamedrejeb.richeditor.utils/WordSegment? // com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheckState.handleSpanClick|handleSpanClick(com.mohamedrejeb.richeditor.model.RichSpanStyle;androidx.compose.ui.text.TextRange;androidx.compose.ui.geometry.Offset){}[0] + final fun onTextChange(com.mohamedrejeb.richeditor.model/RichTextState) // com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheckState.onTextChange|onTextChange(com.mohamedrejeb.richeditor.model.RichTextState){}[0] + final fun runSpellCheck() // com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheckState.runSpellCheck|runSpellCheck(){}[0] +} + +final object com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheck : com.mohamedrejeb.richeditor.model/RichSpanStyle { // com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheck|null[0] + final val acceptNewTextInTheEdges // com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheck.acceptNewTextInTheEdges|{}acceptNewTextInTheEdges[0] + final fun (): kotlin/Boolean // com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheck.acceptNewTextInTheEdges.|(){}[0] + final val spanStyle // com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheck.spanStyle|{}spanStyle[0] + final fun (): kotlin/Function1 // com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheck.spanStyle.|(){}[0] + + final fun (androidx.compose.ui.graphics.drawscope/DrawScope).drawCustomStyle(androidx.compose.ui.text/TextLayoutResult, androidx.compose.ui.text/TextRange, com.mohamedrejeb.richeditor.model/RichTextConfig, kotlin/Float, kotlin/Float) // com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheck.drawCustomStyle|drawCustomStyle@androidx.compose.ui.graphics.drawscope.DrawScope(androidx.compose.ui.text.TextLayoutResult;androidx.compose.ui.text.TextRange;com.mohamedrejeb.richeditor.model.RichTextConfig;kotlin.Float;kotlin.Float){}[0] +} + +final val com.mohamedrejeb.richeditor.compose.spellcheck/com_mohamedrejeb_richeditor_compose_spellcheck_SpellCheck$stableprop // com.mohamedrejeb.richeditor.compose.spellcheck/com_mohamedrejeb_richeditor_compose_spellcheck_SpellCheck$stableprop|#static{}com_mohamedrejeb_richeditor_compose_spellcheck_SpellCheck$stableprop[0] +final val com.mohamedrejeb.richeditor.compose.spellcheck/com_mohamedrejeb_richeditor_compose_spellcheck_SpellCheckMenuState$stableprop // com.mohamedrejeb.richeditor.compose.spellcheck/com_mohamedrejeb_richeditor_compose_spellcheck_SpellCheckMenuState$stableprop|#static{}com_mohamedrejeb_richeditor_compose_spellcheck_SpellCheckMenuState$stableprop[0] +final val com.mohamedrejeb.richeditor.compose.spellcheck/com_mohamedrejeb_richeditor_compose_spellcheck_SpellCheckMenuState_MissSpelling$stableprop // com.mohamedrejeb.richeditor.compose.spellcheck/com_mohamedrejeb_richeditor_compose_spellcheck_SpellCheckMenuState_MissSpelling$stableprop|#static{}com_mohamedrejeb_richeditor_compose_spellcheck_SpellCheckMenuState_MissSpelling$stableprop[0] +final val com.mohamedrejeb.richeditor.compose.spellcheck/com_mohamedrejeb_richeditor_compose_spellcheck_SpellCheckState$stableprop // com.mohamedrejeb.richeditor.compose.spellcheck/com_mohamedrejeb_richeditor_compose_spellcheck_SpellCheckState$stableprop|#static{}com_mohamedrejeb_richeditor_compose_spellcheck_SpellCheckState$stableprop[0] + +final fun (kotlin.collections/List).com.mohamedrejeb.richeditor.compose.spellcheck.utils/spellingIsCorrect(kotlin/String): kotlin/Boolean // com.mohamedrejeb.richeditor.compose.spellcheck.utils/spellingIsCorrect|spellingIsCorrect@kotlin.collections.List(kotlin.String){}[0] +final fun (kotlin/String).com.mohamedrejeb.richeditor.compose.spellcheck.utils/isSpelledCorrectly(kotlin.collections/List): kotlin/Boolean // com.mohamedrejeb.richeditor.compose.spellcheck.utils/isSpelledCorrectly|isSpelledCorrectly@kotlin.String(kotlin.collections.List){}[0] +final fun com.mohamedrejeb.richeditor.compose.spellcheck.ui/SpellCheckedRichTextEditor(androidx.compose.ui/Modifier?, kotlin/Boolean, androidx.compose.ui.text/TextStyle?, androidx.compose.foundation.text/KeyboardOptions?, androidx.compose.foundation.text/KeyboardActions?, kotlin/Boolean, kotlin/Int, kotlin/Int, kotlin/Int, kotlin/Function4?, kotlin/Function1?, androidx.compose.foundation.interaction/MutableInteractionSource?, androidx.compose.ui.graphics/Brush?, com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheckState?, kotlin/Function3, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // com.mohamedrejeb.richeditor.compose.spellcheck.ui/SpellCheckedRichTextEditor|SpellCheckedRichTextEditor(androidx.compose.ui.Modifier?;kotlin.Boolean;androidx.compose.ui.text.TextStyle?;androidx.compose.foundation.text.KeyboardOptions?;androidx.compose.foundation.text.KeyboardActions?;kotlin.Boolean;kotlin.Int;kotlin.Int;kotlin.Int;kotlin.Function4?;kotlin.Function1?;androidx.compose.foundation.interaction.MutableInteractionSource?;androidx.compose.ui.graphics.Brush?;com.mohamedrejeb.richeditor.compose.spellcheck.SpellCheckState?;kotlin.Function3,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0] +final fun com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheckTextContextMenuProvider(androidx.compose.ui/Modifier, com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheckMenuState, kotlin/Function2, androidx.compose.runtime/Composer?, kotlin/Int) // com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheckTextContextMenuProvider|SpellCheckTextContextMenuProvider(androidx.compose.ui.Modifier;com.mohamedrejeb.richeditor.compose.spellcheck.SpellCheckMenuState;kotlin.Function2;androidx.compose.runtime.Composer?;kotlin.Int){}[0] +final fun com.mohamedrejeb.richeditor.compose.spellcheck/com_mohamedrejeb_richeditor_compose_spellcheck_SpellCheck$stableprop_getter(): kotlin/Int // com.mohamedrejeb.richeditor.compose.spellcheck/com_mohamedrejeb_richeditor_compose_spellcheck_SpellCheck$stableprop_getter|com_mohamedrejeb_richeditor_compose_spellcheck_SpellCheck$stableprop_getter(){}[0] +final fun com.mohamedrejeb.richeditor.compose.spellcheck/com_mohamedrejeb_richeditor_compose_spellcheck_SpellCheckMenuState$stableprop_getter(): kotlin/Int // com.mohamedrejeb.richeditor.compose.spellcheck/com_mohamedrejeb_richeditor_compose_spellcheck_SpellCheckMenuState$stableprop_getter|com_mohamedrejeb_richeditor_compose_spellcheck_SpellCheckMenuState$stableprop_getter(){}[0] +final fun com.mohamedrejeb.richeditor.compose.spellcheck/com_mohamedrejeb_richeditor_compose_spellcheck_SpellCheckMenuState_MissSpelling$stableprop_getter(): kotlin/Int // com.mohamedrejeb.richeditor.compose.spellcheck/com_mohamedrejeb_richeditor_compose_spellcheck_SpellCheckMenuState_MissSpelling$stableprop_getter|com_mohamedrejeb_richeditor_compose_spellcheck_SpellCheckMenuState_MissSpelling$stableprop_getter(){}[0] +final fun com.mohamedrejeb.richeditor.compose.spellcheck/com_mohamedrejeb_richeditor_compose_spellcheck_SpellCheckState$stableprop_getter(): kotlin/Int // com.mohamedrejeb.richeditor.compose.spellcheck/com_mohamedrejeb_richeditor_compose_spellcheck_SpellCheckState$stableprop_getter|com_mohamedrejeb_richeditor_compose_spellcheck_SpellCheckState$stableprop_getter(){}[0] +final fun com.mohamedrejeb.richeditor.compose.spellcheck/rememberSpellCheckState(com.darkrockstudios.symspellkt.api/SpellChecker?, androidx.compose.runtime/Composer?, kotlin/Int): com.mohamedrejeb.richeditor.compose.spellcheck/SpellCheckState // com.mohamedrejeb.richeditor.compose.spellcheck/rememberSpellCheckState|rememberSpellCheckState(com.darkrockstudios.symspellkt.api.SpellChecker?;androidx.compose.runtime.Composer?;kotlin.Int){}[0] diff --git a/richeditor-compose-spellcheck/build.gradle.kts b/richeditor-compose-spellcheck/build.gradle.kts new file mode 100644 index 00000000..cd6d5072 --- /dev/null +++ b/richeditor-compose-spellcheck/build.gradle.kts @@ -0,0 +1,84 @@ +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.compose.compiler) + alias(libs.plugins.composeMultiplatform) + alias(libs.plugins.androidLibrary) + alias(libs.plugins.bcv) + id("module.publication") +} + +kotlin { + explicitApi() + applyDefaultHierarchyTemplate() + + androidTarget { + publishLibraryVariants("release") + @OptIn(ExperimentalKotlinGradlePluginApi::class) + compilerOptions { + jvmTarget.set(JvmTarget.JVM_1_8) + } + } + + jvm("desktop") { + @OptIn(ExperimentalKotlinGradlePluginApi::class) + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) + } + } + + js(IR) { + browser() + } + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + browser { + testTask { + enabled = false + } + } + } + + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets.commonMain.dependencies { + implementation(projects.richeditorCompose) + + implementation(compose.ui) + implementation(compose.foundation) + implementation(compose.material3) + + // Spell Check + api(libs.symspellkt) + } + + sourceSets.commonTest.dependencies { + implementation(kotlin("test")) + } +} + +android { + namespace = "com.mohamedrejeb.richeditor.compose.spellcheck" + compileSdk = libs.versions.android.compileSdk.get().toInt() + + defaultConfig { + minSdk = libs.versions.android.minSdk.get().toInt() + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } +} + +apiValidation { + @OptIn(kotlinx.validation.ExperimentalBCVApi::class) + klib { + enabled = true + } +} \ No newline at end of file diff --git a/richeditor-compose-spellcheck/proguard-rules.pro b/richeditor-compose-spellcheck/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/richeditor-compose-spellcheck/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/sample/common/src/commonMain/kotlin/com/mohamedrejeb/richeditor/sample/common/richeditor/SpellCheckSpan.kt b/richeditor-compose-spellcheck/src/commonMain/kotlin/com/mohamedrejeb/richeditor/compose/spellcheck/SpellCheck.kt similarity index 60% rename from sample/common/src/commonMain/kotlin/com/mohamedrejeb/richeditor/sample/common/richeditor/SpellCheckSpan.kt rename to richeditor-compose-spellcheck/src/commonMain/kotlin/com/mohamedrejeb/richeditor/compose/spellcheck/SpellCheck.kt index c8a1c048..90e9869c 100644 --- a/sample/common/src/commonMain/kotlin/com/mohamedrejeb/richeditor/sample/common/richeditor/SpellCheckSpan.kt +++ b/richeditor-compose-spellcheck/src/commonMain/kotlin/com/mohamedrejeb/richeditor/compose/spellcheck/SpellCheck.kt @@ -1,7 +1,9 @@ -package com.mohamedrejeb.richeditor.sample.common.richeditor +package com.mohamedrejeb.richeditor.compose.spellcheck import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.StrokeJoin import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.text.SpanStyle @@ -13,9 +15,15 @@ import com.mohamedrejeb.richeditor.annotation.ExperimentalRichTextApi import com.mohamedrejeb.richeditor.model.RichSpanStyle import com.mohamedrejeb.richeditor.model.RichTextConfig import com.mohamedrejeb.richeditor.utils.getBoundingBoxes +import kotlin.math.PI +import kotlin.math.sin +/** + * RichSpanStyle that draws a Spell Check style red squiggle below the Spanned text. + */ @OptIn(ExperimentalRichTextApi::class) -object SpellCheck: RichSpanStyle { +public object SpellCheck: RichSpanStyle { + override val spanStyle: (RichTextConfig) -> SpanStyle = { SpanStyle() } @@ -35,19 +43,36 @@ object SpellCheck: RichSpanStyle { flattenForFullParagraphs = true, ) + val amplitude = 1.5.dp.toPx() // Height of the wave + val frequency = 0.15f // Controls how many waves appear + boxes.fastForEach { box -> path.moveTo(box.left + startPadding, box.bottom + topPadding) - path.lineTo(box.right + startPadding, box.bottom + topPadding) + + // Create the sine wave path + for (x in 0..box.width.toInt()) { + val xPos = box.left + startPadding + x + val yPos = box.bottom + topPadding + + (amplitude * sin(x * frequency * 2 * PI)).toFloat() + + if (x == 0) { + path.moveTo(xPos, yPos) + } else { + path.lineTo(xPos, yPos) + } + } drawPath( path = path, color = strokeColor, style = Stroke( - width = 2.dp.toPx(), + width = 1.dp.toPx(), + cap = StrokeCap.Round, + join = StrokeJoin.Round ) ) } } override val acceptNewTextInTheEdges: Boolean = false -} +} \ No newline at end of file diff --git a/richeditor-compose-spellcheck/src/commonMain/kotlin/com/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckState.kt b/richeditor-compose-spellcheck/src/commonMain/kotlin/com/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckState.kt new file mode 100644 index 00000000..d24000c2 --- /dev/null +++ b/richeditor-compose-spellcheck/src/commonMain/kotlin/com/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckState.kt @@ -0,0 +1,122 @@ +package com.mohamedrejeb.richeditor.compose.spellcheck + +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.util.fastForEach +import com.darkrockstudios.symspellkt.api.SpellChecker +import com.darkrockstudios.symspellkt.common.SuggestionItem +import com.darkrockstudios.symspellkt.common.Verbosity +import com.darkrockstudios.symspellkt.common.isSpelledCorrectly +import com.darkrockstudios.symspellkt.common.spellingIsCorrect +import com.mohamedrejeb.richeditor.compose.spellcheck.utils.applyCapitalizationStrategy +import com.mohamedrejeb.richeditor.model.RichSpanStyle +import com.mohamedrejeb.richeditor.model.RichTextState +import com.mohamedrejeb.richeditor.utils.WordSegment + +public class SpellCheckState( + public val richTextState: RichTextState, + public var spellChecker: SpellChecker? +) { + private var lastTextHash = -1 + private val misspelledWords = mutableListOf() + + public fun handleSpanClick(span: RichSpanStyle, range: TextRange, click: Offset): WordSegment? { + return if (span is SpellCheck) { + findWordSegmentContainingRange( + misspelledWords, + range + ) + } else { + null + } + } + + public fun correctSpelling(segment: WordSegment, correction: String) { + val currentStyle = richTextState.getSpanStyle(segment.range) + richTextState.replaceTextRange(segment.range, correction) + + val correctionRange = + TextRange(start = segment.range.start, end = segment.range.start + correction.length) + richTextState.addSpanStyle(currentStyle, correctionRange) + } + + /** + * This is a very naive algorithm that just removes all spell check spans and + * reruns the entire spell check again. + */ + public fun runSpellCheck() { + val sp = spellChecker ?: return + + richTextState.apply { + // Remove all existing spell checks + getAllRichSpans() + .filter { it.first is SpellCheck } + .forEach { span -> + removeRichSpan(SpellCheck, span.second) + } + + misspelledWords.clear() + getWords().mapNotNullTo(misspelledWords) { segment -> + val suggestions = sp.lookup(segment.text) + if (suggestions.spellingIsCorrect(segment.text)) { + null + } else { + segment + } + } + + misspelledWords.fastForEach { wordSegment -> + addRichSpan(SpellCheck, wordSegment.range) + } + } + } + + public fun onTextChange(richTextState: RichTextState) { + val newTextHash = richTextState.annotatedString.hashCode() + if (lastTextHash != newTextHash) { + runSpellCheck() + lastTextHash = newTextHash + } + } + + private fun findWordSegmentContainingRange( + segments: List, + range: TextRange + ): WordSegment? { + return segments.find { wordSegment -> + val segmentRange = wordSegment.range + range.start >= segmentRange.start && range.end <= segmentRange.end + } + } + + public fun getSuggestions(word: String): List { + val sp = spellChecker ?: return emptyList() + + val suggestions = sp.lookup(word, verbosity = Verbosity.Closest) + val proposedSuggestions = if (word.isSpelledCorrectly(suggestions).not()) { + // If things are misspelled, see if it just needs to be broken up + val composition = sp.wordBreakSegmentation(word) + val segmentedWord = composition.segmentedString + if (segmentedWord != null + && segmentedWord.equals(word, ignoreCase = true).not() + && suggestions.find { it.term.equals(segmentedWord, ignoreCase = true) } == null + ) { + // Add the segmented suggest as first item if it didn't already exist + listOf(SuggestionItem(segmentedWord, 1.0, 0.1)) + suggestions + } else { + suggestions + } + } else { + emptyList() + } + + return proposedSuggestions.map { suggestionItem -> + suggestionItem.copy( + term = applyCapitalizationStrategy( + source = word, + target = suggestionItem.term + ) + ) + } + } +} \ No newline at end of file diff --git a/richeditor-compose-spellcheck/src/commonMain/kotlin/com/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckTextContextMenuProvider.kt b/richeditor-compose-spellcheck/src/commonMain/kotlin/com/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckTextContextMenuProvider.kt new file mode 100644 index 00000000..16fe1d70 --- /dev/null +++ b/richeditor-compose-spellcheck/src/commonMain/kotlin/com/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckTextContextMenuProvider.kt @@ -0,0 +1,32 @@ +package com.mohamedrejeb.richeditor.compose.spellcheck + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import com.mohamedrejeb.richeditor.utils.WordSegment + +@Composable +public expect fun SpellCheckTextContextMenuProvider( + modifier: Modifier, + spellCheckMenuState: SpellCheckMenuState, + content: @Composable () -> Unit +) + +public data class SpellCheckMenuState( + val spellCheckState: SpellCheckState, +) { + val missSpelling: MutableState = mutableStateOf(null) + + public fun clearSpellCheck() { + missSpelling.value = null + } + + public fun performCorrection(toReplace: WordSegment, correction: String) { + spellCheckState.correctSpelling(toReplace, correction) + clearSpellCheck() + } + + public data class MissSpelling(val wordSegment: WordSegment, val menuPosition: Offset) +} \ No newline at end of file diff --git a/richeditor-compose-spellcheck/src/commonMain/kotlin/com/mohamedrejeb/richeditor/compose/spellcheck/rememberSpellCheckState.kt b/richeditor-compose-spellcheck/src/commonMain/kotlin/com/mohamedrejeb/richeditor/compose/spellcheck/rememberSpellCheckState.kt new file mode 100644 index 00000000..b7250b17 --- /dev/null +++ b/richeditor-compose-spellcheck/src/commonMain/kotlin/com/mohamedrejeb/richeditor/compose/spellcheck/rememberSpellCheckState.kt @@ -0,0 +1,23 @@ +package com.mohamedrejeb.richeditor.compose.spellcheck + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import com.darkrockstudios.symspellkt.api.SpellChecker +import com.mohamedrejeb.richeditor.model.rememberRichTextState + +@Composable +public fun rememberSpellCheckState(spellChecker: SpellChecker?): SpellCheckState { + val richTextState = rememberRichTextState() + val state = remember { SpellCheckState(richTextState, spellChecker) } + + // Run SpellCheck as soon as it is ready + LaunchedEffect(spellChecker) { + if (spellChecker != null) { + state.spellChecker = spellChecker + state.runSpellCheck() + } + } + + return state +} \ No newline at end of file diff --git a/richeditor-compose-spellcheck/src/commonMain/kotlin/com/mohamedrejeb/richeditor/compose/spellcheck/ui/SpellCheckDropdown.kt b/richeditor-compose-spellcheck/src/commonMain/kotlin/com/mohamedrejeb/richeditor/compose/spellcheck/ui/SpellCheckDropdown.kt new file mode 100644 index 00000000..85c30973 --- /dev/null +++ b/richeditor-compose-spellcheck/src/commonMain/kotlin/com/mohamedrejeb/richeditor/compose/spellcheck/ui/SpellCheckDropdown.kt @@ -0,0 +1,58 @@ +package com.mohamedrejeb.richeditor.compose.spellcheck.ui + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.offset +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.unit.IntOffset +import com.darkrockstudios.symspellkt.common.SuggestionItem +import com.mohamedrejeb.richeditor.compose.spellcheck.SpellCheckState +import com.mohamedrejeb.richeditor.utils.WordSegment +import kotlin.math.roundToInt + +@Composable +internal fun SpellCheckDropdown( + word: WordSegment?, + position: Offset, + spellCheckState: SpellCheckState, + dismiss: () -> Unit, + correctSpelling: (WordSegment, String) -> Unit +) { + var suggestionItems by remember { mutableStateOf(emptyList()) } + + LaunchedEffect(word, spellCheckState) { + word ?: return@LaunchedEffect + suggestionItems = spellCheckState.getSuggestions(word.text) + } + + Box(modifier = Modifier.offset { IntOffset(position.x.roundToInt(), position.y.roundToInt())}) { + DropdownMenu( + expanded = word != null, + onDismissRequest = dismiss, + ) { + word ?: return@DropdownMenu + if (suggestionItems.isNotEmpty()) { + suggestionItems.forEach { item -> + DropdownMenuItem( + text = { Text(item.term) }, + onClick = { correctSpelling(word, item.term) }, + ) + } + } else { + DropdownMenuItem( + text = { Text("No suggestions") }, + onClick = dismiss, + ) + } + } + } +} diff --git a/richeditor-compose-spellcheck/src/commonMain/kotlin/com/mohamedrejeb/richeditor/compose/spellcheck/ui/SpellCheckedRichTextEditor.kt b/richeditor-compose-spellcheck/src/commonMain/kotlin/com/mohamedrejeb/richeditor/compose/spellcheck/ui/SpellCheckedRichTextEditor.kt new file mode 100644 index 00000000..1aa45b47 --- /dev/null +++ b/richeditor-compose-spellcheck/src/commonMain/kotlin/com/mohamedrejeb/richeditor/compose/spellcheck/ui/SpellCheckedRichTextEditor.kt @@ -0,0 +1,100 @@ +package com.mohamedrejeb.richeditor.compose.spellcheck.ui + +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.text.TextLayoutResult +import androidx.compose.ui.text.TextStyle +import com.mohamedrejeb.richeditor.compose.spellcheck.SpellCheckMenuState +import com.mohamedrejeb.richeditor.compose.spellcheck.SpellCheckState +import com.mohamedrejeb.richeditor.compose.spellcheck.SpellCheckTextContextMenuProvider +import com.mohamedrejeb.richeditor.compose.spellcheck.rememberSpellCheckState +import com.mohamedrejeb.richeditor.compose.spellcheck.utils.debounceUntilQuiescent +import com.mohamedrejeb.richeditor.model.RichTextState +import com.mohamedrejeb.richeditor.ui.BasicRichTextEditor +import com.mohamedrejeb.richeditor.ui.InteractionType +import com.mohamedrejeb.richeditor.ui.RichSpanClickListener +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import kotlin.time.Duration.Companion.seconds + +@Composable +public fun SpellCheckedRichTextEditor( + modifier: Modifier = Modifier, + enabled: Boolean = true, + textStyle: TextStyle = TextStyle.Default, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions.Default, + singleLine: Boolean = false, + maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, + minLines: Int = 1, + maxLength: Int = Int.MAX_VALUE, + onRichSpanClick: RichSpanClickListener? = null, + onTextLayout: (TextLayoutResult) -> Unit = {}, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + cursorBrush: Brush = SolidColor(Color.Black), + spellCheckState: SpellCheckState = rememberSpellCheckState(null), + decorationBox: @Composable (innerTextField: @Composable () -> Unit) -> Unit = + @Composable { innerTextField -> innerTextField() }, +) { + val menuState by remember(spellCheckState) { mutableStateOf(SpellCheckMenuState(spellCheckState)) } + + val changesFlow = MutableStateFlow(RichTextState()) + val scope = rememberCoroutineScope() + LaunchedEffect(Unit) { + scope.launch { + changesFlow.debounceUntilQuiescent(1.seconds).collect { richTextState -> + spellCheckState.onTextChange(richTextState) + } + } + } + + SpellCheckTextContextMenuProvider( + modifier = modifier, + spellCheckMenuState = menuState, + ) { + BasicRichTextEditor( + modifier = Modifier.fillMaxSize(), + enabled = enabled, + textStyle = textStyle, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + singleLine = singleLine, + maxLines = maxLines, + minLines = minLines, + maxLength = maxLength, + onTextLayout = onTextLayout, + interactionSource = interactionSource, + state = spellCheckState.richTextState, + cursorBrush = cursorBrush, + onRichTextChangedListener = { changesFlow.tryEmit(it) }, + onRichSpanClick = { span, range, click, type -> + return@BasicRichTextEditor if (type == InteractionType.SecondaryClick || type == InteractionType.Tap) { + val correction = spellCheckState.handleSpanClick(span, range, click) + if (correction != null) { + menuState.missSpelling.value = SpellCheckMenuState.MissSpelling(correction, click) + true + } else { + menuState.missSpelling.value = null + onRichSpanClick?.invoke(span, range, click, type) ?: false + } + } else { + menuState.missSpelling.value = null + false + } + }, + decorationBox = decorationBox, + ) + } +} \ No newline at end of file diff --git a/richeditor-compose-spellcheck/src/commonMain/kotlin/com/mohamedrejeb/richeditor/compose/spellcheck/utils/applyCapitalizationStrategy.kt b/richeditor-compose-spellcheck/src/commonMain/kotlin/com/mohamedrejeb/richeditor/compose/spellcheck/utils/applyCapitalizationStrategy.kt new file mode 100644 index 00000000..65eeaafc --- /dev/null +++ b/richeditor-compose-spellcheck/src/commonMain/kotlin/com/mohamedrejeb/richeditor/compose/spellcheck/utils/applyCapitalizationStrategy.kt @@ -0,0 +1,37 @@ +package com.mohamedrejeb.richeditor.compose.spellcheck.utils + +internal fun applyCapitalizationStrategy(source: String, target: String): String { + fun isAllUpperCase(str: String): Boolean = str.all { it.isUpperCase() || !it.isLetter() } + fun isAllLowerCase(str: String): Boolean = str.all { it.isLowerCase() || !it.isLetter() } + fun isTitleCase(str: String): Boolean { + val words = str.split(" ") + return words.size > 1 && words.all { + it.isNotEmpty() && it[0].isUpperCase() && it.substring(1) + .all { char -> char.isLowerCase() } + } + } + + fun isCollapsedTitleCase(str: String): Boolean { + return str.length > 2 && str[0].isUpperCase() && str[1].isLowerCase() && str.substring(2) + .any { char -> char.isUpperCase() } + } + + fun isInitialCaps(str: String): Boolean = + str.isNotEmpty() && str[0].isUpperCase() && str.substring(1) + .all { it.isLowerCase() || !it.isLetter() } + + fun makeTitleCase(target: String): String { + return target.split(" ").joinToString(" ") { word -> + if (word.isNotEmpty()) word[0].uppercase() + word.substring(1).lowercase() else word + } + } + + return when { + isAllUpperCase(source) -> target.uppercase() + isAllLowerCase(source) -> target.lowercase() + isTitleCase(source) -> makeTitleCase(target) + isCollapsedTitleCase(source) -> makeTitleCase(target) + isInitialCaps(source) -> target.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } + else -> target + } +} \ No newline at end of file diff --git a/richeditor-compose-spellcheck/src/commonMain/kotlin/com/mohamedrejeb/richeditor/compose/spellcheck/utils/debounceUntilQuiescent.kt b/richeditor-compose-spellcheck/src/commonMain/kotlin/com/mohamedrejeb/richeditor/compose/spellcheck/utils/debounceUntilQuiescent.kt new file mode 100644 index 00000000..32ed7d2e --- /dev/null +++ b/richeditor-compose-spellcheck/src/commonMain/kotlin/com/mohamedrejeb/richeditor/compose/spellcheck/utils/debounceUntilQuiescent.kt @@ -0,0 +1,20 @@ +package com.mohamedrejeb.richeditor.compose.spellcheck.utils + +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.launch +import kotlin.time.Duration + +internal fun Flow.debounceUntilQuiescent(duration: Duration): Flow = channelFlow { + var job: Job? = null + collect { value -> + job?.cancel() + job = launch { + delay(duration) + send(value) + job = null + } + } +} \ No newline at end of file diff --git a/richeditor-compose-spellcheck/src/desktopMain/kotlin/com/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckTextContextMenuProvider.desktop.kt b/richeditor-compose-spellcheck/src/desktopMain/kotlin/com/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckTextContextMenuProvider.desktop.kt new file mode 100644 index 00000000..3b896052 --- /dev/null +++ b/richeditor-compose-spellcheck/src/desktopMain/kotlin/com/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckTextContextMenuProvider.desktop.kt @@ -0,0 +1,66 @@ +package com.mohamedrejeb.richeditor.compose.spellcheck + +import androidx.compose.foundation.ContextMenuData +import androidx.compose.foundation.ContextMenuItem +import androidx.compose.foundation.LocalContextMenuData +import androidx.compose.foundation.layout.Box +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier + +@Composable +public actual fun SpellCheckTextContextMenuProvider( + modifier: Modifier, + spellCheckMenuState: SpellCheckMenuState, + content: @Composable () -> Unit +) { + val currentContextMenuData = LocalContextMenuData.current + val contextMenuData = remember(spellCheckMenuState.missSpelling.value) { + ContextMenuData( + items = { + val wordSegment = spellCheckMenuState.missSpelling.value?.wordSegment + ?: return@ContextMenuData emptyList() + + val suggestionItems = spellCheckMenuState.spellCheckState.getSuggestions(wordSegment.text) + if (suggestionItems.isNotEmpty()) { + suggestionItems.map { suggestion -> + ContextMenuItem( + suggestion.term, + onClick = { + spellCheckMenuState.performCorrection( + wordSegment, + suggestion.term + ) + } + ) + } + } else { + emptyList() + } + }, + next = currentContextMenuData + ) + } + + LocalContextMenuDataProvider(contextMenuData) { + Box(modifier = modifier) { + content() + } + } +} + +/** + * In order to be able to recompose the menu items, I need this overload. + */ +@Composable +private fun LocalContextMenuDataProvider( + data: ContextMenuData, + content: @Composable () -> Unit +) { + CompositionLocalProvider( + LocalContextMenuData provides data + ) { + content() + } +} \ No newline at end of file diff --git a/richeditor-compose-spellcheck/src/iosMain/kotlin/com/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckTextContextMenuProvider.ios.kt b/richeditor-compose-spellcheck/src/iosMain/kotlin/com/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckTextContextMenuProvider.ios.kt new file mode 100644 index 00000000..da8ac969 --- /dev/null +++ b/richeditor-compose-spellcheck/src/iosMain/kotlin/com/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckTextContextMenuProvider.ios.kt @@ -0,0 +1,27 @@ +package com.mohamedrejeb.richeditor.compose.spellcheck + +import androidx.compose.foundation.layout.Box +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.mohamedrejeb.richeditor.compose.spellcheck.ui.SpellCheckDropdown + +@Composable +public actual fun SpellCheckTextContextMenuProvider( + modifier: Modifier, + spellCheckMenuState: SpellCheckMenuState, + content: @Composable () -> Unit +) { + Box(modifier = modifier) { + content() + + spellCheckMenuState.missSpelling.value?.apply { + SpellCheckDropdown( + wordSegment, + menuPosition, + spellCheckMenuState.spellCheckState, + dismiss = spellCheckMenuState::clearSpellCheck, + correctSpelling = spellCheckMenuState::performCorrection + ) + } + } +} \ No newline at end of file diff --git a/richeditor-compose-spellcheck/src/jsMain/kotlin/com/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckTextContextMenuProvider.js.kt b/richeditor-compose-spellcheck/src/jsMain/kotlin/com/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckTextContextMenuProvider.js.kt new file mode 100644 index 00000000..da8ac969 --- /dev/null +++ b/richeditor-compose-spellcheck/src/jsMain/kotlin/com/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckTextContextMenuProvider.js.kt @@ -0,0 +1,27 @@ +package com.mohamedrejeb.richeditor.compose.spellcheck + +import androidx.compose.foundation.layout.Box +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.mohamedrejeb.richeditor.compose.spellcheck.ui.SpellCheckDropdown + +@Composable +public actual fun SpellCheckTextContextMenuProvider( + modifier: Modifier, + spellCheckMenuState: SpellCheckMenuState, + content: @Composable () -> Unit +) { + Box(modifier = modifier) { + content() + + spellCheckMenuState.missSpelling.value?.apply { + SpellCheckDropdown( + wordSegment, + menuPosition, + spellCheckMenuState.spellCheckState, + dismiss = spellCheckMenuState::clearSpellCheck, + correctSpelling = spellCheckMenuState::performCorrection + ) + } + } +} \ No newline at end of file diff --git a/richeditor-compose-spellcheck/src/main/java/com/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckTextContextMenuProvider.android.kt b/richeditor-compose-spellcheck/src/main/java/com/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckTextContextMenuProvider.android.kt new file mode 100644 index 00000000..da8ac969 --- /dev/null +++ b/richeditor-compose-spellcheck/src/main/java/com/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckTextContextMenuProvider.android.kt @@ -0,0 +1,27 @@ +package com.mohamedrejeb.richeditor.compose.spellcheck + +import androidx.compose.foundation.layout.Box +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.mohamedrejeb.richeditor.compose.spellcheck.ui.SpellCheckDropdown + +@Composable +public actual fun SpellCheckTextContextMenuProvider( + modifier: Modifier, + spellCheckMenuState: SpellCheckMenuState, + content: @Composable () -> Unit +) { + Box(modifier = modifier) { + content() + + spellCheckMenuState.missSpelling.value?.apply { + SpellCheckDropdown( + wordSegment, + menuPosition, + spellCheckMenuState.spellCheckState, + dismiss = spellCheckMenuState::clearSpellCheck, + correctSpelling = spellCheckMenuState::performCorrection + ) + } + } +} \ No newline at end of file diff --git a/richeditor-compose-spellcheck/src/wasmJsMain/kotlin/com/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckTextContextMenuProvider.wasmJs.kt b/richeditor-compose-spellcheck/src/wasmJsMain/kotlin/com/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckTextContextMenuProvider.wasmJs.kt new file mode 100644 index 00000000..da8ac969 --- /dev/null +++ b/richeditor-compose-spellcheck/src/wasmJsMain/kotlin/com/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckTextContextMenuProvider.wasmJs.kt @@ -0,0 +1,27 @@ +package com.mohamedrejeb.richeditor.compose.spellcheck + +import androidx.compose.foundation.layout.Box +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.mohamedrejeb.richeditor.compose.spellcheck.ui.SpellCheckDropdown + +@Composable +public actual fun SpellCheckTextContextMenuProvider( + modifier: Modifier, + spellCheckMenuState: SpellCheckMenuState, + content: @Composable () -> Unit +) { + Box(modifier = modifier) { + content() + + spellCheckMenuState.missSpelling.value?.apply { + SpellCheckDropdown( + wordSegment, + menuPosition, + spellCheckMenuState.spellCheckState, + dismiss = spellCheckMenuState::clearSpellCheck, + correctSpelling = spellCheckMenuState::performCorrection + ) + } + } +} \ No newline at end of file diff --git a/richeditor-compose/api/android/richeditor-compose.api b/richeditor-compose/api/android/richeditor-compose.api index 81bce638..3476635b 100644 --- a/richeditor-compose/api/android/richeditor-compose.api +++ b/richeditor-compose/api/android/richeditor-compose.api @@ -143,6 +143,7 @@ public final class com/mohamedrejeb/richeditor/model/RichTextState { public final fun clearSpanStyles ()V public final fun clearSpanStyles-5zc-tL8 (J)V public final fun copy ()Lcom/mohamedrejeb/richeditor/model/RichTextState; + public final fun getAllRichSpans ()Ljava/util/List; public final fun getAnnotatedString ()Landroidx/compose/ui/text/AnnotatedString; public final fun getComposition-MzsxiRA ()Landroidx/compose/ui/text/TextRange; public final fun getConfig ()Lcom/mohamedrejeb/richeditor/model/RichTextConfig; @@ -155,6 +156,8 @@ public final class com/mohamedrejeb/richeditor/model/RichTextState { public final fun getSelectedLinkUrl ()Ljava/lang/String; public final fun getSelection-d9O1mEE ()J public final fun getSpanStyle-5zc-tL8 (J)Landroidx/compose/ui/text/SpanStyle; + public final fun getTextChanges ()Lkotlinx/coroutines/flow/SharedFlow; + public final fun getWords ()Lkotlin/sequences/Sequence; public final fun isCode ()Z public final fun isCodeSpan ()Z public final fun isLink ()Z @@ -219,8 +222,8 @@ public final class com/mohamedrejeb/richeditor/model/TextPaddingValues { } public final class com/mohamedrejeb/richeditor/ui/BasicRichTextEditorKt { - public static final fun BasicRichTextEditor (Lcom/mohamedrejeb/richeditor/model/RichTextState;Landroidx/compose/ui/Modifier;ZZLandroidx/compose/ui/text/TextStyle;Landroidx/compose/foundation/text/KeyboardOptions;Landroidx/compose/foundation/text/KeyboardActions;ZIIILkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Landroidx/compose/foundation/interaction/MutableInteractionSource;Landroidx/compose/ui/graphics/Brush;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;III)V - public static final fun BasicRichTextEditor (Lcom/mohamedrejeb/richeditor/model/RichTextState;Landroidx/compose/ui/Modifier;ZZLandroidx/compose/ui/text/TextStyle;Landroidx/compose/foundation/text/KeyboardOptions;Landroidx/compose/foundation/text/KeyboardActions;ZZIIILkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Landroidx/compose/foundation/interaction/MutableInteractionSource;Landroidx/compose/ui/graphics/Brush;Lkotlin/jvm/functions/Function3;Landroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/runtime/Composer;III)V + public static final fun BasicRichTextEditor (Lcom/mohamedrejeb/richeditor/model/RichTextState;Landroidx/compose/ui/Modifier;ZZLandroidx/compose/ui/text/TextStyle;Landroidx/compose/foundation/text/KeyboardOptions;Landroidx/compose/foundation/text/KeyboardActions;ZIIILkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function1;Landroidx/compose/foundation/interaction/MutableInteractionSource;Landroidx/compose/ui/graphics/Brush;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;III)V + public static final fun BasicRichTextEditor (Lcom/mohamedrejeb/richeditor/model/RichTextState;Landroidx/compose/ui/Modifier;ZZLandroidx/compose/ui/text/TextStyle;Landroidx/compose/foundation/text/KeyboardOptions;Landroidx/compose/foundation/text/KeyboardActions;ZZIIILkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function1;Landroidx/compose/foundation/interaction/MutableInteractionSource;Landroidx/compose/ui/graphics/Brush;Lkotlin/jvm/functions/Function3;Landroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/runtime/Composer;III)V } public final class com/mohamedrejeb/richeditor/ui/BasicRichTextKt { @@ -236,12 +239,22 @@ public final class com/mohamedrejeb/richeditor/ui/ComposableSingletons$BasicRich public final fun getLambda-2$richeditor_compose_release ()Lkotlin/jvm/functions/Function3; } +public final class com/mohamedrejeb/richeditor/ui/InteractionType : java/lang/Enum { + public static final field DoubleTap Lcom/mohamedrejeb/richeditor/ui/InteractionType; + public static final field PrimaryClick Lcom/mohamedrejeb/richeditor/ui/InteractionType; + public static final field SecondaryClick Lcom/mohamedrejeb/richeditor/ui/InteractionType; + public static final field Tap Lcom/mohamedrejeb/richeditor/ui/InteractionType; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lcom/mohamedrejeb/richeditor/ui/InteractionType; + public static fun values ()[Lcom/mohamedrejeb/richeditor/ui/InteractionType; +} + public final class com/mohamedrejeb/richeditor/ui/material/OutlinedRichTextEditorKt { - public static final fun OutlinedRichTextEditor (Lcom/mohamedrejeb/richeditor/model/RichTextState;Landroidx/compose/ui/Modifier;ZZLandroidx/compose/ui/text/TextStyle;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ZLandroidx/compose/foundation/text/KeyboardOptions;Landroidx/compose/foundation/text/KeyboardActions;ZIIILkotlin/jvm/functions/Function1;Landroidx/compose/foundation/interaction/MutableInteractionSource;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/material/TextFieldColors;Landroidx/compose/runtime/Composer;III)V + public static final fun OutlinedRichTextEditor (Lcom/mohamedrejeb/richeditor/model/RichTextState;Landroidx/compose/ui/Modifier;ZZLandroidx/compose/ui/text/TextStyle;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ZLandroidx/compose/foundation/text/KeyboardOptions;Landroidx/compose/foundation/text/KeyboardActions;ZIIILkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function4;Landroidx/compose/foundation/interaction/MutableInteractionSource;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/material/TextFieldColors;Landroidx/compose/runtime/Composer;IIII)V } public final class com/mohamedrejeb/richeditor/ui/material/RichTextEditorKt { - public static final fun RichTextEditor (Lcom/mohamedrejeb/richeditor/model/RichTextState;Landroidx/compose/ui/Modifier;ZZLandroidx/compose/ui/text/TextStyle;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ZLandroidx/compose/foundation/text/KeyboardOptions;Landroidx/compose/foundation/text/KeyboardActions;ZIIILkotlin/jvm/functions/Function1;Landroidx/compose/foundation/interaction/MutableInteractionSource;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/material/TextFieldColors;Landroidx/compose/runtime/Composer;III)V + public static final fun RichTextEditor (Lcom/mohamedrejeb/richeditor/model/RichTextState;Landroidx/compose/ui/Modifier;ZZLandroidx/compose/ui/text/TextStyle;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ZLandroidx/compose/foundation/text/KeyboardOptions;Landroidx/compose/foundation/text/KeyboardActions;ZIIILkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function4;Landroidx/compose/foundation/interaction/MutableInteractionSource;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/material/TextFieldColors;Landroidx/compose/runtime/Composer;IIII)V } public final class com/mohamedrejeb/richeditor/ui/material/RichTextKt { @@ -249,7 +262,7 @@ public final class com/mohamedrejeb/richeditor/ui/material/RichTextKt { } public final class com/mohamedrejeb/richeditor/ui/material3/OutlinedRichTextEditorKt { - public static final fun OutlinedRichTextEditor (Lcom/mohamedrejeb/richeditor/model/RichTextState;Landroidx/compose/ui/Modifier;ZZLandroidx/compose/ui/text/TextStyle;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ZLandroidx/compose/foundation/text/KeyboardOptions;Landroidx/compose/foundation/text/KeyboardActions;ZIIILkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Landroidx/compose/foundation/interaction/MutableInteractionSource;Landroidx/compose/ui/graphics/Shape;Lcom/mohamedrejeb/richeditor/ui/material3/RichTextEditorColors;Landroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/runtime/Composer;IIII)V + public static final fun OutlinedRichTextEditor (Lcom/mohamedrejeb/richeditor/model/RichTextState;Landroidx/compose/ui/Modifier;ZZLandroidx/compose/ui/text/TextStyle;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ZLandroidx/compose/foundation/text/KeyboardOptions;Landroidx/compose/foundation/text/KeyboardActions;ZIIILkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function1;Landroidx/compose/foundation/interaction/MutableInteractionSource;Landroidx/compose/ui/graphics/Shape;Lcom/mohamedrejeb/richeditor/ui/material3/RichTextEditorColors;Landroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/runtime/Composer;IIII)V } public final class com/mohamedrejeb/richeditor/ui/material3/RichTextEditorColors { @@ -284,7 +297,7 @@ public final class com/mohamedrejeb/richeditor/ui/material3/RichTextEditorDefaul } public final class com/mohamedrejeb/richeditor/ui/material3/RichTextEditorKt { - public static final fun RichTextEditor (Lcom/mohamedrejeb/richeditor/model/RichTextState;Landroidx/compose/ui/Modifier;ZZLandroidx/compose/ui/text/TextStyle;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ZLandroidx/compose/foundation/text/KeyboardOptions;Landroidx/compose/foundation/text/KeyboardActions;ZIIILandroidx/compose/foundation/interaction/MutableInteractionSource;Landroidx/compose/ui/graphics/Shape;Lcom/mohamedrejeb/richeditor/ui/material3/RichTextEditorColors;Landroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/runtime/Composer;IIII)V + public static final fun RichTextEditor (Lcom/mohamedrejeb/richeditor/model/RichTextState;Landroidx/compose/ui/Modifier;ZZLandroidx/compose/ui/text/TextStyle;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ZLandroidx/compose/foundation/text/KeyboardOptions;Landroidx/compose/foundation/text/KeyboardActions;ZIIILkotlin/jvm/functions/Function4;Landroidx/compose/foundation/interaction/MutableInteractionSource;Landroidx/compose/ui/graphics/Shape;Lcom/mohamedrejeb/richeditor/ui/material3/RichTextEditorColors;Landroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/runtime/Composer;IIII)V } public final class com/mohamedrejeb/richeditor/ui/material3/RichTextKt { @@ -304,3 +317,17 @@ public final class com/mohamedrejeb/richeditor/utils/TextLayoutResultExtKt { public static synthetic fun getBoundingBoxes$default (Landroidx/compose/ui/text/TextLayoutResult;IIZILjava/lang/Object;)Ljava/util/List; } +public final class com/mohamedrejeb/richeditor/utils/WordSegment { + public static final field $stable I + public synthetic fun (Ljava/lang/String;JLkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2-d9O1mEE ()J + public final fun copy-FDrldGo (Ljava/lang/String;J)Lcom/mohamedrejeb/richeditor/utils/WordSegment; + public static synthetic fun copy-FDrldGo$default (Lcom/mohamedrejeb/richeditor/utils/WordSegment;Ljava/lang/String;JILjava/lang/Object;)Lcom/mohamedrejeb/richeditor/utils/WordSegment; + public fun equals (Ljava/lang/Object;)Z + public final fun getRange-d9O1mEE ()J + public final fun getText ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + diff --git a/richeditor-compose/api/desktop/richeditor-compose.api b/richeditor-compose/api/desktop/richeditor-compose.api index a4c017d5..2a4feebb 100644 --- a/richeditor-compose/api/desktop/richeditor-compose.api +++ b/richeditor-compose/api/desktop/richeditor-compose.api @@ -143,6 +143,7 @@ public final class com/mohamedrejeb/richeditor/model/RichTextState { public final fun clearSpanStyles ()V public final fun clearSpanStyles-5zc-tL8 (J)V public final fun copy ()Lcom/mohamedrejeb/richeditor/model/RichTextState; + public final fun getAllRichSpans ()Ljava/util/List; public final fun getAnnotatedString ()Landroidx/compose/ui/text/AnnotatedString; public final fun getComposition-MzsxiRA ()Landroidx/compose/ui/text/TextRange; public final fun getConfig ()Lcom/mohamedrejeb/richeditor/model/RichTextConfig; @@ -155,6 +156,8 @@ public final class com/mohamedrejeb/richeditor/model/RichTextState { public final fun getSelectedLinkUrl ()Ljava/lang/String; public final fun getSelection-d9O1mEE ()J public final fun getSpanStyle-5zc-tL8 (J)Landroidx/compose/ui/text/SpanStyle; + public final fun getTextChanges ()Lkotlinx/coroutines/flow/SharedFlow; + public final fun getWords ()Lkotlin/sequences/Sequence; public final fun isCode ()Z public final fun isCodeSpan ()Z public final fun isLink ()Z @@ -219,8 +222,8 @@ public final class com/mohamedrejeb/richeditor/model/TextPaddingValues { } public final class com/mohamedrejeb/richeditor/ui/BasicRichTextEditorKt { - public static final fun BasicRichTextEditor (Lcom/mohamedrejeb/richeditor/model/RichTextState;Landroidx/compose/ui/Modifier;ZZLandroidx/compose/ui/text/TextStyle;Landroidx/compose/foundation/text/KeyboardOptions;Landroidx/compose/foundation/text/KeyboardActions;ZIIILkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Landroidx/compose/foundation/interaction/MutableInteractionSource;Landroidx/compose/ui/graphics/Brush;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;III)V - public static final fun BasicRichTextEditor (Lcom/mohamedrejeb/richeditor/model/RichTextState;Landroidx/compose/ui/Modifier;ZZLandroidx/compose/ui/text/TextStyle;Landroidx/compose/foundation/text/KeyboardOptions;Landroidx/compose/foundation/text/KeyboardActions;ZZIIILkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Landroidx/compose/foundation/interaction/MutableInteractionSource;Landroidx/compose/ui/graphics/Brush;Lkotlin/jvm/functions/Function3;Landroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/runtime/Composer;III)V + public static final fun BasicRichTextEditor (Lcom/mohamedrejeb/richeditor/model/RichTextState;Landroidx/compose/ui/Modifier;ZZLandroidx/compose/ui/text/TextStyle;Landroidx/compose/foundation/text/KeyboardOptions;Landroidx/compose/foundation/text/KeyboardActions;ZIIILkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function1;Landroidx/compose/foundation/interaction/MutableInteractionSource;Landroidx/compose/ui/graphics/Brush;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;III)V + public static final fun BasicRichTextEditor (Lcom/mohamedrejeb/richeditor/model/RichTextState;Landroidx/compose/ui/Modifier;ZZLandroidx/compose/ui/text/TextStyle;Landroidx/compose/foundation/text/KeyboardOptions;Landroidx/compose/foundation/text/KeyboardActions;ZZIIILkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function1;Landroidx/compose/foundation/interaction/MutableInteractionSource;Landroidx/compose/ui/graphics/Brush;Lkotlin/jvm/functions/Function3;Landroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/runtime/Composer;III)V } public final class com/mohamedrejeb/richeditor/ui/BasicRichTextKt { @@ -236,12 +239,22 @@ public final class com/mohamedrejeb/richeditor/ui/ComposableSingletons$BasicRich public final fun getLambda-2$richeditor_compose ()Lkotlin/jvm/functions/Function3; } +public final class com/mohamedrejeb/richeditor/ui/InteractionType : java/lang/Enum { + public static final field DoubleTap Lcom/mohamedrejeb/richeditor/ui/InteractionType; + public static final field PrimaryClick Lcom/mohamedrejeb/richeditor/ui/InteractionType; + public static final field SecondaryClick Lcom/mohamedrejeb/richeditor/ui/InteractionType; + public static final field Tap Lcom/mohamedrejeb/richeditor/ui/InteractionType; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lcom/mohamedrejeb/richeditor/ui/InteractionType; + public static fun values ()[Lcom/mohamedrejeb/richeditor/ui/InteractionType; +} + public final class com/mohamedrejeb/richeditor/ui/material/OutlinedRichTextEditorKt { - public static final fun OutlinedRichTextEditor (Lcom/mohamedrejeb/richeditor/model/RichTextState;Landroidx/compose/ui/Modifier;ZZLandroidx/compose/ui/text/TextStyle;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ZLandroidx/compose/foundation/text/KeyboardOptions;Landroidx/compose/foundation/text/KeyboardActions;ZIIILkotlin/jvm/functions/Function1;Landroidx/compose/foundation/interaction/MutableInteractionSource;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/material/TextFieldColors;Landroidx/compose/runtime/Composer;III)V + public static final fun OutlinedRichTextEditor (Lcom/mohamedrejeb/richeditor/model/RichTextState;Landroidx/compose/ui/Modifier;ZZLandroidx/compose/ui/text/TextStyle;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ZLandroidx/compose/foundation/text/KeyboardOptions;Landroidx/compose/foundation/text/KeyboardActions;ZIIILkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function4;Landroidx/compose/foundation/interaction/MutableInteractionSource;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/material/TextFieldColors;Landroidx/compose/runtime/Composer;IIII)V } public final class com/mohamedrejeb/richeditor/ui/material/RichTextEditorKt { - public static final fun RichTextEditor (Lcom/mohamedrejeb/richeditor/model/RichTextState;Landroidx/compose/ui/Modifier;ZZLandroidx/compose/ui/text/TextStyle;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ZLandroidx/compose/foundation/text/KeyboardOptions;Landroidx/compose/foundation/text/KeyboardActions;ZIIILkotlin/jvm/functions/Function1;Landroidx/compose/foundation/interaction/MutableInteractionSource;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/material/TextFieldColors;Landroidx/compose/runtime/Composer;III)V + public static final fun RichTextEditor (Lcom/mohamedrejeb/richeditor/model/RichTextState;Landroidx/compose/ui/Modifier;ZZLandroidx/compose/ui/text/TextStyle;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ZLandroidx/compose/foundation/text/KeyboardOptions;Landroidx/compose/foundation/text/KeyboardActions;ZIIILkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function4;Landroidx/compose/foundation/interaction/MutableInteractionSource;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/material/TextFieldColors;Landroidx/compose/runtime/Composer;IIII)V } public final class com/mohamedrejeb/richeditor/ui/material/RichTextKt { @@ -249,7 +262,7 @@ public final class com/mohamedrejeb/richeditor/ui/material/RichTextKt { } public final class com/mohamedrejeb/richeditor/ui/material3/OutlinedRichTextEditorKt { - public static final fun OutlinedRichTextEditor (Lcom/mohamedrejeb/richeditor/model/RichTextState;Landroidx/compose/ui/Modifier;ZZLandroidx/compose/ui/text/TextStyle;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ZLandroidx/compose/foundation/text/KeyboardOptions;Landroidx/compose/foundation/text/KeyboardActions;ZIIILkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Landroidx/compose/foundation/interaction/MutableInteractionSource;Landroidx/compose/ui/graphics/Shape;Lcom/mohamedrejeb/richeditor/ui/material3/RichTextEditorColors;Landroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/runtime/Composer;IIII)V + public static final fun OutlinedRichTextEditor (Lcom/mohamedrejeb/richeditor/model/RichTextState;Landroidx/compose/ui/Modifier;ZZLandroidx/compose/ui/text/TextStyle;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ZLandroidx/compose/foundation/text/KeyboardOptions;Landroidx/compose/foundation/text/KeyboardActions;ZIIILkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function1;Landroidx/compose/foundation/interaction/MutableInteractionSource;Landroidx/compose/ui/graphics/Shape;Lcom/mohamedrejeb/richeditor/ui/material3/RichTextEditorColors;Landroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/runtime/Composer;IIII)V } public final class com/mohamedrejeb/richeditor/ui/material3/RichTextEditorColors { @@ -284,7 +297,7 @@ public final class com/mohamedrejeb/richeditor/ui/material3/RichTextEditorDefaul } public final class com/mohamedrejeb/richeditor/ui/material3/RichTextEditorKt { - public static final fun RichTextEditor (Lcom/mohamedrejeb/richeditor/model/RichTextState;Landroidx/compose/ui/Modifier;ZZLandroidx/compose/ui/text/TextStyle;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ZLandroidx/compose/foundation/text/KeyboardOptions;Landroidx/compose/foundation/text/KeyboardActions;ZIIILandroidx/compose/foundation/interaction/MutableInteractionSource;Landroidx/compose/ui/graphics/Shape;Lcom/mohamedrejeb/richeditor/ui/material3/RichTextEditorColors;Landroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/runtime/Composer;IIII)V + public static final fun RichTextEditor (Lcom/mohamedrejeb/richeditor/model/RichTextState;Landroidx/compose/ui/Modifier;ZZLandroidx/compose/ui/text/TextStyle;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ZLandroidx/compose/foundation/text/KeyboardOptions;Landroidx/compose/foundation/text/KeyboardActions;ZIIILkotlin/jvm/functions/Function4;Landroidx/compose/foundation/interaction/MutableInteractionSource;Landroidx/compose/ui/graphics/Shape;Lcom/mohamedrejeb/richeditor/ui/material3/RichTextEditorColors;Landroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/runtime/Composer;IIII)V } public final class com/mohamedrejeb/richeditor/ui/material3/RichTextKt { @@ -304,3 +317,17 @@ public final class com/mohamedrejeb/richeditor/utils/TextLayoutResultExtKt { public static synthetic fun getBoundingBoxes$default (Landroidx/compose/ui/text/TextLayoutResult;IIZILjava/lang/Object;)Ljava/util/List; } +public final class com/mohamedrejeb/richeditor/utils/WordSegment { + public static final field $stable I + public synthetic fun (Ljava/lang/String;JLkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2-d9O1mEE ()J + public final fun copy-FDrldGo (Ljava/lang/String;J)Lcom/mohamedrejeb/richeditor/utils/WordSegment; + public static synthetic fun copy-FDrldGo$default (Lcom/mohamedrejeb/richeditor/utils/WordSegment;Ljava/lang/String;JILjava/lang/Object;)Lcom/mohamedrejeb/richeditor/utils/WordSegment; + public fun equals (Ljava/lang/Object;)Z + public final fun getRange-d9O1mEE ()J + public final fun getText ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + diff --git a/richeditor-compose/api/richeditor-compose.klib.api b/richeditor-compose/api/richeditor-compose.klib.api index bff01575..39b8e073 100644 --- a/richeditor-compose/api/richeditor-compose.klib.api +++ b/richeditor-compose/api/richeditor-compose.klib.api @@ -14,6 +14,19 @@ open annotation class com.mohamedrejeb.richeditor.annotation/InternalRichTextApi constructor () // com.mohamedrejeb.richeditor.annotation/InternalRichTextApi.|(){}[0] } +final enum class com.mohamedrejeb.richeditor.ui/InteractionType : kotlin/Enum { // com.mohamedrejeb.richeditor.ui/InteractionType|null[0] + enum entry DoubleTap // com.mohamedrejeb.richeditor.ui/InteractionType.DoubleTap|null[0] + enum entry PrimaryClick // com.mohamedrejeb.richeditor.ui/InteractionType.PrimaryClick|null[0] + enum entry SecondaryClick // com.mohamedrejeb.richeditor.ui/InteractionType.SecondaryClick|null[0] + enum entry Tap // com.mohamedrejeb.richeditor.ui/InteractionType.Tap|null[0] + + final val entries // com.mohamedrejeb.richeditor.ui/InteractionType.entries|#static{}entries[0] + final fun (): kotlin.enums/EnumEntries // com.mohamedrejeb.richeditor.ui/InteractionType.entries.|#static(){}[0] + + final fun valueOf(kotlin/String): com.mohamedrejeb.richeditor.ui/InteractionType // com.mohamedrejeb.richeditor.ui/InteractionType.valueOf|valueOf#static(kotlin.String){}[0] + final fun values(): kotlin/Array // com.mohamedrejeb.richeditor.ui/InteractionType.values|values#static(){}[0] +} + abstract interface com.mohamedrejeb.richeditor.model/ImageLoader { // com.mohamedrejeb.richeditor.model/ImageLoader|null[0] abstract fun load(kotlin/Any, androidx.compose.runtime/Composer?, kotlin/Int): com.mohamedrejeb.richeditor.model/ImageData? // com.mohamedrejeb.richeditor.model/ImageLoader.load|load(kotlin.Any;androidx.compose.runtime.Composer?;kotlin.Int){}[0] } @@ -165,6 +178,8 @@ final class com.mohamedrejeb.richeditor.model/RichTextState { // com.mohamedreje final fun (): kotlin/String? // com.mohamedrejeb.richeditor.model/RichTextState.selectedLinkText.|(){}[0] final val selectedLinkUrl // com.mohamedrejeb.richeditor.model/RichTextState.selectedLinkUrl|{}selectedLinkUrl[0] final fun (): kotlin/String? // com.mohamedrejeb.richeditor.model/RichTextState.selectedLinkUrl.|(){}[0] + final val textChanges // com.mohamedrejeb.richeditor.model/RichTextState.textChanges|{}textChanges[0] + final fun (): kotlinx.coroutines.flow/SharedFlow // com.mohamedrejeb.richeditor.model/RichTextState.textChanges.|(){}[0] final var annotatedString // com.mohamedrejeb.richeditor.model/RichTextState.annotatedString|{}annotatedString[0] final fun (): androidx.compose.ui.text/AnnotatedString // com.mohamedrejeb.richeditor.model/RichTextState.annotatedString.|(){}[0] @@ -192,9 +207,11 @@ final class com.mohamedrejeb.richeditor.model/RichTextState { // com.mohamedreje final fun clearSpanStyles() // com.mohamedrejeb.richeditor.model/RichTextState.clearSpanStyles|clearSpanStyles(){}[0] final fun clearSpanStyles(androidx.compose.ui.text/TextRange) // com.mohamedrejeb.richeditor.model/RichTextState.clearSpanStyles|clearSpanStyles(androidx.compose.ui.text.TextRange){}[0] final fun copy(): com.mohamedrejeb.richeditor.model/RichTextState // com.mohamedrejeb.richeditor.model/RichTextState.copy|copy(){}[0] + final fun getAllRichSpans(): kotlin.collections/List> // com.mohamedrejeb.richeditor.model/RichTextState.getAllRichSpans|getAllRichSpans(){}[0] final fun getParagraphStyle(androidx.compose.ui.text/TextRange): androidx.compose.ui.text/ParagraphStyle // com.mohamedrejeb.richeditor.model/RichTextState.getParagraphStyle|getParagraphStyle(androidx.compose.ui.text.TextRange){}[0] final fun getRichSpanStyle(androidx.compose.ui.text/TextRange): com.mohamedrejeb.richeditor.model/RichSpanStyle // com.mohamedrejeb.richeditor.model/RichTextState.getRichSpanStyle|getRichSpanStyle(androidx.compose.ui.text.TextRange){}[0] final fun getSpanStyle(androidx.compose.ui.text/TextRange): androidx.compose.ui.text/SpanStyle // com.mohamedrejeb.richeditor.model/RichTextState.getSpanStyle|getSpanStyle(androidx.compose.ui.text.TextRange){}[0] + final fun getWords(): kotlin.sequences/Sequence // com.mohamedrejeb.richeditor.model/RichTextState.getWords|getWords(){}[0] final fun isRichSpan(com.mohamedrejeb.richeditor.model/RichSpanStyle): kotlin/Boolean // com.mohamedrejeb.richeditor.model/RichTextState.isRichSpan|isRichSpan(com.mohamedrejeb.richeditor.model.RichSpanStyle){}[0] final fun isRichSpan(kotlin.reflect/KClass): kotlin/Boolean // com.mohamedrejeb.richeditor.model/RichTextState.isRichSpan|isRichSpan(kotlin.reflect.KClass){}[0] final fun removeCode() // com.mohamedrejeb.richeditor.model/RichTextState.removeCode|removeCode(){}[0] @@ -255,6 +272,22 @@ final class com.mohamedrejeb.richeditor.ui.material3/RichTextEditorColors { // c final fun hashCode(): kotlin/Int // com.mohamedrejeb.richeditor.ui.material3/RichTextEditorColors.hashCode|hashCode(){}[0] } +final class com.mohamedrejeb.richeditor.utils/WordSegment { // com.mohamedrejeb.richeditor.utils/WordSegment|null[0] + constructor (kotlin/String, androidx.compose.ui.text/TextRange) // com.mohamedrejeb.richeditor.utils/WordSegment.|(kotlin.String;androidx.compose.ui.text.TextRange){}[0] + + final val range // com.mohamedrejeb.richeditor.utils/WordSegment.range|{}range[0] + final fun (): androidx.compose.ui.text/TextRange // com.mohamedrejeb.richeditor.utils/WordSegment.range.|(){}[0] + final val text // com.mohamedrejeb.richeditor.utils/WordSegment.text|{}text[0] + final fun (): kotlin/String // com.mohamedrejeb.richeditor.utils/WordSegment.text.|(){}[0] + + final fun component1(): kotlin/String // com.mohamedrejeb.richeditor.utils/WordSegment.component1|component1(){}[0] + final fun component2(): androidx.compose.ui.text/TextRange // com.mohamedrejeb.richeditor.utils/WordSegment.component2|component2(){}[0] + final fun copy(kotlin/String = ..., androidx.compose.ui.text/TextRange = ...): com.mohamedrejeb.richeditor.utils/WordSegment // com.mohamedrejeb.richeditor.utils/WordSegment.copy|copy(kotlin.String;androidx.compose.ui.text.TextRange){}[0] + final fun equals(kotlin/Any?): kotlin/Boolean // com.mohamedrejeb.richeditor.utils/WordSegment.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // com.mohamedrejeb.richeditor.utils/WordSegment.hashCode|hashCode(){}[0] + final fun toString(): kotlin/String // com.mohamedrejeb.richeditor.utils/WordSegment.toString|toString(){}[0] +} + final object com.mohamedrejeb.richeditor.model/DefaultImageLoader : com.mohamedrejeb.richeditor.model/ImageLoader { // com.mohamedrejeb.richeditor.model/DefaultImageLoader|null[0] final fun load(kotlin/Any, androidx.compose.runtime/Composer?, kotlin/Int): com.mohamedrejeb.richeditor.model/ImageData? // com.mohamedrejeb.richeditor.model/DefaultImageLoader.load|load(kotlin.Any;androidx.compose.runtime.Composer?;kotlin.Int){}[0] } @@ -311,6 +344,7 @@ final val com.mohamedrejeb.richeditor.ui.material3.tokens/com_mohamedrejeb_riche final val com.mohamedrejeb.richeditor.ui.material3/com_mohamedrejeb_richeditor_ui_material3_RichTextEditorColors$stableprop // com.mohamedrejeb.richeditor.ui.material3/com_mohamedrejeb_richeditor_ui_material3_RichTextEditorColors$stableprop|#static{}com_mohamedrejeb_richeditor_ui_material3_RichTextEditorColors$stableprop[0] final val com.mohamedrejeb.richeditor.ui.material3/com_mohamedrejeb_richeditor_ui_material3_RichTextEditorDefaults$stableprop // com.mohamedrejeb.richeditor.ui.material3/com_mohamedrejeb_richeditor_ui_material3_RichTextEditorDefaults$stableprop|#static{}com_mohamedrejeb_richeditor_ui_material3_RichTextEditorDefaults$stableprop[0] final val com.mohamedrejeb.richeditor.ui/com_mohamedrejeb_richeditor_ui_RichTextClipboardManager$stableprop // com.mohamedrejeb.richeditor.ui/com_mohamedrejeb_richeditor_ui_RichTextClipboardManager$stableprop|#static{}com_mohamedrejeb_richeditor_ui_RichTextClipboardManager$stableprop[0] +final val com.mohamedrejeb.richeditor.utils/com_mohamedrejeb_richeditor_utils_WordSegment$stableprop // com.mohamedrejeb.richeditor.utils/com_mohamedrejeb_richeditor_utils_WordSegment$stableprop|#static{}com_mohamedrejeb_richeditor_utils_WordSegment$stableprop[0] final fun (androidx.compose.ui.text.style/TextDecoration).com.mohamedrejeb.richeditor.utils/minus(androidx.compose.ui.text.style/TextDecoration): androidx.compose.ui.text.style/TextDecoration // com.mohamedrejeb.richeditor.utils/minus|minus@androidx.compose.ui.text.style.TextDecoration(androidx.compose.ui.text.style.TextDecoration){}[0] final fun (androidx.compose.ui.text/TextLayoutResult).com.mohamedrejeb.richeditor.utils/getBoundingBoxes(kotlin/Int, kotlin/Int, kotlin/Boolean = ...): kotlin.collections/List // com.mohamedrejeb.richeditor.utils/getBoundingBoxes|getBoundingBoxes@androidx.compose.ui.text.TextLayoutResult(kotlin.Int;kotlin.Int;kotlin.Boolean){}[0] @@ -335,17 +369,18 @@ final fun com.mohamedrejeb.richeditor.parser.html/com_mohamedrejeb_richeditor_pa final fun com.mohamedrejeb.richeditor.parser.html/com_mohamedrejeb_richeditor_parser_html_CssEncoder$stableprop_getter(): kotlin/Int // com.mohamedrejeb.richeditor.parser.html/com_mohamedrejeb_richeditor_parser_html_CssEncoder$stableprop_getter|com_mohamedrejeb_richeditor_parser_html_CssEncoder$stableprop_getter(){}[0] final fun com.mohamedrejeb.richeditor.parser.html/com_mohamedrejeb_richeditor_parser_html_RichTextStateHtmlParser$stableprop_getter(): kotlin/Int // com.mohamedrejeb.richeditor.parser.html/com_mohamedrejeb_richeditor_parser_html_RichTextStateHtmlParser$stableprop_getter|com_mohamedrejeb_richeditor_parser_html_RichTextStateHtmlParser$stableprop_getter(){}[0] final fun com.mohamedrejeb.richeditor.parser.markdown/com_mohamedrejeb_richeditor_parser_markdown_RichTextStateMarkdownParser$stableprop_getter(): kotlin/Int // com.mohamedrejeb.richeditor.parser.markdown/com_mohamedrejeb_richeditor_parser_markdown_RichTextStateMarkdownParser$stableprop_getter|com_mohamedrejeb_richeditor_parser_markdown_RichTextStateMarkdownParser$stableprop_getter(){}[0] -final fun com.mohamedrejeb.richeditor.ui.material/OutlinedRichTextEditor(com.mohamedrejeb.richeditor.model/RichTextState, androidx.compose.ui/Modifier?, kotlin/Boolean, kotlin/Boolean, androidx.compose.ui.text/TextStyle?, kotlin/Function2?, kotlin/Function2?, kotlin/Function2?, kotlin/Function2?, kotlin/Boolean, androidx.compose.foundation.text/KeyboardOptions?, androidx.compose.foundation.text/KeyboardActions?, kotlin/Boolean, kotlin/Int, kotlin/Int, kotlin/Int, kotlin/Function1?, androidx.compose.foundation.interaction/MutableInteractionSource?, androidx.compose.ui.graphics/Shape?, androidx.compose.material/TextFieldColors?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // com.mohamedrejeb.richeditor.ui.material/OutlinedRichTextEditor|OutlinedRichTextEditor(com.mohamedrejeb.richeditor.model.RichTextState;androidx.compose.ui.Modifier?;kotlin.Boolean;kotlin.Boolean;androidx.compose.ui.text.TextStyle?;kotlin.Function2?;kotlin.Function2?;kotlin.Function2?;kotlin.Function2?;kotlin.Boolean;androidx.compose.foundation.text.KeyboardOptions?;androidx.compose.foundation.text.KeyboardActions?;kotlin.Boolean;kotlin.Int;kotlin.Int;kotlin.Int;kotlin.Function1?;androidx.compose.foundation.interaction.MutableInteractionSource?;androidx.compose.ui.graphics.Shape?;androidx.compose.material.TextFieldColors?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0] +final fun com.mohamedrejeb.richeditor.ui.material/OutlinedRichTextEditor(com.mohamedrejeb.richeditor.model/RichTextState, androidx.compose.ui/Modifier?, kotlin/Boolean, kotlin/Boolean, androidx.compose.ui.text/TextStyle?, kotlin/Function2?, kotlin/Function2?, kotlin/Function2?, kotlin/Function2?, kotlin/Boolean, androidx.compose.foundation.text/KeyboardOptions?, androidx.compose.foundation.text/KeyboardActions?, kotlin/Boolean, kotlin/Int, kotlin/Int, kotlin/Int, kotlin/Function1?, kotlin/Function4?, androidx.compose.foundation.interaction/MutableInteractionSource?, androidx.compose.ui.graphics/Shape?, androidx.compose.material/TextFieldColors?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int, kotlin/Int) // com.mohamedrejeb.richeditor.ui.material/OutlinedRichTextEditor|OutlinedRichTextEditor(com.mohamedrejeb.richeditor.model.RichTextState;androidx.compose.ui.Modifier?;kotlin.Boolean;kotlin.Boolean;androidx.compose.ui.text.TextStyle?;kotlin.Function2?;kotlin.Function2?;kotlin.Function2?;kotlin.Function2?;kotlin.Boolean;androidx.compose.foundation.text.KeyboardOptions?;androidx.compose.foundation.text.KeyboardActions?;kotlin.Boolean;kotlin.Int;kotlin.Int;kotlin.Int;kotlin.Function1?;kotlin.Function4?;androidx.compose.foundation.interaction.MutableInteractionSource?;androidx.compose.ui.graphics.Shape?;androidx.compose.material.TextFieldColors?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int;kotlin.Int){}[0] final fun com.mohamedrejeb.richeditor.ui.material/RichText(com.mohamedrejeb.richeditor.model/RichTextState, androidx.compose.ui/Modifier?, androidx.compose.ui.graphics/Color, androidx.compose.ui.unit/TextUnit, androidx.compose.ui.text.font/FontStyle?, androidx.compose.ui.text.font/FontWeight?, androidx.compose.ui.text.font/FontFamily?, androidx.compose.ui.unit/TextUnit, androidx.compose.ui.text.style/TextDecoration?, androidx.compose.ui.text.style/TextAlign, androidx.compose.ui.unit/TextUnit, androidx.compose.ui.text.style/TextOverflow, kotlin/Boolean, kotlin/Int, kotlin/Int, kotlin.collections/Map?, kotlin/Function1?, androidx.compose.ui.text/TextStyle?, com.mohamedrejeb.richeditor.model/ImageLoader?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // com.mohamedrejeb.richeditor.ui.material/RichText|RichText(com.mohamedrejeb.richeditor.model.RichTextState;androidx.compose.ui.Modifier?;androidx.compose.ui.graphics.Color;androidx.compose.ui.unit.TextUnit;androidx.compose.ui.text.font.FontStyle?;androidx.compose.ui.text.font.FontWeight?;androidx.compose.ui.text.font.FontFamily?;androidx.compose.ui.unit.TextUnit;androidx.compose.ui.text.style.TextDecoration?;androidx.compose.ui.text.style.TextAlign;androidx.compose.ui.unit.TextUnit;androidx.compose.ui.text.style.TextOverflow;kotlin.Boolean;kotlin.Int;kotlin.Int;kotlin.collections.Map?;kotlin.Function1?;androidx.compose.ui.text.TextStyle?;com.mohamedrejeb.richeditor.model.ImageLoader?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0] -final fun com.mohamedrejeb.richeditor.ui.material/RichTextEditor(com.mohamedrejeb.richeditor.model/RichTextState, androidx.compose.ui/Modifier?, kotlin/Boolean, kotlin/Boolean, androidx.compose.ui.text/TextStyle?, kotlin/Function2?, kotlin/Function2?, kotlin/Function2?, kotlin/Function2?, kotlin/Boolean, androidx.compose.foundation.text/KeyboardOptions?, androidx.compose.foundation.text/KeyboardActions?, kotlin/Boolean, kotlin/Int, kotlin/Int, kotlin/Int, kotlin/Function1?, androidx.compose.foundation.interaction/MutableInteractionSource?, androidx.compose.ui.graphics/Shape?, androidx.compose.material/TextFieldColors?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // com.mohamedrejeb.richeditor.ui.material/RichTextEditor|RichTextEditor(com.mohamedrejeb.richeditor.model.RichTextState;androidx.compose.ui.Modifier?;kotlin.Boolean;kotlin.Boolean;androidx.compose.ui.text.TextStyle?;kotlin.Function2?;kotlin.Function2?;kotlin.Function2?;kotlin.Function2?;kotlin.Boolean;androidx.compose.foundation.text.KeyboardOptions?;androidx.compose.foundation.text.KeyboardActions?;kotlin.Boolean;kotlin.Int;kotlin.Int;kotlin.Int;kotlin.Function1?;androidx.compose.foundation.interaction.MutableInteractionSource?;androidx.compose.ui.graphics.Shape?;androidx.compose.material.TextFieldColors?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0] +final fun com.mohamedrejeb.richeditor.ui.material/RichTextEditor(com.mohamedrejeb.richeditor.model/RichTextState, androidx.compose.ui/Modifier?, kotlin/Boolean, kotlin/Boolean, androidx.compose.ui.text/TextStyle?, kotlin/Function2?, kotlin/Function2?, kotlin/Function2?, kotlin/Function2?, kotlin/Boolean, androidx.compose.foundation.text/KeyboardOptions?, androidx.compose.foundation.text/KeyboardActions?, kotlin/Boolean, kotlin/Int, kotlin/Int, kotlin/Int, kotlin/Function1?, kotlin/Function4?, androidx.compose.foundation.interaction/MutableInteractionSource?, androidx.compose.ui.graphics/Shape?, androidx.compose.material/TextFieldColors?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int, kotlin/Int) // com.mohamedrejeb.richeditor.ui.material/RichTextEditor|RichTextEditor(com.mohamedrejeb.richeditor.model.RichTextState;androidx.compose.ui.Modifier?;kotlin.Boolean;kotlin.Boolean;androidx.compose.ui.text.TextStyle?;kotlin.Function2?;kotlin.Function2?;kotlin.Function2?;kotlin.Function2?;kotlin.Boolean;androidx.compose.foundation.text.KeyboardOptions?;androidx.compose.foundation.text.KeyboardActions?;kotlin.Boolean;kotlin.Int;kotlin.Int;kotlin.Int;kotlin.Function1?;kotlin.Function4?;androidx.compose.foundation.interaction.MutableInteractionSource?;androidx.compose.ui.graphics.Shape?;androidx.compose.material.TextFieldColors?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int;kotlin.Int){}[0] final fun com.mohamedrejeb.richeditor.ui.material3.tokens/com_mohamedrejeb_richeditor_ui_material3_tokens_FiledRichTextEditorTokens$stableprop_getter(): kotlin/Int // com.mohamedrejeb.richeditor.ui.material3.tokens/com_mohamedrejeb_richeditor_ui_material3_tokens_FiledRichTextEditorTokens$stableprop_getter|com_mohamedrejeb_richeditor_ui_material3_tokens_FiledRichTextEditorTokens$stableprop_getter(){}[0] final fun com.mohamedrejeb.richeditor.ui.material3.tokens/com_mohamedrejeb_richeditor_ui_material3_tokens_OutlinedRichTextEditorTokens$stableprop_getter(): kotlin/Int // com.mohamedrejeb.richeditor.ui.material3.tokens/com_mohamedrejeb_richeditor_ui_material3_tokens_OutlinedRichTextEditorTokens$stableprop_getter|com_mohamedrejeb_richeditor_ui_material3_tokens_OutlinedRichTextEditorTokens$stableprop_getter(){}[0] -final fun com.mohamedrejeb.richeditor.ui.material3/OutlinedRichTextEditor(com.mohamedrejeb.richeditor.model/RichTextState, androidx.compose.ui/Modifier?, kotlin/Boolean, kotlin/Boolean, androidx.compose.ui.text/TextStyle?, kotlin/Function2?, kotlin/Function2?, kotlin/Function2?, kotlin/Function2?, kotlin/Function2?, kotlin/Boolean, androidx.compose.foundation.text/KeyboardOptions?, androidx.compose.foundation.text/KeyboardActions?, kotlin/Boolean, kotlin/Int, kotlin/Int, kotlin/Int, kotlin/Function1?, kotlin/Function1?, androidx.compose.foundation.interaction/MutableInteractionSource?, androidx.compose.ui.graphics/Shape?, com.mohamedrejeb.richeditor.ui.material3/RichTextEditorColors?, androidx.compose.foundation.layout/PaddingValues?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int, kotlin/Int) // com.mohamedrejeb.richeditor.ui.material3/OutlinedRichTextEditor|OutlinedRichTextEditor(com.mohamedrejeb.richeditor.model.RichTextState;androidx.compose.ui.Modifier?;kotlin.Boolean;kotlin.Boolean;androidx.compose.ui.text.TextStyle?;kotlin.Function2?;kotlin.Function2?;kotlin.Function2?;kotlin.Function2?;kotlin.Function2?;kotlin.Boolean;androidx.compose.foundation.text.KeyboardOptions?;androidx.compose.foundation.text.KeyboardActions?;kotlin.Boolean;kotlin.Int;kotlin.Int;kotlin.Int;kotlin.Function1?;kotlin.Function1?;androidx.compose.foundation.interaction.MutableInteractionSource?;androidx.compose.ui.graphics.Shape?;com.mohamedrejeb.richeditor.ui.material3.RichTextEditorColors?;androidx.compose.foundation.layout.PaddingValues?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int;kotlin.Int){}[0] +final fun com.mohamedrejeb.richeditor.ui.material3/OutlinedRichTextEditor(com.mohamedrejeb.richeditor.model/RichTextState, androidx.compose.ui/Modifier?, kotlin/Boolean, kotlin/Boolean, androidx.compose.ui.text/TextStyle?, kotlin/Function2?, kotlin/Function2?, kotlin/Function2?, kotlin/Function2?, kotlin/Function2?, kotlin/Boolean, androidx.compose.foundation.text/KeyboardOptions?, androidx.compose.foundation.text/KeyboardActions?, kotlin/Boolean, kotlin/Int, kotlin/Int, kotlin/Int, kotlin/Function1?, kotlin/Function4?, kotlin/Function1?, androidx.compose.foundation.interaction/MutableInteractionSource?, androidx.compose.ui.graphics/Shape?, com.mohamedrejeb.richeditor.ui.material3/RichTextEditorColors?, androidx.compose.foundation.layout/PaddingValues?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int, kotlin/Int) // com.mohamedrejeb.richeditor.ui.material3/OutlinedRichTextEditor|OutlinedRichTextEditor(com.mohamedrejeb.richeditor.model.RichTextState;androidx.compose.ui.Modifier?;kotlin.Boolean;kotlin.Boolean;androidx.compose.ui.text.TextStyle?;kotlin.Function2?;kotlin.Function2?;kotlin.Function2?;kotlin.Function2?;kotlin.Function2?;kotlin.Boolean;androidx.compose.foundation.text.KeyboardOptions?;androidx.compose.foundation.text.KeyboardActions?;kotlin.Boolean;kotlin.Int;kotlin.Int;kotlin.Int;kotlin.Function1?;kotlin.Function4?;kotlin.Function1?;androidx.compose.foundation.interaction.MutableInteractionSource?;androidx.compose.ui.graphics.Shape?;com.mohamedrejeb.richeditor.ui.material3.RichTextEditorColors?;androidx.compose.foundation.layout.PaddingValues?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int;kotlin.Int){}[0] final fun com.mohamedrejeb.richeditor.ui.material3/RichText(com.mohamedrejeb.richeditor.model/RichTextState, androidx.compose.ui/Modifier?, androidx.compose.ui.graphics/Color, androidx.compose.ui.unit/TextUnit, androidx.compose.ui.text.font/FontStyle?, androidx.compose.ui.text.font/FontWeight?, androidx.compose.ui.text.font/FontFamily?, androidx.compose.ui.unit/TextUnit, androidx.compose.ui.text.style/TextDecoration?, androidx.compose.ui.text.style/TextAlign, androidx.compose.ui.unit/TextUnit, androidx.compose.ui.text.style/TextOverflow, kotlin/Boolean, kotlin/Int, kotlin.collections/Map?, kotlin/Function1?, androidx.compose.ui.text/TextStyle?, com.mohamedrejeb.richeditor.model/ImageLoader?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // com.mohamedrejeb.richeditor.ui.material3/RichText|RichText(com.mohamedrejeb.richeditor.model.RichTextState;androidx.compose.ui.Modifier?;androidx.compose.ui.graphics.Color;androidx.compose.ui.unit.TextUnit;androidx.compose.ui.text.font.FontStyle?;androidx.compose.ui.text.font.FontWeight?;androidx.compose.ui.text.font.FontFamily?;androidx.compose.ui.unit.TextUnit;androidx.compose.ui.text.style.TextDecoration?;androidx.compose.ui.text.style.TextAlign;androidx.compose.ui.unit.TextUnit;androidx.compose.ui.text.style.TextOverflow;kotlin.Boolean;kotlin.Int;kotlin.collections.Map?;kotlin.Function1?;androidx.compose.ui.text.TextStyle?;com.mohamedrejeb.richeditor.model.ImageLoader?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0] -final fun com.mohamedrejeb.richeditor.ui.material3/RichTextEditor(com.mohamedrejeb.richeditor.model/RichTextState, androidx.compose.ui/Modifier?, kotlin/Boolean, kotlin/Boolean, androidx.compose.ui.text/TextStyle?, kotlin/Function2?, kotlin/Function2?, kotlin/Function2?, kotlin/Function2?, kotlin/Function2?, kotlin/Boolean, androidx.compose.foundation.text/KeyboardOptions?, androidx.compose.foundation.text/KeyboardActions?, kotlin/Boolean, kotlin/Int, kotlin/Int, kotlin/Int, androidx.compose.foundation.interaction/MutableInteractionSource?, androidx.compose.ui.graphics/Shape?, com.mohamedrejeb.richeditor.ui.material3/RichTextEditorColors?, androidx.compose.foundation.layout/PaddingValues?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int, kotlin/Int) // com.mohamedrejeb.richeditor.ui.material3/RichTextEditor|RichTextEditor(com.mohamedrejeb.richeditor.model.RichTextState;androidx.compose.ui.Modifier?;kotlin.Boolean;kotlin.Boolean;androidx.compose.ui.text.TextStyle?;kotlin.Function2?;kotlin.Function2?;kotlin.Function2?;kotlin.Function2?;kotlin.Function2?;kotlin.Boolean;androidx.compose.foundation.text.KeyboardOptions?;androidx.compose.foundation.text.KeyboardActions?;kotlin.Boolean;kotlin.Int;kotlin.Int;kotlin.Int;androidx.compose.foundation.interaction.MutableInteractionSource?;androidx.compose.ui.graphics.Shape?;com.mohamedrejeb.richeditor.ui.material3.RichTextEditorColors?;androidx.compose.foundation.layout.PaddingValues?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int;kotlin.Int){}[0] +final fun com.mohamedrejeb.richeditor.ui.material3/RichTextEditor(com.mohamedrejeb.richeditor.model/RichTextState, androidx.compose.ui/Modifier?, kotlin/Boolean, kotlin/Boolean, androidx.compose.ui.text/TextStyle?, kotlin/Function2?, kotlin/Function2?, kotlin/Function2?, kotlin/Function2?, kotlin/Function2?, kotlin/Boolean, androidx.compose.foundation.text/KeyboardOptions?, androidx.compose.foundation.text/KeyboardActions?, kotlin/Boolean, kotlin/Int, kotlin/Int, kotlin/Int, kotlin/Function4?, androidx.compose.foundation.interaction/MutableInteractionSource?, androidx.compose.ui.graphics/Shape?, com.mohamedrejeb.richeditor.ui.material3/RichTextEditorColors?, androidx.compose.foundation.layout/PaddingValues?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int, kotlin/Int) // com.mohamedrejeb.richeditor.ui.material3/RichTextEditor|RichTextEditor(com.mohamedrejeb.richeditor.model.RichTextState;androidx.compose.ui.Modifier?;kotlin.Boolean;kotlin.Boolean;androidx.compose.ui.text.TextStyle?;kotlin.Function2?;kotlin.Function2?;kotlin.Function2?;kotlin.Function2?;kotlin.Function2?;kotlin.Boolean;androidx.compose.foundation.text.KeyboardOptions?;androidx.compose.foundation.text.KeyboardActions?;kotlin.Boolean;kotlin.Int;kotlin.Int;kotlin.Int;kotlin.Function4?;androidx.compose.foundation.interaction.MutableInteractionSource?;androidx.compose.ui.graphics.Shape?;com.mohamedrejeb.richeditor.ui.material3.RichTextEditorColors?;androidx.compose.foundation.layout.PaddingValues?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int;kotlin.Int){}[0] final fun com.mohamedrejeb.richeditor.ui.material3/com_mohamedrejeb_richeditor_ui_material3_RichTextEditorColors$stableprop_getter(): kotlin/Int // com.mohamedrejeb.richeditor.ui.material3/com_mohamedrejeb_richeditor_ui_material3_RichTextEditorColors$stableprop_getter|com_mohamedrejeb_richeditor_ui_material3_RichTextEditorColors$stableprop_getter(){}[0] final fun com.mohamedrejeb.richeditor.ui.material3/com_mohamedrejeb_richeditor_ui_material3_RichTextEditorDefaults$stableprop_getter(): kotlin/Int // com.mohamedrejeb.richeditor.ui.material3/com_mohamedrejeb_richeditor_ui_material3_RichTextEditorDefaults$stableprop_getter|com_mohamedrejeb_richeditor_ui_material3_RichTextEditorDefaults$stableprop_getter(){}[0] final fun com.mohamedrejeb.richeditor.ui/BasicRichText(com.mohamedrejeb.richeditor.model/RichTextState, androidx.compose.ui/Modifier?, androidx.compose.ui.text/TextStyle?, kotlin/Function1?, androidx.compose.ui.text.style/TextOverflow, kotlin/Boolean, kotlin/Int, kotlin/Int, kotlin.collections/Map?, com.mohamedrejeb.richeditor.model/ImageLoader?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.mohamedrejeb.richeditor.ui/BasicRichText|BasicRichText(com.mohamedrejeb.richeditor.model.RichTextState;androidx.compose.ui.Modifier?;androidx.compose.ui.text.TextStyle?;kotlin.Function1?;androidx.compose.ui.text.style.TextOverflow;kotlin.Boolean;kotlin.Int;kotlin.Int;kotlin.collections.Map?;com.mohamedrejeb.richeditor.model.ImageLoader?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] -final fun com.mohamedrejeb.richeditor.ui/BasicRichTextEditor(com.mohamedrejeb.richeditor.model/RichTextState, androidx.compose.ui/Modifier?, kotlin/Boolean, kotlin/Boolean, androidx.compose.ui.text/TextStyle?, androidx.compose.foundation.text/KeyboardOptions?, androidx.compose.foundation.text/KeyboardActions?, kotlin/Boolean, kotlin/Boolean, kotlin/Int, kotlin/Int, kotlin/Int, kotlin/Function1?, kotlin/Function1?, androidx.compose.foundation.interaction/MutableInteractionSource?, androidx.compose.ui.graphics/Brush?, kotlin/Function3, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>?, androidx.compose.foundation.layout/PaddingValues, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // com.mohamedrejeb.richeditor.ui/BasicRichTextEditor|BasicRichTextEditor(com.mohamedrejeb.richeditor.model.RichTextState;androidx.compose.ui.Modifier?;kotlin.Boolean;kotlin.Boolean;androidx.compose.ui.text.TextStyle?;androidx.compose.foundation.text.KeyboardOptions?;androidx.compose.foundation.text.KeyboardActions?;kotlin.Boolean;kotlin.Boolean;kotlin.Int;kotlin.Int;kotlin.Int;kotlin.Function1?;kotlin.Function1?;androidx.compose.foundation.interaction.MutableInteractionSource?;androidx.compose.ui.graphics.Brush?;kotlin.Function3,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>?;androidx.compose.foundation.layout.PaddingValues;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0] -final fun com.mohamedrejeb.richeditor.ui/BasicRichTextEditor(com.mohamedrejeb.richeditor.model/RichTextState, androidx.compose.ui/Modifier?, kotlin/Boolean, kotlin/Boolean, androidx.compose.ui.text/TextStyle?, androidx.compose.foundation.text/KeyboardOptions?, androidx.compose.foundation.text/KeyboardActions?, kotlin/Boolean, kotlin/Int, kotlin/Int, kotlin/Int, kotlin/Function1?, kotlin/Function1?, androidx.compose.foundation.interaction/MutableInteractionSource?, androidx.compose.ui.graphics/Brush?, kotlin/Function3, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // com.mohamedrejeb.richeditor.ui/BasicRichTextEditor|BasicRichTextEditor(com.mohamedrejeb.richeditor.model.RichTextState;androidx.compose.ui.Modifier?;kotlin.Boolean;kotlin.Boolean;androidx.compose.ui.text.TextStyle?;androidx.compose.foundation.text.KeyboardOptions?;androidx.compose.foundation.text.KeyboardActions?;kotlin.Boolean;kotlin.Int;kotlin.Int;kotlin.Int;kotlin.Function1?;kotlin.Function1?;androidx.compose.foundation.interaction.MutableInteractionSource?;androidx.compose.ui.graphics.Brush?;kotlin.Function3,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0] +final fun com.mohamedrejeb.richeditor.ui/BasicRichTextEditor(com.mohamedrejeb.richeditor.model/RichTextState, androidx.compose.ui/Modifier?, kotlin/Boolean, kotlin/Boolean, androidx.compose.ui.text/TextStyle?, androidx.compose.foundation.text/KeyboardOptions?, androidx.compose.foundation.text/KeyboardActions?, kotlin/Boolean, kotlin/Boolean, kotlin/Int, kotlin/Int, kotlin/Int, kotlin/Function1?, kotlin/Function4?, kotlin/Function1?, androidx.compose.foundation.interaction/MutableInteractionSource?, androidx.compose.ui.graphics/Brush?, kotlin/Function3, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>?, androidx.compose.foundation.layout/PaddingValues, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // com.mohamedrejeb.richeditor.ui/BasicRichTextEditor|BasicRichTextEditor(com.mohamedrejeb.richeditor.model.RichTextState;androidx.compose.ui.Modifier?;kotlin.Boolean;kotlin.Boolean;androidx.compose.ui.text.TextStyle?;androidx.compose.foundation.text.KeyboardOptions?;androidx.compose.foundation.text.KeyboardActions?;kotlin.Boolean;kotlin.Boolean;kotlin.Int;kotlin.Int;kotlin.Int;kotlin.Function1?;kotlin.Function4?;kotlin.Function1?;androidx.compose.foundation.interaction.MutableInteractionSource?;androidx.compose.ui.graphics.Brush?;kotlin.Function3,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>?;androidx.compose.foundation.layout.PaddingValues;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0] +final fun com.mohamedrejeb.richeditor.ui/BasicRichTextEditor(com.mohamedrejeb.richeditor.model/RichTextState, androidx.compose.ui/Modifier?, kotlin/Boolean, kotlin/Boolean, androidx.compose.ui.text/TextStyle?, androidx.compose.foundation.text/KeyboardOptions?, androidx.compose.foundation.text/KeyboardActions?, kotlin/Boolean, kotlin/Int, kotlin/Int, kotlin/Int, kotlin/Function1?, kotlin/Function4?, kotlin/Function1?, androidx.compose.foundation.interaction/MutableInteractionSource?, androidx.compose.ui.graphics/Brush?, kotlin/Function3, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // com.mohamedrejeb.richeditor.ui/BasicRichTextEditor|BasicRichTextEditor(com.mohamedrejeb.richeditor.model.RichTextState;androidx.compose.ui.Modifier?;kotlin.Boolean;kotlin.Boolean;androidx.compose.ui.text.TextStyle?;androidx.compose.foundation.text.KeyboardOptions?;androidx.compose.foundation.text.KeyboardActions?;kotlin.Boolean;kotlin.Int;kotlin.Int;kotlin.Int;kotlin.Function1?;kotlin.Function4?;kotlin.Function1?;androidx.compose.foundation.interaction.MutableInteractionSource?;androidx.compose.ui.graphics.Brush?;kotlin.Function3,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0] final fun com.mohamedrejeb.richeditor.ui/com_mohamedrejeb_richeditor_ui_RichTextClipboardManager$stableprop_getter(): kotlin/Int // com.mohamedrejeb.richeditor.ui/com_mohamedrejeb_richeditor_ui_RichTextClipboardManager$stableprop_getter|com_mohamedrejeb_richeditor_ui_RichTextClipboardManager$stableprop_getter(){}[0] +final fun com.mohamedrejeb.richeditor.utils/com_mohamedrejeb_richeditor_utils_WordSegment$stableprop_getter(): kotlin/Int // com.mohamedrejeb.richeditor.utils/com_mohamedrejeb_richeditor_utils_WordSegment$stableprop_getter|com_mohamedrejeb_richeditor_utils_WordSegment$stableprop_getter(){}[0] diff --git a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/model/RichSpan.kt b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/model/RichSpan.kt index 81476da9..b1f89693 100644 --- a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/model/RichSpan.kt +++ b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/model/RichSpan.kt @@ -9,12 +9,10 @@ import com.mohamedrejeb.richeditor.annotation.ExperimentalRichTextApi import com.mohamedrejeb.richeditor.paragraph.RichParagraph import com.mohamedrejeb.richeditor.utils.customMerge import com.mohamedrejeb.richeditor.utils.isSpecifiedFieldsEquals -import kotlin.collections.indices /** * A rich span is a part of a rich paragraph. */ -@OptIn(ExperimentalRichTextApi::class) internal class RichSpan( internal val key: Int? = null, val children: MutableList = mutableListOf(), 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 ad6305e2..cf67558d 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 @@ -5,6 +5,7 @@ import androidx.compose.runtime.* import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.listSaver import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.isSpecified @@ -26,8 +27,11 @@ import com.mohamedrejeb.richeditor.parser.html.RichTextStateHtmlParser import com.mohamedrejeb.richeditor.parser.markdown.RichTextStateMarkdownParser import com.mohamedrejeb.richeditor.utils.* import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.* import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.launch import kotlin.math.absoluteValue import kotlin.math.max @@ -169,7 +173,7 @@ public class RichTextState internal constructor( currentAppliedRichSpanStyle } - internal var styledRichSpanList = mutableStateListOf() + internal var styledRichSpanList: SnapshotStateList = mutableStateListOf() private set private var currentAppliedParagraphStyle: ParagraphStyle by mutableStateOf( @@ -1254,6 +1258,24 @@ public class RichTextState internal constructor( styledRichSpanList.addAll(newStyledRichSpanList) } + /** + * Traverses the tree of paragraphs and spans to assemble + * a complete list of [RichSpan] objects. + * + * @return A complete list of [RichSpan] + */ + public fun getAllRichSpans(): List> { + return richParagraphList.flatMap { richParagraph -> + richParagraph.children.flatMap { getAllRichSpans(it) } + } + } + + private fun getAllRichSpans(target: RichSpan): List> { + return listOf(Pair(target.richSpanStyle, target.textRange)) + target.children.flatMap { richSpan -> + richSpan.children.flatMap { getAllRichSpans(it) } + } + } + /** * Handles adding characters to the text field. * This method will update the [richParagraphList] to reflect the new changes. @@ -1743,24 +1765,24 @@ public class RichTextState internal constructor( // If the new paragraph is empty apply style depending on the config if (tempTextFieldValue.selection.collapsed && newParagraph.isEmpty()) { - val newParagraphFirstRichSpan = newParagraph.getFirstNonEmptyChild() - - val isSelectionAtNewRichSpan = - newParagraphFirstRichSpan?.textRange?.min == tempTextFieldValue.selection.min - 1 - - // Check if the cursor is at the new paragraph - if ( - (!config.preserveStyleOnEmptyLine || richSpan.paragraph.isEmpty()) && - isSelectionAtNewRichSpan - ) { - newParagraphFirstRichSpan.spanStyle = SpanStyle() - newParagraphFirstRichSpan.richSpanStyle = RichSpanStyle.Default - } else if ( - config.preserveStyleOnEmptyLine && - isSelectionAtNewRichSpan - ) { - newParagraphFirstRichSpan.spanStyle = currentSpanStyle - newParagraphFirstRichSpan.richSpanStyle = currentRichSpanStyle + newParagraph.getFirstNonEmptyChild()?.let { newParagraphFirstRichSpan -> + val isSelectionAtNewRichSpan = + newParagraphFirstRichSpan.textRange.min == tempTextFieldValue.selection.min - 1 + + // Check if the cursor is at the new paragraph + if ( + (!config.preserveStyleOnEmptyLine || richSpan.paragraph.isEmpty()) && + isSelectionAtNewRichSpan + ) { + newParagraphFirstRichSpan.spanStyle = SpanStyle() + newParagraphFirstRichSpan.richSpanStyle = RichSpanStyle.Default + } else if ( + config.preserveStyleOnEmptyLine && + isSelectionAtNewRichSpan + ) { + newParagraphFirstRichSpan.spanStyle = currentSpanStyle + newParagraphFirstRichSpan.richSpanStyle = currentRichSpanStyle + } } } @@ -2731,7 +2753,7 @@ public class RichTextState internal constructor( return richSpan } - private fun getRichSpanByOffset(offset: Offset): RichSpan? { + internal fun getRichSpanByOffset(offset: Offset): RichSpan? { this.textLayoutResult?.let { textLayoutResult -> val position = textLayoutResult.getOffsetForPosition(offset) return getRichSpanByTextIndex(position, true) @@ -3184,6 +3206,14 @@ public class RichTextState internal constructor( } } + /** + * Returns a sequence of word segments. A "word" is defined as a contiguous string + * of letters or numbers. + * + * @return A sequence of [WordSegment] + */ + public fun getWords(): Sequence = richParagraphList.getWords() + /** * Returns the [RichTextState] as a text string. * diff --git a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/paragraph/type/ParagraphType.kt b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/paragraph/type/ParagraphType.kt index 7e40deba..060f9a9c 100644 --- a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/paragraph/type/ParagraphType.kt +++ b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/paragraph/type/ParagraphType.kt @@ -15,6 +15,6 @@ internal interface ParagraphType { fun copy(): ParagraphType companion object { - val ParagraphType.startText : String get() = startRichSpan.text + public val ParagraphType.startText : String get() = startRichSpan.text } } \ No newline at end of file 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 a6e6ed56..155beb6d 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 @@ -9,20 +9,31 @@ import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.LocalTextStyle -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier +import androidx.compose.ui.composed 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.input.pointer.* import androidx.compose.ui.platform.* +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.text.TextLayoutResult +import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.LayoutDirection +import com.mohamedrejeb.richeditor.model.RichSpanStyle import com.mohamedrejeb.richeditor.model.RichTextState import kotlinx.coroutines.CoroutineScope @@ -62,6 +73,7 @@ import kotlinx.coroutines.CoroutineScope * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true. * @param maxLength the maximum length of the text field. If the text is longer than this value, * it will be ignored. The default value of this parameter is [Int.MAX_VALUE]. + * @param onRichSpanClick A callback to allow handling of click on RichSpans. * @param onTextLayout Callback that is executed when a new text layout is calculated. A * [TextLayoutResult] object that callback provides contains paragraph information, size of the * text, baselines and other details. The callback can be used to add additional decoration or @@ -94,6 +106,7 @@ public fun BasicRichTextEditor( minLines: Int = 1, maxLength: Int = Int.MAX_VALUE, onRichTextChangedListener: RichTextChangedListener? = null, + onRichSpanClick: RichSpanClickListener? = null, onTextLayout: (TextLayoutResult) -> Unit = {}, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, cursorBrush: Brush = SolidColor(Color.Black), @@ -113,6 +126,7 @@ public fun BasicRichTextEditor( minLines = minLines, maxLength = maxLength, onRichTextChangedListener = onRichTextChangedListener, + onRichSpanClick = onRichSpanClick, onTextLayout = onTextLayout, interactionSource = interactionSource, cursorBrush = cursorBrush, @@ -156,6 +170,7 @@ public fun BasicRichTextEditor( * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true. * @param maxLength the maximum length of the text field. If the text is longer than this value, * it will be ignored. The default value of this parameter is [Int.MAX_VALUE]. + * @param onRichSpanClick A callback to allow handling of click on RichSpans. * @param onTextLayout Callback that is executed when a new text layout is calculated. A * [TextLayoutResult] object that callback provides contains paragraph information, size of the * text, baselines and other details. The callback can be used to add additional decoration or @@ -189,6 +204,7 @@ public fun BasicRichTextEditor( minLines: Int = 1, maxLength: Int = Int.MAX_VALUE, onRichTextChangedListener: RichTextChangedListener? = null, + onRichSpanClick: RichSpanClickListener? = null, onTextLayout: (TextLayoutResult) -> Unit = {}, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, cursorBrush: Brush = SolidColor(Color.Black), @@ -219,7 +235,9 @@ public fun BasicRichTextEditor( if (interaction is PressInteraction.Press) { val pressPosition = interaction.pressPosition val topPadding = with(density) { contentPadding.calculateTopPadding().toPx() } - val startPadding = with(density) { contentPadding.calculateStartPadding(layoutDirection).toPx() } + val startPadding = with(density) { + contentPadding.calculateStartPadding(layoutDirection).toPx() + } adjustTextIndicatorOffset( pressPosition = pressPosition, @@ -249,6 +267,20 @@ public fun BasicRichTextEditor( topPadding = with(density) { contentPadding.calculateTopPadding().toPx() }, startPadding = with(density) { contentPadding.calculateStartPadding(layoutDirection).toPx() }, ) + .handleInteractions(onRichSpanClick != null) { type, offset -> + val topPadding = with(density) { contentPadding.calculateTopPadding().toPx() } + val startPadding = with(density) { contentPadding.calculateStartPadding(layoutDirection).toPx() } + val localPosition = offset - Offset(x = startPadding, y = topPadding) + + state.getRichSpanByOffset(localPosition)?.let { clickedSpan -> + onRichSpanClick?.invoke( + clickedSpan.richSpanStyle, + clickedSpan.textRange, + offset, + type + ) + } ?: false + } .then( if (!readOnly) Modifier @@ -318,3 +350,4 @@ internal suspend fun adjustTextIndicatorOffset( } public typealias RichTextChangedListener = (RichTextState) -> Unit +public typealias RichSpanClickListener = (RichSpanStyle, TextRange, Offset, InteractionType) -> Boolean diff --git a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/ui/handleInteractions.kt b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/ui/handleInteractions.kt new file mode 100644 index 00000000..4e177190 --- /dev/null +++ b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/ui/handleInteractions.kt @@ -0,0 +1,69 @@ +package com.mohamedrejeb.richeditor.ui + +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.PointerType +import androidx.compose.ui.input.pointer.isPrimaryPressed +import androidx.compose.ui.input.pointer.isSecondaryPressed +import androidx.compose.ui.input.pointer.pointerInput + +public enum class InteractionType { PrimaryClick, SecondaryClick, Tap, DoubleTap } + +/** + * Provide a unified callback for listening for different types of interactions + */ +internal fun Modifier.handleInteractions( + enabled: Boolean = true, + onInteraction: ((InteractionType, Offset) -> Boolean)? = null +): Modifier = composed { + this + .pointerInput(enabled) { + awaitPointerEventScope { + while (true) { + val event = awaitPointerEvent(PointerEventPass.Main) + if (!enabled) continue + + if (event.type == PointerEventType.Press) { + val eventChange = event.changes.first() + val position = eventChange.position + + when (eventChange.type) { + PointerType.Touch -> { + onInteraction?.invoke( + InteractionType.Tap, + eventChange.position + ) + } + + PointerType.Mouse -> { + if (event.buttons.isPrimaryPressed) { + val consumed = + onInteraction?.invoke( + InteractionType.PrimaryClick, + position + ) + ?: false + if (consumed) { + event.changes.forEach { it.consume() } + } + } else if (event.buttons.isSecondaryPressed) { + val consumed = + onInteraction?.invoke( + InteractionType.SecondaryClick, + position + ) + ?: false + if (consumed) { + event.changes.forEach { it.consume() } + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/ui/material/OutlinedRichTextEditor.kt b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/ui/material/OutlinedRichTextEditor.kt index 290291fb..cf8ee391 100644 --- a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/ui/material/OutlinedRichTextEditor.kt +++ b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/ui/material/OutlinedRichTextEditor.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.unit.dp import com.mohamedrejeb.richeditor.model.RichTextState import com.mohamedrejeb.richeditor.ui.BasicRichTextEditor import com.mohamedrejeb.richeditor.ui.RichTextChangedListener +import com.mohamedrejeb.richeditor.ui.RichSpanClickListener /** * Material Design outlined rich text field @@ -68,6 +69,7 @@ import com.mohamedrejeb.richeditor.ui.RichTextChangedListener * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true. * @param onRichTextChangedListener A callback when the RichTextState changes. + * @param onRichSpanClick A callback to allow handling of click on RichSpans. * @param interactionSource the [MutableInteractionSource] representing the stream of * [Interaction]s for this OutlinedTextField. You can create and pass in your own remembered * [MutableInteractionSource] if you want to observe [Interaction]s and customize the @@ -96,6 +98,7 @@ public fun OutlinedRichTextEditor( minLines: Int = 1, maxLength: Int = Int.MAX_VALUE, onRichTextChangedListener: RichTextChangedListener? = null, + onRichSpanClick: RichSpanClickListener? = null, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, shape: Shape = MaterialTheme.shapes.small, colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors() @@ -135,6 +138,7 @@ public fun OutlinedRichTextEditor( minLines = minLines, maxLength = maxLength, onRichTextChangedListener = onRichTextChangedListener, + onRichSpanClick = onRichSpanClick, decorationBox = @Composable { innerTextField -> TextFieldDefaults.OutlinedTextFieldDecorationBox( value = state.textFieldValue.text, diff --git a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/ui/material/RichTextEditor.kt b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/ui/material/RichTextEditor.kt index 56629e00..5895b638 100644 --- a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/ui/material/RichTextEditor.kt +++ b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/ui/material/RichTextEditor.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.text.input.KeyboardType import com.mohamedrejeb.richeditor.model.RichTextState import com.mohamedrejeb.richeditor.ui.BasicRichTextEditor import com.mohamedrejeb.richeditor.ui.RichTextChangedListener +import com.mohamedrejeb.richeditor.ui.RichSpanClickListener import com.mohamedrejeb.richeditor.ui.material3.RichTextEditor /** @@ -65,6 +66,7 @@ import com.mohamedrejeb.richeditor.ui.material3.RichTextEditor * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true. * @param minLines the minimum height in terms of minimum number of visible lines. It is required * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true. + * @param onRichSpanClick A callback to allow handling of click on RichSpans. * @param interactionSource the [MutableInteractionSource] representing the stream of * [Interaction]s for this TextField. You can create and pass in your own remembered * [MutableInteractionSource] if you want to observe [Interaction]s and customize the @@ -100,6 +102,7 @@ public fun RichTextEditor( minLines: Int = 1, maxLength: Int = Int.MAX_VALUE, onRichTextChangedListener: RichTextChangedListener? = null, + onRichSpanClick: RichSpanClickListener? = null, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, shape: Shape = MaterialTheme.shapes.small.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize), @@ -133,6 +136,7 @@ public fun RichTextEditor( minLines = minLines, maxLength = maxLength, onRichTextChangedListener = onRichTextChangedListener, + onRichSpanClick = onRichSpanClick, decorationBox = @Composable { innerTextField -> // places leading icon, text field with label and placeholder, trailing icon TextFieldDefaults.TextFieldDecorationBox( diff --git a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/ui/material3/OutlinedRichTextEditor.kt b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/ui/material3/OutlinedRichTextEditor.kt index 8a3d7073..1aaa002b 100644 --- a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/ui/material3/OutlinedRichTextEditor.kt +++ b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/ui/material3/OutlinedRichTextEditor.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.unit.* import com.mohamedrejeb.richeditor.model.RichTextState import com.mohamedrejeb.richeditor.ui.BasicRichTextEditor import com.mohamedrejeb.richeditor.ui.RichTextChangedListener +import com.mohamedrejeb.richeditor.ui.RichSpanClickListener import kotlin.math.max import kotlin.math.roundToInt @@ -78,6 +79,8 @@ import kotlin.math.roundToInt * @param maxLength the maximum length of the text field. If the text is longer than this value, * it will be ignored. The default value of this parameter is [Int.MAX_VALUE]. * onTextLayout + * @param onRichSpanClick A callback to allow handling of click on RichSpans. + * @param RichTextChangedListener A callback to allow handling text changes. * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s * for this text field. You can create and pass in your own `remember`ed instance to observe * [Interaction]s and customize the appearance / behavior of this text field in different states. @@ -107,6 +110,7 @@ public fun OutlinedRichTextEditor( minLines: Int = 1, maxLength: Int = Int.MAX_VALUE, onRichTextChangedListener: RichTextChangedListener? = null, + onRichSpanClick: RichSpanClickListener? = null, onTextLayout: (TextLayoutResult) -> Unit = {}, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, shape: Shape = RichTextEditorDefaults.outlinedShape, @@ -148,6 +152,7 @@ public fun OutlinedRichTextEditor( minLines = minLines, maxLength = maxLength, onRichTextChangedListener = onRichTextChangedListener, + onRichSpanClick = onRichSpanClick, onTextLayout = onTextLayout, decorationBox = { innerTextField -> RichTextEditorDefaults.OutlinedRichTextEditorDecorationBox( diff --git a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/ui/material3/RichTextEditor.kt b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/ui/material3/RichTextEditor.kt index f9ae4908..819ab1b2 100644 --- a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/ui/material3/RichTextEditor.kt +++ b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/ui/material3/RichTextEditor.kt @@ -9,7 +9,6 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.selection.LocalTextSelectionColors import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.* -import androidx.compose.material3.Typography import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.remember @@ -21,7 +20,6 @@ import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.takeOrElse import androidx.compose.ui.layout.* -import androidx.compose.ui.layout.layoutId import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.ImeAction @@ -29,6 +27,7 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.* import com.mohamedrejeb.richeditor.model.RichTextState import com.mohamedrejeb.richeditor.ui.BasicRichTextEditor +import com.mohamedrejeb.richeditor.ui.RichSpanClickListener import kotlin.math.max import kotlin.math.roundToInt @@ -76,6 +75,7 @@ import kotlin.math.roundToInt * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true. * @param maxLength the maximum length of the text field. If the text is longer than this value, * it will be ignored. The default value of this parameter is [Int.MAX_VALUE]. + * @param onRichSpanClick A callback to allow handling of click on RichSpans. * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s * for this text field. You can create and pass in your own `remember`ed instance to observe * [Interaction]s and customize the appearance / behavior of this text field in different states. @@ -104,6 +104,7 @@ public fun RichTextEditor( maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, minLines: Int = 1, maxLength: Int = Int.MAX_VALUE, + onRichSpanClick: RichSpanClickListener? = null, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, shape: Shape = RichTextEditorDefaults.filledShape, colors: RichTextEditorColors = RichTextEditorDefaults.richTextEditorColors(), @@ -137,6 +138,7 @@ public fun RichTextEditor( maxLines = maxLines, minLines = minLines, maxLength = maxLength, + onRichSpanClick = onRichSpanClick, interactionSource = interactionSource, cursorBrush = SolidColor(colors.cursorColor(isError).value), decorationBox = @Composable { innerTextField -> diff --git a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/utils/WordSegmentationUtils.kt b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/utils/WordSegmentationUtils.kt new file mode 100644 index 00000000..003bb184 --- /dev/null +++ b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/utils/WordSegmentationUtils.kt @@ -0,0 +1,174 @@ +package com.mohamedrejeb.richeditor.utils + +import androidx.compose.ui.text.TextRange +import com.mohamedrejeb.richeditor.model.RichSpan +import com.mohamedrejeb.richeditor.paragraph.RichParagraph + +public data class WordSegment( + val text: String, + val range: TextRange, +) + +private data class WordSplitState( + var currentOffset: Int = 0, + var pendingWord: StringBuilder = StringBuilder(), + var pendingStartIndex: Int = -1, +) + +/** + * Return a sequence of [WordSegment] and their [TextRange] for all words in the list of + * [RichParagraph] and their nested [RichSpan] children. + * + * @receiver List The list of paragraphs to process. + * @return A sequence of WordSegment + */ +internal fun List.getWords(): Sequence = sequence { + var currentOffset = 0 + for (paragraph in this@getWords) { + // Individual words may not cross a RichParagraph boundary, + // so a new WordSplitState is created for each Paragraph. + val state = WordSplitState(currentOffset) + yieldAll(paragraph.getWords(state)) + currentOffset += paragraph.getTotalLength() + } +} + +/** + * Extracts and yields [WordSegment]s from the tree of [RichSpan] objects within a [RichParagraph]. + * + * @receiver [RichParagraph] The paragraph containing RichSpan children to process. + * @param state [WordSplitState] Tracks partial words and their starting indices across spans. + * @return A sequence of WordSegment objects + * + * This function iterates through the RichSpan children of the paragraph, yielding + * word segments from each. After processing all children, it checks for any remaining + * partial word in the state and yields it if present. + */ +private fun RichParagraph.getWords(state: WordSplitState): Sequence = sequence { + for (span in children) { + yieldAll(span.getWords(state)) + } + state.apply { + // If there is a pending word after all children are processed, yield it. + if (pendingWord.isNotEmpty()) { + yield(WordSegment(pendingWord.toString(), TextRange(pendingStartIndex, currentOffset))) + pendingWord.clear() + pendingStartIndex = -1 + } + } +} + +/** + * Extracts and yields [WordSegment]s and their [TextRange]s from this sub-tree of [RichSpan] objects. + * + * @receiver [RichSpan] The current node in the tree being processed. + * @param state [WordSplitState] Tracks partial words and their starting indices across spans. + * @return A sequence of WordSegments + * + * This function identifies word boundaries in the text of the current [RichSpan], using + * alphanumeric characters as word components and non-alphanumeric characters as delimiters. + * It handles words spanning multiple nodes by leveraging the provided state and processes + * child nodes recursively, yielding their results as part of the sequence. + */ +private fun RichSpan.getWords(state: WordSplitState): Sequence = sequence { + state.apply { + // Process the current span's text if it's not empty. + if (text.isNotEmpty()) { + var localStartIndex = -1 + // Search this spans text for boundaries + for (i in text.indices) { + if (text[i].isLetterOrDigit()) { + // Starting a new word + if (localStartIndex == -1) { + localStartIndex = i + } + } else { + // Calculate the WordSegment TextRange, yield the segment + // and then clear the pending word + suspend fun SequenceScope.returnPendingWord() { + yield( + WordSegment( + pendingWord.toString(), + TextRange(pendingStartIndex, currentOffset + i) + ) + ) + pendingWord.clear() + pendingStartIndex = -1 + } + + // Ending a word + if (localStartIndex != -1) { + // Ending a words that is at least partially in this RichSpan + val word = text.substring(localStartIndex, i) + if (pendingWord.isNotEmpty()) { + // Continue the word from the previous state + pendingWord.append(word) + returnPendingWord() + } else { + yield( + WordSegment( + word, + TextRange(currentOffset + localStartIndex, currentOffset + i) + ) + ) + } + localStartIndex = -1 + } else if (state.pendingStartIndex != -1) { // Ending a word wholly in a previous span + // Word from previous span is ending, nothing in this span to append + if (pendingWord.isNotEmpty()) { + returnPendingWord() + } + } + } + } + + // Handle pending characters at the end of this RichSpan + if (localStartIndex != -1) { + val word = text.substring(localStartIndex) + if (pendingWord.isNotEmpty()) { + // Continue the word from the previous state + pendingWord.append(word) + pendingStartIndex = + if (pendingStartIndex == -1) currentOffset + localStartIndex else pendingStartIndex + } else { + pendingWord.append(word) + pendingStartIndex = currentOffset + localStartIndex + } + } + currentOffset += text.length + } + + // Process each child recursively. + if (children.isNotEmpty()) { + for (child in children) { + yieldAll(child.getWords(state)) + } + } + } +} + +/** + * Calculate the total length of a RichSpan, including all nested children. + */ +private fun RichSpan.getTotalLength(): Int { + var totalLength = text.length + + for (child in children) { + totalLength += child.getTotalLength() + } + return totalLength +} + +/** + * Calculate the total length of a RichParagraph, including all nested spans. + */ +private fun RichParagraph.getTotalLength(): Int { + var totalLength = 1 // Each paragraph counts as a new line, 1 character + if (children.isNotEmpty()) { + for (span in children) { + totalLength += span.getTotalLength() + } + } + + return totalLength +} diff --git a/richeditor-compose/src/commonTest/kotlin/com.mohamedrejeb.richeditor/utils/WordSegmentationUtilsTest.kt b/richeditor-compose/src/commonTest/kotlin/com.mohamedrejeb.richeditor/utils/WordSegmentationUtilsTest.kt new file mode 100644 index 00000000..15edca1e --- /dev/null +++ b/richeditor-compose/src/commonTest/kotlin/com.mohamedrejeb.richeditor/utils/WordSegmentationUtilsTest.kt @@ -0,0 +1,91 @@ +package com.mohamedrejeb.richeditor.utils + +import com.mohamedrejeb.richeditor.annotation.ExperimentalRichTextApi +import com.mohamedrejeb.richeditor.model.RichSpan +import com.mohamedrejeb.richeditor.paragraph.RichParagraph +import kotlin.test.Test +import kotlin.test.assertEquals + +class WordSegmentationUtilsTest { + @Test + fun testRichParagraphWordSequence() { + + val p1 = RichParagraph() + p1.children.addAll( + mutableListOf( + t(p1, "Hello world"), + t(p1, " Kotlin is great!"), + t( + p1, "", + t(p1, " C"), + t(p1, "at"), + ), + ) + ) + + val p2 = RichParagraph() + val nestedSpan = t(p2, "", t(p2, "Nested words here")) + p2.children.addAll(mutableListOf(nestedSpan)) + + val paragraphs = listOf(p1, p2) + + verify( + paragraphs, + "Hello", "world", "Kotlin", "is", "great", "Cat", "Nested", "words", "here" + ) + } + + @Test + fun testRichParagraphWordSequenceTwo() { + val p = RichParagraph() + + val span1 = t( + p, "RichTextEditor", + t( + p, " is a ", + t(p, "composable"), + t(p, " that allows you to edit "), + t(p, "rich text"), + t(p, " content."), + ) + ) + p.children.add(span1) + + val paragraphs = listOf(p) + + verify( + paragraphs, + "RichTextEditor", + "is", + "a", + "composable", + "that", + "allows", + "you", + "to", + "edit", + "rich", + "text", + "content" + ) + } + + private fun verify(paragraphs: List, vararg expectedWords: String) { + // Get all words from the list of paragraphs + var ii = 0 + for ((word, range) in paragraphs.getWords()) { + //println("Word: '$word' at range: [${range.start}, ${range.end})") + assertEquals(expectedWords[ii], word) + ++ii + } + } + + @OptIn(ExperimentalRichTextApi::class) + private fun t(p: RichParagraph, text: String, vararg children: RichSpan): RichSpan { + return RichSpan( + text = text, + paragraph = p, + children = mutableListOf(*children), + ) + } +} \ No newline at end of file diff --git a/sample/common/build.gradle.kts b/sample/common/build.gradle.kts index ff47a336..b19da0b1 100644 --- a/sample/common/build.gradle.kts +++ b/sample/common/build.gradle.kts @@ -64,6 +64,10 @@ kotlin { // Ktor implementation(libs.ktor.client.core) + + // Spell Check + implementation(projects.richeditorComposeSpellcheck) + implementation(libs.symspellkt.fdic) } sourceSets.androidMain.dependencies { diff --git a/sample/common/src/commonMain/composeResources/files/en-80k.fdic b/sample/common/src/commonMain/composeResources/files/en-80k.fdic new file mode 100644 index 00000000..12533060 Binary files /dev/null and b/sample/common/src/commonMain/composeResources/files/en-80k.fdic differ diff --git a/sample/common/src/commonMain/kotlin/com/mohamedrejeb/richeditor/sample/common/components/RichTextStyleRow.kt b/sample/common/src/commonMain/kotlin/com/mohamedrejeb/richeditor/sample/common/components/RichTextStyleRow.kt index 13561e4b..19c1a1e1 100644 --- a/sample/common/src/commonMain/kotlin/com/mohamedrejeb/richeditor/sample/common/components/RichTextStyleRow.kt +++ b/sample/common/src/commonMain/kotlin/com/mohamedrejeb/richeditor/sample/common/components/RichTextStyleRow.kt @@ -24,8 +24,8 @@ import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.mohamedrejeb.richeditor.annotation.ExperimentalRichTextApi +import com.mohamedrejeb.richeditor.compose.spellcheck.SpellCheck import com.mohamedrejeb.richeditor.model.RichTextState -import com.mohamedrejeb.richeditor.sample.common.richeditor.SpellCheck @OptIn(ExperimentalRichTextApi::class) @Composable diff --git a/sample/common/src/commonMain/kotlin/com/mohamedrejeb/richeditor/sample/common/home/HomeContent.kt b/sample/common/src/commonMain/kotlin/com/mohamedrejeb/richeditor/sample/common/home/HomeContent.kt index 9c1d5a16..c8b84248 100644 --- a/sample/common/src/commonMain/kotlin/com/mohamedrejeb/richeditor/sample/common/home/HomeContent.kt +++ b/sample/common/src/commonMain/kotlin/com/mohamedrejeb/richeditor/sample/common/home/HomeContent.kt @@ -1,10 +1,20 @@ package com.mohamedrejeb.richeditor.sample.common.home -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.ime +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.* +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment @@ -18,6 +28,7 @@ import com.mohamedrejeb.richeditor.sample.common.htmleditor.HtmlEditorScreen import com.mohamedrejeb.richeditor.sample.common.markdowneditor.MarkdownEditorScreen import com.mohamedrejeb.richeditor.sample.common.richeditor.RichEditorScreen import com.mohamedrejeb.richeditor.sample.common.slack.SlackDemoScreen +import com.mohamedrejeb.richeditor.sample.common.spellcheck.SpellCheckScreen import com.mohamedrejeb.richeditor.ui.material3.RichText @OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @@ -106,6 +117,16 @@ fun HomeContent() { Text("Slack Clone Demo") } } + + item { + Button( + onClick = { + navigator.push(SpellCheckScreen) + } + ) { + Text("Spell Check Demo") + } + } } } } \ No newline at end of file diff --git a/sample/common/src/commonMain/kotlin/com/mohamedrejeb/richeditor/sample/common/richeditor/RichEditorContent.kt b/sample/common/src/commonMain/kotlin/com/mohamedrejeb/richeditor/sample/common/richeditor/RichEditorContent.kt index 9b868276..aba2afa7 100644 --- a/sample/common/src/commonMain/kotlin/com/mohamedrejeb/richeditor/sample/common/richeditor/RichEditorContent.kt +++ b/sample/common/src/commonMain/kotlin/com/mohamedrejeb/richeditor/sample/common/richeditor/RichEditorContent.kt @@ -1,25 +1,50 @@ package com.mohamedrejeb.richeditor.sample.common.richeditor -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.ime +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material3.* +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow +import com.mohamedrejeb.richeditor.compose.spellcheck.SpellCheck import com.mohamedrejeb.richeditor.model.rememberRichTextState import com.mohamedrejeb.richeditor.sample.common.components.RichTextStyleRow import com.mohamedrejeb.richeditor.sample.common.ui.theme.ComposeRichEditorTheme import com.mohamedrejeb.richeditor.ui.BasicRichTextEditor +import com.mohamedrejeb.richeditor.ui.InteractionType import com.mohamedrejeb.richeditor.ui.material3.OutlinedRichTextEditor import com.mohamedrejeb.richeditor.ui.material3.RichText import com.mohamedrejeb.richeditor.ui.material3.RichTextEditor -@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) +@OptIn(ExperimentalMaterial3Api::class) @Composable fun RichEditorContent() { val navigator = LocalNavigator.currentOrThrow @@ -50,8 +75,7 @@ fun RichEditorContent() { } ) }, - modifier = Modifier - .fillMaxSize() + modifier = Modifier.fillMaxSize() ) { paddingValue -> LazyColumn( contentPadding = paddingValue, @@ -81,10 +105,50 @@ fun RichEditorContent() { } item { - BasicRichTextEditor( - modifier = Modifier.fillMaxWidth(), - state = basicRichTextState, - ) + Box { + var spellCheckExpanded by remember { mutableStateOf(null) } + + BasicRichTextEditor( + modifier = Modifier.fillMaxWidth(), + state = basicRichTextState, + onRichSpanClick = { span, range, offset, type -> + println("clicked: $type") + if (type == InteractionType.PrimaryClick || type == InteractionType.DoubleTap) { + if (span is SpellCheck) { + println("Spell check clicked") + println("Range: $range") + println("Offset: $offset") + spellCheckExpanded = offset + true + } else { + false + } + } else { + false + } + } + ) + + DropdownMenu( + expanded = spellCheckExpanded != null, + onDismissRequest = { spellCheckExpanded = null }, + offset = DpOffset( + x = spellCheckExpanded?.x?.dp ?: 0.dp, + y = spellCheckExpanded?.y?.dp ?: 0.dp, + ), + ) { + DropdownMenuItem( + text = { Text("Spelling") }, + onClick = {} + ) + + DropdownMenuItem( + text = { Text("Spelling") }, + onClick = {} + ) + } + + } } item { @@ -147,6 +211,17 @@ fun RichEditorContent() { state = outlinedRichTextState, onRichTextChangedListener = { println("Rich text changed!") + }, + onRichSpanClick = { span, range, offset, type -> + println("clicked: $type") + if (span is SpellCheck) { + println("Spell check clicked") + println("Range: $range") + println("Offset: $offset") + true + } else { + false + } } ) } diff --git a/sample/common/src/commonMain/kotlin/com/mohamedrejeb/richeditor/sample/common/spellcheck/MobyDick.kt b/sample/common/src/commonMain/kotlin/com/mohamedrejeb/richeditor/sample/common/spellcheck/MobyDick.kt new file mode 100644 index 00000000..8e5e3606 --- /dev/null +++ b/sample/common/src/commonMain/kotlin/com/mohamedrejeb/richeditor/sample/common/spellcheck/MobyDick.kt @@ -0,0 +1,24 @@ +package com.mohamedrejeb.richeditor.sample.common.spellcheck + +const val MobyDick = """ +# MOBY DICK; OR THE WHALE +## by Herman Melville +*1851* + +### CHAPTER 1 +### Loomings + + Call me Ishmael. Some years ago- never mind how long precisely- having little or no money in my purse, and nothing particular to interest me on shore, I thought I would sail about a little and see the watery part of the world. It is a way I have of driving off the spleen and regulating the circulation. Whenever I find myself growing grim about the mouth; whenever it is a damp, drizzly November in my soul; whenever I find myself involuntarily pausing before coffin warehouses, and bringing up the rear of every funeral I meet; and especially whenever my hypos get such an upper hand of me, that it requires a strong moral principle to prevent me from deliberately stepping into the street, and methodically knocking people's hats off- then, I account it high time to get to sea as soon as I can. This is my substitute for pistol and ball. With a philosophical flourish Cato throws himself upon his sword; I quietly take to the ship. There is nothing surprising in this. If they but knew it, almost all men in their degree, some time or other, cherish very nearly the same feelings towards the ocean with me. + + There now is your insular city of the Manhattoes, belted round by wharves as Indian isles by coral reefs- commerce surrounds it with her surf. Right and left, the streets take you waterward. Its extreme downtown is the battery, where that noble mole is washed by waves, and cooled by breezes, which a few hours previous were out of sight of land. Look at the crowds of water-gazers there. + + Circumambulate the city of a dreamy Sabbath afternoon. Go from Corlears Hook to Coenties Slip, and from thence, by Whitehall, northward. What do you see?- Posted like silent sentinels all around the town, stand thousands upon thousands of mortal men fixed in ocean reveries. Some leaning against the spiles; some seated upon the pier-heads; some looking over the bulwarks of ships from China; some high aloft in the rigging, as if striving to get a still better seaward peep. But these are all landsmen; of week days pent up in lath and plaster- tied to counters, nailed to benches, clinched to desks. How then is this? Are the green fields gone? What do they here? + + But look! here come more crowds, pacing straight for the water, and seemingly bound for a dive. Strange! Nothing will content them but the extremest limit of the land; loitering under the shady lee of yonder warehouses will not suffice. No. They must get just as nigh the water as they possibly can without falling And there they stand- miles of them- leagues. Inlanders all, they come from lanes and alleys, streets avenues- north, east, south, and west. Yet here they all unite. Tell me, does the magnetic virtue of the needles of the compasses of all those ships attract them thither? + + Once more. Say you are in the country; in some high land of lakes. Take almost any path you please, and ten to one it carries you down in a dale, and leaves you there by a pool in the stream. There is magic in it. Let the most absent-minded of men be plunged in his deepest reveries- stand that man on his legs, set his feet a-going, and he will infallibly lead you to water, if water there be in all that region. Should you ever be athirst in the great American desert, try this experiment, if your caravan happen to be supplied with a metaphysical professor. Yes, as every one knows, meditation and water are wedded for ever. + + But here is an artist. He desires to paint you the dreamiest, shadiest, quietest, most enchanting bit of romantic landscape in all the valley of the Saco. What is the chief element he employs? There stand his trees, each with a hollow trunk, as if a hermit and a crucifix were within; and here sleeps his meadow, and there sleep his cattle; and up from yonder cottage goes a sleepy smoke. Deep into distant woodlands winds a mazy way, reaching to overlapping spurs of mountains bathed in their hill-side blue. But though the picture lies thus tranced, and though this pine-tree shakes down its sighs like leaves upon this shepherd's head, yet all were vain, unless the shepherd's eye were fixed upon the magic stream before him. Go visit the Prairies in June, when for scores on scores of miles you wade knee-deep among Tiger-lilies- what is the one charm wanting?- Water- there is not a drop of water there! Were Niagara but a cataract of sand, would you travel your thousand miles to see it? Why did the poor poet of Tennessee, upon suddenly receiving two handfuls of silver, deliberate whether to buy him a coat, which he sadly needed, or invest his money in a pedestrian trip to Rockaway Beach? Why is almost every robust healthy boy with a robust healthy soul in him, at some time or other crazy to go to sea? Why upon your first voyage as a passenger, did you yourself feel such a mystical vibration, when first told that you and your ship were now out of sight of land? Why did the old Persians hold the sea holy? Why did the Greeks give it a separate deity, and own brother of Jove? Surely all this is not without meaning. And still deeper the meaning of that story of Narcissus, who because he could not grasp the tormenting, mild image he saw in the fountain, plunged into it and was drowned. But that same image, we ourselves see in all rivers and oceans. It is the image of the ungraspable phantom of life; and this is the key to it all. + + Now, when I say that I am in the habit of going to sea whenever I begin to grow hazy about the eyes, and begin to be over conscious of my lungs, I do not mean to have it inferred that I ever go to sea as a passenger. For to go as a passenger you must needs have a purse, and a purse is but a rag unless you have something in it. Besides, passengers get sea-sick- grow quarrelsome- don't sleep of nights- do not enjoy themselves much, as a general thing;- no, I never go as a passenger; nor, though I am something of a salt, do I ever go to sea as a Commodore, or a Captain, or a Cook. I abandon the glory and distinction of such offices to those who like them. For my part, I abominate all honorable respectable toils, trials, and tribulations of every kind whatsoever. It is quite as much as I can do to take care of myself, without taking care of ships, barques, brigs, schooners, and what not. And as for going as cook,- though I confess there is considerable glory in that, a cook being a sort of officer on ship-board- yet, somehow, I never fancied broiling fowls;- though once broiled, judiciously buttered, and judgmatically salted and peppered, there is no one who will speak more respectfully, not to say reverentially, of a broiled fowl than I will. It is out of the idolatrous dotings of the old Egyptians upon broiled ibis and roasted river horse, that you see the mummies of those creatures in their huge bakehouses the pyramids. +""" \ No newline at end of file diff --git a/sample/common/src/commonMain/kotlin/com/mohamedrejeb/richeditor/sample/common/spellcheck/SpellCheckContent.kt b/sample/common/src/commonMain/kotlin/com/mohamedrejeb/richeditor/sample/common/spellcheck/SpellCheckContent.kt new file mode 100644 index 00000000..d54f1be8 --- /dev/null +++ b/sample/common/src/commonMain/kotlin/com/mohamedrejeb/richeditor/sample/common/spellcheck/SpellCheckContent.kt @@ -0,0 +1,112 @@ +package com.mohamedrejeb.richeditor.sample.common.spellcheck + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.ime +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Check +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.dp +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow +import com.mohamedrejeb.richeditor.compose.spellcheck.SpellCheckState +import com.mohamedrejeb.richeditor.compose.spellcheck.ui.SpellCheckedRichTextEditor +import com.mohamedrejeb.richeditor.compose.spellcheck.rememberSpellCheckState +import com.mohamedrejeb.richeditor.sample.common.components.RichTextStyleRow +import com.mohamedrejeb.richeditor.sample.common.ui.theme.ComposeRichEditorTheme + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SpellCheckContent() { + val navigator = LocalNavigator.currentOrThrow + + ComposeRichEditorTheme { + Scaffold( + topBar = { + TopAppBar( + title = { Text("Compose Spell Check") }, + navigationIcon = { + IconButton( + onClick = { navigator.pop() } + ) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") + } + } + ) + }, + modifier = Modifier.fillMaxSize() + ) { paddingValue -> + + val spellChecker by rememberSampleSpellChecker() + val spellCheckState: SpellCheckState = rememberSpellCheckState(spellChecker) + + LaunchedEffect(Unit) { +// spellCheckState.richTextState.setHtml( +// """ +//

RichTextEditor is a composable that allows you to edit rich text content.

+// """.trimIndent() +// ) + spellCheckState.richTextState.setMarkdown(MobyDick) + } + + Column( + modifier = Modifier + .padding(paddingValue) + .windowInsetsPadding(WindowInsets.ime) + .fillMaxSize() + .padding(20.dp) + ) { + RichTextStyleRow( + modifier = Modifier.fillMaxWidth(), + state = spellCheckState.richTextState, + ) + + SpellCheckedRichTextEditor( + modifier = Modifier.fillMaxWidth().weight(1f).padding(16.dp), + textStyle = TextStyle.Default.copy(color = Color.White), + cursorBrush = SolidColor(Color.White), + spellCheckState = spellCheckState, + ) + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.End, + ) { + if (spellChecker == null) { + CircularProgressIndicator(modifier = Modifier.size(25.dp)) + Text(" Loading Dictionary...") + } else { + Icon( + imageVector = Icons.Default.Check, + contentDescription = "Loaded", + ) + Text(" Spell Check Ready!") + } + } + } + } + } +} \ No newline at end of file diff --git a/sample/common/src/commonMain/kotlin/com/mohamedrejeb/richeditor/sample/common/spellcheck/SpellCheckScreen.kt b/sample/common/src/commonMain/kotlin/com/mohamedrejeb/richeditor/sample/common/spellcheck/SpellCheckScreen.kt new file mode 100644 index 00000000..678f0086 --- /dev/null +++ b/sample/common/src/commonMain/kotlin/com/mohamedrejeb/richeditor/sample/common/spellcheck/SpellCheckScreen.kt @@ -0,0 +1,13 @@ +package com.mohamedrejeb.richeditor.sample.common.spellcheck + +import androidx.compose.runtime.Composable +import cafe.adriel.voyager.core.screen.Screen + +object SpellCheckScreen: Screen { + + @Composable + override fun Content() { + SpellCheckContent() + } + +} \ No newline at end of file diff --git a/sample/common/src/commonMain/kotlin/com/mohamedrejeb/richeditor/sample/common/spellcheck/rememberSampleSpellChecker.kt b/sample/common/src/commonMain/kotlin/com/mohamedrejeb/richeditor/sample/common/spellcheck/rememberSampleSpellChecker.kt new file mode 100644 index 00000000..0f05affa --- /dev/null +++ b/sample/common/src/commonMain/kotlin/com/mohamedrejeb/richeditor/sample/common/spellcheck/rememberSampleSpellChecker.kt @@ -0,0 +1,28 @@ +package com.mohamedrejeb.richeditor.sample.common.spellcheck + +import androidx.compose.runtime.* +import com.darkrockstudios.symspell.fdic.loadFdicFile +import com.darkrockstudios.symspellkt.api.SpellChecker +import com.darkrockstudios.symspellkt.common.SpellCheckSettings +import com.darkrockstudios.symspellkt.impl.SymSpell +import com.mohamedrejeb.richeditor.common.generated.resources.Res +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.jetbrains.compose.resources.ExperimentalResourceApi + +@OptIn(ExperimentalResourceApi::class) +@Composable +fun rememberSampleSpellChecker(): MutableState { + val scope = rememberCoroutineScope() + val spellChecker = remember { mutableStateOf(null) } + + LaunchedEffect(Unit) { + scope.launch(Dispatchers.Default) { + val checker = SymSpell(spellCheckSettings = SpellCheckSettings(topK = 5)) + checker.dictionary.loadFdicFile(Res.readBytes("files/en-80k.fdic")) + spellChecker.value = checker + } + } + + return spellChecker +} diff --git a/settings.gradle.kts b/settings.gradle.kts index ab674a05..7ec2e82b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -28,6 +28,7 @@ plugins { include( ":richeditor-compose", ":richeditor-compose-coil3", + ":richeditor-compose-spellcheck", ":sample:android", ":sample:desktop",