Fixes misalignment of Clickable Texts
This commit is contained in:
@@ -24,7 +24,6 @@ import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.BasicText
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.foundation.text.InlineTextContent
|
||||
import androidx.compose.foundation.text.appendInlineContent
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
@@ -298,27 +297,24 @@ fun CreateClickableText(
|
||||
suffix: String?,
|
||||
maxLines: Int = Int.MAX_VALUE,
|
||||
overrideColor: Color? = null,
|
||||
fontWeight: FontWeight = FontWeight.Normal,
|
||||
fontWeight: FontWeight? = null,
|
||||
route: String,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
val currentStyle = LocalTextStyle.current
|
||||
val primaryColor = MaterialTheme.colorScheme.primary
|
||||
val onBackgroundColor = MaterialTheme.colorScheme.onBackground
|
||||
|
||||
val clickablePartStyle =
|
||||
remember(primaryColor, overrideColor) {
|
||||
currentStyle
|
||||
.copy(color = overrideColor ?: primaryColor, fontWeight = fontWeight)
|
||||
.toSpanStyle()
|
||||
}
|
||||
SpanStyle(
|
||||
color = overrideColor ?: primaryColor,
|
||||
fontWeight = fontWeight,
|
||||
)
|
||||
|
||||
val nonClickablePartStyle =
|
||||
remember(onBackgroundColor, overrideColor) {
|
||||
currentStyle
|
||||
.copy(color = overrideColor ?: onBackgroundColor, fontWeight = fontWeight)
|
||||
.toSpanStyle()
|
||||
}
|
||||
SpanStyle(
|
||||
color = overrideColor ?: onBackgroundColor,
|
||||
fontWeight = fontWeight,
|
||||
)
|
||||
|
||||
val text =
|
||||
remember(clickablePartStyle, nonClickablePartStyle, clickablePart, suffix) {
|
||||
@@ -333,11 +329,45 @@ fun CreateClickableText(
|
||||
ClickableText(
|
||||
text = text,
|
||||
maxLines = maxLines,
|
||||
style = currentStyle,
|
||||
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
|
||||
fun CreateTextWithEmoji(
|
||||
text: String,
|
||||
|
||||
@@ -28,11 +28,10 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
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.appendInlineContent
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ProvideTextStyle
|
||||
@@ -49,7 +48,6 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.takeOrElse
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalFontFamilyResolver
|
||||
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.buildAnnotatedString
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.em
|
||||
import com.halilibo.richtext.markdown.Markdown
|
||||
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
|
||||
private fun RenderRegular(
|
||||
content: String,
|
||||
@@ -183,76 +249,66 @@ private fun RenderRegular(
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
RenderRegular(content, tags) { word, state ->
|
||||
if (canPreview) {
|
||||
RenderWordWithPreview(
|
||||
word,
|
||||
state,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
} else {
|
||||
RenderWordWithoutPreview(
|
||||
word,
|
||||
state,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
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 currentTextColor = LocalContentColor.current
|
||||
|
||||
val textStyle =
|
||||
remember(currentTextStyle, currentTextColor) {
|
||||
remember(currentTextStyle) {
|
||||
currentTextStyle.copy(
|
||||
lineHeight = 1.4.em,
|
||||
color = currentTextStyle.color.takeOrElse { currentTextColor },
|
||||
)
|
||||
}
|
||||
|
||||
val spaceWidth = measureSpaceWidth(textStyle)
|
||||
|
||||
Column {
|
||||
if (canPreview) {
|
||||
// FlowRow doesn't work well with paragraphs. So we need to split them
|
||||
state.paragraphs.forEach { paragraph ->
|
||||
val direction =
|
||||
// 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
|
||||
}
|
||||
|
||||
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(
|
||||
word,
|
||||
state,
|
||||
backgroundColor,
|
||||
textStyle,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} 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(
|
||||
word,
|
||||
state,
|
||||
backgroundColor,
|
||||
textStyle,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
},
|
||||
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,
|
||||
state: RichTextViewerState,
|
||||
backgroundColor: MutableState<Color>,
|
||||
style: TextStyle,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
@@ -291,10 +346,10 @@ private fun RenderWordWithoutPreview(
|
||||
is LinkSegment -> ClickableUrl(word.segmentText, word.segmentText)
|
||||
is EmojiSegment -> RenderCustomEmoji(word.segmentText, state)
|
||||
// Don't offer to pay invoices
|
||||
is InvoiceSegment -> NormalWord(word.segmentText, style)
|
||||
is InvoiceSegment -> Text(word.segmentText)
|
||||
// Don't offer to withdraw
|
||||
is WithdrawSegment -> NormalWord(word.segmentText, style)
|
||||
is CashuSegment -> NormalWord(word.segmentText, style)
|
||||
is WithdrawSegment -> Text(word.segmentText)
|
||||
is CashuSegment -> Text(word.segmentText)
|
||||
is EmailSegment -> ClickableEmail(word.segmentText)
|
||||
is PhoneSegment -> ClickablePhone(word.segmentText)
|
||||
is BechSegment -> BechLink(word.segmentText, false, backgroundColor, accountViewModel, nav)
|
||||
@@ -302,7 +357,7 @@ private fun RenderWordWithoutPreview(
|
||||
is HashIndexUserSegment -> TagLink(word, accountViewModel, nav)
|
||||
is HashIndexEventSegment -> TagLink(word, false, backgroundColor, accountViewModel, nav)
|
||||
is SchemelessUrlSegment -> NoProtocolUrlRenderer(word)
|
||||
is RegularTextSegment -> NormalWord(word.segmentText, style)
|
||||
is RegularTextSegment -> Text(word.segmentText)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,7 +366,6 @@ private fun RenderWordWithPreview(
|
||||
word: Segment,
|
||||
state: RichTextViewerState,
|
||||
backgroundColor: MutableState<Color>,
|
||||
style: TextStyle,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
@@ -329,7 +383,7 @@ private fun RenderWordWithPreview(
|
||||
is HashIndexUserSegment -> TagLink(word, accountViewModel, nav)
|
||||
is HashIndexEventSegment -> TagLink(word, true, backgroundColor, accountViewModel, nav)
|
||||
is SchemelessUrlSegment -> NoProtocolUrlRenderer(word)
|
||||
is RegularTextSegment -> NormalWord(word.segmentText, style)
|
||||
is RegularTextSegment -> Text(word.segmentText)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,23 +401,7 @@ private fun ZoomableContentView(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NormalWord(
|
||||
word: String,
|
||||
style: TextStyle,
|
||||
) {
|
||||
BasicText(
|
||||
text = word,
|
||||
style = style,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NoProtocolUrlRenderer(word: SchemelessUrlSegment) {
|
||||
RenderUrl(word)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderUrl(segment: SchemelessUrlSegment) {
|
||||
private fun NoProtocolUrlRenderer(segment: SchemelessUrlSegment) {
|
||||
ClickableUrl(segment.url, "https://${segment.url}")
|
||||
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) } }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user