1 /*
2  * Copyright 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.compose.ui.text
18 
19 import androidx.compose.runtime.Immutable
20 import androidx.compose.runtime.Stable
21 import androidx.compose.ui.text.internal.checkPrecondition
22 import androidx.compose.ui.text.style.Hyphens
23 import androidx.compose.ui.text.style.LineBreak
24 import androidx.compose.ui.text.style.LineHeightStyle
25 import androidx.compose.ui.text.style.TextAlign
26 import androidx.compose.ui.text.style.TextDirection
27 import androidx.compose.ui.text.style.TextIndent
28 import androidx.compose.ui.text.style.TextMotion
29 import androidx.compose.ui.text.style.lerp
30 import androidx.compose.ui.unit.LayoutDirection
31 import androidx.compose.ui.unit.TextUnit
32 import androidx.compose.ui.unit.isSpecified
33 import androidx.compose.ui.unit.isUnspecified
34 import kotlin.jvm.JvmName
35 
36 private val DefaultLineHeight = TextUnit.Unspecified
37 
38 /**
39  * Paragraph styling configuration for a paragraph. The difference between [SpanStyle] and
40  * `ParagraphStyle` is that, `ParagraphStyle` can be applied to a whole [Paragraph] while
41  * [SpanStyle] can be applied at the character level. Once a portion of the text is marked with a
42  * `ParagraphStyle`, that portion will be separated from the remaining as if a line feed character
43  * was added.
44  *
45  * @sample androidx.compose.ui.text.samples.ParagraphStyleSample
46  * @sample androidx.compose.ui.text.samples.ParagraphStyleAnnotatedStringsSample
47  * @param textAlign The alignment of the text within the lines of the paragraph.
48  * @param textDirection The algorithm to be used to resolve the final text direction: Left To Right
49  *   or Right To Left.
50  * @param lineHeight Line height for the [Paragraph] in [TextUnit] unit, e.g. SP or EM.
51  * @param textIndent The indentation of the paragraph.
52  * @param platformStyle Platform specific [ParagraphStyle] parameters.
53  * @param lineHeightStyle the configuration for line height such as vertical alignment of the line,
54  *   whether to apply additional space as a result of line height to top of first line top and
55  *   bottom of last line. The configuration is applied only when a [lineHeight] is defined. When
56  *   null, [LineHeightStyle.Default] is used.
57  * @param lineBreak The line breaking configuration for the text.
58  * @param hyphens The configuration of hyphenation.
59  * @param textMotion Text character placement, whether to optimize for animated or static text.
60  * @see Paragraph
61  * @see AnnotatedString
62  * @see SpanStyle
63  * @see TextStyle
64  */
65 @Immutable
66 class ParagraphStyle(
67     val textAlign: TextAlign = TextAlign.Unspecified,
68     val textDirection: TextDirection = TextDirection.Unspecified,
69     val lineHeight: TextUnit = TextUnit.Unspecified,
70     val textIndent: TextIndent? = null,
71     val platformStyle: PlatformParagraphStyle? = null,
72     val lineHeightStyle: LineHeightStyle? = null,
73     val lineBreak: LineBreak = LineBreak.Unspecified,
74     val hyphens: Hyphens = Hyphens.Unspecified,
75     val textMotion: TextMotion? = null
76 ) : AnnotatedString.Annotation {
77     @Deprecated("Kept for backwards compatibility.", level = DeprecationLevel.WARNING)
78     @get:JvmName("getTextAlign-buA522U") // b/320819734
79     @Suppress("unused", "RedundantNullableReturnType", "PropertyName")
80     val deprecated_boxing_textAlign: TextAlign?
81         get() = this.textAlign
82 
83     @Deprecated("Kept for backwards compatibility.", level = DeprecationLevel.WARNING)
84     @get:JvmName("getTextDirection-mmuk1to") // b/320819734
85     @Suppress("unused", "RedundantNullableReturnType", "PropertyName")
86     val deprecated_boxing_textDirection: TextDirection?
87         get() = this.textDirection
88 
89     @Deprecated("Kept for backwards compatibility.", level = DeprecationLevel.WARNING)
90     @get:JvmName("getHyphens-EaSxIns") // b/320819734
91     @Suppress("unused", "RedundantNullableReturnType", "PropertyName")
92     val deprecated_boxing_hyphens: Hyphens?
93         get() = this.hyphens
94 
95     @Deprecated("Kept for backwards compatibility.", level = DeprecationLevel.WARNING)
96     @get:JvmName("getLineBreak-LgCVezo") // b/320819734
97     @Suppress("unused", "RedundantNullableReturnType", "PropertyName")
98     val deprecated_boxing_lineBreak: LineBreak?
99         get() = this.lineBreak
100 
101     @Deprecated(
102         "ParagraphStyle constructors that take nullable TextAlign, " +
103             "TextDirection, LineBreak, and Hyphens are deprecated. Please use a new constructor " +
104             "where these parameters are non-nullable. Null value has been replaced by a special " +
105             "Unspecified object for performance reason.",
106         level = DeprecationLevel.HIDDEN
107     )
108     constructor(
109         textAlign: TextAlign? = null,
110         textDirection: TextDirection? = null,
111         lineHeight: TextUnit = TextUnit.Unspecified,
112         textIndent: TextIndent? = null,
113         platformStyle: PlatformParagraphStyle? = null,
114         lineHeightStyle: LineHeightStyle? = null,
115         lineBreak: LineBreak? = null,
116         hyphens: Hyphens? = null,
117         textMotion: TextMotion? = null
118     ) : this(
119         textAlign = textAlign ?: TextAlign.Unspecified,
120         textDirection = textDirection ?: TextDirection.Unspecified,
121         lineHeight = lineHeight,
122         textIndent = textIndent,
123         platformStyle = platformStyle,
124         lineHeightStyle = lineHeightStyle,
125         lineBreak = lineBreak ?: LineBreak.Unspecified,
126         hyphens = hyphens ?: Hyphens.Unspecified,
127         textMotion = textMotion
128     )
129 
130     @Deprecated(
131         "ParagraphStyle constructors that do not take new stable parameters " +
132             "like LineHeightStyle, LineBreak, Hyphens are deprecated. Please use the new stable " +
133             "constructor.",
134         level = DeprecationLevel.HIDDEN
135     )
136     constructor(
137         textAlign: TextAlign? = null,
138         textDirection: TextDirection? = null,
139         lineHeight: TextUnit = TextUnit.Unspecified,
140         textIndent: TextIndent? = null
141     ) : this(
142         textAlign = textAlign ?: TextAlign.Unspecified,
143         textDirection = textDirection ?: TextDirection.Unspecified,
144         lineHeight = lineHeight,
145         textIndent = textIndent,
146         platformStyle = null,
147         lineHeightStyle = null,
148         lineBreak = LineBreak.Unspecified,
149         hyphens = Hyphens.Unspecified,
150         textMotion = null
151     )
152 
153     @Deprecated(
154         "ParagraphStyle constructors that do not take new stable parameters " +
155             "like LineHeightStyle, LineBreak, Hyphens are deprecated. Please use the new stable " +
156             "constructors.",
157         level = DeprecationLevel.HIDDEN
158     )
159     constructor(
160         textAlign: TextAlign? = null,
161         textDirection: TextDirection? = null,
162         lineHeight: TextUnit = TextUnit.Unspecified,
163         textIndent: TextIndent? = null,
164         platformStyle: PlatformParagraphStyle? = null,
165         lineHeightStyle: LineHeightStyle? = null
166     ) : this(
167         textAlign = textAlign ?: TextAlign.Unspecified,
168         textDirection = textDirection ?: TextDirection.Unspecified,
169         lineHeight = lineHeight,
170         textIndent = textIndent,
171         platformStyle = platformStyle,
172         lineHeightStyle = lineHeightStyle,
173         lineBreak = LineBreak.Unspecified,
174         hyphens = Hyphens.Unspecified,
175         textMotion = null
176     )
177 
178     @Deprecated(
179         "ParagraphStyle constructors that do not take new stable parameters " +
180             "like LineBreak, Hyphens, TextMotion are deprecated. Please use the new stable " +
181             "constructors.",
182         level = DeprecationLevel.HIDDEN
183     )
184     constructor(
185         textAlign: TextAlign? = null,
186         textDirection: TextDirection? = null,
187         lineHeight: TextUnit = TextUnit.Unspecified,
188         textIndent: TextIndent? = null,
189         platformStyle: PlatformParagraphStyle? = null,
190         lineHeightStyle: LineHeightStyle? = null,
191         lineBreak: LineBreak? = null,
192         hyphens: Hyphens? = null
193     ) : this(
194         textAlign = textAlign ?: TextAlign.Unspecified,
195         textDirection = textDirection ?: TextDirection.Unspecified,
196         lineHeight = lineHeight,
197         textIndent = textIndent,
198         platformStyle = platformStyle,
199         lineHeightStyle = lineHeightStyle,
200         lineBreak = lineBreak ?: LineBreak.Unspecified,
201         hyphens = hyphens ?: Hyphens.Unspecified,
202         textMotion = null
203     )
204 
205     init {
206         if (lineHeight != TextUnit.Unspecified) {
207             // Since we are checking if it's negative, no need to convert Sp into Px at this point.
<lambda>null208             checkPrecondition(lineHeight.value >= 0f) {
209                 "lineHeight can't be negative (${lineHeight.value})"
210             }
211         }
212     }
213 
214     /**
215      * Returns a new paragraph style that is a combination of this style and the given [other]
216      * style.
217      *
218      * If the given paragraph style is null, returns this paragraph style.
219      */
220     @Stable
mergenull221     fun merge(other: ParagraphStyle? = null): ParagraphStyle {
222         if (other == null) return this
223 
224         return fastMerge(
225             textAlign = other.textAlign,
226             textDirection = other.textDirection,
227             lineHeight = other.lineHeight,
228             textIndent = other.textIndent,
229             platformStyle = other.platformStyle,
230             lineHeightStyle = other.lineHeightStyle,
231             lineBreak = other.lineBreak,
232             hyphens = other.hyphens,
233             textMotion = other.textMotion
234         )
235     }
236 
237     /** Plus operator overload that applies a [merge]. */
plusnull238     @Stable operator fun plus(other: ParagraphStyle): ParagraphStyle = this.merge(other)
239 
240     @Deprecated(
241         "ParagraphStyle copy constructors that do not take new stable parameters " +
242             "like LineHeightStyle, LineBreak, Hyphens are deprecated. Please use the new stable " +
243             "copy constructor.",
244         level = DeprecationLevel.HIDDEN
245     )
246     fun copy(
247         textAlign: TextAlign? = this.textAlign,
248         textDirection: TextDirection? = this.textDirection,
249         lineHeight: TextUnit = this.lineHeight,
250         textIndent: TextIndent? = this.textIndent
251     ): ParagraphStyle {
252         return ParagraphStyle(
253             textAlign = textAlign ?: TextAlign.Unspecified,
254             textDirection = textDirection ?: TextDirection.Unspecified,
255             lineHeight = lineHeight,
256             textIndent = textIndent,
257             platformStyle = this.platformStyle,
258             lineHeightStyle = this.lineHeightStyle,
259             lineBreak = this.lineBreak,
260             hyphens = this.hyphens,
261             textMotion = this.textMotion
262         )
263     }
264 
265     @Deprecated(
266         "ParagraphStyle copy constructors that do not take new stable parameters " +
267             "like LineHeightStyle, LineBreak, Hyphens are deprecated. Please use the new stable " +
268             "copy constructor.",
269         level = DeprecationLevel.HIDDEN
270     )
copynull271     fun copy(
272         textAlign: TextAlign? = this.textAlign,
273         textDirection: TextDirection? = this.textDirection,
274         lineHeight: TextUnit = this.lineHeight,
275         textIndent: TextIndent? = this.textIndent,
276         platformStyle: PlatformParagraphStyle? = this.platformStyle,
277         lineHeightStyle: LineHeightStyle? = this.lineHeightStyle
278     ): ParagraphStyle {
279         return ParagraphStyle(
280             textAlign = textAlign ?: TextAlign.Unspecified,
281             textDirection = textDirection ?: TextDirection.Unspecified,
282             lineHeight = lineHeight,
283             textIndent = textIndent,
284             platformStyle = platformStyle,
285             lineHeightStyle = lineHeightStyle,
286             lineBreak = this.lineBreak,
287             hyphens = this.hyphens,
288             textMotion = this.textMotion
289         )
290     }
291 
292     @Deprecated(
293         "ParagraphStyle copy constructors that do not take new stable parameters " +
294             "like LineBreak, Hyphens, TextMotion are deprecated. Please use the new stable " +
295             "copy constructor.",
296         level = DeprecationLevel.HIDDEN
297     )
copynull298     fun copy(
299         textAlign: TextAlign? = this.textAlign,
300         textDirection: TextDirection? = this.textDirection,
301         lineHeight: TextUnit = this.lineHeight,
302         textIndent: TextIndent? = this.textIndent,
303         platformStyle: PlatformParagraphStyle? = this.platformStyle,
304         lineHeightStyle: LineHeightStyle? = this.lineHeightStyle,
305         lineBreak: LineBreak? = this.lineBreak,
306         hyphens: Hyphens? = this.hyphens
307     ): ParagraphStyle {
308         return ParagraphStyle(
309             textAlign = textAlign ?: TextAlign.Unspecified,
310             textDirection = textDirection ?: TextDirection.Unspecified,
311             lineHeight = lineHeight,
312             textIndent = textIndent,
313             platformStyle = platformStyle,
314             lineHeightStyle = lineHeightStyle,
315             lineBreak = lineBreak ?: LineBreak.Unspecified,
316             hyphens = hyphens ?: Hyphens.Unspecified,
317             textMotion = this.textMotion
318         )
319     }
320 
321     @Deprecated(
322         "ParagraphStyle copy constructors that take nullable TextAlign, " +
323             "TextDirection, LineBreak, and Hyphens are deprecated. Please use a new constructor " +
324             "where these parameters are non-nullable. Null value has been replaced by a special " +
325             "Unspecified object for performance reason.",
326         level = DeprecationLevel.HIDDEN
327     )
copynull328     fun copy(
329         textAlign: TextAlign? = this.textAlign,
330         textDirection: TextDirection? = this.textDirection,
331         lineHeight: TextUnit = this.lineHeight,
332         textIndent: TextIndent? = this.textIndent,
333         platformStyle: PlatformParagraphStyle? = this.platformStyle,
334         lineHeightStyle: LineHeightStyle? = this.lineHeightStyle,
335         lineBreak: LineBreak? = this.lineBreak,
336         hyphens: Hyphens? = this.hyphens,
337         textMotion: TextMotion? = this.textMotion
338     ): ParagraphStyle {
339         return ParagraphStyle(
340             textAlign = textAlign ?: TextAlign.Unspecified,
341             textDirection = textDirection ?: TextDirection.Unspecified,
342             lineHeight = lineHeight,
343             textIndent = textIndent,
344             platformStyle = platformStyle,
345             lineHeightStyle = lineHeightStyle,
346             lineBreak = lineBreak ?: LineBreak.Unspecified,
347             hyphens = hyphens ?: Hyphens.Unspecified,
348             textMotion = textMotion
349         )
350     }
351 
copynull352     fun copy(
353         textAlign: TextAlign = this.textAlign,
354         textDirection: TextDirection = this.textDirection,
355         lineHeight: TextUnit = this.lineHeight,
356         textIndent: TextIndent? = this.textIndent,
357         platformStyle: PlatformParagraphStyle? = this.platformStyle,
358         lineHeightStyle: LineHeightStyle? = this.lineHeightStyle,
359         lineBreak: LineBreak = this.lineBreak,
360         hyphens: Hyphens = this.hyphens,
361         textMotion: TextMotion? = this.textMotion
362     ): ParagraphStyle {
363         return ParagraphStyle(
364             textAlign = textAlign,
365             textDirection = textDirection,
366             lineHeight = lineHeight,
367             textIndent = textIndent,
368             platformStyle = platformStyle,
369             lineHeightStyle = lineHeightStyle,
370             lineBreak = lineBreak,
371             hyphens = hyphens,
372             textMotion = textMotion
373         )
374     }
375 
equalsnull376     override fun equals(other: Any?): Boolean {
377         if (this === other) return true
378         if (other !is ParagraphStyle) return false
379 
380         if (textAlign != other.textAlign) return false
381         if (textDirection != other.textDirection) return false
382         if (lineHeight != other.lineHeight) return false
383         if (textIndent != other.textIndent) return false
384         if (platformStyle != other.platformStyle) return false
385         if (lineHeightStyle != other.lineHeightStyle) return false
386         if (lineBreak != other.lineBreak) return false
387         if (hyphens != other.hyphens) return false
388         if (textMotion != other.textMotion) return false
389 
390         return true
391     }
392 
hashCodenull393     override fun hashCode(): Int {
394         var result = textAlign.hashCode()
395         result = 31 * result + textDirection.hashCode()
396         result = 31 * result + lineHeight.hashCode()
397         result = 31 * result + (textIndent?.hashCode() ?: 0)
398         result = 31 * result + (platformStyle?.hashCode() ?: 0)
399         result = 31 * result + (lineHeightStyle?.hashCode() ?: 0)
400         result = 31 * result + lineBreak.hashCode()
401         result = 31 * result + hyphens.hashCode()
402         result = 31 * result + (textMotion?.hashCode() ?: 0)
403         return result
404     }
405 
toStringnull406     override fun toString(): String {
407         return "ParagraphStyle(" +
408             "textAlign=$textAlign, " +
409             "textDirection=$textDirection, " +
410             "lineHeight=$lineHeight, " +
411             "textIndent=$textIndent, " +
412             "platformStyle=$platformStyle, " +
413             "lineHeightStyle=$lineHeightStyle, " +
414             "lineBreak=$lineBreak, " +
415             "hyphens=$hyphens, " +
416             "textMotion=$textMotion" +
417             ")"
418     }
419 }
420 
421 /**
422  * Interpolate between two [ParagraphStyle]s.
423  *
424  * This will not work well if the styles don't set the same fields.
425  *
426  * The [fraction] argument represents position on the timeline, with 0.0 meaning that the
427  * interpolation has not started, returning [start] (or something equivalent to [start]), 1.0
428  * meaning that the interpolation has finished, returning [stop] (or something equivalent to
429  * [stop]), and values in between meaning that the interpolation is at the relevant point on the
430  * timeline between [start] and [stop]. The interpolation can be extrapolated beyond 0.0 and 1.0, so
431  * negative values and values greater than 1.0 are valid.
432  */
433 @Stable
lerpnull434 fun lerp(start: ParagraphStyle, stop: ParagraphStyle, fraction: Float): ParagraphStyle {
435     return ParagraphStyle(
436         textAlign = lerpDiscrete(start.textAlign, stop.textAlign, fraction),
437         textDirection = lerpDiscrete(start.textDirection, stop.textDirection, fraction),
438         lineHeight = lerpTextUnitInheritable(start.lineHeight, stop.lineHeight, fraction),
439         textIndent =
440             lerp(start.textIndent ?: TextIndent.None, stop.textIndent ?: TextIndent.None, fraction),
441         platformStyle = lerpPlatformStyle(start.platformStyle, stop.platformStyle, fraction),
442         lineHeightStyle = lerpDiscrete(start.lineHeightStyle, stop.lineHeightStyle, fraction),
443         lineBreak = lerpDiscrete(start.lineBreak, stop.lineBreak, fraction),
444         hyphens = lerpDiscrete(start.hyphens, stop.hyphens, fraction),
445         textMotion = lerpDiscrete(start.textMotion, stop.textMotion, fraction)
446     )
447 }
448 
lerpPlatformStylenull449 private fun lerpPlatformStyle(
450     start: PlatformParagraphStyle?,
451     stop: PlatformParagraphStyle?,
452     fraction: Float
453 ): PlatformParagraphStyle? {
454     if (start == null && stop == null) return null
455     val startNonNull = start ?: PlatformParagraphStyle.Default
456     val stopNonNull = stop ?: PlatformParagraphStyle.Default
457     return lerp(startNonNull, stopNonNull, fraction)
458 }
459 
resolveParagraphStyleDefaultsnull460 internal fun resolveParagraphStyleDefaults(style: ParagraphStyle, direction: LayoutDirection) =
461     ParagraphStyle(
462         textAlign =
463             if (style.textAlign == TextAlign.Unspecified) TextAlign.Start else style.textAlign,
464         textDirection = resolveTextDirection(direction, style.textDirection),
465         lineHeight = if (style.lineHeight.isUnspecified) DefaultLineHeight else style.lineHeight,
466         textIndent = style.textIndent ?: TextIndent.None,
467         platformStyle = style.platformStyle,
468         lineHeightStyle = style.lineHeightStyle,
469         lineBreak =
470             if (style.lineBreak == LineBreak.Unspecified) LineBreak.Simple else style.lineBreak,
471         hyphens = if (style.hyphens == Hyphens.Unspecified) Hyphens.None else style.hyphens,
472         textMotion = style.textMotion ?: TextMotion.Static
473     )
474 
475 internal fun ParagraphStyle.fastMerge(
476     textAlign: TextAlign,
477     textDirection: TextDirection,
478     lineHeight: TextUnit,
479     textIndent: TextIndent?,
480     platformStyle: PlatformParagraphStyle?,
481     lineHeightStyle: LineHeightStyle?,
482     lineBreak: LineBreak,
483     hyphens: Hyphens,
484     textMotion: TextMotion?
485 ): ParagraphStyle {
486     // prioritize the parameters to Text in diffs here
487     /** textAlign: TextAlign? lineHeight: TextUnit */
488 
489     // any new vals should do a pre-merge check here
490     val requiresAlloc =
491         textAlign != TextAlign.Unspecified && textAlign != this.textAlign ||
492             lineHeight.isSpecified && lineHeight != this.lineHeight ||
493             textIndent != null && textIndent != this.textIndent ||
494             textDirection != TextDirection.Unspecified && textDirection != this.textDirection ||
495             platformStyle != null && platformStyle != this.platformStyle ||
496             lineHeightStyle != null && lineHeightStyle != this.lineHeightStyle ||
497             lineBreak != LineBreak.Unspecified && lineBreak != this.lineBreak ||
498             hyphens != Hyphens.Unspecified && hyphens != this.hyphens ||
499             textMotion != null && textMotion != this.textMotion
500 
501     if (!requiresAlloc) {
502         return this
503     }
504 
505     return ParagraphStyle(
506         lineHeight =
507             if (lineHeight.isUnspecified) {
508                 this.lineHeight
509             } else {
510                 lineHeight
511             },
512         textIndent = textIndent ?: this.textIndent,
513         textAlign = if (textAlign != TextAlign.Unspecified) textAlign else this.textAlign,
514         textDirection =
515             if (textDirection != TextDirection.Unspecified) textDirection else this.textDirection,
516         platformStyle = mergePlatformStyle(platformStyle),
517         lineHeightStyle = lineHeightStyle ?: this.lineHeightStyle,
518         lineBreak = if (lineBreak != LineBreak.Unspecified) lineBreak else this.lineBreak,
519         hyphens = if (hyphens != Hyphens.Unspecified) hyphens else this.hyphens,
520         textMotion = textMotion ?: this.textMotion
521     )
522 }
523 
mergePlatformStylenull524 private fun ParagraphStyle.mergePlatformStyle(
525     other: PlatformParagraphStyle?
526 ): PlatformParagraphStyle? {
527     if (platformStyle == null) return other
528     if (other == null) return platformStyle
529     return platformStyle.merge(other)
530 }
531