Skip to content

Commit

Permalink
Migrate to the new LinkAnnotation API (#425)
Browse files Browse the repository at this point in the history
* replace deprecated UrlAnnotation with LinkAnnotation

* extract determinePointerIcon()

* Replace deprecated UrlAnnotation with LinkAnnotation

* replace deprecated ClickableText

* Introduced LinkAnnotation.Clickable to drive the behaviour of links
* Replaced SimpleClickableText with the good ol' Text
* Successfully wired onUrlClick and onTextClick lambdas
and the enabled flag.

Closes #418

* update api files

* add disabled UI for links

When links are disabled, we show something that looks like a link,
but it's not clickable and has no 👆 pointer icon.

* add a checkbox to toggle links in the Markdown standalone showcase
  • Loading branch information
hamen authored Jul 9, 2024
1 parent 9cda4bf commit 08970fe
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 108 deletions.
8 changes: 6 additions & 2 deletions markdown/core/api/core.api
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,7 @@ public class org/jetbrains/jewel/markdown/rendering/DefaultInlineMarkdownRendere
public static final field Companion Lorg/jetbrains/jewel/markdown/rendering/DefaultInlineMarkdownRenderer$Companion;
public fun <init> (Ljava/util/List;)V
public fun <init> ([Lorg/jetbrains/jewel/markdown/extensions/MarkdownProcessorExtension;)V
public fun renderAsAnnotatedString (Ljava/lang/Iterable;Lorg/jetbrains/jewel/markdown/rendering/InlinesStyling;Z)Landroidx/compose/ui/text/AnnotatedString;
public fun renderAsAnnotatedString (Ljava/lang/Iterable;Lorg/jetbrains/jewel/markdown/rendering/InlinesStyling;ZLkotlin/jvm/functions/Function1;)Landroidx/compose/ui/text/AnnotatedString;
}

public final class org/jetbrains/jewel/markdown/rendering/DefaultInlineMarkdownRenderer$Companion {
Expand Down Expand Up @@ -486,14 +486,18 @@ public class org/jetbrains/jewel/markdown/rendering/DefaultMarkdownBlockRenderer

public abstract interface class org/jetbrains/jewel/markdown/rendering/InlineMarkdownRenderer {
public static final field Companion Lorg/jetbrains/jewel/markdown/rendering/InlineMarkdownRenderer$Companion;
public abstract fun renderAsAnnotatedString (Ljava/lang/Iterable;Lorg/jetbrains/jewel/markdown/rendering/InlinesStyling;Z)Landroidx/compose/ui/text/AnnotatedString;
public abstract fun renderAsAnnotatedString (Ljava/lang/Iterable;Lorg/jetbrains/jewel/markdown/rendering/InlinesStyling;ZLkotlin/jvm/functions/Function1;)Landroidx/compose/ui/text/AnnotatedString;
}

public final class org/jetbrains/jewel/markdown/rendering/InlineMarkdownRenderer$Companion {
public final fun default (Ljava/util/List;)Lorg/jetbrains/jewel/markdown/rendering/InlineMarkdownRenderer;
public static synthetic fun default$default (Lorg/jetbrains/jewel/markdown/rendering/InlineMarkdownRenderer$Companion;Ljava/util/List;ILjava/lang/Object;)Lorg/jetbrains/jewel/markdown/rendering/InlineMarkdownRenderer;
}

public final class org/jetbrains/jewel/markdown/rendering/InlineMarkdownRenderer$DefaultImpls {
public static synthetic fun renderAsAnnotatedString$default (Lorg/jetbrains/jewel/markdown/rendering/InlineMarkdownRenderer;Ljava/lang/Iterable;Lorg/jetbrains/jewel/markdown/rendering/InlinesStyling;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Landroidx/compose/ui/text/AnnotatedString;
}

public final class org/jetbrains/jewel/markdown/rendering/InlinesStyling {
public static final field $stable I
public static final field Companion Lorg/jetbrains/jewel/markdown/rendering/InlinesStyling$Companion;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,41 @@ import androidx.compose.foundation.text.appendInlineContent
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.AnnotatedString.Builder
import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.UrlAnnotation
import androidx.compose.ui.text.buildAnnotatedString
import org.commonmark.renderer.text.TextContentRenderer
import org.jetbrains.jewel.foundation.ExperimentalJewelApi
import org.jetbrains.jewel.markdown.InlineMarkdown
import org.jetbrains.jewel.markdown.extensions.MarkdownProcessorExtension

@ExperimentalJewelApi
public open class DefaultInlineMarkdownRenderer(rendererExtensions: List<MarkdownProcessorExtension>) : InlineMarkdownRenderer {
public open class DefaultInlineMarkdownRenderer(
rendererExtensions: List<MarkdownProcessorExtension>,
) : InlineMarkdownRenderer {
public constructor(vararg extensions: MarkdownProcessorExtension) : this(extensions.toList())

private val plainTextRenderer =
TextContentRenderer.builder()
TextContentRenderer
.builder()
.extensions(rendererExtensions.map { it.textRendererExtension })
.build()

public override fun renderAsAnnotatedString(
inlineMarkdown: Iterable<InlineMarkdown>,
styling: InlinesStyling,
enabled: Boolean,
onUrlClicked: ((String) -> Unit)?,
): AnnotatedString =
buildAnnotatedString {
appendInlineMarkdownFrom(inlineMarkdown, styling, enabled)
appendInlineMarkdownFrom(inlineMarkdown, styling, enabled, onUrlClicked)
}

@OptIn(ExperimentalTextApi::class)
private fun Builder.appendInlineMarkdownFrom(
inlineMarkdown: Iterable<InlineMarkdown>,
styling: InlinesStyling,
enabled: Boolean,
onUrlClicked: ((String) -> Unit)? = null,
) {
for (child in inlineMarkdown) {
when (child) {
Expand All @@ -60,7 +63,15 @@ public open class DefaultInlineMarkdownRenderer(rendererExtensions: List<Markdow

is InlineMarkdown.Link -> {
withStyles(styling.link.withEnabled(enabled), child) {
pushUrlAnnotation(UrlAnnotation(it.nativeNode.destination))
if (enabled) {
val destination = it.nativeNode.destination
val link =
LinkAnnotation.Clickable(
tag = destination,
linkInteractionListener = { _ -> onUrlClicked?.invoke(destination) },
)
pushLink(link)
}
appendInlineMarkdownFrom(it.children, styling, enabled)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import androidx.compose.animation.core.tween
import androidx.compose.foundation.HorizontalScrollbar
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
Expand All @@ -20,7 +22,6 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.rememberScrollbarAdapter
import androidx.compose.foundation.text.ClickableText
import androidx.compose.foundation.text.selection.DisableSelection
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
Expand All @@ -42,7 +43,6 @@ import androidx.compose.ui.input.pointer.pointerHoverIcon
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection.Ltr
Expand Down Expand Up @@ -109,7 +109,8 @@ public open class DefaultMarkdownBlockRenderer(
is Paragraph -> render(block, rootStyling.paragraph, enabled, onUrlClick, onTextClick)
ThematicBreak -> renderThematicBreak(rootStyling.thematicBreak)
is CustomBlock -> {
rendererExtensions.find { it.blockRenderer.canRender(block) }
rendererExtensions
.find { it.blockRenderer.canRender(block) }
?.blockRenderer
?.render(block, blockRenderer = this, inlineRenderer, enabled, onUrlClick, onTextClick)
}
Expand All @@ -124,14 +125,29 @@ public open class DefaultMarkdownBlockRenderer(
onUrlClick: (String) -> Unit,
onTextClick: () -> Unit,
) {
val renderedContent = rememberRenderedContent(block, styling.inlinesStyling, enabled)
SimpleClickableText(
val renderedContent =
rememberRenderedContent(
block,
styling.inlinesStyling,
enabled,
onUrlClick,
)
val textColor =
styling.inlinesStyling.textStyle.color
.takeOrElse { LocalContentColor.current }
.takeOrElse { styling.inlinesStyling.textStyle.color }
val mergedStyle = styling.inlinesStyling.textStyle.merge(TextStyle(color = textColor))
val interactionSource = remember { MutableInteractionSource() }

Text(
modifier =
Modifier.clickable(
interactionSource = interactionSource,
indication = null,
onClick = onTextClick,
),
text = renderedContent,
textStyle = styling.inlinesStyling.textStyle,
color = styling.inlinesStyling.textStyle.color.takeOrElse { LocalContentColor.current },
enabled = enabled,
onUnhandledClick = onTextClick,
onUrlClick = onUrlClick,
style = mergedStyle,
)
}

Expand Down Expand Up @@ -162,17 +178,20 @@ public open class DefaultMarkdownBlockRenderer(
onUrlClick: (String) -> Unit,
onTextClick: () -> Unit,
) {
val renderedContent = rememberRenderedContent(block, styling.inlinesStyling, enabled)
val renderedContent =
rememberRenderedContent(
block,
styling.inlinesStyling,
enabled,
onUrlClick,
)
Heading(
renderedContent,
styling.inlinesStyling.textStyle,
styling.padding,
styling.underlineWidth,
styling.underlineColor,
styling.underlineGap,
enabled,
onUrlClick,
onTextClick,
)
}

Expand All @@ -184,18 +203,15 @@ public open class DefaultMarkdownBlockRenderer(
underlineWidth: Dp,
underlineColor: Color,
underlineGap: Dp,
enabled: Boolean,
onUrlClick: (String) -> Unit,
onTextClick: () -> Unit,
) {
Column(modifier = Modifier.padding(paddingValues)) {
SimpleClickableText(
val textColor =
textStyle.color
.takeOrElse { LocalContentColor.current.takeOrElse { textStyle.color } }
val mergedStyle = textStyle.merge(TextStyle(color = textColor))
Text(
text = renderedContent,
textStyle = textStyle,
color = textStyle.color.takeOrElse { LocalContentColor.current },
enabled = enabled,
onUnhandledClick = onTextClick,
onUrlClick = onUrlClick,
style = mergedStyle,
)

if (underlineWidth > 0.dp && underlineColor.isSpecified) {
Expand All @@ -214,21 +230,21 @@ public open class DefaultMarkdownBlockRenderer(
onTextClick: () -> Unit,
) {
Column(
Modifier.drawBehind {
val isLtr = layoutDirection == Ltr
val lineWidthPx = styling.lineWidth.toPx()
val x = if (isLtr) lineWidthPx / 2 else size.width - lineWidthPx / 2

drawLine(
styling.lineColor,
Offset(x, 0f),
Offset(x, size.height),
lineWidthPx,
styling.strokeCap,
styling.pathEffect,
)
}
.padding(styling.padding),
Modifier
.drawBehind {
val isLtr = layoutDirection == Ltr
val lineWidthPx = styling.lineWidth.toPx()
val x = if (isLtr) lineWidthPx / 2 else size.width - lineWidthPx / 2

drawLine(
styling.lineColor,
Offset(x, 0f),
Offset(x, size.height),
lineWidthPx,
styling.strokeCap,
styling.pathEffect,
)
}.padding(styling.padding),
verticalArrangement = Arrangement.spacedBy(rootStyling.blockVerticalSpacing),
) {
CompositionLocalProvider(LocalContentColor provides styling.textColor) {
Expand Down Expand Up @@ -278,7 +294,8 @@ public open class DefaultMarkdownBlockRenderer(
style = styling.numberStyle,
color = styling.numberStyle.color.takeOrElse { LocalContentColor.current },
modifier =
Modifier.widthIn(min = styling.numberMinWidth)
Modifier
.widthIn(min = styling.numberMinWidth)
.pointerHoverIcon(PointerIcon.Default, overrideDescendants = true),
textAlign = styling.numberTextAlign,
)
Expand Down Expand Up @@ -357,7 +374,8 @@ public open class DefaultMarkdownBlockRenderer(
) {
HorizontallyScrollingContainer(
isScrollable = styling.scrollsHorizontally,
Modifier.background(styling.background, styling.shape)
Modifier
.background(styling.background, styling.shape)
.border(styling.borderWidth, styling.borderColor, styling.shape)
.then(if (styling.fillWidth) Modifier.fillMaxWidth() else Modifier),
) {
Expand All @@ -366,7 +384,8 @@ public open class DefaultMarkdownBlockRenderer(
style = styling.editorTextStyle,
color = styling.editorTextStyle.color.takeOrElse { LocalContentColor.current },
modifier =
Modifier.padding(styling.padding)
Modifier
.padding(styling.padding)
.pointerHoverIcon(PointerIcon.Default, overrideDescendants = true),
)
}
Expand All @@ -379,7 +398,8 @@ public open class DefaultMarkdownBlockRenderer(
) {
HorizontallyScrollingContainer(
isScrollable = styling.scrollsHorizontally,
Modifier.background(styling.background, styling.shape)
Modifier
.background(styling.background, styling.shape)
.border(styling.borderWidth, styling.borderColor, styling.shape)
.then(if (styling.fillWidth) Modifier.fillMaxWidth() else Modifier),
) {
Expand Down Expand Up @@ -419,7 +439,7 @@ public open class DefaultMarkdownBlockRenderer(
infoText: String,
alignment: Alignment.Horizontal,
textStyle: TextStyle,
modifier: Modifier,
modifier: Modifier = Modifier,
) {
Column(modifier, horizontalAlignment = alignment) {
DisableSelection {
Expand Down Expand Up @@ -450,53 +470,9 @@ public open class DefaultMarkdownBlockRenderer(
block: BlockWithInlineMarkdown,
styling: InlinesStyling,
enabled: Boolean,
onUrlClick: ((String) -> Unit)? = null,
) = remember(block.inlineContent, styling, enabled) {
inlineRenderer.renderAsAnnotatedString(block.inlineContent, styling, enabled)
}

@OptIn(ExperimentalTextApi::class)
@Composable
private fun SimpleClickableText(
text: AnnotatedString,
textStyle: TextStyle,
enabled: Boolean,
modifier: Modifier = Modifier,
color: Color = Color.Unspecified,
onUrlClick: (String) -> Unit,
onUnhandledClick: () -> Unit,
) {
var hoverPointerIcon by remember { mutableStateOf(PointerIcon.Default) }
val actualPointerIcon =
remember(hoverPointerIcon, enabled) {
if (enabled) hoverPointerIcon else PointerIcon.Default
}

val textColor = color.takeOrElse { LocalContentColor.current.takeOrElse { textStyle.color } }

val mergedStyle = textStyle.merge(TextStyle(color = textColor))

ClickableText(
text = text,
style = mergedStyle,
modifier = modifier.pointerHoverIcon(actualPointerIcon, true),
onHover = { offset ->
hoverPointerIcon =
if (offset == null || text.getUrlAnnotations(offset, offset).isEmpty()) {
PointerIcon.Default
} else {
PointerIcon.Hand
}
},
) { offset ->
if (!enabled) return@ClickableText

val span = text.getUrlAnnotations(offset, offset).firstOrNull()
if (span != null) {
onUrlClick(span.item.url)
} else {
onUnhandledClick()
}
}
inlineRenderer.renderAsAnnotatedString(block.inlineContent, styling, enabled, onUrlClick)
}

@Composable
Expand All @@ -511,7 +487,8 @@ public open class DefaultMarkdownBlockRenderer(
content = {
val scrollState = rememberScrollState()
Box(
Modifier.layoutId("mainContent")
Modifier
.layoutId("mainContent")
.then(if (isScrollable) Modifier.horizontalScroll(scrollState) else Modifier),
) {
content()
Expand All @@ -529,7 +506,8 @@ public open class DefaultMarkdownBlockRenderer(

HorizontalScrollbar(
rememberScrollbarAdapter(scrollState),
Modifier.layoutId("containerHScrollbar")
Modifier
.layoutId("containerHScrollbar")
.padding(start = 2.dp, end = 2.dp, bottom = 2.dp)
.alpha(alpha),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public interface InlineMarkdownRenderer {
inlineMarkdown: Iterable<InlineMarkdown>,
styling: InlinesStyling,
enabled: Boolean,
onUrlClicked: ((String) -> Unit)? = null,
): AnnotatedString

public companion object {
Expand Down
Loading

0 comments on commit 08970fe

Please sign in to comment.