Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -230,14 +230,19 @@ private val DefaultAstNodeComposer = object : AstBlockNodeComposer {
when (val astNodeType = astNode.type) {
is AstDocument -> visitChildren(astNode)
is AstBlockQuote -> {
BlockQuote {
BlockQuote(
markdownAnimationState = markdownAnimationState,
richTextRenderOptions = richTextRenderOptions,
) {
visitChildren(astNode)
}
}

is AstUnorderedList -> {
FormattedList(
listType = Unordered,
markdownAnimationState = markdownAnimationState,
richTextRenderOptions = richTextRenderOptions,
items = astNode.filterChildrenType<AstListItem>().toList()
) { astListItem ->
// if this list item has no child, it should at least emit a single pixel layout.
Expand All @@ -252,6 +257,8 @@ private val DefaultAstNodeComposer = object : AstBlockNodeComposer {
is AstOrderedList -> {
FormattedList(
listType = Ordered,
markdownAnimationState = markdownAnimationState,
richTextRenderOptions = richTextRenderOptions,
items = astNode.childrenSequence().toList(),
startIndex = astNodeType.startNumber - 1,
) { astListItem ->
Expand All @@ -265,7 +272,10 @@ private val DefaultAstNodeComposer = object : AstBlockNodeComposer {
}

is AstThematicBreak -> {
HorizontalRule()
HorizontalRule(
markdownAnimationState = markdownAnimationState,
richTextRenderOptions = richTextRenderOptions,
)
}

is AstHeading -> {
Expand All @@ -281,11 +291,19 @@ private val DefaultAstNodeComposer = object : AstBlockNodeComposer {
}

is AstIndentedCodeBlock -> {
CodeBlock(text = astNodeType.literal.trim())
CodeBlock(
text = astNodeType.literal.trim(),
markdownAnimationState = markdownAnimationState,
richTextRenderOptions = richTextRenderOptions,
)
}

is AstFencedCodeBlock -> {
CodeBlock(text = astNodeType.literal.trim())
CodeBlock(
text = astNodeType.literal.trim(),
markdownAnimationState = markdownAnimationState,
richTextRenderOptions = richTextRenderOptions,
)
}

is AstHtmlBlock -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ internal fun RichTextScope.MarkdownRichText(
Text(
text = richText,
modifier = modifier,
isLeafText = astNode.links.next == null && astNode.links.parent?.links?.next == null,
renderOptions = richTextRenderOptions,
sharedAnimationState = markdownAnimationState,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ internal fun RichTextScope.RenderTable(
markdownAnimationState: MarkdownAnimationState,
) {
Table(
markdownAnimationState = markdownAnimationState,
richTextRenderOptions = richtextRenderOptions,
headerRow = {
node.filterChildrenType<AstTableHeader>()
.firstOrNull()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,16 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.offset
import androidx.compose.ui.unit.sp
import com.halilibo.richtext.ui.BlockQuoteGutter.BarGutter
import com.halilibo.richtext.ui.string.MarkdownAnimationState
import com.halilibo.richtext.ui.string.RichTextRenderOptions

internal val DefaultBlockQuoteGutter = BarGutter()

Expand Down Expand Up @@ -67,13 +70,18 @@ public interface BlockQuoteGutter {
/**
* Draws a block quote, with a [BlockQuoteGutter] drawn beside the children on the start side.
*/
@Composable public fun RichTextScope.BlockQuote(children: @Composable RichTextScope.() -> Unit) {
@Composable public fun RichTextScope.BlockQuote(
markdownAnimationState: MarkdownAnimationState = remember { MarkdownAnimationState() },
richTextRenderOptions: RichTextRenderOptions = RichTextRenderOptions(),
children: @Composable RichTextScope.() -> Unit
) {
val gutter = currentRichTextStyle.resolveDefaults().blockQuoteGutter!!
val spacing = gutter.verticalContentPadding ?: with(LocalDensity.current) {
currentRichTextStyle.resolveDefaults().paragraphSpacing!!.toDp() / 2
}

Layout(content = {
val alpha = rememberMarkdownFade(richTextRenderOptions, markdownAnimationState)
Layout(modifier = Modifier.alpha(alpha.value), content = {
Copy link

@rz-openai rz-openai Aug 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using .graphicsLayer { } block to defer animated value reads to the draw phase would improve perf

with(gutter) { drawGutter() }
BasicRichText(
modifier = Modifier.padding(top = spacing, bottom = spacing),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.sp
import com.halilibo.richtext.ui.string.MarkdownAnimationState
import com.halilibo.richtext.ui.string.RichTextRenderOptions

/**
* Defines how [CodeBlock]s are rendered.
Expand Down Expand Up @@ -57,9 +61,15 @@ internal fun CodeBlockStyle.resolveDefaults() = CodeBlockStyle(
*/
@Composable public fun RichTextScope.CodeBlock(
text: String,
markdownAnimationState: MarkdownAnimationState = remember { MarkdownAnimationState() },
richTextRenderOptions: RichTextRenderOptions = RichTextRenderOptions(),
wordWrap: Boolean? = null
) {
CodeBlock(wordWrap = wordWrap) {
CodeBlock(
wordWrap = wordWrap,
markdownAnimationState = markdownAnimationState,
richTextRenderOptions = richTextRenderOptions,
) {
Text(text)
}
}
Expand All @@ -72,6 +82,8 @@ internal fun CodeBlockStyle.resolveDefaults() = CodeBlockStyle(
*/
@Composable public fun RichTextScope.CodeBlock(
wordWrap: Boolean? = null,
markdownAnimationState: MarkdownAnimationState = remember { MarkdownAnimationState() },
richTextRenderOptions: RichTextRenderOptions = RichTextRenderOptions(),
children: @Composable RichTextScope.() -> Unit
) {
val codeBlockStyle = currentRichTextStyle.resolveDefaults().codeBlockStyle!!
Expand All @@ -81,12 +93,14 @@ internal fun CodeBlockStyle.resolveDefaults() = CodeBlockStyle(
codeBlockStyle.padding!!.toDp()
}
val resolvedWordWrap = wordWrap ?: codeBlockStyle.wordWrap!!
val alpha = rememberMarkdownFade(richTextRenderOptions, markdownAnimationState)

CodeBlockLayout(
wordWrap = resolvedWordWrap
) { layoutModifier ->
Box(
modifier = layoutModifier
.alpha(alpha.value)
.then(modifier)
.padding(blockPadding)
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,14 @@

package com.halilibo.richtext.ui

import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.selection.DisableSelection
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
Expand Down Expand Up @@ -225,7 +220,7 @@ private val LocalListLevel = compositionLocalOf { 0 }
itemSpacing = itemSpacing,
prefixPadding = PaddingValues(start = markerIndent, end = contentsIndent),
prefixForIndex = { index ->
val alpha = rememberAnimation(richTextRenderOptions, markdownAnimationState)
val alpha = rememberMarkdownFade(richTextRenderOptions, markdownAnimationState)
Box(modifier = Modifier.alpha(alpha.value)) {
when (listType) {
Ordered -> listStyle.orderedMarkers!!().drawMarker(currentLevel, startIndex + index)
Expand All @@ -245,26 +240,6 @@ private val LocalListLevel = compositionLocalOf { 0 }
)
}

@Composable private fun rememberAnimation(
richTextRenderOptions: RichTextRenderOptions,
markdownAnimationState: MarkdownAnimationState,
): State<Float> {
val targetAlpha = remember {
mutableFloatStateOf(if (richTextRenderOptions.animate) 0f else 1f)
}
LaunchedEffect(Unit) {
targetAlpha.value = 1f
}
val alpha = animateFloatAsState(
targetAlpha.value,
tween(
richTextRenderOptions.textFadeInMs,
delayMillis = markdownAnimationState.toDelayMs(),
)
)
return alpha
}

@Composable private fun PrefixListLayout(
count: Int,
itemSpacing: Dp,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.halilibo.richtext.ui.string.MarkdownAnimationState
import com.halilibo.richtext.ui.string.RichTextRenderOptions

@Immutable
public data class HorizontalRuleStyle(
Expand All @@ -31,15 +35,20 @@ internal fun HorizontalRuleStyle.resolveDefaults() = HorizontalRuleStyle(
/**
* A simple horizontal line drawn with the current content color.
*/
@Composable public fun RichTextScope.HorizontalRule() {
@Composable public fun RichTextScope.HorizontalRule(
markdownAnimationState: MarkdownAnimationState = remember { MarkdownAnimationState() },
richTextRenderOptions: RichTextRenderOptions = RichTextRenderOptions(),
) {
val resolvedStyle = currentRichTextStyle.resolveDefaults()
val horizontalRuleStyle = resolvedStyle.horizontalRuleStyle
val color = horizontalRuleStyle?.color ?: currentContentColor.copy(alpha = .2f)
val spacing = horizontalRuleStyle?.spacing ?: with(LocalDensity.current) {
resolvedStyle.paragraphSpacing!!.toDp()
}
val alpha = rememberMarkdownFade(richTextRenderOptions, markdownAnimationState)
Box(
Modifier
.alpha(alpha.value)
.padding(top = spacing, bottom = spacing)
.fillMaxWidth()
.height(1.dp)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.halilibo.richtext.ui

import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import com.halilibo.richtext.ui.string.MarkdownAnimationState
import com.halilibo.richtext.ui.string.RichTextRenderOptions

@Composable
internal fun rememberMarkdownFade(
richTextRenderOptions: RichTextRenderOptions,
markdownAnimationState: MarkdownAnimationState,
): State<Float> {
val targetAlpha = remember {
mutableFloatStateOf(if (richTextRenderOptions.animate) 0f else 1f)
}
LaunchedEffect(Unit) {
targetAlpha.value = 1f

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see this code was just moved, but we would skip a recomposition step by using a simple animate call to update the state value instead of animateFloatAsState, and also prevents us from creating something like an Animatable in cases we don't need to animate.

}
val alpha = animateFloatAsState(
targetAlpha.value,
tween(
durationMillis = richTextRenderOptions.textFadeInMs,
delayMillis = markdownAnimationState.toDelayMs(),
)
)
LaunchedEffect(Unit) {
markdownAnimationState.addAnimation(richTextRenderOptions)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we need to unregister it?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addAnimation only ticks the delay counter up, we don't need to unregister it globally

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha

}
return alpha
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
Expand All @@ -22,6 +23,8 @@ import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.sp
import com.halilibo.richtext.ui.ColumnArrangement.Adaptive
import com.halilibo.richtext.ui.ColumnArrangement.Uniform
import com.halilibo.richtext.ui.string.MarkdownAnimationState
import com.halilibo.richtext.ui.string.RichTextRenderOptions
import kotlin.math.max
import kotlin.math.roundToInt

Expand Down Expand Up @@ -112,10 +115,13 @@ private class RowBuilder : RichTextTableCellScope {
@Composable
public fun RichTextScope.Table(
modifier: Modifier = Modifier,
markdownAnimationState: MarkdownAnimationState = remember { MarkdownAnimationState() },
richTextRenderOptions: RichTextRenderOptions = RichTextRenderOptions(),
headerRow: (RichTextTableCellScope.() -> Unit)? = null,
bodyRows: RichTextTableRowScope.() -> Unit
) {
val tableStyle = currentRichTextStyle.resolveDefaults().tableStyle!!
val alpha = rememberMarkdownFade(richTextRenderOptions, markdownAnimationState)
val contentColor = currentContentColor
val header = remember(headerRow) {
headerRow?.let { RowBuilder().apply(headerRow).row }
Expand Down Expand Up @@ -182,10 +188,11 @@ public fun RichTextScope.Table(
}
}

val baseModifier = modifier.alpha(alpha.value)
val tableModifier = if (columnArrangement is Adaptive) {
modifier.horizontalScroll(rememberScrollState())
baseModifier.horizontalScroll(rememberScrollState())
} else {
modifier
baseModifier
}

val borderColor = tableStyle.borderColor!!.takeOrElse { contentColor }
Expand Down Expand Up @@ -240,4 +247,4 @@ private fun Modifier.drawTableBorders(
)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,7 @@ public data class RichTextRenderOptions(
val debounceMs: Int = 100050,
val delayMs: Int = 70,
val delayExponent: Double = 0.7,
val maxPhraseLength: Int = 30,
val phraseMarkersOverride: List<Char>? = null,
val onTextAnimate: () -> Unit = {},
val onPhraseAnimate: () -> Unit = {},
) {
public companion object {
public val Default: RichTextRenderOptions = RichTextRenderOptions()
Expand Down
Loading