Draw table cells as span-linked text, and hit-test links by glyph

Table rows are now ours: each cell is a LinkedText, so a link in a cell is a
span with a string annotation and one tap detector per cell rather than the
layout node Compose builds for every LinkAnnotation -- the cost the paragraph
change removed everywhere else. The renderer's outer table (width, sideways
scroll, corners, dividers) stays; the row and cell were the only parts it
offered no slot for. Cells still wrap and align to the top, with the same
semantics the renderer gave them.

Found while checking it: the hit test took the layout's nearest caret as the
glyph under the finger, so a tap on the right half of any link glyph named
the character after it and opened nothing. That was flaky in paragraphs
already; it now checks the glyph on either side of the caret.

Verified on the emulator: a linked cell and an autolink cell open their
addresses, a plain cell opens nothing, a six-column table still scrolls
sideways, and a link-free table draws as before.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Fable 5.1 committed 2026-09-02 18:58:37 -04:00
1 parent 3c3b4e62fd
commit a9ab9b3e5a
2 files changed
+92 -61

No files matched your search

@@ -44,26 +44,35 @@ import org.intellij.markdown.flavours.gfm.GFMTokenTypes
* style never defined a pressed style, so nothing visible changes.
*
* Every block the renderer dispatches through its component table comes here, which includes the
* paragraphs inside lists, quotes and alerts. Table cells do not: the table draws its own cells and
* offers no slot for them, so a link in a cell keeps the renderer's path -- correct, and dearer.
* Reference-style links stay there too; the renderer resolves those against its definitions.
* paragraphs inside lists, quotes and alerts, and so does every table cell through
* [LinkedTableRow]. Reference-style links are the one kind still drawn the renderer's way; it
* resolves those against its definitions.
*/
@Composable
fun LinkedText(model: MarkdownComponentModel, style: TextStyle, heading: Boolean = false) {
LinkedText(
model.content,
model.node,
style,
if (heading) Modifier.semantics { heading() } else Modifier,
)
}
/** The inline content of [node] within [content], drawn as [LinkedText] describes. */
@Composable
fun LinkedText(content: String, node: ASTNode, style: TextStyle, modifier: Modifier = Modifier) {
val settings = plainLinkSettings()
val text =
remember(model.content, model.node, style) {
model.content.buildMarkdownAnnotatedString(model.node, style, settings)
remember(content, node, style) {
content.buildMarkdownAnnotatedString(node, style, settings)
}
val uriHandler = LocalUriHandler.current
val layout = remember { Ref<TextLayoutResult>() }
MarkdownText(
content = text,
node = model.node,
node = node,
modifier =
Modifier.then(if (heading) Modifier.semantics { heading() } else Modifier).pointerInput(
text
) {
modifier.pointerInput(text) {
detectTapGestures { position ->
val url = text.linkAt(layout.value, position) ?: return@detectTapGestures
uriHandler.openUri(url)
@@ -74,12 +83,22 @@ fun LinkedText(model: MarkdownComponentModel, style: TextStyle, heading: Boolean
)
}
/** The address under [position], if a link's glyph is there rather than merely nearest to it. */
/**
* The address under [position], if a link's glyph is there rather than merely nearest to it.
*
* The layout answers with a caret, the boundary nearest the finger, so a tap on the right half of a
* glyph names the character after it; the glyph under the finger is the one on either side of that
* boundary whose box holds the point. Checked with the box rather than assumed, so a tap past the
* end of a line ending in a link opens nothing.
*/
private fun AnnotatedString.linkAt(layout: TextLayoutResult?, position: Offset): String? {
layout ?: return null
val offset = layout.getOffsetForPosition(position)
if (offset >= length || !layout.getBoundingBox(offset).contains(position)) return null
return getStringAnnotations(LINK_URL, offset, offset).firstOrNull()?.item
val caret = layout.getOffsetForPosition(position)
val glyph =
(caret - 1..caret).firstOrNull {
it in 0 until length && layout.getBoundingBox(it).contains(position)
} ?: return null
return getStringAnnotations(LINK_URL, glyph, glyph + 1).firstOrNull()?.item
}
private const val LINK_URL = "url"