Draw text on the platform directly, own the table's outer box, and parse a live reply a block at a time

Three costs left in the renderer's composition layer, taken one at a time:

The text leaf. Every paragraph went through the renderer's text composable,
which exists to place inline images and charged each text for the
possibility: a placement callback, a derived map of inline content, a
semantics group and a size animation. A paragraph with no image -- nearly
all of them -- now goes straight to BasicText, with the renderer's own rule
for a style that names no colour. One with an image keeps the old path.

The table. The renderer decided "spread or scroll" with a BoxWithConstraints,
a subcomposition. LinkedTable does it with one layout modifier placed after
the horizontal scroll: fillMaxWidth fixes the minimum to the room, the scroll
passes that minimum through while lifting the maximum, and the modifier sizes
the rows to the larger of the room and the columns' floor. Rows no longer
need a width handed to them or a row index from a composition local.

The live reply. Every delta reparsed the whole message off-thread; for a
long reply that was tens of milliseconds hundreds of times, every core busy
while the frame's thread waited. LiveParse freezes every top-level block that
a later block has started after -- markdown's block rules make that safe --
and reparses only the tail. Each piece is keyed by where it starts in the
message, so a block keeps its composition when it freezes.

Verified on the emulator: the block-kind fixture draws the same with links
opening from a paragraph and a bullet and plain text opening nothing; a
six-column table still scrolls sideways; a 58-word mixed stream of list,
fence, table and quote drew every block as it arrived, 47 tail reparses at
1.7ms mean against whole-message parses before. ktfmt, build and lint clean
but for the AGP 9.4.0 notice.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Fable 5.1 committed 2026-09-03 00:00:01 -04:00
1 parent 48bb7de304
commit 4bc69e8f9c
2 files changed
+251 -125

No files matched your search

@@ -1,10 +1,12 @@
package com.example.aiapp
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.isSpecified
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.node.Ref
import androidx.compose.ui.platform.LocalUriHandler
@@ -17,6 +19,7 @@ import androidx.compose.ui.text.TextStyle
import com.mikepenz.markdown.annotator.AnnotatorSettings
import com.mikepenz.markdown.annotator.annotatorSettings
import com.mikepenz.markdown.annotator.buildMarkdownAnnotatedString
import com.mikepenz.markdown.compose.LocalMarkdownColors
import com.mikepenz.markdown.compose.components.MarkdownComponentModel
import com.mikepenz.markdown.compose.elements.MarkdownText
import com.mikepenz.markdown.model.markdownAnnotator
@@ -79,21 +82,41 @@ fun LinkedText(content: String, node: ASTNode, style: TextStyle, modifier: Modif
}
val uriHandler = LocalUriHandler.current
val layout = remember { Ref<TextLayoutResult>() }
MarkdownText(
content = text,
node = node,
modifier =
modifier.pointerInput(text) {
detectTapGestures { position ->
val url = text.linkAt(layout.value, position) ?: return@detectTapGestures
uriHandler.openUri(url)
}
},
style = style,
onTextLayout = { result, _ -> layout.value = result },
)
val tapping =
modifier.pointerInput(text) {
detectTapGestures { position ->
val url = text.linkAt(layout.value, position) ?: return@detectTapGestures
uriHandler.openUri(url)
}
}
// The renderer's text composable exists to place inline images, and it charges every text
// for the possibility: a placement callback, a derived map of inline content, a semantics
// group and a size animation, per paragraph. Almost no paragraph has an image, so those go
// straight to the platform text; the few that do keep the renderer's path.
if (remember(node) { node.hasImage() }) {
MarkdownText(
content = text,
node = node,
modifier = tapping,
style = style,
onTextLayout = { result, _ -> layout.value = result },
)
} else {
// The renderer's own rule for a style that names no colour: the theme's text colour.
val color = if (style.color.isSpecified) style.color else LocalMarkdownColors.current.text
BasicText(
text = text,
modifier = tapping,
style = style,
color = { color },
onTextLayout = { layout.value = it },
)
}
}
private fun ASTNode.hasImage(): Boolean =
type == MarkdownElementTypes.IMAGE || children.any { it.hasImage() }
/**
* The address under [position], if a link's glyph is there rather than merely nearest to it.
*