TextFieldTextStyles


Provides access to the styles applied to the text within a TextFieldState.

This interface provides a style query API similar to TextFieldBuffer, but returns immutable data.

Use this interface when you only need to read the current text styles (e.g., to update a formatting toolbar UI). If you need to mutate the text styles, use TextFieldState.edit and the corresponding methods on TextFieldBuffer.

Do not use this interface to query styles within a TextFieldBuffer edit block. The data returned will not reflect any ongoing changes made to the buffer, as it only returns the unupdated state from before the edit block began.

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.material.Button
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.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 derivedStateOf {
    val selection = state.selection
    if (selection.collapsed) {
        false
    } else {
        val spanStyles = state.textStyles.getSpanStyles(selection.min, selection.max)
        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.min, selection.max).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.min, selection.max).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)
}

Summary

Public functions

List<AnnotatedString.Range<ParagraphStyle>>
getParagraphStyles(start: Int, end: Int)

Returns a list of AnnotatedString.Ranges representing the ParagraphStyles that intersect with the given range defined by start (inclusive) and end (exclusive).

Cmn
List<AnnotatedString.Range<SpanStyle>>
getSpanStyles(start: Int, end: Int)

Returns a list of AnnotatedString.Ranges representing the SpanStyles that intersect with the given range defined by start (inclusive) and end (exclusive).

Cmn

Public functions

getParagraphStyles

fun getParagraphStyles(start: Int, end: Int): List<AnnotatedString.Range<ParagraphStyle>>

Returns a list of AnnotatedString.Ranges representing the ParagraphStyles that intersect with the given range defined by start (inclusive) and end (exclusive).

Styles are returned in the same order they were originally added to the buffer.

A style intersects with the range if it overlaps with it at any point. For non-empty ranges, this means style.start < end and start < style.end.

Example Query Range: [5, 15)

0    5    10   15   20   25
|----|----|----|----|----|
[---------) Query Range [5, 15)

[-------------------) Style [0, 20) (Contains query) -> Returned
[----) Style [8, 12) (Inside query) -> Returned
[----------) Style [0, 10) (Overlap start) -> Returned
[----) Style [0, 5) (Touching start) -> NOT Returned
[----------) Style [15, 25)(Touching end) -> NOT Returned

Example Collapsed Query: [10, 10)

0    5    10   15   20   25
|----|----|----|----|----|
| Query Range [10, 10)

[-------------------) Style [0, 20) (Contains query) -> Returned
[---------) Style [0, 10) (Touching end) -> NOT Returned
[----------) Style [10, 20)(Touching start) -> Returned
Parameters
start: Int

The start index of the range to query, inclusive.

end: Int

The end index of the range to query, exclusive.

Returns
List<AnnotatedString.Range<ParagraphStyle>>

A list of AnnotatedString.Ranges representing the ParagraphStyles overlapping with the queried range.

getSpanStyles

fun getSpanStyles(start: Int, end: Int): List<AnnotatedString.Range<SpanStyle>>

Returns a list of AnnotatedString.Ranges representing the SpanStyles that intersect with the given range defined by start (inclusive) and end (exclusive).

Styles are returned in the same order they were originally added to the buffer.

A style intersects with the range if it overlaps with it at any point. For non-empty ranges, this means style.start < end and start < style.end.

Example Query Range: [5, 15)

0    5    10   15   20   25
|----|----|----|----|----|
[---------) Query Range [5, 15)

[-------------------) Style [0, 20) (Contains query) -> Returned
[----) Style [8, 12) (Inside query) -> Returned
[----------) Style [0, 10) (Overlap start) -> Returned
[----) Style [0, 5) (Touching start) -> NOT Returned
[----------) Style [15, 25)(Touching end) -> NOT Returned

Example Collapsed Query: [10, 10)

0    5    10   15   20   25
|----|----|----|----|----|
| Query Range [10, 10)

[-------------------) Style [0, 20) (Contains query) -> Returned
[---------) Style [0, 10) (Touching end) -> NOT Returned
[----------) Style [10, 20)(Touching start) -> Returned
Parameters
start: Int

The start index of the range to query, inclusive.

end: Int

The end index of the range to query, exclusive.

Returns
List<AnnotatedString.Range<SpanStyle>>

A list of AnnotatedString.Ranges representing the SpanStyles overlapping with the queried range.