Skip to content

Commit

Permalink
Merge pull request #130 from halilozercan/halilozercan/link-click-han…
Browse files Browse the repository at this point in the history
…dler

Moved LinkClickHandler from Markdown modules to richtext-ui
  • Loading branch information
halilozercan authored Feb 10, 2024
2 parents 29a94b4 + 4e4f00b commit 165a138
Show file tree
Hide file tree
Showing 12 changed files with 100 additions and 67 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -118,14 +118,12 @@ import com.halilibo.richtext.ui.resolveDefaults

RichText(
style = richTextStyle,
linkClickHandler = {
Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
},
modifier = Modifier.padding(8.dp),
) {
BasicMarkdown(
astNode = astNode,
onLinkClicked = {
Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
}
)
BasicMarkdown(astNode)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ import kotlinx.coroutines.launch
appendPreviewSentence(Superscript)
appendPreviewSentence(Code)
appendPreviewSentence(
Link { toggleLink = !toggleLink },
Link(""),
if (toggleLink) "clicked link" else "link"
)
append("Here, ")
Expand Down Expand Up @@ -96,7 +96,7 @@ import kotlinx.coroutines.launch
}
}
}
RichText {
RichText(linkClickHandler = { toggleLink = !toggleLink }) {
Text(text)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,10 @@ fun main(): Unit = singleWindowApplication(
modifier = Modifier
.weight(1f)
.verticalScroll(rememberScrollState()),
style = richTextStyle
style = richTextStyle,
linkClickHandler = {
println("Link clicked destination=$it")
}
) {
Markdown(content = text)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import com.halilibo.richtext.markdown.BasicMarkdown
import com.halilibo.richtext.commonmark.MarkdownParseOptions.Companion
import com.halilibo.richtext.markdown.BasicMarkdown
import com.halilibo.richtext.markdown.node.AstNode
import com.halilibo.richtext.ui.RichTextScope

Expand All @@ -19,8 +19,7 @@ import com.halilibo.richtext.ui.RichTextScope
@Composable
public fun RichTextScope.Markdown(
content: String,
markdownParseOptions: MarkdownParseOptions = Companion.Default,
onLinkClicked: ((String) -> Unit)? = null
markdownParseOptions: MarkdownParseOptions = Companion.Default
) {
val commonmarkAstNodeParser = remember(markdownParseOptions) {
CommonmarkAstNodeParser(markdownParseOptions)
Expand All @@ -35,7 +34,7 @@ public fun RichTextScope.Markdown(
}

astRootNode?.let {
BasicMarkdown(astNode = it, onLinkClicked = onLinkClicked)
BasicMarkdown(astNode = it)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
package com.halilibo.richtext.markdown

import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.semantics
import com.halilibo.richtext.markdown.node.AstBlockQuote
import com.halilibo.richtext.markdown.node.AstUnorderedList
import com.halilibo.richtext.markdown.node.AstDocument
import com.halilibo.richtext.markdown.node.AstFencedCodeBlock
import com.halilibo.richtext.markdown.node.AstHeading
Expand All @@ -29,6 +23,7 @@ import com.halilibo.richtext.markdown.node.AstTableRoot
import com.halilibo.richtext.markdown.node.AstTableRow
import com.halilibo.richtext.markdown.node.AstText
import com.halilibo.richtext.markdown.node.AstThematicBreak
import com.halilibo.richtext.markdown.node.AstUnorderedList
import com.halilibo.richtext.ui.BlockQuote
import com.halilibo.richtext.ui.CodeBlock
import com.halilibo.richtext.ui.FormattedList
Expand All @@ -46,22 +41,10 @@ import com.halilibo.richtext.ui.string.richTextString
* Designed to be a building block that should be wrapped with a specific parser.
*
* @param astNode Root node of Markdown tree. This can be obtained via a parser.
* @param onLinkClicked A function to invoke when a link is clicked from rendered content.
*/
@Composable
public fun RichTextScope.BasicMarkdown(
astNode: AstNode,
onLinkClicked: ((String) -> Unit)? = null
) {
val onLinkClickedState = rememberUpdatedState(onLinkClicked)
val realLinkClickedHandler = onLinkClickedState.value ?: LocalUriHandler.current.let {
remember {
{ url -> it.openUri(url) }
}
}
CompositionLocalProvider(LocalOnLinkClicked provides realLinkClickedHandler) {
RecursiveRenderMarkdownAst(astNode = astNode)
}
public fun RichTextScope.BasicMarkdown(astNode: AstNode) {
RecursiveRenderMarkdownAst(astNode)
}

/**
Expand Down Expand Up @@ -185,11 +168,3 @@ internal fun RichTextScope.visitChildren(node: AstNode?) {
RecursiveRenderMarkdownAst(astNode = it)
}
}

/**
* An internal ambient to pass through OnLinkClicked function from root [BasicMarkdown] composable
* to children that render links. Although being explicit is preferred, recursive calls to
* [visitChildren] increases verbosity with each extra argument.
*/
internal val LocalOnLinkClicked =
compositionLocalOf<(String) -> Unit> { error("OnLinkClicked is not provided") }
Original file line number Diff line number Diff line change
Expand Up @@ -57,19 +57,15 @@ import com.halilibo.richtext.ui.string.withFormat
*/
@Composable
internal fun RichTextScope.MarkdownRichText(astNode: AstNode, modifier: Modifier = Modifier) {
val onLinkClicked = LocalOnLinkClicked.current
// Assume that only RichText nodes reside below this level.
val richText = remember(astNode, onLinkClicked) {
computeRichTextString(astNode, onLinkClicked)
val richText = remember(astNode) {
computeRichTextString(astNode)
}

Text(text = richText, modifier = modifier)
}

private fun computeRichTextString(
astNode: AstNode,
onLinkClicked: (String) -> Unit
): RichTextString {
private fun computeRichTextString(astNode: AstNode): RichTextString {
val richTextStringBuilder = RichTextString.Builder()

// Modified pre-order traversal with pushFormat, popFormat support.
Expand Down Expand Up @@ -115,7 +111,7 @@ private fun computeRichTextString(
null
}
is AstLink -> richTextStringBuilder.pushFormat(RichTextString.Format.Link(
onClick = { onLinkClicked(currentNodeType.destination) }
destination = currentNodeType.destination
))
is AstSoftLineBreak -> {
richTextStringBuilder.append(" ")
Expand All @@ -131,9 +127,7 @@ private fun computeRichTextString(
null
}
is AstLinkReferenceDefinition -> richTextStringBuilder.pushFormat(
RichTextString.Format.Link(
onClick = { onLinkClicked(currentNodeType.destination) }
))
RichTextString.Format.Link(destination = currentNodeType.destination))
else -> null
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.Modifier
import com.halilibo.richtext.ui.BasicRichText
import com.halilibo.richtext.ui.LinkClickHandler
import com.halilibo.richtext.ui.RichTextScope
import com.halilibo.richtext.ui.RichTextStyle
import com.halilibo.richtext.ui.RichTextThemeProvider
Expand All @@ -23,12 +24,14 @@ import com.halilibo.richtext.ui.RichTextThemeProvider
public fun RichText(
modifier: Modifier = Modifier,
style: RichTextStyle? = null,
linkClickHandler: LinkClickHandler? = null,
children: @Composable RichTextScope.() -> Unit
) {
RichTextMaterialTheme {
BasicRichText(
modifier = modifier,
style = style,
linkClickHandler = linkClickHandler,
children = children
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.Modifier
import com.halilibo.richtext.ui.BasicRichText
import com.halilibo.richtext.ui.LinkClickHandler
import com.halilibo.richtext.ui.RichTextScope
import com.halilibo.richtext.ui.RichTextStyle
import com.halilibo.richtext.ui.RichTextThemeProvider
Expand All @@ -23,12 +24,14 @@ import com.halilibo.richtext.ui.RichTextThemeProvider
public fun RichText(
modifier: Modifier = Modifier,
style: RichTextStyle? = null,
linkClickHandler: LinkClickHandler? = null,
children: @Composable RichTextScope.() -> Unit
) {
RichTextMaterialTheme {
BasicRichText(
modifier = modifier,
style = style,
linkClickHandler = linkClickHandler,
children = children
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package com.halilibo.richtext.ui
import androidx.compose.foundation.layout.Arrangement.spacedBy
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity

Expand All @@ -15,18 +16,21 @@ import androidx.compose.ui.platform.LocalDensity
public fun BasicRichText(
modifier: Modifier = Modifier,
style: RichTextStyle? = null,
linkClickHandler: LinkClickHandler? = null,
children: @Composable RichTextScope.() -> Unit
) {
with(RichTextScope) {
RestartListLevel {
WithStyle(style) {
val resolvedStyle = currentRichTextStyle.resolveDefaults()
val blockSpacing = with(LocalDensity.current) {
resolvedStyle.paragraphSpacing!!.toDp()
}
CompositionLocalProvider(LocalLinkClickHandler provides linkClickHandler) {
val resolvedStyle = currentRichTextStyle.resolveDefaults()
val blockSpacing = with(LocalDensity.current) {
resolvedStyle.paragraphSpacing!!.toDp()
}

Column(modifier = modifier, verticalArrangement = spacedBy(blockSpacing)) {
children()
Column(modifier = modifier, verticalArrangement = spacedBy(blockSpacing)) {
children()
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.halilibo.richtext.ui

import androidx.compose.runtime.compositionLocalOf

/**
* Handler that will be triggered when a [Format.Link] is clicked.
*/
public fun interface LinkClickHandler {

public fun onClick(url: String)
}

/**
* An internal composition local to pass through LinkClickHandler from root [BasicRichText]
* composable to children that render links.
*/
internal val LocalLinkClickHandler = compositionLocalOf<LinkClickHandler?> { null }
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ public data class RichTextString internal constructor(
) = richTextStyle.codeStyle
}

public data class Link(val onClick: () -> Unit) : Format() {
public data class Link(val destination: String) : Format() {
override fun getStyle(
richTextStyle: RichTextStringStyle,
contentColor: Color
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.platform.UriHandler
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.style.TextOverflow
import com.halilibo.richtext.ui.ClickableText
import com.halilibo.richtext.ui.LinkClickHandler
import com.halilibo.richtext.ui.LocalLinkClickHandler
import com.halilibo.richtext.ui.RichTextScope
import com.halilibo.richtext.ui.currentContentColor
import com.halilibo.richtext.ui.currentRichTextStyle
Expand Down Expand Up @@ -36,16 +40,12 @@ public fun RichTextScope.Text(

val inlineContents = remember(text) { text.getInlineContents() }

BoxWithConstraints(modifier = modifier) {
val inlineTextContents = manageInlineTextContents(
inlineContents = inlineContents,
textConstraints = constraints
)

if (inlineContents.isEmpty()) {
// cheap path
val linkClickHandler = LocalLinkClickHandler.current ?: LocalUriHandler.current
ClickableText(
text = annotated,
onTextLayout = onTextLayout,
inlineContent = inlineTextContents,
softWrap = softWrap,
overflow = overflow,
maxLines = maxLines,
Expand All @@ -55,9 +55,46 @@ public fun RichTextScope.Text(
onClick = { offset ->
annotated.getConsumableAnnotations(text.formatObjects, offset)
.firstOrNull()
?.let { link -> link.onClick() }
?.let { link ->
when (linkClickHandler) {
is LinkClickHandler -> linkClickHandler.onClick(link.destination)
is UriHandler -> linkClickHandler.openUri(link.destination)
}
}
}
)
} else {
// expensive constraints reading path
BoxWithConstraints(modifier = modifier) {
val inlineTextContents = manageInlineTextContents(
inlineContents = inlineContents,
textConstraints = constraints
)

val linkClickHandler = LocalLinkClickHandler.current ?: LocalUriHandler.current

ClickableText(
text = annotated,
onTextLayout = onTextLayout,
inlineContent = inlineTextContents,
softWrap = softWrap,
overflow = overflow,
maxLines = maxLines,
isOffsetClickable = { offset ->
annotated.getConsumableAnnotations(text.formatObjects, offset).any()
},
onClick = { offset ->
annotated.getConsumableAnnotations(text.formatObjects, offset)
.firstOrNull()
?.let { link ->
when (linkClickHandler) {
is LinkClickHandler -> linkClickHandler.onClick(link.destination)
is UriHandler -> linkClickHandler.openUri(link.destination)
}
}
}
)
}
}
}

Expand Down

0 comments on commit 165a138

Please sign in to comment.