1 /*
<lambda>null2  * Copyright 2023 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.foundation.text.modifiers
18 
19 import androidx.compose.foundation.text.DefaultMinLines
20 import androidx.compose.foundation.text.TextAutoSize
21 import androidx.compose.ui.Modifier
22 import androidx.compose.ui.geometry.Offset
23 import androidx.compose.ui.geometry.Rect
24 import androidx.compose.ui.geometry.Size
25 import androidx.compose.ui.graphics.Color
26 import androidx.compose.ui.graphics.ColorProducer
27 import androidx.compose.ui.graphics.Shadow
28 import androidx.compose.ui.graphics.drawscope.ContentDrawScope
29 import androidx.compose.ui.graphics.drawscope.Fill
30 import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
31 import androidx.compose.ui.graphics.isSpecified
32 import androidx.compose.ui.layout.AlignmentLine
33 import androidx.compose.ui.layout.FirstBaseline
34 import androidx.compose.ui.layout.IntrinsicMeasurable
35 import androidx.compose.ui.layout.IntrinsicMeasureScope
36 import androidx.compose.ui.layout.LastBaseline
37 import androidx.compose.ui.layout.Measurable
38 import androidx.compose.ui.layout.MeasureResult
39 import androidx.compose.ui.layout.MeasureScope
40 import androidx.compose.ui.node.DrawModifierNode
41 import androidx.compose.ui.node.LayoutModifierNode
42 import androidx.compose.ui.node.SemanticsModifierNode
43 import androidx.compose.ui.node.invalidateDraw
44 import androidx.compose.ui.node.invalidateLayer
45 import androidx.compose.ui.node.invalidateMeasurement
46 import androidx.compose.ui.node.invalidateSemantics
47 import androidx.compose.ui.semantics.SemanticsPropertyReceiver
48 import androidx.compose.ui.semantics.clearTextSubstitution
49 import androidx.compose.ui.semantics.getTextLayoutResult
50 import androidx.compose.ui.semantics.isShowingTextSubstitution
51 import androidx.compose.ui.semantics.setTextSubstitution
52 import androidx.compose.ui.semantics.showTextSubstitution
53 import androidx.compose.ui.semantics.text
54 import androidx.compose.ui.semantics.textSubstitution
55 import androidx.compose.ui.text.AnnotatedString
56 import androidx.compose.ui.text.Placeholder
57 import androidx.compose.ui.text.TextLayoutInput
58 import androidx.compose.ui.text.TextLayoutResult
59 import androidx.compose.ui.text.TextStyle
60 import androidx.compose.ui.text.font.FontFamily
61 import androidx.compose.ui.text.style.TextDecoration
62 import androidx.compose.ui.text.style.TextOverflow
63 import androidx.compose.ui.unit.Constraints
64 import androidx.compose.ui.unit.Constraints.Companion.fitPrioritizingWidth
65 import androidx.compose.ui.unit.Density
66 import androidx.compose.ui.util.fastRoundToInt
67 import androidx.compose.ui.util.trace
68 
69 /** Node that implements Text for [AnnotatedString] or [onTextLayout] parameters. */
70 internal class TextAnnotatedStringNode(
71     private var text: AnnotatedString,
72     private var style: TextStyle,
73     private var fontFamilyResolver: FontFamily.Resolver,
74     private var onTextLayout: ((TextLayoutResult) -> Unit)? = null,
75     private var overflow: TextOverflow = TextOverflow.Clip,
76     private var softWrap: Boolean = true,
77     private var maxLines: Int = Int.MAX_VALUE,
78     private var minLines: Int = DefaultMinLines,
79     private var placeholders: List<AnnotatedString.Range<Placeholder>>? = null,
80     private var onPlaceholderLayout: ((List<Rect?>) -> Unit)? = null,
81     private var selectionController: SelectionController? = null,
82     private var overrideColor: ColorProducer? = null,
83     private var autoSize: TextAutoSize? = null,
84     private var onShowTranslation: ((TextSubstitutionValue) -> Unit)? = null
85 ) : Modifier.Node(), LayoutModifierNode, DrawModifierNode, SemanticsModifierNode {
86     override val shouldAutoInvalidate: Boolean
87         get() = false
88 
89     @Suppress("PrimitiveInCollection")
90     private var baselineCache: MutableMap<AlignmentLine, Int>? = null
91 
92     private var _layoutCache: MultiParagraphLayoutCache? = null
93     private val layoutCache: MultiParagraphLayoutCache
94         get() {
95             if (_layoutCache == null) {
96                 _layoutCache =
97                     MultiParagraphLayoutCache(
98                         text,
99                         style,
100                         fontFamilyResolver,
101                         overflow,
102                         softWrap,
103                         maxLines,
104                         minLines,
105                         placeholders,
106                         autoSize
107                     )
108             }
109             return _layoutCache!!
110         }
111 
112     /**
113      * Get the layout cache for the current state of the node.
114      *
115      * If text substitution is active, this will return the layout cache for the substitution.
116      * Otherwise, it will return the layout cache for the original text.
117      */
118     private fun getLayoutCache(density: Density): MultiParagraphLayoutCache {
119         textSubstitution?.let { textSubstitutionValue ->
120             if (textSubstitutionValue.isShowingSubstitution) {
121                 textSubstitutionValue.layoutCache?.let { cache ->
122                     return cache.also { it.density = density }
123                 }
124             }
125         }
126         return layoutCache.also { it.density = density }
127     }
128 
129     /** Element has draw parameters to update */
130     fun updateDraw(color: ColorProducer?, style: TextStyle): Boolean {
131         var changed = false
132         if (color != this.overrideColor) {
133             changed = true
134         }
135         overrideColor = color
136         changed = changed || !style.hasSameDrawAffectingAttributes(this.style)
137         return changed
138     }
139 
140     /** Element has text parameters to update */
141     internal fun updateText(text: AnnotatedString): Boolean {
142         val charDiff = this.text.text != text.text
143         val annotationDiff = !this.text.hasEqualAnnotations(text)
144         val anyDiff = charDiff || annotationDiff
145 
146         if (anyDiff) {
147             this.text = text
148         }
149         if (charDiff) {
150             clearSubstitution()
151         }
152         return anyDiff
153     }
154 
155     /** Element has layout parameters to update */
156     fun updateLayoutRelatedArgs(
157         style: TextStyle,
158         placeholders: List<AnnotatedString.Range<Placeholder>>?,
159         minLines: Int,
160         maxLines: Int,
161         softWrap: Boolean,
162         fontFamilyResolver: FontFamily.Resolver,
163         overflow: TextOverflow,
164         autoSize: TextAutoSize?
165     ): Boolean {
166         var changed: Boolean
167 
168         changed = !this.style.hasSameLayoutAffectingAttributes(style)
169         this.style = style
170 
171         if (this.placeholders != placeholders) {
172             this.placeholders = placeholders
173             changed = true
174         }
175 
176         if (this.minLines != minLines) {
177             this.minLines = minLines
178             changed = true
179         }
180 
181         if (this.maxLines != maxLines) {
182             this.maxLines = maxLines
183             changed = true
184         }
185 
186         if (this.softWrap != softWrap) {
187             this.softWrap = softWrap
188             changed = true
189         }
190 
191         if (this.fontFamilyResolver != fontFamilyResolver) {
192             this.fontFamilyResolver = fontFamilyResolver
193             changed = true
194         }
195 
196         if (this.overflow != overflow) {
197             this.overflow = overflow
198             changed = true
199         }
200 
201         if (this.autoSize != autoSize) {
202             this.autoSize = autoSize
203             changed = true
204         }
205 
206         return changed
207     }
208 
209     /** Element has callback parameters to update */
210     fun updateCallbacks(
211         onTextLayout: ((TextLayoutResult) -> Unit)?,
212         onPlaceholderLayout: ((List<Rect?>) -> Unit)?,
213         selectionController: SelectionController?,
214         onShowTranslation: ((TextSubstitutionValue) -> Unit)?
215     ): Boolean {
216         var changed = false
217 
218         if (this.onTextLayout !== onTextLayout) {
219             this.onTextLayout = onTextLayout
220             changed = true
221         }
222 
223         if (this.onPlaceholderLayout !== onPlaceholderLayout) {
224             this.onPlaceholderLayout = onPlaceholderLayout
225             changed = true
226         }
227 
228         if (this.selectionController != selectionController) {
229             this.selectionController = selectionController
230             changed = true
231         }
232 
233         if (this.onShowTranslation !== onShowTranslation) {
234             this.onShowTranslation = onShowTranslation
235             changed = true
236         }
237         return changed
238     }
239 
240     /** Do appropriate invalidate calls based on the results of update above. */
241     fun doInvalidations(
242         drawChanged: Boolean,
243         textChanged: Boolean,
244         layoutChanged: Boolean,
245         callbacksChanged: Boolean
246     ) {
247         // bring caches up to date even if the node is detached in case it is used again later
248         if (textChanged || layoutChanged || callbacksChanged) {
249             layoutCache.update(
250                 text = text,
251                 style = style,
252                 fontFamilyResolver = fontFamilyResolver,
253                 overflow = overflow,
254                 softWrap = softWrap,
255                 maxLines = maxLines,
256                 minLines = minLines,
257                 placeholders = placeholders,
258                 autoSize = autoSize
259             )
260         }
261 
262         if (!isAttached) {
263             // no-up for !isAttached. The node will invalidate when attaching again.
264             return
265         }
266         if (textChanged || (drawChanged && semanticsTextLayoutResult != null)) {
267             invalidateSemantics()
268         }
269 
270         if (textChanged || layoutChanged || callbacksChanged) {
271             invalidateMeasurement()
272             invalidateDraw()
273         }
274         if (drawChanged) {
275             invalidateDraw()
276         }
277     }
278 
279     private var semanticsTextLayoutResult: ((MutableList<TextLayoutResult>) -> Boolean)? = null
280 
281     data class TextSubstitutionValue(
282         val original: AnnotatedString,
283         var substitution: AnnotatedString,
284         var isShowingSubstitution: Boolean = false,
285         var layoutCache: MultiParagraphLayoutCache? = null,
286         // TODO(b/283944749): add animation
287     )
288 
289     internal var textSubstitution: TextSubstitutionValue? = null
290 
291     private fun setSubstitution(updatedText: AnnotatedString): Boolean {
292         val currentTextSubstitution = textSubstitution
293         // updatedText is currently always returned as a plain text so we ignore inline content
294         // which is represented by placeholders. If translation ever supports annotations, we
295         // would need to recalculate the placeholders
296         if (currentTextSubstitution != null) {
297             if (updatedText == currentTextSubstitution.substitution) {
298                 return false
299             }
300             currentTextSubstitution.substitution = updatedText
301             currentTextSubstitution.layoutCache?.update(
302                 updatedText,
303                 style,
304                 fontFamilyResolver,
305                 overflow,
306                 softWrap,
307                 maxLines,
308                 minLines,
309                 placeholders = emptyList(),
310                 autoSize
311             ) ?: return false
312         } else {
313             val newTextSubstitution = TextSubstitutionValue(text, updatedText)
314             val substitutionLayoutCache =
315                 MultiParagraphLayoutCache(
316                     updatedText,
317                     style,
318                     fontFamilyResolver,
319                     overflow,
320                     softWrap,
321                     maxLines,
322                     minLines,
323                     placeholders = emptyList(),
324                     autoSize
325                 )
326             substitutionLayoutCache.density = layoutCache.density
327             newTextSubstitution.layoutCache = substitutionLayoutCache
328             textSubstitution = newTextSubstitution
329         }
330         return true
331     }
332 
333     /** Call whenever text substitution changes state */
334     private fun invalidateForTranslate() {
335         invalidateSemantics()
336         invalidateMeasurement()
337         invalidateDraw()
338     }
339 
340     internal fun clearSubstitution() {
341         textSubstitution = null
342     }
343 
344     override fun SemanticsPropertyReceiver.applySemantics() {
345         var localSemanticsTextLayoutResult = semanticsTextLayoutResult
346         if (localSemanticsTextLayoutResult == null) {
347             localSemanticsTextLayoutResult = { textLayoutResult ->
348                 val inputLayout = layoutCache.layoutOrNull
349                 val layout =
350                     inputLayout
351                         ?.copy(
352                             layoutInput =
353                                 TextLayoutInput(
354                                     text = inputLayout.layoutInput.text,
355                                     style =
356                                         this@TextAnnotatedStringNode.style.merge(
357                                             color = overrideColor?.invoke() ?: Color.Unspecified
358                                         ),
359                                     placeholders = inputLayout.layoutInput.placeholders,
360                                     maxLines = inputLayout.layoutInput.maxLines,
361                                     softWrap = inputLayout.layoutInput.softWrap,
362                                     overflow = inputLayout.layoutInput.overflow,
363                                     density = inputLayout.layoutInput.density,
364                                     layoutDirection = inputLayout.layoutInput.layoutDirection,
365                                     fontFamilyResolver = inputLayout.layoutInput.fontFamilyResolver,
366                                     constraints = inputLayout.layoutInput.constraints
367                                 )
368                         )
369                         ?.also { textLayoutResult.add(it) }
370                 layout != null
371             }
372             semanticsTextLayoutResult = localSemanticsTextLayoutResult
373         }
374 
375         text = this@TextAnnotatedStringNode.text
376         val currentTextSubstitution = this@TextAnnotatedStringNode.textSubstitution
377         if (currentTextSubstitution != null) {
378             textSubstitution = currentTextSubstitution.substitution
379             isShowingTextSubstitution = currentTextSubstitution.isShowingSubstitution
380         }
381 
382         setTextSubstitution { updatedText ->
383             setSubstitution(updatedText)
384 
385             invalidateForTranslate()
386 
387             true
388         }
389         showTextSubstitution {
390             if (this@TextAnnotatedStringNode.textSubstitution == null) {
391                 return@showTextSubstitution false
392             }
393             onShowTranslation?.invoke(this@TextAnnotatedStringNode.textSubstitution!!)
394 
395             this@TextAnnotatedStringNode.textSubstitution?.isShowingSubstitution = it
396 
397             invalidateForTranslate()
398 
399             true
400         }
401         clearTextSubstitution {
402             clearSubstitution()
403 
404             invalidateForTranslate()
405 
406             true
407         }
408         getTextLayoutResult(action = localSemanticsTextLayoutResult)
409     }
410 
411     fun measureNonExtension(
412         measureScope: MeasureScope,
413         measurable: Measurable,
414         constraints: Constraints
415     ): MeasureResult {
416         return measureScope.measure(measurable, constraints)
417     }
418 
419     /** Text layout is performed here. */
420     override fun MeasureScope.measure(
421         measurable: Measurable,
422         constraints: Constraints
423     ): MeasureResult {
424         trace("TextAnnotatedStringNode:measure") {
425             val layoutCache = getLayoutCache(this)
426 
427             val didChangeLayout = layoutCache.layoutWithConstraints(constraints, layoutDirection)
428             val textLayoutResult = layoutCache.textLayoutResult
429 
430             // ensure measure restarts when hasStaleResolvedFonts by reading in measure
431             textLayoutResult.multiParagraph.intrinsics.hasStaleResolvedFonts
432 
433             if (didChangeLayout) {
434                 invalidateLayer()
435                 onTextLayout?.invoke(textLayoutResult)
436                 selectionController?.updateTextLayout(textLayoutResult)
437 
438                 @Suppress("PrimitiveInCollection") val cache = baselineCache ?: LinkedHashMap(2)
439                 cache[FirstBaseline] = textLayoutResult.firstBaseline.fastRoundToInt()
440                 cache[LastBaseline] = textLayoutResult.lastBaseline.fastRoundToInt()
441                 baselineCache = cache
442             }
443 
444             // first share the placeholders
445             onPlaceholderLayout?.invoke(textLayoutResult.placeholderRects)
446 
447             // then allow children to measure _inside_ our final box, with the above placeholders
448             val placeable =
449                 measurable.measure(
450                     fitPrioritizingWidth(
451                         minWidth = textLayoutResult.size.width,
452                         maxWidth = textLayoutResult.size.width,
453                         minHeight = textLayoutResult.size.height,
454                         maxHeight = textLayoutResult.size.height
455                     )
456                 )
457 
458             return layout(
459                 textLayoutResult.size.width,
460                 textLayoutResult.size.height,
461                 baselineCache!!
462             ) {
463                 placeable.place(0, 0)
464             }
465         }
466     }
467 
468     fun minIntrinsicWidthNonExtension(
469         intrinsicMeasureScope: IntrinsicMeasureScope,
470         measurable: IntrinsicMeasurable,
471         height: Int
472     ): Int {
473         return intrinsicMeasureScope.minIntrinsicWidth(measurable, height)
474     }
475 
476     override fun IntrinsicMeasureScope.minIntrinsicWidth(
477         measurable: IntrinsicMeasurable,
478         height: Int
479     ): Int {
480         return getLayoutCache(this).minIntrinsicWidth(layoutDirection)
481     }
482 
483     fun minIntrinsicHeightNonExtension(
484         intrinsicMeasureScope: IntrinsicMeasureScope,
485         measurable: IntrinsicMeasurable,
486         width: Int
487     ): Int {
488         return intrinsicMeasureScope.minIntrinsicHeight(measurable, width)
489     }
490 
491     override fun IntrinsicMeasureScope.minIntrinsicHeight(
492         measurable: IntrinsicMeasurable,
493         width: Int
494     ): Int = getLayoutCache(this).intrinsicHeight(width, layoutDirection)
495 
496     fun maxIntrinsicWidthNonExtension(
497         intrinsicMeasureScope: IntrinsicMeasureScope,
498         measurable: IntrinsicMeasurable,
499         height: Int
500     ): Int = intrinsicMeasureScope.maxIntrinsicWidth(measurable, height)
501 
502     override fun IntrinsicMeasureScope.maxIntrinsicWidth(
503         measurable: IntrinsicMeasurable,
504         height: Int
505     ): Int = getLayoutCache(this).maxIntrinsicWidth(layoutDirection)
506 
507     fun maxIntrinsicHeightNonExtension(
508         intrinsicMeasureScope: IntrinsicMeasureScope,
509         measurable: IntrinsicMeasurable,
510         width: Int
511     ): Int = intrinsicMeasureScope.maxIntrinsicHeight(measurable, width)
512 
513     override fun IntrinsicMeasureScope.maxIntrinsicHeight(
514         measurable: IntrinsicMeasurable,
515         width: Int
516     ): Int = getLayoutCache(this).intrinsicHeight(width, layoutDirection)
517 
518     fun drawNonExtension(contentDrawScope: ContentDrawScope) {
519         return contentDrawScope.draw()
520     }
521 
522     override fun ContentDrawScope.draw() {
523         if (!isAttached) {
524             // no-up for !isAttached. The node will invalidate when attaching again.
525             return
526         }
527 
528         selectionController?.draw(this)
529         drawIntoCanvas { canvas ->
530             val layoutCache = getLayoutCache(this)
531             val textLayoutResult = layoutCache.textLayoutResult
532             val localParagraph = textLayoutResult.multiParagraph
533             val willClip = textLayoutResult.hasVisualOverflow && overflow != TextOverflow.Visible
534             if (willClip) {
535                 val width = textLayoutResult.size.width.toFloat()
536                 val height = textLayoutResult.size.height.toFloat()
537                 val bounds = Rect(Offset.Zero, Size(width, height))
538                 canvas.save()
539                 canvas.clipRect(bounds)
540             }
541             try {
542                 val textDecoration = style.textDecoration ?: TextDecoration.None
543                 val shadow = style.shadow ?: Shadow.None
544                 val drawStyle = style.drawStyle ?: Fill
545                 val brush = style.brush
546                 if (brush != null) {
547                     val alpha = style.alpha
548                     localParagraph.paint(
549                         canvas = canvas,
550                         brush = brush,
551                         alpha = alpha,
552                         shadow = shadow,
553                         drawStyle = drawStyle,
554                         decoration = textDecoration
555                     )
556                 } else {
557                     val overrideColorVal = overrideColor?.invoke() ?: Color.Unspecified
558                     val color =
559                         if (overrideColorVal.isSpecified) {
560                             overrideColorVal
561                         } else if (style.color.isSpecified) {
562                             style.color
563                         } else {
564                             Color.Black
565                         }
566                     localParagraph.paint(
567                         canvas = canvas,
568                         color = color,
569                         shadow = shadow,
570                         drawStyle = drawStyle,
571                         decoration = textDecoration
572                     )
573                 }
574             } finally {
575                 if (willClip) {
576                     canvas.restore()
577                 }
578             }
579 
580             // draw inline content and links indication
581             val hasLinks =
582                 if (textSubstitution?.isShowingSubstitution == true) {
583                     false
584                 } else {
585                     text.hasLinks()
586                 }
587             if (hasLinks || !placeholders.isNullOrEmpty()) {
588                 drawContent()
589             }
590         }
591     }
592 }
593 
hasLinksnull594 internal fun AnnotatedString.hasLinks() = hasLinkAnnotations(0, length)
595