Close a card by pressing its words, and let a selection be put away
A markdown paragraph took every tap that landed on its glyphs, so an opened peer message or memory note could be shut anywhere except on the text -- which is most of it, and reads as a card that has stopped working. Measured on the emulator: with a handler on the text the tap did nothing at all, and with the handler removed the same tap shut the card. The words now do the shutting, through a composition local, since the renderer composes those paragraphs out of its own component table and there is nothing between the card and them to pass a parameter through. The link handler is bounded by the long-press timeout, so holding to select is not a tap. The other half is the tap that puts a selection away, which used to shut whatever card the words were in. The container clears the selection from that same press, milliseconds before the card reads it, so the answer is taken at composition instead -- what was true when the reader touched the screen. Selection colours are the app's own. Material's 40% of primary is a tint of whatever is behind it, and over the near-black a code block sits on it composited to a smudge, so selecting a line of code looked like nothing had happened. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
359649bc73
commit
acdf00ab1d
10 files changed
+336
-69
No files matched your search
@@ -1,9 +1,13 @@
|
||||
package com.example.aiapp
|
||||
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.gestures.awaitEachGesture
|
||||
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||
import androidx.compose.foundation.gestures.waitForUpOrCancellation
|
||||
import androidx.compose.foundation.text.BasicText
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.isSpecified
|
||||
@@ -88,16 +92,40 @@ fun LinkedText(content: String, node: ASTNode, style: TextStyle, modifier: Modif
|
||||
content.buildMarkdownAnnotatedString(node, style, settings)
|
||||
}
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val onPlainTap = LocalMarkdownTap.current
|
||||
val layout = remember { Ref<TextLayoutResult>() }
|
||||
// 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 =
|
||||
modifier.pointerInput(text) {
|
||||
detectTapGestures { position ->
|
||||
val url = text.linkAt(layout.value, position) ?: return@detectTapGestures
|
||||
uriHandler.openUri(url)
|
||||
// A tap here is either a link or the card's; see [LocalMarkdownTap] for why the
|
||||
// second one has to be answered from inside the text rather than left to the card.
|
||||
modifier.pointerInput(text, onPlainTap) {
|
||||
awaitEachGesture {
|
||||
// Unconsumed is not required: something outside may already be tracking this
|
||||
// press, and it is still the press that may land on a link.
|
||||
awaitFirstDown(requireUnconsumed = false)
|
||||
// A tap and nothing else. Null when the gesture became something somebody
|
||||
// else's -- a scroll, or a press held past the long-press timeout, which is
|
||||
// how a selection starts. The timeout is the load-bearing half: without it a
|
||||
// press held for a second and released was still an up with nothing consumed,
|
||||
// so holding a peer message to select from it shut the card instead.
|
||||
val up =
|
||||
withTimeoutOrNull(viewConfiguration.longPressTimeoutMillis) {
|
||||
waitForUpOrCancellation()
|
||||
} ?: return@awaitEachGesture
|
||||
val url = text.linkAt(layout.value, up.position)
|
||||
when {
|
||||
url != null -> {
|
||||
up.consume()
|
||||
uriHandler.openUri(url)
|
||||
}
|
||||
onPlainTap != null -> {
|
||||
up.consume()
|
||||
onPlainTap()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
style = style,
|
||||
@@ -106,6 +134,39 @@ fun LinkedText(content: String, node: ASTNode, style: TextStyle, modifier: Modif
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* What a tap on markdown text means when it lands on no link -- shutting the card it is drawn in,
|
||||
* usually -- or null where a plain tap means nothing.
|
||||
*
|
||||
* A composition local because there is nowhere else to put it. The paragraphs of a message are
|
||||
* composed by the renderer's own dispatch out of its component table, so nothing between a card and
|
||||
* the text inside it is ours to pass a parameter through; the renderer already hands its colours,
|
||||
* its typography and its components down the same way.
|
||||
*
|
||||
* It exists because a pointer-input node over the glyphs takes the tap and the card's own click
|
||||
* handler never sees it. Measured on the emulator against an opened peer message: with a handler on
|
||||
* the text -- consuming or not -- a tap on its words did nothing at all, and with the handler
|
||||
* removed entirely the same tap shut the card. So a card whose body is markdown cannot be shut by
|
||||
* pressing its words unless the words do the shutting, and "nothing happens when I press it" is
|
||||
* indistinguishable from a card that has stopped working.
|
||||
*
|
||||
* Provided as a value that outlives a recomposition (see [rememberMarkdownTap]), since a fresh
|
||||
* lambda per composition would invalidate every paragraph reading it.
|
||||
*/
|
||||
val LocalMarkdownTap = compositionLocalOf<(() -> Unit)?> { null }
|
||||
|
||||
/**
|
||||
* [onTap] as a stable value to provide for [LocalMarkdownTap].
|
||||
*
|
||||
* The identity stays put while the behaviour follows the latest [onTap], which is what keeps
|
||||
* providing it from invalidating the text under it on every recomposition of the card.
|
||||
*/
|
||||
@Composable
|
||||
fun rememberMarkdownTap(onTap: () -> Unit): () -> Unit {
|
||||
val latest = rememberUpdatedState(onTap)
|
||||
return remember { { latest.value() } }
|
||||
}
|
||||
|
||||
/**
|
||||
* The address under [position], if a link's glyph is there rather than merely nearest to it.
|
||||
*
|
||||
|
||||
Reference in new issue
Block a user