Skip to content

Commit

Permalink
Add callback for handling click on RichSpans
Browse files Browse the repository at this point in the history
This update introduces a callback function `onRichSpanClick` to allow apps to handle clicks on RichSpans in the RichTextEditor. The updates also modify the visibility of some classes and properties to support this- `RichSpan`, `ParagraphType` and `getRichSpanByOffset` method in `RichTextState` are now publicly visible.
  • Loading branch information
Wavesonics committed Nov 13, 2024
1 parent 08db783 commit 37d70b6
Show file tree
Hide file tree
Showing 9 changed files with 40 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ import kotlin.collections.indices
/**
* A rich span is a part of a rich paragraph.
*/
@OptIn(ExperimentalRichTextApi::class)
internal class RichSpan(
class RichSpan(
internal val key: Int? = null,
val children: MutableList<RichSpan> = mutableListOf(),
var paragraph: RichParagraph,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public class RichTextState internal constructor(

internal var singleParagraphMode by mutableStateOf(false)

internal var textLayoutResult: TextLayoutResult? by mutableStateOf(null)
var textLayoutResult: TextLayoutResult? by mutableStateOf(null)
private set

private var lastPressPosition: Offset? by mutableStateOf(null)
Expand Down Expand Up @@ -2722,7 +2722,7 @@ public class RichTextState internal constructor(
return richSpan
}

private fun getRichSpanByOffset(offset: Offset): RichSpan? {
fun getRichSpanByOffset(offset: Offset): RichSpan? {
this.textLayoutResult?.let { textLayoutResult ->
val position = textLayoutResult.getOffsetForPosition(offset)
return getRichSpanByTextIndex(position, true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import com.mohamedrejeb.richeditor.paragraph.type.ParagraphType
import com.mohamedrejeb.richeditor.paragraph.type.ParagraphType.Companion.startText
import com.mohamedrejeb.richeditor.ui.test.getRichTextStyleTreeRepresentation

internal class RichParagraph(
class RichParagraph(
val key: Int = 0,
val children: MutableList<RichSpan> = mutableListOf(),
var paragraphStyle: ParagraphStyle = DefaultParagraphStyle,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import androidx.compose.ui.text.ParagraphStyle
import com.mohamedrejeb.richeditor.model.RichSpan
import com.mohamedrejeb.richeditor.model.RichTextConfig

internal interface ParagraphType {
interface ParagraphType {

fun getStyle(config: RichTextConfig): ParagraphStyle

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,16 @@ import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.TextLayoutResult
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.RichSpan
import com.mohamedrejeb.richeditor.model.RichTextState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch


/**
Expand Down Expand Up @@ -65,6 +66,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
Expand Down Expand Up @@ -96,6 +98,7 @@ public fun BasicRichTextEditor(
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),
Expand All @@ -114,6 +117,7 @@ public fun BasicRichTextEditor(
maxLines = maxLines,
minLines = minLines,
maxLength = maxLength,
onRichSpanClick = onRichSpanClick,
onTextLayout = onTextLayout,
interactionSource = interactionSource,
cursorBrush = cursorBrush,
Expand Down Expand Up @@ -157,6 +161,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
Expand Down Expand Up @@ -189,6 +194,7 @@ public fun BasicRichTextEditor(
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),
Expand All @@ -197,7 +203,6 @@ public fun BasicRichTextEditor(
contentPadding: PaddingValues
) {
val scope = rememberCoroutineScope()
val uriHandler = LocalUriHandler.current
val density = LocalDensity.current
val localTextStyle = LocalTextStyle.current
val layoutDirection = LocalLayoutDirection.current
Expand All @@ -213,13 +218,15 @@ public fun BasicRichTextEditor(
state.singleParagraphMode = singleParagraph
}

LaunchedEffect(interactionSource) {
scope.launch {
interactionSource.interactions.collect { interaction ->
if (interaction is PressInteraction.Press) {
val clickedLink = state.getLinkByOffset(interaction.pressPosition)
if (clickedLink != null) {
uriHandler.openUri(clickedLink)
if(onRichSpanClick != null) {
// Start listening for rich span clicks
LaunchedEffect(interactionSource) {
scope.launch {
interactionSource.interactions.collect { interaction ->
if (interaction is PressInteraction.Press) {
state.getRichSpanByOffset(interaction.pressPosition)?.let { clickedSpan ->
onRichSpanClick(clickedSpan)
}
}
}
}
Expand Down Expand Up @@ -328,4 +335,6 @@ internal suspend fun adjustTextIndicatorOffset(
y = pressPosition.y - topPadding
),
)
}
}

typealias RichSpanClickListener = (RichSpan) -> Unit
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import com.mohamedrejeb.richeditor.model.RichTextState
import com.mohamedrejeb.richeditor.ui.BasicRichTextEditor
import com.mohamedrejeb.richeditor.ui.RichSpanClickListener

/**
* Material Design outlined rich text field
Expand Down Expand Up @@ -65,6 +66,7 @@ import com.mohamedrejeb.richeditor.ui.BasicRichTextEditor
* 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 OutlinedTextField. You can create and pass in your own remembered
* [MutableInteractionSource] if you want to observe [Interaction]s and customize the
Expand Down Expand Up @@ -92,6 +94,7 @@ public fun OutlinedRichTextEditor(
maxLines: Int = Int.MAX_VALUE,
minLines: Int = 1,
maxLength: Int = Int.MAX_VALUE,
onRichSpanClick: RichSpanClickListener? = null,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
shape: Shape = MaterialTheme.shapes.small,
colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors()
Expand Down Expand Up @@ -130,6 +133,7 @@ public fun OutlinedRichTextEditor(
maxLines = maxLines,
minLines = minLines,
maxLength = maxLength,
onRichSpanClick = onRichSpanClick,
decorationBox = @Composable { innerTextField ->
TextFieldDefaults.OutlinedTextFieldDecorationBox(
value = state.textFieldValue.text,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import com.mohamedrejeb.richeditor.model.RichTextState
import com.mohamedrejeb.richeditor.ui.BasicRichTextEditor
import com.mohamedrejeb.richeditor.ui.RichSpanClickListener
import com.mohamedrejeb.richeditor.ui.material3.RichTextEditor

/**
Expand Down Expand Up @@ -64,6 +65,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
Expand Down Expand Up @@ -98,6 +100,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 =
MaterialTheme.shapes.small.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize),
Expand Down Expand Up @@ -130,6 +133,7 @@ public fun RichTextEditor(
maxLines = maxLines,
minLines = minLines,
maxLength = maxLength,
onRichSpanClick = onRichSpanClick,
decorationBox = @Composable { innerTextField ->
// places leading icon, text field with label and placeholder, trailing icon
TextFieldDefaults.TextFieldDecorationBox(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,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

Expand Down Expand Up @@ -77,6 +78,7 @@ 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 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.
Expand Down Expand Up @@ -105,6 +107,7 @@ public fun OutlinedRichTextEditor(
maxLines: Int = Int.MAX_VALUE,
minLines: Int = 1,
maxLength: Int = Int.MAX_VALUE,
onRichSpanClick: RichSpanClickListener? = null,
onTextLayout: (TextLayoutResult) -> Unit = {},
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
shape: Shape = RichTextEditorDefaults.outlinedShape,
Expand Down Expand Up @@ -145,6 +148,7 @@ public fun OutlinedRichTextEditor(
maxLines = maxLines,
minLines = minLines,
maxLength = maxLength,
onRichSpanClick = onRichSpanClick,
onTextLayout = onTextLayout,
decorationBox = { innerTextField ->
RichTextEditorDefaults.OutlinedRichTextEditorDecorationBox(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -21,14 +20,14 @@ 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
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

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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 ->
Expand Down

0 comments on commit 37d70b6

Please sign in to comment.