Fixes misalignment of Clickable Texts

This commit is contained in:
Vitor Pamplona
2024-02-15 11:00:15 -05:00
parent ed7dcf94f8
commit 215989108b
2 changed files with 165 additions and 97 deletions
@@ -24,7 +24,6 @@ import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.BasicText import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.text.ClickableText
import androidx.compose.foundation.text.InlineTextContent import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.foundation.text.appendInlineContent import androidx.compose.foundation.text.appendInlineContent
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
@@ -298,27 +297,24 @@ fun CreateClickableText(
suffix: String?, suffix: String?,
maxLines: Int = Int.MAX_VALUE, maxLines: Int = Int.MAX_VALUE,
overrideColor: Color? = null, overrideColor: Color? = null,
fontWeight: FontWeight = FontWeight.Normal, fontWeight: FontWeight? = null,
route: String, route: String,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
val currentStyle = LocalTextStyle.current
val primaryColor = MaterialTheme.colorScheme.primary val primaryColor = MaterialTheme.colorScheme.primary
val onBackgroundColor = MaterialTheme.colorScheme.onBackground val onBackgroundColor = MaterialTheme.colorScheme.onBackground
val clickablePartStyle = val clickablePartStyle =
remember(primaryColor, overrideColor) { SpanStyle(
currentStyle color = overrideColor ?: primaryColor,
.copy(color = overrideColor ?: primaryColor, fontWeight = fontWeight) fontWeight = fontWeight,
.toSpanStyle() )
}
val nonClickablePartStyle = val nonClickablePartStyle =
remember(onBackgroundColor, overrideColor) { SpanStyle(
currentStyle color = overrideColor ?: onBackgroundColor,
.copy(color = overrideColor ?: onBackgroundColor, fontWeight = fontWeight) fontWeight = fontWeight,
.toSpanStyle() )
}
val text = val text =
remember(clickablePartStyle, nonClickablePartStyle, clickablePart, suffix) { remember(clickablePartStyle, nonClickablePartStyle, clickablePart, suffix) {
@@ -333,11 +329,45 @@ fun CreateClickableText(
ClickableText( ClickableText(
text = text, text = text,
maxLines = maxLines, maxLines = maxLines,
style = currentStyle,
onClick = { nav(route) }, onClick = { nav(route) },
) )
} }
@Composable
fun ClickableText(
text: AnnotatedString,
modifier: Modifier = Modifier,
style: TextStyle = LocalTextStyle.current,
softWrap: Boolean = true,
overflow: TextOverflow = TextOverflow.Clip,
maxLines: Int = Int.MAX_VALUE,
onTextLayout: (TextLayoutResult) -> Unit = {},
onClick: (Int) -> Unit,
) {
val layoutResult = remember { mutableStateOf<TextLayoutResult?>(null) }
val pressIndicator =
Modifier.pointerInput(onClick) {
detectTapGestures { pos ->
layoutResult.value?.let { layoutResult ->
onClick(layoutResult.getOffsetForPosition(pos))
}
}
}
Text(
text = text,
modifier = modifier.then(pressIndicator),
style = style,
softWrap = softWrap,
overflow = overflow,
maxLines = maxLines,
onTextLayout = {
layoutResult.value = it
onTextLayout(it)
},
)
}
@Composable @Composable
fun CreateTextWithEmoji( fun CreateTextWithEmoji(
text: String, text: String,
@@ -28,11 +28,10 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.text.BasicText import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.InlineTextContent import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.foundation.text.appendInlineContent import androidx.compose.foundation.text.appendInlineContent
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle import androidx.compose.material3.ProvideTextStyle
@@ -49,7 +48,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFontFamilyResolver import androidx.compose.ui.platform.LocalFontFamilyResolver
import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalLayoutDirection
@@ -62,8 +60,10 @@ import androidx.compose.ui.text.TextMeasurer
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.em import androidx.compose.ui.unit.em
import com.halilibo.richtext.markdown.Markdown import com.halilibo.richtext.markdown.Markdown
import com.halilibo.richtext.markdown.MarkdownParseOptions import com.halilibo.richtext.markdown.MarkdownParseOptions
@@ -174,7 +174,73 @@ fun RichTextViewer(
} }
} }
@OptIn(ExperimentalLayoutApi::class) @Preview()
@Composable
fun RenderRegularPreview() {
val backgroundColor = MaterialTheme.colorScheme.background
val backgroundColorState =
remember {
mutableStateOf(backgroundColor)
}
val nav: (String) -> Unit = {}
Column(modifier = Modifier.padding(10.dp)) {
RenderRegular(
"nostr:npub1e0z776cpe0gllgktjk54fuzv8pdfxmq6smsmh8xd7t8s7n474n9smk0txy but i'm Monthly funding" +
" 7 other humans vitor@vitorpamplona.com at the moment so spread #test a bit thin, but won't always be the case.",
EmptyTagList,
) { word, state ->
when (word) {
// is ImageSegment -> ZoomableContentView(word.segmentText, state, accountViewModel)
// is LinkSegment -> LoadUrlPreview(word.segmentText, word.segmentText, accountViewModel)
is EmojiSegment -> RenderCustomEmoji(word.segmentText, state)
is InvoiceSegment -> MayBeInvoicePreview(word.segmentText)
is WithdrawSegment -> MayBeWithdrawal(word.segmentText)
// is CashuSegment -> CashuPreview(word.segmentText, accountViewModel)
is EmailSegment -> ClickableEmail(word.segmentText)
is PhoneSegment -> ClickablePhone(word.segmentText)
is BechSegment -> {
CreateClickableText(
word.segmentText.substring(0, 10),
"",
1,
route = "",
nav = nav,
)
}
is HashTagSegment -> HashTag(word, nav)
// is HashIndexUserSegment -> TagLink(word, accountViewModel, nav)
// is HashIndexEventSegment -> TagLink(word, true, backgroundColorState, accountViewModel, nav)
is SchemelessUrlSegment -> NoProtocolUrlRenderer(word)
is RegularTextSegment -> Text(word.segmentText)
}
}
RenderRegular(
"#Amethyst v0.84.1: ncryptsec support (NIP-49)",
EmptyTagList,
) { word, state ->
when (word) {
// is ImageSegment -> ZoomableContentView(word.segmentText, state, accountViewModel)
// is LinkSegment -> LoadUrlPreview(word.segmentText, word.segmentText, accountViewModel)
is EmojiSegment -> RenderCustomEmoji(word.segmentText, state)
is InvoiceSegment -> MayBeInvoicePreview(word.segmentText)
is WithdrawSegment -> MayBeWithdrawal(word.segmentText)
// is CashuSegment -> CashuPreview(word.segmentText, accountViewModel)
is EmailSegment -> ClickableEmail(word.segmentText)
is PhoneSegment -> ClickablePhone(word.segmentText)
// is BechSegment -> BechLink(word.segmentText, true, backgroundColor, accountViewModel, nav)
is HashTagSegment -> HashTag(word, nav)
// is HashIndexUserSegment -> TagLink(word, accountViewModel, nav)
// is HashIndexEventSegment -> TagLink(word, true, backgroundColorState, accountViewModel, nav)
is SchemelessUrlSegment -> NoProtocolUrlRenderer(word)
is RegularTextSegment -> Text(word.segmentText)
}
}
}
}
@Composable @Composable
private fun RenderRegular( private fun RenderRegular(
content: String, content: String,
@@ -184,77 +250,67 @@ private fun RenderRegular(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
val state by remember(content, tags) { mutableStateOf(CachedRichTextParser.parseText(content, tags)) } RenderRegular(content, tags) { word, state ->
val currentTextStyle = LocalTextStyle.current
val currentTextColor = LocalContentColor.current
val textStyle =
remember(currentTextStyle, currentTextColor) {
currentTextStyle.copy(
lineHeight = 1.4.em,
color = currentTextStyle.color.takeOrElse { currentTextColor },
)
}
val spaceWidth = measureSpaceWidth(textStyle)
Column {
if (canPreview) { if (canPreview) {
// FlowRow doesn't work well with paragraphs. So we need to split them
state.paragraphs.forEach { paragraph ->
val direction =
if (paragraph.isRTL) {
LayoutDirection.Rtl
} else {
LayoutDirection.Ltr
}
CompositionLocalProvider(LocalLayoutDirection provides direction) {
FlowRow(
modifier = Modifier.align(if (paragraph.isRTL) Alignment.End else Alignment.Start),
horizontalArrangement = Arrangement.spacedBy(spaceWidth),
) {
paragraph.words.forEach { word ->
RenderWordWithPreview( RenderWordWithPreview(
word, word,
state, state,
backgroundColor, backgroundColor,
textStyle,
accountViewModel, accountViewModel,
nav, nav,
) )
}
}
}
}
} else { } else {
// FlowRow doesn't work well with paragraphs. So we need to split them
state.paragraphs.forEach { paragraph ->
val direction =
if (paragraph.isRTL) {
LayoutDirection.Rtl
} else {
LayoutDirection.Ltr
}
CompositionLocalProvider(LocalLayoutDirection provides direction) {
FlowRow(
horizontalArrangement = Arrangement.spacedBy(spaceWidth),
modifier = Modifier.align(if (paragraph.isRTL) Alignment.End else Alignment.Start),
) {
paragraph.words.forEach { word ->
RenderWordWithoutPreview( RenderWordWithoutPreview(
word, word,
state, state,
backgroundColor, backgroundColor,
textStyle,
accountViewModel, accountViewModel,
nav, nav,
) )
} }
} }
} }
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun RenderRegular(
content: String,
tags: ImmutableListOfLists<String>,
wordRenderer: @Composable (Segment, RichTextViewerState) -> Unit,
) {
val state by remember(content, tags) { mutableStateOf(CachedRichTextParser.parseText(content, tags)) }
val spaceWidth = measureSpaceWidth(LocalTextStyle.current)
val currentTextStyle = LocalTextStyle.current
val textStyle =
remember(currentTextStyle) {
currentTextStyle.copy(
lineHeight = 1.4.em,
)
}
Column {
// FlowRow doesn't work well with paragraphs. So we need to split them
state.paragraphs.forEach { paragraph ->
CompositionLocalProvider(
LocalLayoutDirection provides
if (paragraph.isRTL) {
LayoutDirection.Rtl
} else {
LayoutDirection.Ltr
},
LocalTextStyle provides textStyle,
) {
FlowRow(
modifier = Modifier.align(if (paragraph.isRTL) Alignment.End else Alignment.Start),
horizontalArrangement = Arrangement.spacedBy(spaceWidth),
) {
paragraph.words.forEach { word ->
wordRenderer(word, state)
}
}
} }
} }
} }
@@ -281,7 +337,6 @@ private fun RenderWordWithoutPreview(
word: Segment, word: Segment,
state: RichTextViewerState, state: RichTextViewerState,
backgroundColor: MutableState<Color>, backgroundColor: MutableState<Color>,
style: TextStyle,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
@@ -291,10 +346,10 @@ private fun RenderWordWithoutPreview(
is LinkSegment -> ClickableUrl(word.segmentText, word.segmentText) is LinkSegment -> ClickableUrl(word.segmentText, word.segmentText)
is EmojiSegment -> RenderCustomEmoji(word.segmentText, state) is EmojiSegment -> RenderCustomEmoji(word.segmentText, state)
// Don't offer to pay invoices // Don't offer to pay invoices
is InvoiceSegment -> NormalWord(word.segmentText, style) is InvoiceSegment -> Text(word.segmentText)
// Don't offer to withdraw // Don't offer to withdraw
is WithdrawSegment -> NormalWord(word.segmentText, style) is WithdrawSegment -> Text(word.segmentText)
is CashuSegment -> NormalWord(word.segmentText, style) is CashuSegment -> Text(word.segmentText)
is EmailSegment -> ClickableEmail(word.segmentText) is EmailSegment -> ClickableEmail(word.segmentText)
is PhoneSegment -> ClickablePhone(word.segmentText) is PhoneSegment -> ClickablePhone(word.segmentText)
is BechSegment -> BechLink(word.segmentText, false, backgroundColor, accountViewModel, nav) is BechSegment -> BechLink(word.segmentText, false, backgroundColor, accountViewModel, nav)
@@ -302,7 +357,7 @@ private fun RenderWordWithoutPreview(
is HashIndexUserSegment -> TagLink(word, accountViewModel, nav) is HashIndexUserSegment -> TagLink(word, accountViewModel, nav)
is HashIndexEventSegment -> TagLink(word, false, backgroundColor, accountViewModel, nav) is HashIndexEventSegment -> TagLink(word, false, backgroundColor, accountViewModel, nav)
is SchemelessUrlSegment -> NoProtocolUrlRenderer(word) is SchemelessUrlSegment -> NoProtocolUrlRenderer(word)
is RegularTextSegment -> NormalWord(word.segmentText, style) is RegularTextSegment -> Text(word.segmentText)
} }
} }
@@ -311,7 +366,6 @@ private fun RenderWordWithPreview(
word: Segment, word: Segment,
state: RichTextViewerState, state: RichTextViewerState,
backgroundColor: MutableState<Color>, backgroundColor: MutableState<Color>,
style: TextStyle,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
@@ -329,7 +383,7 @@ private fun RenderWordWithPreview(
is HashIndexUserSegment -> TagLink(word, accountViewModel, nav) is HashIndexUserSegment -> TagLink(word, accountViewModel, nav)
is HashIndexEventSegment -> TagLink(word, true, backgroundColor, accountViewModel, nav) is HashIndexEventSegment -> TagLink(word, true, backgroundColor, accountViewModel, nav)
is SchemelessUrlSegment -> NoProtocolUrlRenderer(word) is SchemelessUrlSegment -> NoProtocolUrlRenderer(word)
is RegularTextSegment -> NormalWord(word.segmentText, style) is RegularTextSegment -> Text(word.segmentText)
} }
} }
@@ -347,23 +401,7 @@ private fun ZoomableContentView(
} }
@Composable @Composable
private fun NormalWord( private fun NoProtocolUrlRenderer(segment: SchemelessUrlSegment) {
word: String,
style: TextStyle,
) {
BasicText(
text = word,
style = style,
)
}
@Composable
private fun NoProtocolUrlRenderer(word: SchemelessUrlSegment) {
RenderUrl(word)
}
@Composable
private fun RenderUrl(segment: SchemelessUrlSegment) {
ClickableUrl(segment.url, "https://${segment.url}") ClickableUrl(segment.url, "https://${segment.url}")
segment.extras?.let { it1 -> Text(it1) } segment.extras?.let { it1 -> Text(it1) }
} }
@@ -649,7 +687,7 @@ fun HashTag(
} }
} }
segment.extras?.ifBlank { "" }?.let { withStyle(regularText) { append(it) } } segment.extras?.let { withStyle(regularText) { append(it) } }
} }
} }