@@ -85,7 +85,9 @@ import com.mikepenz.markdown.compose.components.markdownComponents
85import com.mikepenz.markdown.m3.Markdown
85import com.mikepenz.markdown.m3.Markdown
86import com.mikepenz.markdown.model.DefaultMarkdownAnnotator
86import com.mikepenz.markdown.model.DefaultMarkdownAnnotator
87import com.mikepenz.markdown.model.DefaultMarkdownAnnotatorConfig
87import com.mikepenz.markdown.model.DefaultMarkdownAnnotatorConfig
88+import com.mikepenz.markdown.model.MarkdownAnimations
88import com.mikepenz.markdown.model.MarkdownState
89import com.mikepenz.markdown.model.MarkdownState
90+import com.mikepenz.markdown.model.markdownAnimations
89import com.mikepenz.markdown.model.rememberMarkdownState
91import com.mikepenz.markdown.model.rememberMarkdownState
90import org.intellij.markdown.MarkdownElementTypes
92import org.intellij.markdown.MarkdownElementTypes
91import org.intellij.markdown.ast.ASTNode
93import org.intellij.markdown.ast.ASTNode
@@ -209,6 +211,21 @@ private fun MarkdownBody(
211 val context = LocalContext.current
213 val context = LocalContext.current
214+ // No-op [MarkdownAnimations] disables the library's [animateContentSize] wrapper applied
215+ // by [com.mikepenz.markdown.compose.elements.MarkdownText] via [MarkdownAnimations.animateTextSize].
216+ // That modifier triggers draw-phase remeasures through the nested SubcomposeLayout
217+ // (BasicText-with-links uses SubcomposeLayout for link hit-testing); during streaming and
218+ // during the Loading→Success state transition, these remeasures crash with an NPE in
219+ // AnimatedContentTransitionScopeImpl$ChildData.isTarget (an R8-rewritten frame throwing
220+ // NullPointerException). Overriding paragraph/heading components to [MarkdownTextNoAnim]
221+ // (which renders via [BasicText] directly) avoids the wrapper for those paths, but the
222+ // library's MarkdownText is still reached through the `text` component (bare TEXT nodes,
223+ // which appear during partial streaming parses), the `checkbox` component (GFM task lists),
224+ // and the inline-image path. Disabling animateTextSize globally closes all of them at once
225+ // — the size animation is decorative (only inline image placeholder→final size swaps), and
226+ // the crash it causes is a regression in the library's interaction with Compose's
227+ // SubcomposeLayout under streaming/recomposition bursts.
228+ val animations: MarkdownAnimations = markdownAnimations(animateTextSize = { this })
212 // Intercept link taps so the scheme can be validated before the library fires an
229 // Intercept link taps so the scheme can be validated before the library fires an
213 // implicit ACTION_VIEW. A hallucinated javascript:/file:/intent: URL must not launch
230 // implicit ACTION_VIEW. A hallucinated javascript:/file:/intent: URL must not launch
214 // an unexpected intent chooser. Taps are routed to a dialog offering Open (only for
231 // an unexpected intent chooser. Taps are routed to a dialog offering Open (only for
@@ -370,6 +387,7 @@ private fun MarkdownBody(
370 modifier = modifier.semantics { liveRegion = LiveRegionMode.Polite },
387 modifier = modifier.semantics { liveRegion = LiveRegionMode.Polite },
371 components = components,
388 components = components,
372 annotator = effectiveAnnotator,
389 annotator = effectiveAnnotator,
390+ animations = animations,
375 // A blinking caret at the end of a streaming reply gives a "live" typewriter feel.
393 // A blinking caret at the end of a streaming reply gives a "live" typewriter feel.
@@ -393,6 +411,7 @@ private fun MarkdownBody(
393 modifier = modifier.semantics { liveRegion = LiveRegionMode.Polite },
411 modifier = modifier.semantics { liveRegion = LiveRegionMode.Polite },
394 components = components,
412 components = components,
395 annotator = effectiveAnnotator,
413 annotator = effectiveAnnotator,
414+ animations = animations,