package com.suno.android.common_ui.components.text import androidx.compose.animation.animateContentSize import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.withStyle import com.suno.android.common_res.R as CommonResR @Composable fun ExpandableText( text: String, color: Color, style: TextStyle, maxLines: Int, isExpanded: Boolean, onExpandToggleRequest: (() -> Unit), modifier: Modifier = Modifier, ) { if (text.isBlank()) return val hapticFeedback = LocalHapticFeedback.current var isExpandable by remember(text) { mutableStateOf(false) } var lastCharIndex by remember(text) { mutableIntStateOf(0) } val showMoreText = stringResource(CommonResR.string.caption_see_more) val showLessText = stringResource(CommonResR.string.caption_see_less) val annotatedText = remember(text, isExpandable, isExpanded, lastCharIndex) { createAnnotatedText( text = text, isExpandable = isExpandable, isExpanded = isExpanded, showMoreText = showMoreText, showLessText = showLessText, endIndex = lastCharIndex, ) } Text( modifier = modifier .fillMaxWidth() .semantics { if (isExpandable) { role = Role.Button contentDescription = if (isExpanded) showLessText else showMoreText } } .clickable( enabled = isExpandable, interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = { hapticFeedback.performHapticFeedback(HapticFeedbackType.Confirm) onExpandToggleRequest() }, ) .animateContentSize(), text = annotatedText, color = color, maxLines = if (isExpanded) Int.MAX_VALUE else maxLines, style = style, onTextLayout = { textLayoutResult -> if (!isExpanded && textLayoutResult.hasVisualOverflow) { isExpandable = true lastCharIndex = textLayoutResult.getLineEnd(maxLines - 1) } }, ) } private fun createAnnotatedText( text: String, isExpandable: Boolean, isExpanded: Boolean, showMoreText: String, showLessText: String, endIndex: Int, ): AnnotatedString = buildAnnotatedString { if (isExpandable) { if (isExpanded) { append(text) append(" ") withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { append(showLessText) } } else { val maxTruncateIndex = (endIndex - showMoreText.length - 1).coerceIn(0, text.length) if (maxTruncateIndex > 0) { val truncatedText = text.take(maxTruncateIndex) .dropLastWhile { it.isWhitespace() || it == '.' } if (truncatedText.isNotEmpty()) { append(truncatedText) append(" ") withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { append(showMoreText) } return@buildAnnotatedString } } append(text) } } else { append(text) } }