Last active
May 11, 2025 10:09
-
-
Save kevinvanmierlo/4bd011479c66eed598852ffeacdc0156 to your computer and use it in GitHub Desktop.
Revisions
-
kevinvanmierlo renamed this gist
Sep 6, 2022 . 1 changed file with 0 additions and 0 deletions.There are no files selected for viewing
File renamed without changes. -
kevinvanmierlo revised this gist
Sep 5, 2022 . 3 changed files with 6 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,3 +1,4 @@ /* MIT License Copyright (c) 2022 Kevin van Mierlo @@ -19,6 +20,7 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ @Composable fun TestMentions() { This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,3 +1,4 @@ /* MIT License Copyright (c) 2022 Kevin van Mierlo @@ -19,6 +20,7 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import androidx.compose.runtime.* import androidx.compose.ui.graphics.Color This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,3 +1,4 @@ /* MIT License Copyright (c) 2022 Kevin van Mierlo @@ -19,6 +20,7 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ class MentionImpl : MentionOption() { var title = "" -
kevinvanmierlo created this gist
Sep 5, 2022 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,69 @@ MIT License Copyright (c) 2022 Kevin van Mierlo Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. @Composable fun TestMentions() { val mentionTextFieldValueState = remember { mutableStateOf(TextFieldValue("")) } val mentionTextFieldValue by mentionTextFieldValueState val mentionHandler = remember { MentionHandler<MentionImpl>(mentionTextFieldValueState) } Column( modifier = Modifier.fillMaxSize() ) { TestMentionsOptionsList(modifier = Modifier.fillMaxWidth().weight(1f), mentionHandler = mentionHandler) TestMentionsTextField(mentionHandler = mentionHandler, mentionTextFieldValue) } } @Composable private fun TestMentionsOptionsList(modifier: Modifier = Modifier, mentionHandler: MentionHandler<MentionImpl>) { val shouldShowMentionOptions by remember { derivedStateOf { mentionHandler.shouldShowMentionOptions && mentionHandler.mentionOptions.isNotEmpty() } } if(shouldShowMentionOptions) { LazyColumn( modifier = modifier ) { items(mentionHandler.mentionOptions.size) { index -> val item = mentionHandler.mentionOptions[index] ListItemCell( title = item.title, onClick = { mentionHandler.clickedMentionOption(item) } ) } } } } @Composable private fun TestMentionsTextField(mentionHandler: MentionHandler<MentionImpl>, textFieldValue: TextFieldValue) { mentionHandler.HandleProgrammaticallyTextChange() TextField( modifier = Modifier.fillMaxWidth(), value = textFieldValue, onValueChange = { mentionHandler.handleTextFieldValueChanged(newTextFieldValue = it) }, visualTransformation = mentionHandler.mentionTransformation ) } This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,206 @@ MIT License Copyright (c) 2022 Kevin van Mierlo Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. import androidx.compose.runtime.* import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.input.* import androidx.compose.ui.text.style.TextDecoration import kotlin.math.max import kotlin.math.min class MentionHandler<MD : MentionOption>(var textFieldValueState: MutableState<TextFieldValue>) { var shouldShowMentionOptions by mutableStateOf(false) val mentionOptions = mutableStateListOf<MD>() val mentions = mutableListOf<Mention<MD>>() var textFieldValue by textFieldValueState var lastTextFieldValueChange: TextFieldValue? = null var onMentionFilter: (query: String) -> Unit = {} // This is used so the mentions will be styled var mentionTransformation = object : VisualTransformation { var mentionColor = Color.Blue override fun filter(text: AnnotatedString): TransformedText { return TransformedText( text = getMentionAnnotatedString(text), offsetMapping = OffsetMapping.Identity ) } fun getMentionAnnotatedString(text: AnnotatedString): AnnotatedString { return buildAnnotatedString { append(text) mentions.forEach { val styleStartIndex = min(text.length, max(0, it.selection.first)) addStyle( style = SpanStyle( color = mentionColor, textDecoration = TextDecoration.Underline, ), start = styleStartIndex, end = max(styleStartIndex, min(text.length, it.selection.last + 1)) ) } } } } private fun getTextBeforeSelection(maxChars: Int = Int.MAX_VALUE) = textFieldValueState.value.getTextBeforeSelection(maxChars) // Check if user is typing and possibly mentioning someone (ignores mentions already added) private fun getMentionInProgressRange(textBeforeSelection: AnnotatedString = getTextBeforeSelection()): IntRange? { val mentionTriggerIndex = textBeforeSelection.lastIndexOf('@') if (mentionTriggerIndex < 0) { return null } // Stop trying after typing 20 characters if ((textBeforeSelection.length - mentionTriggerIndex) > 20) { return null } if (mentions.firstOrNull { it.selection.first == mentionTriggerIndex } == null) { return mentionTriggerIndex until textBeforeSelection.length } return null } // Go through the text and find where the texts differentiate private fun getDiffRange(indexOfDiffStart: Int, oldText: String, newText: String): Pair<IntRange, IntRange> { val newLastIndex = max(0, newText.length) val newStartIndex = min(indexOfDiffStart, newLastIndex) val oldLastIndex = max(0, oldText.length) val oldStartIndex = min(indexOfDiffStart, oldLastIndex) var loopIndex = oldStartIndex var oldTextIndex = -1 while(loopIndex <= oldLastIndex) { // From where texts differentiates, loop through old text to find at what index the texts will be the same again oldTextIndex = newText.indexOf(oldText.substring(loopIndex, oldLastIndex)) if(oldTextIndex >= 0) { break } loopIndex++ } if(oldTextIndex >= 0) { return Pair(first = oldStartIndex .. loopIndex, second = newStartIndex .. max(0, oldTextIndex)) } return Pair(first = oldStartIndex .. oldLastIndex, second = newStartIndex .. newLastIndex) } /** * @param ignoreMention This is for if a new mention has been added, so it won't automatically be deleted again */ fun handleTextFieldValueChanged( oldTextFieldValue: TextFieldValue = textFieldValue, newTextFieldValue: TextFieldValue, ignoreMention: Mention<MD>? = null ) { lastTextFieldValueChange = newTextFieldValue if (oldTextFieldValue.text.contentEquals(newTextFieldValue.text)) { // Content stayed the same, probably cursor change } else { val indexOfDiff = oldTextFieldValue.text.indexOfDifference(newTextFieldValue.text) if (indexOfDiff >= 0) { val (oldDiffRange, newDiffRange) = getDiffRange(indexOfDiff, oldTextFieldValue.text, newTextFieldValue.text) // If it's not the ignore mention and we have edited within the mention range, remove mention mentions.removeIf { it !== ignoreMention && oldDiffRange.first <= it.selection.last && oldDiffRange.last > it.selection.first } // Go through mentions and check if an edit happened before the mention. If so, move the range so it is correct again mentions.forEach { mention -> if (newDiffRange.first <= mention.selection.first) { val diff = newDiffRange.length() - oldDiffRange.length() mention.selection = (mention.selection.first + diff)..(mention.selection.last + diff) } } } } textFieldValue = newTextFieldValue // Check if we are working on a mention, if so, show mentions if possible and call filter callback val textBeforeSelection = getTextBeforeSelection() getMentionInProgressRange(textBeforeSelection)?.let { mentionInProgressRange -> val mentionText = textBeforeSelection.substring(startIndex = mentionInProgressRange.first + 1) shouldShowMentionOptions = true onMentionFilter(mentionText) } ?: kotlin.run { shouldShowMentionOptions = false } } fun clickedMentionOption(mentionOption: MD) { getMentionInProgressRange()?.let { lastMentionRange -> val replacementString = "@${mentionOption.mentionTitle}" // Also include a space for easy typing val newText = textFieldValue.text.replaceRange(lastMentionRange, "${replacementString} ") val newMention = Mention( option = mentionOption, selection = lastMentionRange.first until (lastMentionRange.first + replacementString.length) ) mentions.add(newMention) // Handle textfield changed so other mentions will be adjusted to this position handleTextFieldValueChanged( newTextFieldValue = textFieldValue.copy(text = newText, selection = TextRange(newMention.selection.last + 2 /* After mention and a space */)), ignoreMention = newMention ) // Clear mention options because we added the mention mentionOptions.clear() } } // This is for if we programmatically set the text, we won't get a callback, this will make sure we do so we can handle it @Composable fun HandleProgrammaticallyTextChange() { if(lastTextFieldValueChange?.text?.contentEquals(textFieldValue.text) == false) { handleTextFieldValueChanged( oldTextFieldValue = lastTextFieldValueChange!!, newTextFieldValue = textFieldValue ) } } } // Find first difference between two strings private fun String.indexOfDifference(otherString: String): Int { if (this.contentEquals(otherString)) { return -1 } for (i in 0 until min(this.length, otherString.length)) { if (this[i] != otherString[i]) { return i } } if (this.length != otherString.length) { return min(this.length, otherString.length) } return -1 } private fun IntRange.length(): Int { return last - first } This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,28 @@ MIT License Copyright (c) 2022 Kevin van Mierlo Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. class MentionImpl : MentionOption() { var title = "" override val mentionTitle: String get() = title }