Styling up the LocationDetail

This commit is contained in:
Thomas Andres Gomez 2023-08-11 15:06:36 +02:00
parent e3cd0bdd4b
commit b9d14d12ff
11 changed files with 156 additions and 55 deletions

View file

@ -100,7 +100,7 @@ fun LexiconItem(
true -> emptyList() true -> emptyList()
else -> listOf( else -> listOf(
AnnotatedString.Range( AnnotatedString.Range(
item = typography.dropCapMediumSpan, item = typography.bodyDropCapSpan,
start = 0, start = 0,
end = 1, end = 1,
) )

View file

@ -31,7 +31,6 @@ import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.pixelized.rplexicon.ui.composable.AsyncImage import com.pixelized.rplexicon.ui.composable.AsyncImage
import com.pixelized.rplexicon.ui.theme.LexiconTheme import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.ui.theme.colors.LexiconColors
import com.pixelized.rplexicon.utilitary.extentions.lexicon import com.pixelized.rplexicon.utilitary.extentions.lexicon
import com.skydoves.landscapist.ImageOptions import com.skydoves.landscapist.ImageOptions
@ -39,12 +38,13 @@ import com.skydoves.landscapist.ImageOptions
fun FantasyMap( fun FantasyMap(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
state: FantasyMapState, state: FantasyMapState,
model: () -> Any?,
imageOptions: ImageOptions = ImageOptions(), imageOptions: ImageOptions = ImageOptions(),
@DrawableRes previewPlaceholder: Int, @DrawableRes previewPlaceholder: Int,
item: State<LocationDetailUio>, items: State<List<AnnotatedMarqueeUio>>,
highlight: State<Offset>, highlight: State<Offset>,
selectedItem: State<Int>, selectedItem: State<Int>,
onMarquee: (MarqueeUio) -> Unit, onMarquee: (AnnotatedMarqueeUio) -> Unit,
onTap: (Offset) -> Unit, onTap: (Offset) -> Unit,
) { ) {
val lexiconTheme = MaterialTheme.lexicon val lexiconTheme = MaterialTheme.lexicon
@ -68,7 +68,7 @@ fun FantasyMap(
} }
LaunchedEffect(key1 = "CenterOnMarquee:${selectedItem.value}") { LaunchedEffect(key1 = "CenterOnMarquee:${selectedItem.value}") {
item.value.marquees.getOrNull(selectedItem.value)?.position items.value.getOrNull(selectedItem.value)?.position
?.let { state.pan(state.computeMarqueeOffset(it)) } ?.let { state.pan(state.computeMarqueeOffset(it)) }
} }
@ -115,7 +115,7 @@ fun FantasyMap(
.drawWithContent { .drawWithContent {
drawContent() drawContent()
if (animatedMarqueeAlpha.value > 0f) { if (animatedMarqueeAlpha.value > 0f) {
item.value.marquees.forEachIndexed { index, item -> items.value.forEachIndexed { index, item ->
if (item.position != Offset.Unspecified) { if (item.position != Offset.Unspecified) {
drawMarque( drawMarque(
theme = lexiconTheme, theme = lexiconTheme,
@ -147,7 +147,7 @@ fun FantasyMap(
) )
) )
} else { } else {
val marquee = item.value.marquees val marquee = items.value
.asReversed() .asReversed()
.firstOrNull { item -> .firstOrNull { item ->
if (item.position != Offset.Unspecified) { if (item.position != Offset.Unspecified) {
@ -163,7 +163,7 @@ fun FantasyMap(
} }
) )
}, },
imageModel = { item.value.map }, imageModel = model,
imageOptions = imageOptions, imageOptions = imageOptions,
previewPlaceholder = previewPlaceholder, previewPlaceholder = previewPlaceholder,
) )

View file

@ -3,6 +3,7 @@ package com.pixelized.rplexicon.ui.screens.location.detail
import android.content.res.Configuration import android.content.res.Configuration
import android.net.Uri import android.net.Uri
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.ScrollState import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.gestures.scrollBy import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@ -36,6 +37,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
@ -43,13 +45,18 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntOffset
@ -62,7 +69,6 @@ import com.pixelized.rplexicon.ui.composable.Handle
import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost
import com.pixelized.rplexicon.ui.theme.LexiconTheme import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.extentions.lexicon import com.pixelized.rplexicon.utilitary.extentions.lexicon
import com.pixelized.rplexicon.utilitary.rememberTextSize
import com.skydoves.landscapist.ImageOptions import com.skydoves.landscapist.ImageOptions
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -77,6 +83,13 @@ data class LocationDetailUio(
val marquees: List<MarqueeUio>, val marquees: List<MarqueeUio>,
) )
@Stable
data class AnnotatedLocationDetailUio(
val name: AnnotatedString,
val map: Uri,
val marquees: List<AnnotatedMarqueeUio>,
)
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun LocationDetail( fun LocationDetail(
@ -94,6 +107,7 @@ fun LocationDetail(
val ok = stringResource(id = android.R.string.ok) val ok = stringResource(id = android.R.string.ok)
val location = rememberAnnotation(item = viewModel.location)
val snackJob = remember { mutableStateOf<Job?>(null) } val snackJob = remember { mutableStateOf<Job?>(null) }
val mapHighlight = remember { mutableStateOf(Offset.Unspecified) } val mapHighlight = remember { mutableStateOf(Offset.Unspecified) }
val selectedIndex = remember { mutableStateOf(0) } val selectedIndex = remember { mutableStateOf(0) }
@ -107,7 +121,7 @@ fun LocationDetail(
scrollState = scroll, scrollState = scroll,
pagerState = pager, pagerState = pager,
fantasyMapState = fantasy, fantasyMapState = fantasy,
item = viewModel.location, item = location,
selectedIndex = selectedIndex, selectedIndex = selectedIndex,
mapHighlight = mapHighlight, mapHighlight = mapHighlight,
onBack = { onBack = {
@ -115,7 +129,7 @@ fun LocationDetail(
}, },
onMarquee = { onMarquee = {
scope.launch { scope.launch {
val index = max(viewModel.location.value.marquees.indexOf(it), 0) val index = max(location.value.marquees.indexOf(it), 0)
selectedIndex.value = index selectedIndex.value = index
pager.animateScrollToPage(page = index) pager.animateScrollToPage(page = index)
} }
@ -195,18 +209,19 @@ private fun LocationContent(
scrollState: ScrollState, scrollState: ScrollState,
pagerState: PagerState, pagerState: PagerState,
fantasyMapState: FantasyMapState, fantasyMapState: FantasyMapState,
item: State<LocationDetailUio>, item: State<AnnotatedLocationDetailUio>,
selectedIndex: State<Int>, selectedIndex: State<Int>,
mapHighlight: State<Offset>, mapHighlight: State<Offset>,
onBack: () -> Unit, onBack: () -> Unit,
onMarquee: (MarqueeUio) -> Unit, onMarquee: (AnnotatedMarqueeUio) -> Unit,
onMapTap: (Offset) -> Unit, onMapTap: (Offset) -> Unit,
onTouch: (Boolean) -> Unit, onTouch: (Boolean) -> Unit,
onCenter: () -> Unit, onCenter: () -> Unit,
onZoomIn: () -> Unit, onZoomIn: () -> Unit,
onZoomOut: () -> Unit, onZoomOut: () -> Unit,
) { ) {
val itemNameSize = rememberTextSize(style = MaterialTheme.typography.headlineSmall) val density = LocalDensity.current
val itemNameSize = remember { mutableStateOf(0.dp) }
val filledIconButtonColors = IconButtonDefaults.filledIconButtonColors( val filledIconButtonColors = IconButtonDefaults.filledIconButtonColors(
containerColor = MaterialTheme.colorScheme.surface, containerColor = MaterialTheme.colorScheme.surface,
@ -261,9 +276,10 @@ private fun LocationContent(
.align(alignment = Alignment.Center) .align(alignment = Alignment.Center)
.offset { IntOffset(x = 0, y = scrollState.value / 2) }, .offset { IntOffset(x = 0, y = scrollState.value / 2) },
state = fantasyMapState, state = fantasyMapState,
model = { item.value.map },
previewPlaceholder = R.drawable.im_brulkhai, previewPlaceholder = R.drawable.im_brulkhai,
imageOptions = ImageOptions(contentScale = ContentScale.Fit), imageOptions = ImageOptions(contentScale = ContentScale.Fit),
item = item, items = remember { derivedStateOf { item.value.marquees } },
selectedItem = selectedIndex, selectedItem = selectedIndex,
highlight = mapHighlight, highlight = mapHighlight,
onMarquee = onMarquee, onMarquee = onMarquee,
@ -323,18 +339,32 @@ private fun LocationContent(
Handle( Handle(
modifier = Modifier modifier = Modifier
.align(alignment = Alignment.CenterHorizontally) .align(alignment = Alignment.CenterHorizontally)
.padding(top = 16.dp) .padding(vertical = 16.dp)
) )
Text( Column(
modifier = Modifier modifier = Modifier
.align(alignment = Alignment.CenterHorizontally) .onSizeChanged { itemNameSize.value = with(density) { it.height.toDp() } }
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
.padding(top = 16.dp), .fillMaxWidth(),
textAlign = TextAlign.Center, horizontalAlignment = Alignment.CenterHorizontally,
style = MaterialTheme.typography.headlineSmall, ) {
text = item.value.name, Text(
) textAlign = TextAlign.Center,
style = MaterialTheme.typography.headlineSmall,
text = item.value.name,
)
Image(
modifier = Modifier
.height(24.dp)
.graphicsLayer { rotationZ = 180f },
painter = painterResource(id = R.drawable.art_divider_1),
contentScale = ContentScale.FillWidth,
alignment = Alignment.Center,
colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.onSurface),
contentDescription = null,
)
}
HorizontalPager( HorizontalPager(
modifier = Modifier modifier = Modifier
@ -350,7 +380,7 @@ private fun LocationContent(
MarqueeItem( MarqueeItem(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(this@constraint.maxHeight - 32.dp - itemNameSize.height), .height(this@constraint.maxHeight - 32.dp - itemNameSize.value),
marquee = marquee, marquee = marquee,
) )
} }
@ -378,6 +408,31 @@ private fun HandlePagerScroll(
} }
} }
@Composable
@Stable
fun rememberAnnotation(item: State<LocationDetailUio>): State<AnnotatedLocationDetailUio> {
val typography = MaterialTheme.lexicon.typography
return remember(item) {
derivedStateOf {
AnnotatedLocationDetailUio(
name = AnnotatedString(
text = item.value.name,
spanStyles = listOf(
AnnotatedString.Range(
item = typography.titleDropCapSpan,
start = 0,
end = Integer.min(1, item.value.name.length),
)
)
),
map = item.value.map,
marquees = item.value.marquees.map { it.annotate(typography) },
)
}
}
}
@Composable @Composable
@Stable @Stable
private fun rememberSnapConnection( private fun rememberSnapConnection(
@ -437,7 +492,7 @@ private fun LocationPreview() {
scrollState = rememberScrollState(), scrollState = rememberScrollState(),
pagerState = rememberPagerState(), pagerState = rememberPagerState(),
fantasyMapState = rememberFantasyMapState(), fantasyMapState = rememberFantasyMapState(),
item = remember { item = rememberAnnotation(item = remember {
mutableStateOf( mutableStateOf(
LocationDetailUio( LocationDetailUio(
name = "Daggerfall", name = "Daggerfall",
@ -456,7 +511,7 @@ private fun LocationPreview() {
), ),
) )
) )
}, }),
selectedIndex = remember { mutableStateOf(0) }, selectedIndex = remember { mutableStateOf(0) },
mapHighlight = remember { mutableStateOf(Offset(0.5f, 0.5f)) }, mapHighlight = remember { mutableStateOf(Offset(0.5f, 0.5f)) },
onBack = { }, onBack = { },

View file

@ -2,7 +2,6 @@ package com.pixelized.rplexicon.ui.screens.location.detail
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.geometry.Offset
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.pixelized.rplexicon.repository.LocationRepository import com.pixelized.rplexicon.repository.LocationRepository

View file

@ -2,8 +2,7 @@ package com.pixelized.rplexicon.ui.screens.location.detail
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
@ -14,11 +13,16 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.pixelized.rplexicon.ui.theme.LexiconTheme import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.ui.theme.typography.LexiconTypography
import com.pixelized.rplexicon.utilitary.LOS_HOLLOW
import com.pixelized.rplexicon.utilitary.extentions.annotateWithDropCap
import com.pixelized.rplexicon.utilitary.extentions.lexicon
@Stable @Stable
data class MarqueeUio( data class MarqueeUio(
@ -27,23 +31,49 @@ data class MarqueeUio(
val description: String?, val description: String?,
) )
@Stable
data class AnnotatedMarqueeUio(
val name: AnnotatedString,
val position: Offset,
val description: AnnotatedString?,
)
@Stable
fun MarqueeUio.annotate(
typography: LexiconTypography
): AnnotatedMarqueeUio {
return AnnotatedMarqueeUio(
name = name.annotateWithDropCap(style = typography.bodyDropCapSpan),
position = position,
description = description?.annotateWithDropCap(style = typography.bodyDropCapSpan),
)
}
@Composable @Composable
fun MarqueeItem( fun MarqueeItem(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
marquee: MarqueeUio, marquee: AnnotatedMarqueeUio,
) { ) {
Column( Column(
modifier = modifier, modifier = modifier,
verticalArrangement = Arrangement.spacedBy(space = 8.dp) verticalArrangement = Arrangement.spacedBy(space = 8.dp)
) { ) {
Text( Row(
modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp),
textAlign = TextAlign.Center, ) {
overflow = TextOverflow.Ellipsis, Text(
maxLines = 1, modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.titleMedium, text = LOS_HOLLOW,
text = marquee.name, )
) Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.titleMedium,
textAlign = TextAlign.Center,
overflow = TextOverflow.Ellipsis,
maxLines = 3,
text = marquee.name,
)
}
marquee.description?.let { marquee.description?.let {
Text( Text(
modifier = Modifier.verticalScroll(rememberScrollState()), modifier = Modifier.verticalScroll(rememberScrollState()),
@ -65,6 +95,8 @@ private fun MarqueeItemPreview() {
name = "Name", name = "Name",
position = Offset.Zero, position = Offset.Zero,
description = "description", description = "description",
).annotate(
typography = MaterialTheme.lexicon.typography,
) )
) )
} }

View file

@ -77,7 +77,7 @@ fun LocationItem(
true -> emptyList() true -> emptyList()
else -> listOf( else -> listOf(
AnnotatedString.Range( AnnotatedString.Range(
item = typography.dropCapMediumSpan, item = typography.bodyDropCapSpan,
start = 0, start = 0,
end = 1, end = 1,
) )

View file

@ -34,7 +34,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ColorMatrix
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
@ -129,7 +128,7 @@ private fun QuestDetailUio.QuestStep.annotate(): AnnotatedQuestDetailUio.Annotat
text = description, text = description,
spanStyles = listOf( spanStyles = listOf(
AnnotatedString.Range( AnnotatedString.Range(
item = typography.dropCapLargeSpan, item = typography.bodyDropCapSpan,
start = 0, start = 0,
end = min(1, description.length), end = min(1, description.length),
) )

View file

@ -81,7 +81,7 @@ fun QuestItem(
true -> emptyList() true -> emptyList()
else -> listOf( else -> listOf(
AnnotatedString.Range( AnnotatedString.Range(
item = typography.dropCapMediumSpan, item = typography.bodyDropCapSpan,
start = 0, start = 0,
end = 1, end = 1,
) )

View file

@ -49,7 +49,7 @@ fun lexiconDimen(
thickness = 4.dp, thickness = 4.dp,
), ),
map: LexiconDimens.Map = LexiconDimens.Map( map: LexiconDimens.Map = LexiconDimens.Map(
mapSnapPx = with(density) { 64.dp.roundToPx() }, mapSnapPx = with(density) { 128.dp.roundToPx() },
marqueeRadiusPx = with(density) { 12.dp.roundToPx() }, marqueeRadiusPx = with(density) { 12.dp.roundToPx() },
marqueeStrokePx = with(density) { 2.dp.roundToPx() }, marqueeStrokePx = with(density) { 2.dp.roundToPx() },
crossRadiusPx = with(density) { 12.dp.roundToPx() / sqrt(2f) }.toInt(), crossRadiusPx = with(density) { 12.dp.roundToPx() / sqrt(2f) }.toInt(),

View file

@ -24,22 +24,19 @@ val stampFontFamily = FontFamily(
@Stable @Stable
class LexiconTypography( class LexiconTypography(
val base: Typography = Typography(), val base: Typography = Typography(),
val dropCapMedium: TextStyle = base.displaySmall.copy( val stamp: TextStyle = base.headlineLarge.copy(
fontFamily = stampFontFamily,
),
val bodyDropCapSpan: SpanStyle = base.displaySmall.copy(
fontFamily = regalFontFamily, fontFamily = regalFontFamily,
baselineShift = BaselineShift(-0.3f), baselineShift = BaselineShift(-0.3f),
letterSpacing = (-6).sp letterSpacing = (-6).sp
), ).toSpanStyle(),
val dropCapLarge: TextStyle = base.displayMedium.copy( val titleDropCapSpan: SpanStyle = base.displayLarge.copy(
fontFamily = regalFontFamily, fontFamily = regalFontFamily,
baselineShift = BaselineShift.Subscript, baselineShift = BaselineShift(-0.3f),
letterSpacing = (-8).sp letterSpacing = (-8).sp
), ).toSpanStyle()
val stamp: TextStyle = base.headlineLarge.copy( )
fontFamily = stampFontFamily,
)
) {
val dropCapMediumSpan: SpanStyle = dropCapMedium.toSpanStyle()
val dropCapLargeSpan: SpanStyle = dropCapLarge.toSpanStyle()
}
fun lexiconTypography() = LexiconTypography() fun lexiconTypography() = LexiconTypography()

View file

@ -1,6 +1,10 @@
package com.pixelized.rplexicon.utilitary.extentions package com.pixelized.rplexicon.utilitary.extentions
import android.net.Uri import android.net.Uri
import android.view.accessibility.AccessibilityNodeInfo
import androidx.compose.runtime.Stable
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.core.net.toUri import androidx.core.net.toUri
val String.ARG: String get() = "$this={$this}" val String.ARG: String get() = "$this={$this}"
@ -24,3 +28,18 @@ fun String?.toUriOrNull(): Uri? = try {
null null
} }
private val dropCapRegex = Regex("(?:\n\n)(.)")
@Stable
fun String.annotateWithDropCap(
style: SpanStyle,
) = AnnotatedString(
text = this,
spanStyles = listOf(
AnnotatedString.Range(
item = style,
start = 0,
end = Integer.min(1, this.length),
),
) + dropCapRegex.annotatedSpan(input = this, spanStyle = style)
)