/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.compose.foundation.text
import androidx.compose.foundation.fastMapIndexedNotNull
import androidx.compose.foundation.text.modifiers.SelectableTextAnnotatedStringElement
import androidx.compose.foundation.text.modifiers.SelectionController
import androidx.compose.foundation.text.modifiers.TextAnnotatedStringElement
import androidx.compose.foundation.text.modifiers.TextStringSimpleElement
import androidx.compose.foundation.text.selection.LocalSelectionRegistrar
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasurePolicy
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.platform.LocalFontFamilyResolver
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.Placeholder
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.util.fastForEach
import kotlin.math.floor
import kotlin.math.roundToInt
/**
* Implementation of [BasicText] using the new Modifier system.
*
* All features are the same.
*/
@Composable
internal fun TextUsingModifier(
text: String,
modifier: Modifier = Modifier,
style: TextStyle = TextStyle.Default,
onTextLayout: ((TextLayoutResult) -> Unit)? = null,
overflow: TextOverflow = TextOverflow.Clip,
softWrap: Boolean = true,
maxLines: Int = Int.MAX_VALUE,
minLines: Int = 1,
) {
validateMinMaxLines(minLines, maxLines)
val selectionRegistrar = LocalSelectionRegistrar.current
val selectionController = if (selectionRegistrar != null) {
val backgroundSelectionColor = LocalTextSelectionColors.current.backgroundColor
remember(selectionRegistrar, backgroundSelectionColor) {
SelectionController(
selectionRegistrar,
backgroundSelectionColor
)
}
} else {
null
}
val finalModifier = if (selectionController != null || onTextLayout != null) {
modifier
// TODO(b/274781644): Remove this graphicsLayer
.graphicsLayer()
.textModifier(
AnnotatedString(text),
style = style,
onTextLayout = onTextLayout,
overflow = overflow,
softWrap = softWrap,
maxLines = maxLines,
minLines = minLines,
fontFamilyResolver = LocalFontFamilyResolver.current,
placeholders = null,
onPlaceholderLayout = null,
selectionController = selectionController
)
} else {
modifier
// TODO(b/274781644): Remove this graphicsLayer
.graphicsLayer() then TextStringSimpleElement(
text,
style,
LocalFontFamilyResolver.current,
overflow,
softWrap,
maxLines,
minLines
)
}
Layout(finalModifier, EmptyMeasurePolicy)
}
/**
* Implementation of [BasicText] using the new Modifier system.
*
* All features are the same.
*/
@Composable
internal fun TextUsingModifier(
text: AnnotatedString,
modifier: Modifier = Modifier,
style: TextStyle = TextStyle.Default,
onTextLayout: ((TextLayoutResult) -> Unit)? = null,
overflow: TextOverflow = TextOverflow.Clip,
softWrap: Boolean = true,
maxLines: Int = Int.MAX_VALUE,
minLines: Int = 1,
inlineContent: Map<String, InlineTextContent>? = null,
) {
validateMinMaxLines(minLines, maxLines)
val selectionRegistrar = LocalSelectionRegistrar.current
val selectionController = if (selectionRegistrar != null) {
val backgroundSelectionColor = LocalTextSelectionColors.current.backgroundColor
remember(selectionRegistrar, backgroundSelectionColor) {
SelectionController(
selectionRegistrar,
backgroundSelectionColor
)
}
} else {
null
}
if (!text.hasInlineContent()) {
// this is the same as text: String, use all the early exits
Layout(
modifier = modifier
// TODO(b/274781644): Remove this graphicsLayer
.graphicsLayer()
.textModifier(
text = text,
style = style,
onTextLayout = onTextLayout,
overflow = overflow,
softWrap = softWrap,
maxLines = maxLines,
minLines = minLines,
fontFamilyResolver = LocalFontFamilyResolver.current,
placeholders = null,
onPlaceholderLayout = null,
selectionController = selectionController
),
EmptyMeasurePolicy
)
} else {
// do the inline content allocs
val (placeholders, inlineComposables) = text.resolveInlineContent(inlineContent)
val measuredPlaceholderPositions = remember {
mutableStateOf<List<Rect?>?>(null)
}
Layout(
content = { InlineChildren(text, inlineComposables) },
modifier = modifier
// TODO(b/274781644): Remove this graphicsLayer
.graphicsLayer()
.textModifier(
text = text,
style = style,
onTextLayout = onTextLayout,
overflow = overflow,
softWrap = softWrap,
maxLines = maxLines,
minLines = minLines,
fontFamilyResolver = LocalFontFamilyResolver.current,
placeholders = placeholders,
onPlaceholderLayout = { measuredPlaceholderPositions.value = it },
selectionController = selectionController
),
measurePolicy = TextMeasurePolicy { measuredPlaceholderPositions.value }
)
}
}
private object EmptyMeasurePolicy : MeasurePolicy {
private val placementBlock: Placeable.PlacementScope.() -> Unit = {}
override fun MeasureScope.measure(
measurables: List<Measurable>,
constraints: Constraints
): MeasureResult {
return layout(constraints.maxWidth, constraints.maxHeight, placementBlock = placementBlock)
}
}
private class TextMeasurePolicy(
private val placements: () -> List<Rect?>?
) : MeasurePolicy {
override fun MeasureScope.measure(
measurables: List<Measurable>,
constraints: Constraints
): MeasureResult {
val toPlace = placements()?.fastMapIndexedNotNull { index, rect ->
// PlaceholderRect will be null if it's ellipsized. In that case, the corresponding
// inline children won't be measured or placed.
rect?.let {
Pair(
measurables[index].measure(
Constraints(
maxWidth = floor(it.width).toInt(),
maxHeight = floor(it.height).toInt()
)
),
IntOffset(it.left.roundToInt(), it.top.roundToInt())
)
}
}
return layout(
constraints.maxWidth,
constraints.maxHeight,
) {
toPlace?.fastForEach { (placeable, position) ->
placeable.place(position)
}
}
}
}
private fun Modifier.textModifier(
text: AnnotatedString,
style: TextStyle,
onTextLayout: ((TextLayoutResult) -> Unit)?,
overflow: TextOverflow,
softWrap: Boolean,
maxLines: Int,
minLines: Int,
fontFamilyResolver: FontFamily.Resolver,
placeholders: List<AnnotatedString.Range<Placeholder>>?,
onPlaceholderLayout: ((List<Rect?>) -> Unit)?,
selectionController: SelectionController?
): Modifier {
if (selectionController == null) {
val staticTextModifier = TextAnnotatedStringElement(
text,
style,
fontFamilyResolver,
onTextLayout,
overflow,
softWrap,
maxLines,
minLines,
placeholders,
onPlaceholderLayout,
null
)
return this then Modifier /* selection position */ then staticTextModifier
} else {
val selectableTextModifier = SelectableTextAnnotatedStringElement(
text,
style,
fontFamilyResolver,
onTextLayout,
overflow,
softWrap,
maxLines,
minLines,
placeholders,
onPlaceholderLayout,
selectionController
)
return this then selectionController.modifier then selectableTextModifier
}
}