package org.terst.doot.widget.ui import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.text.BasicText import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue 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.input.pointer.pointerInput import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.sp enum class LinkKind { URL, PHONE } data class TextLink(val range: IntRange, val kind: LinkKind, val target: String) private val URL_REGEX = Regex("""https?://[^\s<>"']+""") private val PHONE_REGEX = Regex("""\+?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}""") /** * Finds URL and phone-number spans in text. Phone matches that overlap an * already-found URL span are dropped (a URL's digits could coincidentally * match the phone pattern; the URL match wins). Intentionally simple -- * US-style phone numbers, not a full i18n phone-number parser. */ internal fun findLinks(text: String): List { val links = mutableListOf() for (match in URL_REGEX.findAll(text)) { links.add(TextLink(match.range, LinkKind.URL, match.value)) } for (match in PHONE_REGEX.findAll(text)) { val overlaps = links.any { it.range.first <= match.range.last && match.range.first <= it.range.last } if (!overlaps) { links.add(TextLink(match.range, LinkKind.PHONE, match.value)) } } return links.sortedBy { it.range.first } } /** * Renders text with URLs and phone numbers underlined and tappable. * Doesn't use Compose's LinkAnnotation API (unconfirmed whether it's fully * wired into Text's click handling at this project's resolved Compose UI * version, 1.6.1) -- instead resolves taps manually via * TextLayoutResult.getOffsetForPosition, which works on any Compose version. */ @Composable fun LinkifiedText( text: String, modifier: Modifier = Modifier, onOpenUrl: (String) -> Unit, onDialPhone: (String) -> Unit ) { val links = remember(text) { findLinks(text) } val annotated = remember(text, links) { buildAnnotatedString { append(text) for (link in links) { addStyle( SpanStyle(color = Color(0xFF60A5FA), textDecoration = TextDecoration.Underline), link.range.first, link.range.last + 1 ) } } } var layoutResult by remember { mutableStateOf(null) } BasicText( text = annotated, modifier = modifier.pointerInput(links) { detectTapGestures { offset -> val layout = layoutResult ?: return@detectTapGestures val charOffset = layout.getOffsetForPosition(offset) links.firstOrNull { charOffset in it.range }?.let { link -> when (link.kind) { LinkKind.URL -> onOpenUrl(link.target) LinkKind.PHONE -> onDialPhone(link.target) } } } }, style = TextStyle(color = Color.White.copy(alpha = 0.85f), fontSize = 14.sp), onTextLayout = { layoutResult = it } ) }