blob: 0ceae79d1b6becaa5c61786322e312769cb9f035 [file] [log] [blame]
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:Suppress("Deprecation")
package androidx.compose.ui.text
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.graphics.isUnspecified
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.text.intl.LocaleList
import androidx.compose.ui.text.style.BaselineShift
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextGeometricTransform
import androidx.compose.ui.text.style.TextIndent
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.util.fastMap
/**
* Utility function to be able to save nullable values. It also enables not to use with() scope
* for readability/syntactic purposes.
*/
internal fun <T : Saver<Original, Saveable>, Original, Saveable> save(
value: Original?,
saver: T,
scope: SaverScope
): Any {
return value?.let { with(saver) { scope.save(value) } } ?: false
}
/**
* Utility function to restore nullable values. It also enables not to use with() scope
* for readability/syntactic purposes.
*/
internal inline fun <T : Saver<Original, Saveable>, Original, Saveable, reified Result> restore(
value: Saveable?,
saver: T
): Result? {
// Most of the types we save are nullable. However, value classes are usually not but instead
// have a special Unspecified value. In that case we delegate handling of the "false"
// value restoration to the corresponding saver that will restore "false" as an Unspecified
// of the corresponding type.
if (value == false && saver !is NonNullValueClassSaver<*, *>) return null
return value?.let { with(saver) { restore(value) } as Result }
}
/**
* Use for non-null value classes where the Unspecified value needs to be separately handled,
* for example as in the [OffsetSaver]
*/
private interface NonNullValueClassSaver<Original, Saveable : Any> : Saver<Original, Saveable>
private fun <Original, Saveable : Any> NonNullValueClassSaver(
save: SaverScope.(value: Original) -> Saveable?,
restore: (value: Saveable) -> Original?
): NonNullValueClassSaver<Original, Saveable> {
return object : NonNullValueClassSaver<Original, Saveable> {
override fun SaverScope.save(value: Original) = save.invoke(this, value)
override fun restore(value: Saveable) = restore.invoke(value)
}
}
/**
* Utility function to save nullable values that does not require a Saver.
*/
internal fun <T> save(value: T?): T? {
return value
}
/**
* Utility function to restore nullable values that does not require a Saver.
*/
internal inline fun <reified Result> restore(value: Any?): Result? {
return value?.let { it as Result }
}
internal val AnnotatedStringSaver = Saver<AnnotatedString, Any>(
save = {
arrayListOf(
save(it.text),
save(it.spanStyles, AnnotationRangeListSaver, this),
save(it.paragraphStyles, AnnotationRangeListSaver, this),
save(it.annotations, AnnotationRangeListSaver, this),
)
},
restore = {
val list = it as List<Any?>
// lift these to make types work
val spanStylesOrNull: List<AnnotatedString.Range<SpanStyle>>? =
restore(list[1], AnnotationRangeListSaver)
val paragraphStylesOrNull: List<AnnotatedString.Range<ParagraphStyle>>? =
restore(list[2], AnnotationRangeListSaver)
AnnotatedString(
text = restore(list[0])!!,
spanStylesOrNull = spanStylesOrNull?.ifEmpty { null },
paragraphStylesOrNull = paragraphStylesOrNull?.ifEmpty { null },
annotations = restore(list[3], AnnotationRangeListSaver),
)
}
)
private val AnnotationRangeListSaver = Saver<List<AnnotatedString.Range<out Any>>, Any>(
save = {
it.fastMap { range ->
save(range, AnnotationRangeSaver, this)
}
},
restore = {
@Suppress("UNCHECKED_CAST")
val list = it as List<Any>
list.fastMap { item ->
val range: AnnotatedString.Range<out Any> = restore(item, AnnotationRangeSaver)!!
range
}
}
)
private enum class AnnotationType {
Paragraph,
Span,
VerbatimTts,
Url, // UrlAnnotation
Link, // LinkAnnotation.Url
Clickable,
String
}
@OptIn(ExperimentalTextApi::class)
private val AnnotationRangeSaver = Saver<AnnotatedString.Range<out Any>, Any>(
save = {
val marker = when (it.item) {
is ParagraphStyle -> AnnotationType.Paragraph
is SpanStyle -> AnnotationType.Span
is VerbatimTtsAnnotation -> AnnotationType.VerbatimTts
is UrlAnnotation -> AnnotationType.Url
is LinkAnnotation.Url -> AnnotationType.Link
is LinkAnnotation.Clickable -> AnnotationType.Clickable
else -> AnnotationType.String
}
val item = when (marker) {
AnnotationType.Paragraph -> save(it.item as ParagraphStyle, ParagraphStyleSaver, this)
AnnotationType.Span -> save(it.item as SpanStyle, SpanStyleSaver, this)
AnnotationType.VerbatimTts -> save(
it.item as VerbatimTtsAnnotation,
VerbatimTtsAnnotationSaver,
this
)
AnnotationType.Url -> save(
it.item as UrlAnnotation,
UrlAnnotationSaver,
this
)
AnnotationType.Link -> save(
it.item as LinkAnnotation.Url,
LinkSaver,
this
)
AnnotationType.Clickable -> save(
it.item as LinkAnnotation.Clickable,
ClickableSaver,
this
)
AnnotationType.String -> save(it.item)
}
arrayListOf(
save(marker),
item,
save(it.start),
save(it.end),
save(it.tag)
)
},
restore = {
@Suppress("UNCHECKED_CAST")
val list = it as List<Any>
val marker: AnnotationType = restore(list[0])!!
val start: Int = restore(list[2])!!
val end: Int = restore(list[3])!!
val tag: String = restore(list[4])!!
when (marker) {
AnnotationType.Paragraph -> {
val item: ParagraphStyle = restore(list[1], ParagraphStyleSaver)!!
AnnotatedString.Range(item = item, start = start, end = end, tag = tag)
}
AnnotationType.Span -> {
val item: SpanStyle = restore(list[1], SpanStyleSaver)!!
AnnotatedString.Range(item = item, start = start, end = end, tag = tag)
}
AnnotationType.VerbatimTts -> {
val item: VerbatimTtsAnnotation = restore(list[1], VerbatimTtsAnnotationSaver)!!
AnnotatedString.Range(item = item, start = start, end = end, tag = tag)
}
AnnotationType.Url -> {
val item: UrlAnnotation = restore(list[1], UrlAnnotationSaver)!!
AnnotatedString.Range(item = item, start = start, end = end, tag = tag)
}
AnnotationType.Link -> {
val item: LinkAnnotation.Url = restore(list[1], LinkSaver)!!
AnnotatedString.Range(item = item, start = start, end = end, tag = tag)
}
AnnotationType.Clickable -> {
val item: LinkAnnotation.Clickable = restore(list[1], ClickableSaver)!!
AnnotatedString.Range(item = item, start = start, end = end, tag = tag)
}
AnnotationType.String -> {
val item: String = restore(list[1])!!
AnnotatedString.Range(item = item, start = start, end = end, tag = tag)
}
}
}
)
private val VerbatimTtsAnnotationSaver = Saver<VerbatimTtsAnnotation, Any>(
save = { save(it.verbatim) },
restore = { VerbatimTtsAnnotation(restore(it)!!) }
)
@OptIn(ExperimentalTextApi::class)
private val UrlAnnotationSaver = Saver<UrlAnnotation, Any>(
save = { save(it.url) },
restore = { UrlAnnotation(restore(it)!!) }
)
private val LinkSaver = Saver<LinkAnnotation.Url, Any>(
save = {
arrayListOf(
save(it.url),
save(it.style, SpanStyleSaver, this),
save(it.focusedStyle, SpanStyleSaver, this),
save(it.hoveredStyle, SpanStyleSaver, this),
save(it.pressedStyle, SpanStyleSaver, this)
)
},
restore = {
val list = it as List<Any?>
val url: String = restore(list[0])!!
val styleOrNull: SpanStyle? = restore(list[1], SpanStyleSaver)
val focusedStyleOrNull: SpanStyle? = restore(list[2], SpanStyleSaver)
val hoveredStyleOrNull: SpanStyle? = restore(list[3], SpanStyleSaver)
val pressedStyleOrNull: SpanStyle? = restore(list[4], SpanStyleSaver)
LinkAnnotation.Url(
url = url,
style = styleOrNull,
focusedStyle = focusedStyleOrNull,
hoveredStyle = hoveredStyleOrNull,
pressedStyle = pressedStyleOrNull
)
}
)
private val ClickableSaver = Saver<LinkAnnotation.Clickable, Any>(
save = {
arrayListOf(
save(it.tag),
save(it.style, SpanStyleSaver, this),
save(it.focusedStyle, SpanStyleSaver, this),
save(it.hoveredStyle, SpanStyleSaver, this),
save(it.pressedStyle, SpanStyleSaver, this)
)
},
restore = {
val list = it as List<Any?>
val tag: String = restore(list[0])!!
val styleOrNull: SpanStyle? = restore(list[1], SpanStyleSaver)
val focusedStyleOrNull: SpanStyle? = restore(list[2], SpanStyleSaver)
val hoveredStyleOrNull: SpanStyle? = restore(list[3], SpanStyleSaver)
val pressedStyleOrNull: SpanStyle? = restore(list[4], SpanStyleSaver)
LinkAnnotation.Clickable(
tag = tag,
style = styleOrNull,
focusedStyle = focusedStyleOrNull,
hoveredStyle = hoveredStyleOrNull,
pressedStyle = pressedStyleOrNull,
linkInteractionListener = null
)
}
)
internal val ParagraphStyleSaver = Saver<ParagraphStyle, Any>(
save = {
arrayListOf(
save(it.textAlign),
save(it.textDirection),
save(it.lineHeight, TextUnit.Saver, this),
save(it.textIndent, TextIndent.Saver, this)
)
},
restore = {
val list = it as List<Any?>
ParagraphStyle(
textAlign = restore(list[0])!!,
textDirection = restore(list[1])!!,
lineHeight = restore(list[2], TextUnit.Saver)!!,
textIndent = restore(list[3], TextIndent.Saver)
)
}
)
internal val SpanStyleSaver = Saver<SpanStyle, Any>(
save = {
arrayListOf(
save(it.color, Color.Saver, this),
save(it.fontSize, TextUnit.Saver, this),
save(it.fontWeight, FontWeight.Saver, this),
save(it.fontStyle),
save(it.fontSynthesis),
save(-1), // TODO save fontFamily
save(it.fontFeatureSettings),
save(it.letterSpacing, TextUnit.Saver, this),
save(it.baselineShift, BaselineShift.Saver, this),
save(it.textGeometricTransform, TextGeometricTransform.Saver, this),
save(it.localeList, LocaleList.Saver, this),
save(it.background, Color.Saver, this),
save(it.textDecoration, TextDecoration.Saver, this),
save(it.shadow, Shadow.Saver, this)
)
},
restore = {
val list = it as List<Any?>
SpanStyle(
color = restore(list[0], Color.Saver)!!,
fontSize = restore(list[1], TextUnit.Saver)!!,
fontWeight = restore(list[2], FontWeight.Saver),
fontStyle = restore(list[3]),
fontSynthesis = restore(list[4]),
// val fontFamily = list[5] // TODO restore fontFamily
fontFeatureSettings = restore(list[6]),
letterSpacing = restore(list[7], TextUnit.Saver)!!,
baselineShift = restore(list[8], BaselineShift.Saver),
textGeometricTransform = restore(list[9], TextGeometricTransform.Saver),
localeList = restore(list[10], LocaleList.Saver),
background = restore(list[11], Color.Saver)!!,
textDecoration = restore(list[12], TextDecoration.Saver),
shadow = restore(list[13], Shadow.Saver)
)
}
)
internal val TextDecoration.Companion.Saver: Saver<TextDecoration, Any>
get() = TextDecorationSaver
private val TextDecorationSaver = Saver<TextDecoration, Any>(
save = { it.mask },
restore = { TextDecoration(it as Int) }
)
internal val TextGeometricTransform.Companion.Saver: Saver<TextGeometricTransform, Any>
get() = TextGeometricTransformSaver
private val TextGeometricTransformSaver = Saver<TextGeometricTransform, Any>(
save = { arrayListOf(it.scaleX, it.skewX) },
restore = {
@Suppress("UNCHECKED_CAST")
val list = it as List<Float>
TextGeometricTransform(scaleX = list[0], skewX = list[1])
}
)
internal val TextIndent.Companion.Saver: Saver<TextIndent, Any>
get() = TextIndentSaver
private val TextIndentSaver = Saver<TextIndent, Any>(
save = {
arrayListOf(
save(it.firstLine, TextUnit.Saver, this),
save(it.restLine, TextUnit.Saver, this)
)
},
restore = {
@Suppress("UNCHECKED_CAST")
val list = it as List<Any>
TextIndent(
firstLine = restore(list[0], TextUnit.Saver)!!,
restLine = restore(list[1], TextUnit.Saver)!!
)
}
)
internal val FontWeight.Companion.Saver: Saver<FontWeight, Any>
get() = FontWeightSaver
private val FontWeightSaver = Saver<FontWeight, Any>(
save = { it.weight },
restore = { FontWeight(it as Int) }
)
internal val BaselineShift.Companion.Saver: Saver<BaselineShift, Any>
get() = BaselineShiftSaver
private val BaselineShiftSaver = Saver<BaselineShift, Any>(
save = { it.multiplier },
restore = {
BaselineShift(it as Float)
}
)
internal val TextRange.Companion.Saver: Saver<TextRange, Any>
get() = TextRangeSaver
private val TextRangeSaver = Saver<TextRange, Any>(
save = {
arrayListOf(save(it.start), save(it.end))
},
restore = {
@Suppress("UNCHECKED_CAST")
val list = it as List<Any>
TextRange(restore(list[0])!!, restore(list[1])!!)
}
)
internal val Shadow.Companion.Saver: Saver<Shadow, Any>
get() = ShadowSaver
private val ShadowSaver = Saver<Shadow, Any>(
save = {
arrayListOf(
save(it.color, Color.Saver, this),
save(it.offset, Offset.Saver, this),
save(it.blurRadius)
)
},
restore = {
@Suppress("UNCHECKED_CAST")
val list = it as List<Any>
Shadow(
color = restore(list[0], Color.Saver)!!,
offset = restore(list[1], Offset.Saver)!!,
blurRadius = restore(list[2])!!
)
}
)
internal val Color.Companion.Saver: Saver<Color, Any>
get() = ColorSaver
private val ColorSaver = NonNullValueClassSaver<Color, Any>(
save = {
if (it.isUnspecified) { false } else { it.toArgb() }
},
restore = {
if (it == false) { Color.Unspecified } else { Color(it as Int) }
}
)
internal val TextUnit.Companion.Saver: Saver<TextUnit, Any>
get() = TextUnitSaver
private val TextUnitSaver = NonNullValueClassSaver<TextUnit, Any>(
save = {
if (it == TextUnit.Unspecified) {
false
} else {
arrayListOf(save(it.value), save(it.type))
}
},
restore = {
if (it == false) {
TextUnit.Unspecified
} else {
@Suppress("UNCHECKED_CAST")
val list = it as List<Any>
TextUnit(restore(list[0])!!, restore(list[1])!!)
}
}
)
internal val Offset.Companion.Saver: Saver<Offset, Any>
get() = OffsetSaver
private val OffsetSaver = NonNullValueClassSaver<Offset, Any>(
save = {
if (it == Offset.Unspecified) {
false
} else {
arrayListOf(save(it.x), save(it.y))
}
},
restore = {
if (it == false) {
Offset.Unspecified
} else {
val list = it as List<Any?>
Offset(restore(list[0])!!, restore(list[1])!!)
}
}
)
internal val LocaleList.Companion.Saver: Saver<LocaleList, Any>
get() = LocaleListSaver
private val LocaleListSaver = Saver<LocaleList, Any>(
save = {
it.localeList.fastMap { locale ->
save(locale, Locale.Saver, this)
}
},
restore = {
@Suppress("UNCHECKED_CAST")
val list = it as List<Any>
LocaleList(list.fastMap { item -> restore(item, Locale.Saver)!! })
}
)
internal val Locale.Companion.Saver: Saver<Locale, Any>
get() = LocaleSaver
private val LocaleSaver = Saver<Locale, Any>(
save = { it.toLanguageTag() },
restore = { Locale(languageTag = it as String) }
)