mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-04-01 09:36:10 +00:00
rewrite
This commit is contained in:
@@ -4379,28 +4379,24 @@ sealed class Format {
|
||||
@Serializable @SerialName("phone") class Phone: Format()
|
||||
@Serializable @SerialName("unknown") class Unknown: Format()
|
||||
|
||||
val style: SpanStyle @Composable get() = when (this) {
|
||||
fun style(colors: Colors, typography: Typography): SpanStyle = when (this) {
|
||||
is Bold -> SpanStyle(fontWeight = FontWeight.Bold)
|
||||
is Italic -> SpanStyle(fontStyle = FontStyle.Italic)
|
||||
is StrikeThrough -> SpanStyle(textDecoration = TextDecoration.LineThrough)
|
||||
is Snippet -> SpanStyle(fontFamily = FontFamily.Monospace)
|
||||
is Secret -> SpanStyle(color = Color.Transparent, background = SecretColor)
|
||||
is Small -> SpanStyle(fontSize = MaterialTheme.typography.body2.fontSize, color = MaterialTheme.colors.secondary)
|
||||
is Colored -> SpanStyle(color = this.color.uiColor)
|
||||
is Uri -> linkStyle
|
||||
is HyperLink -> linkStyle
|
||||
is SimplexLink -> linkStyle
|
||||
is Command -> SpanStyle(color = MaterialTheme.colors.primary, fontFamily = FontFamily.Monospace)
|
||||
is Small -> SpanStyle(fontSize = typography.body2.fontSize, color = colors.secondary)
|
||||
is Colored -> SpanStyle(color = this.color.uiColor(colors))
|
||||
is Uri, is HyperLink, is SimplexLink, is Email, is Phone -> linkStyle(colors)
|
||||
is Command -> SpanStyle(color = colors.primary, fontFamily = FontFamily.Monospace)
|
||||
is Mention -> SpanStyle(fontWeight = FontWeight.Medium)
|
||||
is Email -> linkStyle
|
||||
is Phone -> linkStyle
|
||||
is Unknown -> SpanStyle()
|
||||
}
|
||||
|
||||
val isSimplexLink = this is SimplexLink
|
||||
|
||||
companion object {
|
||||
val linkStyle @Composable get() = SpanStyle(color = MaterialTheme.colors.primary, textDecoration = TextDecoration.Underline)
|
||||
fun linkStyle(colors: Colors) = SpanStyle(color = colors.primary, textDecoration = TextDecoration.Underline)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4432,15 +4428,15 @@ enum class FormatColor(val color: String) {
|
||||
black("black"),
|
||||
white("white");
|
||||
|
||||
val uiColor: Color @Composable get() = when (this) {
|
||||
fun uiColor(colors: Colors): Color = when (this) {
|
||||
red -> Color.Red
|
||||
green -> SimplexGreen
|
||||
blue -> SimplexBlue
|
||||
yellow -> WarningYellow
|
||||
cyan -> Color.Cyan
|
||||
magenta -> Color.Magenta
|
||||
black -> MaterialTheme.colors.onBackground
|
||||
white -> MaterialTheme.colors.onBackground
|
||||
black -> colors.onBackground
|
||||
white -> colors.onBackground
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2205,7 +2205,7 @@ fun BoxScope.ChatItemsList(
|
||||
}
|
||||
}
|
||||
|
||||
val selectionModifier = SelectionHandler(LocalSelectionManager.current, listState)
|
||||
val selectionModifier = SelectionHandler(LocalSelectionManager.current, listState, mergedItems, linkMode)
|
||||
|
||||
LazyColumnWithScrollBar(
|
||||
Modifier.align(Alignment.BottomCenter).then(selectionModifier),
|
||||
|
||||
@@ -16,7 +16,6 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.key.*
|
||||
import androidx.compose.ui.input.pointer.*
|
||||
@@ -26,10 +25,10 @@ import androidx.compose.ui.layout.positionInWindow
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalViewConfiguration
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.TextLayoutResult
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.platform.Log
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.appPlatform
|
||||
import chat.simplex.common.views.chat.item.buildMsgAnnotatedString
|
||||
import chat.simplex.common.views.helpers.generalGetString
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
@@ -37,78 +36,43 @@ import kotlinx.coroutines.*
|
||||
|
||||
val SelectionHighlightColor = Color(0x4D0066FF)
|
||||
|
||||
@Stable
|
||||
data class SelectionCoords(
|
||||
val startY: Float,
|
||||
val startX: Float,
|
||||
val endY: Float,
|
||||
val endX: Float
|
||||
) {
|
||||
val isReversed: Boolean get() = startY > endY
|
||||
val topY: Float get() = minOf(startY, endY)
|
||||
val bottomY: Float get() = maxOf(startY, endY)
|
||||
val topX: Float get() = if (isReversed) endX else startX
|
||||
val bottomX: Float get() = if (isReversed) startX else endX
|
||||
}
|
||||
|
||||
data class CapturedText(
|
||||
val itemId: Long,
|
||||
val yPosition: Float,
|
||||
val highlightRange: IntRange,
|
||||
val text: String
|
||||
data class SelectionRange(
|
||||
val startIndex: Int,
|
||||
val startOffset: Int,
|
||||
val endIndex: Int,
|
||||
val endOffset: Int
|
||||
)
|
||||
|
||||
interface SelectionParticipant {
|
||||
val itemId: Long
|
||||
fun getYBounds(): ClosedFloatingPointRange<Float>?
|
||||
fun getTextLayoutResult(): TextLayoutResult?
|
||||
fun getSelectableEnd(): Int
|
||||
fun getAnnotatedText(): String
|
||||
fun calculateHighlightRange(coords: SelectionCoords): IntRange?
|
||||
}
|
||||
|
||||
enum class SelectionState { Idle, Selecting, Selected }
|
||||
|
||||
class SelectionManager {
|
||||
var selectionState by mutableStateOf(SelectionState.Idle)
|
||||
private set
|
||||
|
||||
var coords by mutableStateOf<SelectionCoords?>(null)
|
||||
var range by mutableStateOf<SelectionRange?>(null)
|
||||
private set
|
||||
|
||||
var lastPointerWindowY: Float = 0f
|
||||
private set
|
||||
var lastPointerWindowX: Float = 0f
|
||||
private set
|
||||
var focusWindowY by mutableStateOf(0f)
|
||||
var focusWindowX by mutableStateOf(0f)
|
||||
|
||||
private val participants = mutableListOf<SelectionParticipant>()
|
||||
val captured = mutableStateMapOf<Long, CapturedText>()
|
||||
|
||||
fun register(participant: SelectionParticipant) {
|
||||
participants.add(participant)
|
||||
if (selectionState == SelectionState.Selecting) {
|
||||
coords?.let { recomputeParticipant(participant, it) }
|
||||
}
|
||||
}
|
||||
|
||||
fun unregister(participant: SelectionParticipant) {
|
||||
participants.remove(participant)
|
||||
}
|
||||
|
||||
fun startSelection(startY: Float, startX: Float) {
|
||||
coords = SelectionCoords(startY, startX, startY, startX)
|
||||
fun startSelection(startIndex: Int) {
|
||||
range = SelectionRange(startIndex, 0, startIndex, 0)
|
||||
selectionState = SelectionState.Selecting
|
||||
lastPointerWindowY = startY
|
||||
lastPointerWindowX = startX
|
||||
captured.clear()
|
||||
}
|
||||
|
||||
fun updateSelection(endY: Float, endX: Float) {
|
||||
val current = coords ?: return
|
||||
coords = current.copy(endY = endY, endX = endX)
|
||||
lastPointerWindowY = endY
|
||||
lastPointerWindowX = endX
|
||||
recomputeAll()
|
||||
fun setAnchorOffset(offset: Int) {
|
||||
val r = range ?: return
|
||||
range = r.copy(startOffset = offset, endOffset = offset)
|
||||
}
|
||||
|
||||
fun updateFocusIndex(index: Int) {
|
||||
val r = range ?: return
|
||||
range = r.copy(endIndex = index)
|
||||
}
|
||||
|
||||
fun updateFocusOffset(offset: Int) {
|
||||
val r = range ?: return
|
||||
range = r.copy(endOffset = offset)
|
||||
}
|
||||
|
||||
fun endSelection() {
|
||||
@@ -116,115 +80,70 @@ class SelectionManager {
|
||||
}
|
||||
|
||||
fun clearSelection() {
|
||||
coords = null
|
||||
range = null
|
||||
selectionState = SelectionState.Idle
|
||||
captured.clear()
|
||||
}
|
||||
|
||||
private fun recomputeAll() {
|
||||
val c = coords ?: return
|
||||
val visibleInRange = mutableMapOf<Long, SelectionParticipant>()
|
||||
val visibleOutOfRange = mutableSetOf<Long>()
|
||||
fun computeHighlightRange(index: Int): IntRange? {
|
||||
val r = range ?: return null
|
||||
val lo = minOf(r.startIndex, r.endIndex)
|
||||
val hi = maxOf(r.startIndex, r.endIndex)
|
||||
if (index < lo || index > hi) return null
|
||||
val forward = r.startIndex <= r.endIndex
|
||||
val startOff = if (forward) r.startOffset else r.endOffset
|
||||
val endOff = if (forward) r.endOffset else r.startOffset
|
||||
return when {
|
||||
index == lo && index == hi -> minOf(startOff, endOff) until maxOf(startOff, endOff)
|
||||
index == lo -> startOff until Int.MAX_VALUE
|
||||
index == hi -> 0 until endOff
|
||||
else -> 0 until Int.MAX_VALUE
|
||||
}
|
||||
}
|
||||
|
||||
for (p in participants) {
|
||||
val bounds = p.getYBounds()
|
||||
if (bounds != null && bounds.start <= c.bottomY && bounds.endInclusive >= c.topY) {
|
||||
visibleInRange[p.itemId] = p
|
||||
} else {
|
||||
visibleOutOfRange.add(p.itemId)
|
||||
fun getSelectedText(
|
||||
items: List<MergedItem>,
|
||||
linkMode: SimplexLinkMode
|
||||
): String {
|
||||
val r = range ?: return ""
|
||||
val lo = minOf(r.startIndex, r.endIndex)
|
||||
val hi = maxOf(r.startIndex, r.endIndex)
|
||||
val forward = r.startIndex <= r.endIndex
|
||||
val startOff = if (forward) r.startOffset else r.endOffset
|
||||
val endOff = if (forward) r.endOffset else r.startOffset
|
||||
return (lo..hi).mapNotNull { idx ->
|
||||
val ci = items.getOrNull(idx)?.newest()?.item ?: return@mapNotNull null
|
||||
val text = buildMsgAnnotatedString(
|
||||
text = ci.text, formattedText = ci.formattedText,
|
||||
sender = null, senderBold = false, prefix = null,
|
||||
mentions = ci.mentions, userMemberId = null,
|
||||
toggleSecrets = false, sendCommandMsg = false, linkMode = linkMode
|
||||
).text
|
||||
when {
|
||||
idx == lo && idx == hi -> text.substring(
|
||||
startOff.coerceAtMost(text.length),
|
||||
endOff.coerceAtMost(text.length)
|
||||
)
|
||||
idx == lo -> text.substring(startOff.coerceAtMost(text.length))
|
||||
idx == hi -> text.substring(0, endOff.coerceAtMost(text.length))
|
||||
else -> text
|
||||
}
|
||||
}
|
||||
|
||||
visibleOutOfRange.forEach { captured.remove(it) }
|
||||
|
||||
for ((_, p) in visibleInRange) {
|
||||
recomputeParticipant(p, c)
|
||||
}
|
||||
}
|
||||
|
||||
private fun recomputeParticipant(participant: SelectionParticipant, coords: SelectionCoords) {
|
||||
val bounds = participant.getYBounds() ?: return
|
||||
Log.d("TextSelection", "recompute item=${participant.itemId} bounds=${bounds.start}..${bounds.endInclusive} coords.topY=${coords.topY} coords.bottomY=${coords.bottomY}")
|
||||
val highlightRange = participant.calculateHighlightRange(coords) ?: return
|
||||
Log.d("TextSelection", " highlightRange=$highlightRange selectableEnd=${participant.getSelectableEnd()}")
|
||||
val selectableEnd = participant.getSelectableEnd()
|
||||
val clampedStart = highlightRange.first.coerceIn(0, selectableEnd)
|
||||
val clampedEnd = highlightRange.last.coerceIn(0, selectableEnd)
|
||||
if (clampedStart >= clampedEnd) return
|
||||
|
||||
val annotatedText = participant.getAnnotatedText()
|
||||
val text = if (clampedEnd <= annotatedText.length) {
|
||||
annotatedText.substring(clampedStart, clampedEnd)
|
||||
} else {
|
||||
annotatedText.substring(clampedStart.coerceAtMost(annotatedText.length))
|
||||
}
|
||||
|
||||
captured[participant.itemId] = CapturedText(
|
||||
itemId = participant.itemId,
|
||||
yPosition = bounds.start,
|
||||
highlightRange = clampedStart until clampedEnd,
|
||||
text = text
|
||||
)
|
||||
}
|
||||
|
||||
fun getSelectedText(): String {
|
||||
return captured.values
|
||||
.sortedBy { it.yPosition }
|
||||
.joinToString("\n") { it.text }
|
||||
}
|
||||
|
||||
fun getHighlightRange(itemId: Long): IntRange? {
|
||||
return captured[itemId]?.highlightRange
|
||||
}
|
||||
}
|
||||
|
||||
fun calculateRangeForElement(
|
||||
bounds: Rect?,
|
||||
layout: TextLayoutResult?,
|
||||
selectableEnd: Int,
|
||||
coords: SelectionCoords
|
||||
): IntRange? {
|
||||
bounds ?: return null
|
||||
layout ?: return null
|
||||
if (selectableEnd <= 0) return null
|
||||
|
||||
val isFirst = bounds.top <= coords.topY && bounds.bottom > coords.topY
|
||||
val isLast = bounds.top < coords.bottomY && bounds.bottom >= coords.bottomY
|
||||
val isMiddle = bounds.top > coords.topY && bounds.bottom < coords.bottomY
|
||||
|
||||
return when {
|
||||
isMiddle -> 0 until selectableEnd
|
||||
isFirst && isLast -> {
|
||||
val s = layout.getOffsetForPosition(Offset(coords.topX - bounds.left, coords.topY - bounds.top))
|
||||
val e = layout.getOffsetForPosition(Offset(coords.bottomX - bounds.left, coords.bottomY - bounds.top))
|
||||
minOf(s, e) until maxOf(s, e)
|
||||
}
|
||||
isFirst -> {
|
||||
val s = layout.getOffsetForPosition(Offset(coords.topX - bounds.left, coords.topY - bounds.top))
|
||||
s until selectableEnd
|
||||
}
|
||||
isLast -> {
|
||||
val e = layout.getOffsetForPosition(Offset(coords.bottomX - bounds.left, coords.bottomY - bounds.top))
|
||||
0 until e
|
||||
}
|
||||
else -> null
|
||||
}.joinToString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
val LocalSelectionManager = staticCompositionLocalOf<SelectionManager?> { null }
|
||||
|
||||
|
||||
private const val AUTO_SCROLL_ZONE_PX = 40f
|
||||
private const val MIN_SCROLL_SPEED = 2f
|
||||
private const val MAX_SCROLL_SPEED = 20f
|
||||
|
||||
/**
|
||||
* Composable that installs selection effects and returns a Modifier for the LazyColumn.
|
||||
* Also emits the copy button UI in the BoxScope.
|
||||
*/
|
||||
@Composable
|
||||
fun BoxScope.SelectionHandler(
|
||||
manager: SelectionManager?,
|
||||
listState: State<LazyListState>
|
||||
listState: State<LazyListState>,
|
||||
mergedItems: State<MergedItems>,
|
||||
linkMode: SimplexLinkMode
|
||||
): Modifier {
|
||||
if (manager == null || !appPlatform.isDesktop) return Modifier
|
||||
|
||||
@@ -237,15 +156,13 @@ fun BoxScope.SelectionHandler(
|
||||
val scope = rememberCoroutineScope()
|
||||
var autoScrollJob by remember { mutableStateOf<Job?>(null) }
|
||||
|
||||
// Re-evaluate selection on scroll (handles mouse wheel and auto-scroll)
|
||||
// Re-evaluate focus index on scroll during active drag
|
||||
LaunchedEffect(manager) {
|
||||
snapshotFlow { listState.value.firstVisibleItemScrollOffset }
|
||||
.collect {
|
||||
if (manager.selectionState == SelectionState.Selecting) {
|
||||
manager.updateSelection(
|
||||
manager.lastPointerWindowY,
|
||||
manager.lastPointerWindowX
|
||||
)
|
||||
val idx = resolveIndexAtY(listState.value, manager.focusWindowY - positionInWindow.y)
|
||||
if (idx != null) manager.updateFocusIndex(idx)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -254,7 +171,7 @@ fun BoxScope.SelectionHandler(
|
||||
if (manager.selectionState == SelectionState.Selected) {
|
||||
SelectionCopyButton(
|
||||
onCopy = {
|
||||
clipboard.setText(AnnotatedString(manager.getSelectedText()))
|
||||
clipboard.setText(AnnotatedString(manager.getSelectedText(mergedItems.value.items, linkMode)))
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -268,7 +185,7 @@ fun BoxScope.SelectionHandler(
|
||||
&& event.key == Key.C
|
||||
&& event.type == KeyEventType.KeyDown
|
||||
) {
|
||||
clipboard.setText(AnnotatedString(manager.getSelectedText()))
|
||||
clipboard.setText(AnnotatedString(manager.getSelectedText(mergedItems.value.items, linkMode)))
|
||||
true
|
||||
} else false
|
||||
}
|
||||
@@ -282,10 +199,10 @@ fun BoxScope.SelectionHandler(
|
||||
awaitEachGesture {
|
||||
val down = awaitPointerEvent(PointerEventPass.Initial)
|
||||
val firstChange = down.changes.first()
|
||||
if (!firstChange.pressed) return@awaitEachGesture // skip hover, scroll
|
||||
if (!firstChange.pressed) return@awaitEachGesture
|
||||
|
||||
val wasSelected = manager.selectionState == SelectionState.Selected
|
||||
if (wasSelected) firstChange.consume() // prevent link/menu activation on click-to-clear
|
||||
if (wasSelected) firstChange.consume()
|
||||
|
||||
val localStart = firstChange.position
|
||||
val windowStart = localStart + positionInWindow
|
||||
@@ -300,11 +217,10 @@ fun BoxScope.SelectionHandler(
|
||||
autoScrollJob?.cancel()
|
||||
autoScrollJob = null
|
||||
if (isDragging) {
|
||||
manager.endSelection() // Selecting → Selected
|
||||
manager.endSelection()
|
||||
} else if (wasSelected) {
|
||||
manager.clearSelection() // Selected → Idle
|
||||
manager.clearSelection()
|
||||
}
|
||||
// Idle + click: do nothing, event passed through to children
|
||||
break
|
||||
}
|
||||
|
||||
@@ -312,15 +228,26 @@ fun BoxScope.SelectionHandler(
|
||||
|
||||
if (!isDragging && totalDrag.getDistance() > touchSlop) {
|
||||
isDragging = true
|
||||
Log.d("TextSelection", "startSelection: localStart=$localStart posInWindow=$positionInWindow windowStart=$windowStart")
|
||||
manager.startSelection(windowStart.y, windowStart.x) // → Selecting
|
||||
val localY = firstChange.position.y
|
||||
val idx = resolveIndexAtY(listState.value, localY)
|
||||
if (idx != null) {
|
||||
manager.startSelection(idx)
|
||||
manager.focusWindowY = windowStart.y
|
||||
manager.focusWindowX = windowStart.x
|
||||
}
|
||||
try { focusRequester.requestFocus() } catch (_: Exception) {}
|
||||
change.consume()
|
||||
}
|
||||
|
||||
if (isDragging) {
|
||||
val windowPos = change.position + positionInWindow
|
||||
manager.updateSelection(windowPos.y, windowPos.x)
|
||||
manager.focusWindowY = windowPos.y
|
||||
manager.focusWindowX = windowPos.x
|
||||
|
||||
val localY = change.position.y
|
||||
val idx = resolveIndexAtY(listState.value, localY)
|
||||
if (idx != null) manager.updateFocusIndex(idx)
|
||||
|
||||
change.consume()
|
||||
|
||||
// Auto-scroll: direction-aware
|
||||
@@ -336,9 +263,9 @@ fun BoxScope.SelectionHandler(
|
||||
autoScrollJob = scope.launch {
|
||||
while (isActive && manager.selectionState == SelectionState.Selecting) {
|
||||
val curEdge = if (draggingDown) {
|
||||
viewportBottom - manager.lastPointerWindowY
|
||||
viewportBottom - manager.focusWindowY
|
||||
} else {
|
||||
manager.lastPointerWindowY - viewportTop
|
||||
manager.focusWindowY - viewportTop
|
||||
}
|
||||
if (curEdge >= AUTO_SCROLL_ZONE_PX) break
|
||||
|
||||
@@ -358,6 +285,13 @@ fun BoxScope.SelectionHandler(
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveIndexAtY(listState: LazyListState, localY: Float): Int? {
|
||||
val visibleItems = listState.layoutInfo.visibleItemsInfo
|
||||
return visibleItems.find { item ->
|
||||
localY >= item.offset && localY < item.offset + item.size
|
||||
}?.index
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BoxScope.SelectionCopyButton(onCopy: () -> Unit) {
|
||||
Row(
|
||||
|
||||
@@ -7,9 +7,6 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.layout.boundsInWindow
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
@@ -23,39 +20,34 @@ val largeEmojiFont: TextStyle = TextStyle(fontSize = 48.sp, fontFamily = EmojiFo
|
||||
val mediumEmojiFont: TextStyle = TextStyle(fontSize = 36.sp, fontFamily = EmojiFont)
|
||||
|
||||
@Composable
|
||||
fun EmojiItemView(chatItem: ChatItem, timedMessagesTTL: Int?, showViaProxy: Boolean, showTimestamp: Boolean) {
|
||||
fun EmojiItemView(chatItem: ChatItem, timedMessagesTTL: Int?, showViaProxy: Boolean, showTimestamp: Boolean, selectionIndex: Int = -1) {
|
||||
val selectionManager = LocalSelectionManager.current
|
||||
val boundsState = remember { mutableStateOf<Rect?>(null) }
|
||||
val currentEmojiText = rememberUpdatedState(chatItem.content.text.trim())
|
||||
val emojiText = chatItem.content.text.trim()
|
||||
|
||||
if (selectionManager != null) {
|
||||
val participant = remember(chatItem.id) {
|
||||
object : SelectionParticipant {
|
||||
override val itemId = chatItem.id
|
||||
override fun getYBounds() = boundsState.value?.let { it.top..it.bottom }
|
||||
override fun getTextLayoutResult() = null
|
||||
override fun getSelectableEnd() = currentEmojiText.value.length
|
||||
override fun getAnnotatedText() = currentEmojiText.value
|
||||
override fun calculateHighlightRange(coords: SelectionCoords): IntRange? {
|
||||
val bounds = boundsState.value ?: return null
|
||||
return if (bounds.top <= coords.bottomY && bounds.bottom >= coords.topY)
|
||||
0 until currentEmojiText.value.length
|
||||
else null
|
||||
}
|
||||
}
|
||||
if (selectionManager != null && selectionIndex >= 0) {
|
||||
val isAnchor = remember(selectionIndex) {
|
||||
derivedStateOf { selectionManager.range?.startIndex == selectionIndex && selectionManager.selectionState == SelectionState.Selecting }
|
||||
}
|
||||
DisposableEffect(participant) {
|
||||
selectionManager.register(participant)
|
||||
onDispose { selectionManager.unregister(participant) }
|
||||
LaunchedEffect(isAnchor.value) {
|
||||
if (!isAnchor.value) return@LaunchedEffect
|
||||
selectionManager.setAnchorOffset(0)
|
||||
}
|
||||
|
||||
val isFocus = remember(selectionIndex) {
|
||||
derivedStateOf { selectionManager.range?.endIndex == selectionIndex && selectionManager.selectionState == SelectionState.Selecting }
|
||||
}
|
||||
if (isFocus.value) {
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { selectionManager.focusWindowY }
|
||||
.collect { selectionManager.updateFocusOffset(emojiText.length) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val isSelected = selectionManager?.getHighlightRange(chatItem.id) != null
|
||||
val isSelected = selectionManager?.computeHighlightRange(selectionIndex) != null
|
||||
|
||||
Column(
|
||||
Modifier
|
||||
.padding(vertical = 8.dp, horizontal = 12.dp)
|
||||
.onGloballyPositioned { boundsState.value = it.boundsInWindow() },
|
||||
Modifier.padding(vertical = 8.dp, horizontal = 12.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Box(if (isSelected) Modifier.background(SelectionHighlightColor) else Modifier) {
|
||||
|
||||
@@ -20,6 +20,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.*
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chat.*
|
||||
@@ -367,42 +368,59 @@ fun CIMarkdownText(
|
||||
onLinkLongClick: (link: String) -> Unit = {},
|
||||
showViaProxy: Boolean,
|
||||
showTimestamp: Boolean,
|
||||
prefix: AnnotatedString? = null
|
||||
prefix: AnnotatedString? = null,
|
||||
selectionIndex: Int = -1
|
||||
) {
|
||||
val selectionManager = LocalSelectionManager.current
|
||||
val boundsState = remember { mutableStateOf<Rect?>(null) }
|
||||
val layoutResultState = remember { mutableStateOf<TextLayoutResult?>(null) }
|
||||
val selectableEnd = remember { mutableIntStateOf(Int.MAX_VALUE) }
|
||||
val annotatedTextState = remember { mutableStateOf("") }
|
||||
val chatInfo = chat.chatInfo
|
||||
val text = if (ci.meta.isLive) ci.content.msgContent?.text ?: ci.text else ci.text
|
||||
|
||||
if (selectionManager != null && ci.meta.isLive != true) {
|
||||
val currentText = rememberUpdatedState(text)
|
||||
val participant = remember(ci.id) {
|
||||
object : SelectionParticipant {
|
||||
override val itemId = ci.id
|
||||
override fun getYBounds() = boundsState.value?.let { it.top..it.bottom }
|
||||
override fun getTextLayoutResult() = layoutResultState.value
|
||||
override fun getSelectableEnd() = selectableEnd.intValue
|
||||
override fun getAnnotatedText(): String {
|
||||
val at = annotatedTextState.value
|
||||
return if (at.isNotEmpty()) at else currentText.value
|
||||
}
|
||||
override fun calculateHighlightRange(coords: SelectionCoords) =
|
||||
calculateRangeForElement(
|
||||
boundsState.value, layoutResultState.value,
|
||||
selectableEnd.intValue, coords
|
||||
)
|
||||
}
|
||||
val contentLength = remember(text, ci.formattedText, ci.mentions) {
|
||||
buildMsgAnnotatedString(
|
||||
text = text, formattedText = if (text.isEmpty()) emptyList() else ci.formattedText,
|
||||
sender = null, senderBold = true, prefix = prefix,
|
||||
mentions = ci.mentions, userMemberId = when {
|
||||
chatInfo is ChatInfo.Group -> chatInfo.groupInfo.membership.memberId
|
||||
else -> null
|
||||
},
|
||||
toggleSecrets = true, sendCommandMsg = chatInfo.useCommands && chat.chatInfo.sndReady,
|
||||
linkMode = linkMode
|
||||
).text.length
|
||||
}
|
||||
|
||||
if (selectionManager != null && ci.meta.isLive != true && selectionIndex >= 0) {
|
||||
val isAnchor = remember(selectionIndex) {
|
||||
derivedStateOf { selectionManager.range?.startIndex == selectionIndex && selectionManager.selectionState == SelectionState.Selecting }
|
||||
}
|
||||
DisposableEffect(participant) {
|
||||
selectionManager.register(participant)
|
||||
onDispose { selectionManager.unregister(participant) }
|
||||
LaunchedEffect(isAnchor.value) {
|
||||
if (!isAnchor.value) return@LaunchedEffect
|
||||
val bounds = boundsState.value ?: return@LaunchedEffect
|
||||
val layout = layoutResultState.value ?: return@LaunchedEffect
|
||||
val offset = layout.getOffsetForPosition(
|
||||
Offset(selectionManager.focusWindowX - bounds.left, selectionManager.focusWindowY - bounds.top)
|
||||
)
|
||||
selectionManager.setAnchorOffset(offset.coerceAtMost(contentLength))
|
||||
}
|
||||
|
||||
val isFocus = remember(selectionIndex) {
|
||||
derivedStateOf { selectionManager.range?.endIndex == selectionIndex && selectionManager.selectionState == SelectionState.Selecting }
|
||||
}
|
||||
if (isFocus.value) {
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { selectionManager.focusWindowY to selectionManager.focusWindowX }
|
||||
.collect { (py, px) ->
|
||||
val bounds = boundsState.value ?: return@collect
|
||||
val layout = layoutResultState.value ?: return@collect
|
||||
val offset = layout.getOffsetForPosition(Offset(px - bounds.left, py - bounds.top))
|
||||
selectionManager.updateFocusOffset(offset.coerceAtMost(contentLength))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val highlightRange = selectionManager?.getHighlightRange(ci.id)
|
||||
val highlightRange = selectionManager?.computeHighlightRange(selectionIndex)
|
||||
|
||||
Box(
|
||||
Modifier
|
||||
@@ -418,8 +436,6 @@ fun CIMarkdownText(
|
||||
else -> null
|
||||
},
|
||||
uriHandler = uriHandler, senderBold = true, onLinkLongClick = onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp, prefix = prefix,
|
||||
selectableEnd = selectableEnd,
|
||||
annotatedTextState = annotatedTextState,
|
||||
selectionRange = highlightRange,
|
||||
onTextLayoutResult = { layoutResultState.value = it }
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.text.BasicText
|
||||
import androidx.compose.foundation.text.InlineTextContent
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
@@ -57,6 +57,123 @@ private fun typingIndicator(recent: Boolean, typingIdx: Int): AnnotatedString =
|
||||
private fun typing(w: FontWeight = FontWeight.Light): AnnotatedString =
|
||||
AnnotatedString(".", SpanStyle(fontWeight = w))
|
||||
|
||||
fun buildMsgAnnotatedString(
|
||||
text: CharSequence,
|
||||
formattedText: List<FormattedText>?,
|
||||
sender: String?,
|
||||
senderBold: Boolean,
|
||||
prefix: AnnotatedString?,
|
||||
mentions: Map<String, CIMention>?,
|
||||
userMemberId: String?,
|
||||
toggleSecrets: Boolean,
|
||||
showSecrets: Map<String, Boolean> = emptyMap(),
|
||||
sendCommandMsg: Boolean,
|
||||
linkMode: SimplexLinkMode,
|
||||
colors: Colors? = null,
|
||||
typography: Typography? = null
|
||||
): AnnotatedString = buildAnnotatedString {
|
||||
fun styled(format: Format, content: () -> Unit) {
|
||||
val s = if (colors != null && typography != null) format.style(colors, typography) else null
|
||||
if (s != null) withStyle(s) { content() } else content()
|
||||
}
|
||||
appendSender(this, sender, senderBold)
|
||||
if (prefix != null) append(prefix)
|
||||
if (formattedText == null) {
|
||||
if (text is String) append(text)
|
||||
else if (text is AnnotatedString) append(text)
|
||||
} else {
|
||||
for ((i, ft) in formattedText.withIndex()) {
|
||||
if (ft.format == null) append(ft.text)
|
||||
else when(ft.format) {
|
||||
is Format.Bold, is Format.Italic, is Format.StrikeThrough, is Format.Snippet,
|
||||
is Format.Small, is Format.Colored -> styled(ft.format) { append(ft.text) }
|
||||
is Format.Secret -> {
|
||||
if (toggleSecrets) {
|
||||
val key = i.toString()
|
||||
withAnnotation(tag = "SECRET", annotation = key) {
|
||||
if (showSecrets[key] == true) append(ft.text) else styled(ft.format) { append(ft.text) }
|
||||
}
|
||||
} else {
|
||||
styled(ft.format) { append(ft.text) }
|
||||
}
|
||||
}
|
||||
is Format.Mention -> {
|
||||
val mention = mentions?.get(ft.format.memberName)
|
||||
if (mention != null) {
|
||||
if (mention.memberRef != null) {
|
||||
val displayName = mention.memberRef.displayName
|
||||
val name = if (mention.memberRef.localAlias.isNullOrEmpty()) {
|
||||
displayName
|
||||
} else {
|
||||
"${mention.memberRef.localAlias} ($displayName)"
|
||||
}
|
||||
val ftStyle = if (colors != null && typography != null) ft.format.style(colors, typography) else null
|
||||
val mentionStyle = if (ftStyle != null && colors != null && mention.memberId == userMemberId) ftStyle.copy(color = colors.primary) else ftStyle
|
||||
if (mentionStyle != null) withStyle(mentionStyle) { append(mentionText(name)) } else append(mentionText(name))
|
||||
} else {
|
||||
styled(ft.format) { append(mentionText(ft.format.memberName)) }
|
||||
}
|
||||
} else {
|
||||
append(ft.text)
|
||||
}
|
||||
}
|
||||
is Format.Command ->
|
||||
if (!sendCommandMsg) {
|
||||
append(ft.text)
|
||||
} else {
|
||||
val cmd = ft.format.commandStr
|
||||
withAnnotation(tag = "COMMAND", annotation = cmd) {
|
||||
styled(ft.format) { append("/$cmd") }
|
||||
}
|
||||
}
|
||||
is Format.Uri -> {
|
||||
val s = ft.text
|
||||
val link = if (s.startsWith("http://") || s.startsWith("https://")) s else "https://$s"
|
||||
withAnnotation(tag = "WEB_URL", annotation = link) {
|
||||
styled(ft.format) { append(ft.text) }
|
||||
}
|
||||
}
|
||||
is Format.HyperLink -> {
|
||||
withAnnotation(tag = "WEB_URL", annotation = ft.format.linkUri) {
|
||||
styled(ft.format) { append(ft.format.showText ?: ft.text) }
|
||||
}
|
||||
}
|
||||
is Format.SimplexLink -> {
|
||||
val link =
|
||||
if (linkMode == SimplexLinkMode.BROWSER && ft.format.showText == null && !ft.text.startsWith("[")) ft.text
|
||||
else ft.format.simplexUri
|
||||
val t = ft.format.showText ?: if (linkMode == SimplexLinkMode.DESCRIPTION) ft.format.linkType.description else null
|
||||
withAnnotation(tag = "SIMPLEX_URL", annotation = link) {
|
||||
if (t == null) {
|
||||
styled(ft.format) { append(ft.text) }
|
||||
} else {
|
||||
val ftStyle = if (colors != null && typography != null) ft.format.style(colors, typography) else null
|
||||
if (ftStyle != null) {
|
||||
withStyle(ftStyle) { append("$t ") }
|
||||
withStyle(ftStyle.copy(fontStyle = FontStyle.Italic)) { append(ft.format.viaHosts) }
|
||||
} else {
|
||||
append("$t ")
|
||||
append(ft.format.viaHosts)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is Format.Email -> {
|
||||
withAnnotation(tag = "OTHER_URL", annotation = "mailto:${ft.text}") {
|
||||
styled(ft.format) { append(ft.text) }
|
||||
}
|
||||
}
|
||||
is Format.Phone -> {
|
||||
withAnnotation(tag = "OTHER_URL", annotation = "tel:${ft.text}") {
|
||||
styled(ft.format) { append(ft.text) }
|
||||
}
|
||||
}
|
||||
is Format.Unknown -> append(ft.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MarkdownText (
|
||||
text: CharSequence,
|
||||
@@ -80,8 +197,6 @@ fun MarkdownText (
|
||||
showViaProxy: Boolean = false,
|
||||
showTimestamp: Boolean = true,
|
||||
prefix: AnnotatedString? = null,
|
||||
selectableEnd: MutableIntState? = null,
|
||||
annotatedTextState: MutableState<String>? = null,
|
||||
selectionRange: IntRange? = null,
|
||||
onTextLayoutResult: ((TextLayoutResult) -> Unit)? = null
|
||||
) {
|
||||
@@ -131,135 +246,38 @@ fun MarkdownText (
|
||||
}
|
||||
)
|
||||
}
|
||||
val contentAnnotated = buildMsgAnnotatedString(
|
||||
text = text, formattedText = formattedText, sender = sender, senderBold = senderBold,
|
||||
prefix = prefix, mentions = mentions, userMemberId = userMemberId,
|
||||
toggleSecrets = toggleSecrets, showSecrets = showSecrets,
|
||||
sendCommandMsg = sendCommandMsg != null, linkMode = linkMode,
|
||||
colors = MaterialTheme.colors, typography = MaterialTheme.typography
|
||||
)
|
||||
val contentLength = contentAnnotated.text.length
|
||||
val clampedSelectionRange = selectionRange?.let {
|
||||
it.first until minOf(it.last, contentLength)
|
||||
}
|
||||
if (formattedText == null) {
|
||||
val annotatedText = buildAnnotatedString {
|
||||
inlineContent?.first?.invoke(this)
|
||||
appendSender(this, sender, senderBold)
|
||||
if (prefix != null) append(prefix)
|
||||
if (text is String) append(text)
|
||||
else if (text is AnnotatedString) append(text)
|
||||
selectableEnd?.intValue = this.length
|
||||
append(contentAnnotated)
|
||||
if (meta?.isLive == true) {
|
||||
append(typingIndicator(meta.recent, typingIdx))
|
||||
}
|
||||
if (meta != null) withStyle(reserveTimestampStyle) { append(reserve) }
|
||||
}
|
||||
annotatedTextState?.value = annotatedText.text
|
||||
if (meta?.isLive == true) {
|
||||
Text(annotatedText, style = style, modifier = modifier, maxLines = maxLines, overflow = overflow, inlineContent = inlineContent?.second ?: mapOf())
|
||||
} else {
|
||||
SelectableText(annotatedText, style = style, modifier = modifier, maxLines = maxLines, overflow = overflow, selectionRange = selectionRange, onTextLayoutResult = onTextLayoutResult)
|
||||
SelectableText(annotatedText, style = style, modifier = modifier, maxLines = maxLines, overflow = overflow, selectionRange = clampedSelectionRange, onTextLayoutResult = onTextLayoutResult)
|
||||
}
|
||||
} else {
|
||||
var hasLinks = false
|
||||
var hasSecrets = false
|
||||
var hasCommands = false
|
||||
val hasLinks = formattedText.any { it.format is Format.Uri || it.format is Format.HyperLink || it.format is Format.SimplexLink || it.format is Format.Email || it.format is Format.Phone }
|
||||
val hasSecrets = toggleSecrets && formattedText.any { it.format is Format.Secret }
|
||||
val hasCommands = sendCommandMsg != null && formattedText.any { it.format is Format.Command }
|
||||
val annotatedText = buildAnnotatedString {
|
||||
inlineContent?.first?.invoke(this)
|
||||
appendSender(this, sender, senderBold)
|
||||
if (prefix != null) append(prefix)
|
||||
for ((i, ft) in formattedText.withIndex()) {
|
||||
if (ft.format == null) append(ft.text)
|
||||
else when(ft.format) {
|
||||
is Format.Bold -> withStyle(ft.format.style) { append(ft.text) }
|
||||
is Format.Italic -> withStyle(ft.format.style) { append(ft.text) }
|
||||
is Format.StrikeThrough -> withStyle(ft.format.style) { append(ft.text) }
|
||||
is Format.Snippet -> withStyle(ft.format.style) { append(ft.text) }
|
||||
is Format.Small -> withStyle(ft.format.style) { append(ft.text) }
|
||||
is Format.Colored -> withStyle(ft.format.style) { append(ft.text) }
|
||||
is Format.Secret -> {
|
||||
val ftStyle = ft.format.style
|
||||
if (toggleSecrets) {
|
||||
hasSecrets = true
|
||||
val key = i.toString()
|
||||
withAnnotation(tag = "SECRET", annotation = key) {
|
||||
if (showSecrets[key] == true) append(ft.text) else withStyle(ftStyle) { append(ft.text) }
|
||||
}
|
||||
} else {
|
||||
withStyle(ftStyle) { append(ft.text) }
|
||||
}
|
||||
}
|
||||
is Format.Mention -> {
|
||||
val mention = mentions?.get(ft.format.memberName)
|
||||
if (mention != null) {
|
||||
val ftStyle = ft.format.style
|
||||
if (mention.memberRef != null) {
|
||||
val displayName = mention.memberRef.displayName
|
||||
val name = if (mention.memberRef.localAlias.isNullOrEmpty()) {
|
||||
displayName
|
||||
} else {
|
||||
"${mention.memberRef.localAlias} ($displayName)"
|
||||
}
|
||||
val mentionStyle = if (mention.memberId == userMemberId) ftStyle.copy(color = MaterialTheme.colors.primary) else ftStyle
|
||||
withStyle(mentionStyle) { append(mentionText(name)) }
|
||||
} else {
|
||||
withStyle(ftStyle) { append(mentionText(ft.format.memberName)) }
|
||||
}
|
||||
} else {
|
||||
append(ft.text)
|
||||
}
|
||||
}
|
||||
is Format.Command ->
|
||||
if (sendCommandMsg == null) {
|
||||
append(ft.text)
|
||||
} else {
|
||||
hasCommands = true
|
||||
val ftStyle = ft.format.style
|
||||
val cmd = ft.format.commandStr
|
||||
withAnnotation(tag = "COMMAND", annotation = cmd) {
|
||||
withStyle(ftStyle) { append("/$cmd") }
|
||||
}
|
||||
}
|
||||
is Format.Uri -> {
|
||||
hasLinks = true
|
||||
val ftStyle = Format.linkStyle
|
||||
val s = ft.text
|
||||
val link = if (s.startsWith("http://") || s.startsWith("https://")) s else "https://$s"
|
||||
withAnnotation(tag = "WEB_URL", annotation = link) {
|
||||
withStyle(ftStyle) { append(ft.text) }
|
||||
}
|
||||
}
|
||||
is Format.HyperLink -> {
|
||||
hasLinks = true
|
||||
val ftStyle = Format.linkStyle
|
||||
withAnnotation(tag = "WEB_URL", annotation = ft.format.linkUri) {
|
||||
withStyle(ftStyle) { append(ft.format.showText ?: ft.text) }
|
||||
}
|
||||
}
|
||||
is Format.SimplexLink -> {
|
||||
hasLinks = true
|
||||
val ftStyle = Format.linkStyle
|
||||
val link =
|
||||
if (linkMode == SimplexLinkMode.BROWSER && ft.format.showText == null && !ft.text.startsWith("[")) ft.text
|
||||
else ft.format.simplexUri
|
||||
val t = ft.format.showText ?: if (linkMode == SimplexLinkMode.DESCRIPTION) ft.format.linkType.description else null
|
||||
withAnnotation(tag = "SIMPLEX_URL", annotation = link) {
|
||||
if (t == null) {
|
||||
withStyle(ftStyle) { append(ft.text) }
|
||||
} else {
|
||||
withStyle(ftStyle) { append("$t ") }
|
||||
withStyle(ftStyle.copy(fontStyle = FontStyle.Italic)) { append(ft.format.viaHosts) }
|
||||
}
|
||||
}
|
||||
}
|
||||
is Format.Email -> {
|
||||
hasLinks = true
|
||||
val ftStyle = Format.linkStyle
|
||||
withAnnotation(tag = "OTHER_URL", annotation = "mailto:${ft.text}") {
|
||||
withStyle(ftStyle) { append(ft.text) }
|
||||
}
|
||||
}
|
||||
is Format.Phone -> {
|
||||
hasLinks = true
|
||||
val ftStyle = Format.linkStyle
|
||||
withAnnotation(tag = "OTHER_URL", annotation = "tel:${ft.text}") {
|
||||
withStyle(ftStyle) { append(ft.text) }
|
||||
}
|
||||
}
|
||||
is Format.Unknown -> append(ft.text)
|
||||
}
|
||||
}
|
||||
selectableEnd?.intValue = this.length
|
||||
append(contentAnnotated)
|
||||
if (meta?.isLive == true) {
|
||||
append(typingIndicator(meta.recent, typingIdx))
|
||||
}
|
||||
@@ -268,7 +286,6 @@ fun MarkdownText (
|
||||
withStyle(reserveTimestampStyle) { append("\n" + metaText) }
|
||||
else */if (meta != null) withStyle(reserveTimestampStyle) { append(reserve) }
|
||||
}
|
||||
annotatedTextState?.value = annotatedText.text
|
||||
if ((hasLinks && uriHandler != null) || hasSecrets || (hasCommands && sendCommandMsg != null)) {
|
||||
val icon = remember { mutableStateOf(PointerIcon.Default) }
|
||||
ClickableText(annotatedText, style = style, selectionRange = selectionRange, modifier = modifier.pointerHoverIcon(icon.value), maxLines = maxLines, overflow = overflow,
|
||||
|
||||
@@ -3,7 +3,7 @@ package chat.simplex.common.views.usersettings
|
||||
import SectionBottomSpacer
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
@@ -38,7 +38,7 @@ fun MarkdownHelpView() {
|
||||
Row {
|
||||
MdSyntax("!1 $colored!")
|
||||
Text(buildAnnotatedString {
|
||||
withStyle(Format.Colored(FormatColor.red).style) { append(colored) }
|
||||
withStyle(Format.Colored(FormatColor.red).style(MaterialTheme.colors, MaterialTheme.typography)) { append(colored) }
|
||||
append(" (")
|
||||
appendColor(this, "1", FormatColor.red, ", ")
|
||||
appendColor(this, "2", FormatColor.green, ", ")
|
||||
@@ -52,7 +52,7 @@ fun MarkdownHelpView() {
|
||||
MdSyntax("#$secret#")
|
||||
SelectionContainer {
|
||||
Text(buildAnnotatedString {
|
||||
withStyle(Format.Secret().style) { append(secret) }
|
||||
withStyle(Format.Secret().style(MaterialTheme.colors, MaterialTheme.typography)) { append(secret) }
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -72,14 +72,14 @@ fun MdFormat(markdown: String, example: String, format: Format) {
|
||||
Row {
|
||||
MdSyntax(markdown)
|
||||
Text(buildAnnotatedString {
|
||||
withStyle(format.style) { append(example) }
|
||||
withStyle(format.style(MaterialTheme.colors, MaterialTheme.typography)) { append(example) }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun appendColor(b: AnnotatedString.Builder, s: String, c: FormatColor, after: String) {
|
||||
b.withStyle(Format.Colored(c).style) { append(s)}
|
||||
b.withStyle(Format.Colored(c).style(MaterialTheme.colors, MaterialTheme.typography)) { append(s)}
|
||||
b.append(after)
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user