@@ -14,11 +14,13 @@ import androidx.compose.foundation.layout.height
14
14
import androidx.compose.foundation.layout.padding
15
15
import androidx.compose.foundation.layout.width
16
16
import androidx.compose.foundation.layout.widthIn
17
+ import androidx.compose.foundation.text.InlineTextContent
17
18
import androidx.compose.foundation.text.selection.DisableSelection
18
19
import androidx.compose.runtime.Composable
19
20
import androidx.compose.runtime.CompositionLocalProvider
20
21
import androidx.compose.runtime.collectAsState
21
22
import androidx.compose.runtime.getValue
23
+ import androidx.compose.runtime.mutableStateOf
22
24
import androidx.compose.runtime.remember
23
25
import androidx.compose.ui.Alignment
24
26
import androidx.compose.ui.Modifier
@@ -30,15 +32,25 @@ import androidx.compose.ui.graphics.isSpecified
30
32
import androidx.compose.ui.graphics.takeOrElse
31
33
import androidx.compose.ui.input.pointer.PointerIcon
32
34
import androidx.compose.ui.input.pointer.pointerHoverIcon
35
+ import androidx.compose.ui.platform.LocalDensity
33
36
import androidx.compose.ui.text.AnnotatedString
37
+ import androidx.compose.ui.text.Placeholder
38
+ import androidx.compose.ui.text.PlaceholderVerticalAlign
34
39
import androidx.compose.ui.text.TextStyle
35
40
import androidx.compose.ui.unit.Dp
36
41
import androidx.compose.ui.unit.LayoutDirection.Ltr
37
42
import androidx.compose.ui.unit.dp
43
+ import androidx.compose.ui.unit.sp
44
+ import coil3.compose.AsyncImage
45
+ import coil3.compose.LocalPlatformContext
46
+ import coil3.request.ImageRequest
47
+ import coil3.size.Size
38
48
import org.jetbrains.jewel.foundation.ExperimentalJewelApi
39
49
import org.jetbrains.jewel.foundation.code.MimeType
40
50
import org.jetbrains.jewel.foundation.code.highlighting.LocalCodeHighlighter
41
51
import org.jetbrains.jewel.foundation.theme.LocalContentColor
52
+ import org.jetbrains.jewel.foundation.util.JewelLogger
53
+ import org.jetbrains.jewel.markdown.InlineMarkdown
42
54
import org.jetbrains.jewel.markdown.MarkdownBlock
43
55
import org.jetbrains.jewel.markdown.MarkdownBlock.BlockQuote
44
56
import org.jetbrains.jewel.markdown.MarkdownBlock.CodeBlock
@@ -106,6 +118,8 @@ public open class DefaultMarkdownBlockRenderer(
106
118
}
107
119
}
108
120
121
+ private data class ImageSize (val width : Int , val height : Int )
122
+
109
123
@Composable
110
124
override fun render (
111
125
block : Paragraph ,
@@ -128,6 +142,7 @@ public open class DefaultMarkdownBlockRenderer(
128
142
.clickable(interactionSource = interactionSource, indication = null , onClick = onTextClick),
129
143
text = renderedContent,
130
144
style = mergedStyle,
145
+ inlineContent = renderedImages(block),
131
146
)
132
147
}
133
148
@@ -447,6 +462,50 @@ public open class DefaultMarkdownBlockRenderer(
447
462
inlineRenderer.renderAsAnnotatedString(block.inlineContent, styling, enabled, onUrlClick)
448
463
}
449
464
465
+ @Composable
466
+ private fun renderedImages (blockInlineContent : WithInlineMarkdown ): Map <String , InlineTextContent > =
467
+ getImages(blockInlineContent).associate { image -> image.source to imageContent(image) }
468
+
469
+ @Composable
470
+ private fun imageContent (image : InlineMarkdown .Image ): InlineTextContent {
471
+ val knownSize = remember(image.source) { mutableStateOf<ImageSize ?>(null ) }
472
+ return InlineTextContent (
473
+ with (LocalDensity .current) {
474
+ // `toSp` ensures that the placeholder size matches the original image size in
475
+ // pixels.
476
+ // This approach doesn't allow images from appearing larger with different screen
477
+ // scaling,
478
+ // but simply maintains behavior consistent with standalone AsyncImage rendering.
479
+ Placeholder (
480
+ width = knownSize.value?.width?.dp?.toSp() ? : 0 .sp,
481
+ height = knownSize.value?.height?.dp?.toSp() ? : 1 .sp,
482
+ placeholderVerticalAlign = PlaceholderVerticalAlign .Bottom ,
483
+ )
484
+ }
485
+ ) {
486
+ AsyncImage (
487
+ model =
488
+ ImageRequest .Builder (LocalPlatformContext .current)
489
+ .data(image.source)
490
+ // make sure image doesn't get downscaled to the placeholder size
491
+ .size(Size .ORIGINAL )
492
+ .build(),
493
+ contentDescription = image.title,
494
+ onSuccess = { state ->
495
+ // onSuccess should only be called once, but adding additional protection from
496
+ // unnecessary rerender
497
+ if (knownSize.value == null ) {
498
+ knownSize.value = state.result.image.let { ImageSize (it.width, it.height) }
499
+ }
500
+ },
501
+ onError = { error ->
502
+ JewelLogger .getInstance(" Jewel" ).warn(" AsyncImage loading: ${error.result.throwable} " )
503
+ },
504
+ modifier = knownSize.value?.let { Modifier .height(it.height.dp).width(it.width.dp) } ? : Modifier ,
505
+ )
506
+ }
507
+ }
508
+
450
509
@Composable
451
510
protected fun MaybeScrollingContainer (
452
511
isScrollable : Boolean ,
@@ -475,3 +534,21 @@ public open class DefaultMarkdownBlockRenderer(
475
534
override operator fun plus (extension : MarkdownRendererExtension ): MarkdownBlockRenderer =
476
535
DefaultMarkdownBlockRenderer (rootStyling, rendererExtensions = rendererExtensions + extension, inlineRenderer)
477
536
}
537
+
538
+ private fun getImages (input : WithInlineMarkdown ): List <InlineMarkdown .Image > = buildList {
539
+ fun collectImagesRecursively (items : List <InlineMarkdown >) {
540
+ for (item in items) {
541
+ when (item) {
542
+ is InlineMarkdown .Image -> {
543
+ if (item.source.isNotBlank()) add(item)
544
+ }
545
+ is WithInlineMarkdown -> {
546
+ collectImagesRecursively(item.inlineContent)
547
+ }
548
+
549
+ else -> {}
550
+ }
551
+ }
552
+ }
553
+ collectImagesRecursively(input.inlineContent)
554
+ }
0 commit comments