# TrackedRange

Artifact: [androidx.compose.foundation:foundation](https://developer.android.com/jetpack/androidx/releases/compose-foundation) [View Source](https://cs.android.com/search?q=file:androidx/compose/foundation/text/input/TrackedRange.kt+class:androidx.compose.foundation.text.input.TrackedRange) Added in [1.12.0](https://developer.android.com/jetpack/androidx/releases/compose-foundation#1.12.0) Common/All
- Cmn

  ```
  class TrackedRange<T : Any?>
  ```

*** ** * ** ***

A style applied on the text that is tracked by `https://developer.android.com/reference/kotlin/androidx/compose/foundation/text/input/TextFieldBuffer`, returned by `https://developer.android.com/reference/kotlin/androidx/compose/foundation/text/input/TextFieldBuffer#addStyle(androidx.compose.ui.text.SpanStyle,kotlin.Int,kotlin.Int)`.

`TrackedRange` acts as a unique handle to a specific style range. Its properties (such as the style object, its range, and expand policy) can be queried and updated using extension properties on `https://developer.android.com/reference/kotlin/androidx/compose/foundation/text/input/TextFieldBuffer`:

- `TrackedRange<*>.textRange`

- `TrackedRange<*>.expandPolicy`

- `TrackedRange<*>.isValid`

- `TrackedRange<SpanStyle>.spanStyle`

- `TrackedRange<ParagraphStyle>.paragraphStyle`

All the extension properties reflect the up-to-date state of the style range. e.g. The `textRange` of this `https://developer.android.com/reference/kotlin/androidx/compose/foundation/text/input/TrackedRange` will automatically update when the text is edited. If the style's range collapses to zero length due to text edits, the style will be removed and `valid` will return false.

This object's lifecycle is bound to the `https://developer.android.com/reference/kotlin/androidx/compose/foundation/text/input/TextFieldBuffer` which is returned by `https://developer.android.com/reference/kotlin/androidx/compose/foundation/text/input/TextFieldState#edit(kotlin.Function1)`, `https://developer.android.com/reference/kotlin/androidx/compose/foundation/text/input/InputTransformation#(androidx.compose.foundation.text.input.TextFieldBuffer).transformInput()` and `https://developer.android.com/reference/kotlin/androidx/compose/foundation/text/input/OutputTransformation#(androidx.compose.foundation.text.input.TextFieldBuffer).transformOutput()`. Do not keep a reference of it.

```kotlin
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.input.ExpandPolicy
import androidx.compose.foundation.text.input.InputTransformation
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.delete
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.Text
import androidx.compose.runtime.remember
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight

// This sample demonstrates how to use the `TrackedRange` API to track and modify text ranges
// dynamically. It implements a basic Markdown-like behavior where text typed inside double
// asterisks (e.g., **bold**) is automatically bolded, and the asterisks are removed.
val state = rememberTextFieldState("")

fun IntRange.toTextRange(): TextRange {
    // Unlike IntRange, TextRange is exclusive at the end.
    return TextRange(first, last + 1)
}

val inputTransformation = remember {
    InputTransformation {
        val text = asCharSequence().toString()
        val matches = "\\*\\*([^*]+)\\*\\*".toRegex().findAll(text).toList()
        matches
            .map { match ->
                val contentRange = match.groups[0]!!.range
                // Apply bold style to the text inside asterisks (including the
                // asterisks for now).
                addStyle(
                    SpanStyle(fontWeight = FontWeight.Bold),
                    contentRange.toTextRange(),
                    ExpandPolicy.InsideOnly,
                )
            }
            .forEach { trackedRange ->
                // Remove the asterisks here.

                // `trackedRange` simplifies this logic: normally, deleting characters at
                // the start would shift the end index. However, because `trackedRange`
                // automatically tracks text updates and adjusts its offsets
                // dynamically, we can safely delete the target range without having to
                // calculate the offset manually.
                delete(trackedRange.textRange.start, trackedRange.textRange.start + 2)
                delete(trackedRange.textRange.end - 2, trackedRange.textRange.end)
            }
    }
}

Column {
    Text("Type **text** below to automatically bold it.")

    BasicTextField(
        state = state,
        textStyle = LocalTextStyle.current,
        inputTransformation = inputTransformation,
    )
}
```

```kotlin
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.input.ExpandPolicy
import androidx.compose.foundation.text.input.TextFieldBuffer
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.material3.Button
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.Text
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp

// This sample demonstrates a realistic rich-text editor scenario using the `TrackedRange` and
// `TextFieldTextStyles` APIs. It implements a "Toggle Bold" formatting function on the current
// selection.

// For simplicity, this sample keeps bold styles non-overlapping and contiguous, assuming they
// are
// applied exclusively through this method.
val state = rememberTextFieldState("Hello World")

// This derived state calculates whether the current selection is completely covered by
// bold text styles. This ensures the "Bold" toggle button accurately reflects the
// state of the selected text.
val isSelection100PercentBold by remember {
    derivedStateOf {
        val selection = state.selection
        if (selection.collapsed) {
            false
        } else {
            val spanStyles = state.textStyles.getSpanStyles(selection)
            var boldCoverage = 0
            for (style in spanStyles) {
                if (style.item.fontWeight == FontWeight.Bold) {
                    val overlapStart = maxOf(style.start, selection.min)
                    val overlapEnd = minOf(style.end, selection.max)
                    if (overlapEnd > overlapStart) {
                        boldCoverage += (overlapEnd - overlapStart)
                    }
                }
            }
            boldCoverage == selection.length
        }
    }
}

fun TextFieldBuffer.unBoldSelection() {
    // Query existing bold styles in the selection
    val intersectingStyles =
        getSpanStyles(selection).filter { it.spanStyle.fontWeight == FontWeight.Bold }
    // We modify or remove existing styles to exclude the selected range
    for (style in intersectingStyles) {
        val range = style.textRange
        if (range.start >= selection.min && range.end <= selection.max) {
            // The style is fully inside the selection. Remove it.
            removeStyle(style)
        } else if (range.start < selection.min && range.end > selection.max) {
            // The style completely covers the selection. We need to split it.
            val oldEnd = range.end
            // Truncate the start part
            style.textRange = TextRange(range.start, selection.min)
            // Add a new style for the end part
            addStyle(
                SpanStyle(fontWeight = FontWeight.Bold),
                TextRange(selection.max, oldEnd),
                ExpandPolicy.AtEnd,
            )
        } else if (range.start < selection.min) {
            // The style overlaps with the start of the selection. Truncate it.
            style.textRange = TextRange(range.start, selection.min)
        } else {
            // The style overlaps with the end of the selection. Truncate it.
            style.textRange = TextRange(selection.max, range.end)
        }
    }
}

fun TextFieldBuffer.boldSelection() {
    // Query existing bold styles in the selection
    val intersectingStyles =
        getSpanStyles(selection).filter { it.spanStyle.fontWeight == FontWeight.Bold }
    // To keep bold styles non-overlapping, we merge any intersecting bold
    // styles with the new selection range into a single contiguous bold style.
    var mergedStart = selection.min
    var mergedEnd = selection.max

    for (style in intersectingStyles) {
        mergedStart = minOf(mergedStart, style.textRange.start)
        mergedEnd = maxOf(mergedEnd, style.textRange.end)
        // Remove the fragmented style
        removeStyle(style)
    }

    addStyle(
        SpanStyle(fontWeight = FontWeight.Bold),
        TextRange(mergedStart, mergedEnd),
        ExpandPolicy.AtEnd,
    )
}

Column {
    Button(
        onClick = {
            state.edit {
                val selection = this.selection
                if (selection.collapsed) return@edit
                if (isSelection100PercentBold) {
                    unBoldSelection()
                } else {
                    boldSelection()
                }
            }
        }
    ) {
        Text(
            "B",
            fontWeight = if (isSelection100PercentBold) FontWeight.Bold else FontWeight.Normal,
        )
    }

    BasicTextField(state = state, textStyle = LocalTextStyle.current)
}
```

```kotlin
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.material3.Text
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp

// Wipe the bold style on a given range using the TrackedRange.textRange API
val state = TextFieldState("Hello World")

state.edit {
    // Assume we want to "wipe" all bold styles from the first 5 characters.
    val rangeToWipe = TextRange(0, 5)

    // Get all span styles that intersect with the wipe range.
    getSpanStyles(rangeToWipe).forEach { trackedRange ->
        if (trackedRange.spanStyle.fontWeight == FontWeight.Bold) {
            val current = trackedRange.textRange

            if (rangeToWipe.start <= current.start && current.end <= rangeToWipe.end) {
                // Case 1: The bold style is entirely within the wipe range, remove it.
                removeStyle(trackedRange)
            } else if (current.start < rangeToWipe.start && rangeToWipe.end < current.end) {
                // Case 2: The wipe range is in the middle: split the style into two parts.
                val oldEnd = current.end
                // Truncate the original style to end at the start of the wipe range.
                trackedRange.textRange = TextRange(current.start, rangeToWipe.start)
                // Add a new bold style starting after the wipe range.
                addStyle(trackedRange.spanStyle, rangeToWipe.end, oldEnd)
            } else if (current.start < rangeToWipe.start) {
                // Case 3: Overlap at the start of wipe: truncate the style's end.
                trackedRange.textRange = TextRange(current.start, rangeToWipe.start)
            } else {
                // Case 4: Overlap at the end of wipe: truncate the style's start.
                trackedRange.textRange = TextRange(rangeToWipe.end, current.end)
            }
        }
    }
}
```

| See also |
|---|---|
| `https://developer.android.com/reference/kotlin/androidx/compose/foundation/text/input/TextFieldBuffer` |   |