1 /*
<lambda>null2  * Copyright 2020 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 @file:Suppress("DEPRECATION")
18 
19 package androidx.compose.foundation.text
20 
21 import androidx.compose.foundation.text.selection.visibleBounds
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.Canvas
26 import androidx.compose.ui.graphics.Color
27 import androidx.compose.ui.graphics.Paint
28 import androidx.compose.ui.graphics.isUnspecified
29 import androidx.compose.ui.layout.LayoutCoordinates
30 import androidx.compose.ui.layout.findRootCoordinates
31 import androidx.compose.ui.text.AnnotatedString
32 import androidx.compose.ui.text.Paragraph
33 import androidx.compose.ui.text.SpanStyle
34 import androidx.compose.ui.text.TextLayoutResult
35 import androidx.compose.ui.text.TextPainter
36 import androidx.compose.ui.text.TextRange
37 import androidx.compose.ui.text.TextStyle
38 import androidx.compose.ui.text.font.FontFamily
39 import androidx.compose.ui.text.input.EditCommand
40 import androidx.compose.ui.text.input.EditProcessor
41 import androidx.compose.ui.text.input.ImeAction
42 import androidx.compose.ui.text.input.ImeOptions
43 import androidx.compose.ui.text.input.OffsetMapping
44 import androidx.compose.ui.text.input.TextFieldValue
45 import androidx.compose.ui.text.input.TextInputService
46 import androidx.compose.ui.text.input.TextInputSession
47 import androidx.compose.ui.text.input.TransformedText
48 import androidx.compose.ui.text.style.TextDecoration
49 import androidx.compose.ui.text.style.TextOverflow
50 import androidx.compose.ui.unit.Constraints
51 import androidx.compose.ui.unit.Density
52 import androidx.compose.ui.unit.IntSize
53 import androidx.compose.ui.unit.LayoutDirection
54 import kotlin.jvm.JvmStatic
55 import kotlin.math.max
56 import kotlin.math.min
57 
58 // visible for testing
59 internal const val DefaultWidthCharCount = 10 // min width for TextField is 10 chars long
60 internal val EmptyTextReplacement = "H".repeat(DefaultWidthCharCount) // just a reference character.
61 
62 /**
63  * Computed the default width and height for TextField.
64  *
65  * The bounding box or x-advance of the empty text is empty, i.e. 0x0 box or 0px advance. However
66  * this is not useful for TextField since text field want to reserve some amount of height for
67  * accepting touch for starting text input. In Android, uses FontMetrics of the first font in the
68  * fallback chain to compute this height, this is because custom font may have different
69  * ascender/descender from the default font in Android.
70  *
71  * Until we have font metrics APIs, use the height of reference text as a workaround.
72  */
73 internal fun computeSizeForDefaultText(
74     style: TextStyle,
75     density: Density,
76     fontFamilyResolver: FontFamily.Resolver,
77     text: String = EmptyTextReplacement,
78     maxLines: Int = 1
79 ): IntSize {
80     val paragraph =
81         Paragraph(
82             text = text,
83             style = style,
84             spanStyles = listOf(),
85             maxLines = maxLines,
86             overflow = TextOverflow.Clip,
87             density = density,
88             fontFamilyResolver = fontFamilyResolver,
89             constraints = Constraints()
90         )
91     return IntSize(paragraph.minIntrinsicWidth.ceilToIntPx(), paragraph.height.ceilToIntPx())
92 }
93 
94 internal class TextFieldDelegate {
95     companion object {
96         /**
97          * Process text layout with given constraint.
98          *
99          * @param textDelegate The text painter
100          * @param constraints The layout constraints
101          * @return the bounding box size(width and height) of the layout result
102          */
103         @JvmStatic
layoutnull104         internal fun layout(
105             textDelegate: TextDelegate,
106             constraints: Constraints,
107             layoutDirection: LayoutDirection,
108             prevResultText: TextLayoutResult? = null
109         ): Triple<Int, Int, TextLayoutResult> {
110             val layoutResult = textDelegate.layout(constraints, layoutDirection, prevResultText)
111             return Triple(layoutResult.size.width, layoutResult.size.height, layoutResult)
112         }
113 
114         /**
115          * Draw the text content to the canvas
116          *
117          * @param canvas The target canvas.
118          * @param value The editor state
119          * @param selectionPreviewHighlightRange Range to be highlighted to preview a handwriting
120          *   selection gesture
121          * @param deletionPreviewHighlightRange Range to be highlighted to preview a handwriting
122          *   deletion gesture
123          * @param offsetMapping The offset map
124          * @param textLayoutResult The text layout result
125          * @param highlightPaint Paint used to draw highlight backgrounds
126          * @param selectionBackgroundColor The selection highlight background color
127          */
128         @JvmStatic
drawnull129         internal fun draw(
130             canvas: Canvas,
131             value: TextFieldValue,
132             selectionPreviewHighlightRange: TextRange,
133             deletionPreviewHighlightRange: TextRange,
134             offsetMapping: OffsetMapping,
135             textLayoutResult: TextLayoutResult,
136             highlightPaint: Paint,
137             selectionBackgroundColor: Color
138         ) {
139             if (!selectionPreviewHighlightRange.collapsed) {
140                 highlightPaint.color = selectionBackgroundColor
141                 drawHighlight(
142                     canvas,
143                     selectionPreviewHighlightRange,
144                     offsetMapping,
145                     textLayoutResult,
146                     highlightPaint
147                 )
148             } else if (!deletionPreviewHighlightRange.collapsed) {
149                 val textColor =
150                     textLayoutResult.layoutInput.style.color.takeUnless { it.isUnspecified }
151                         ?: Color.Black
152                 highlightPaint.color = textColor.copy(alpha = textColor.alpha * 0.2f)
153                 drawHighlight(
154                     canvas,
155                     deletionPreviewHighlightRange,
156                     offsetMapping,
157                     textLayoutResult,
158                     highlightPaint
159                 )
160             } else if (!value.selection.collapsed) {
161                 highlightPaint.color = selectionBackgroundColor
162                 drawHighlight(
163                     canvas,
164                     value.selection,
165                     offsetMapping,
166                     textLayoutResult,
167                     highlightPaint
168                 )
169             }
170             TextPainter.paint(canvas, textLayoutResult)
171         }
172 
drawHighlightnull173         private fun drawHighlight(
174             canvas: Canvas,
175             range: TextRange,
176             offsetMapping: OffsetMapping,
177             textLayoutResult: TextLayoutResult,
178             paint: Paint
179         ) {
180             val start = offsetMapping.originalToTransformed(range.min)
181             val end = offsetMapping.originalToTransformed(range.max)
182             if (start != end) {
183                 val selectionPath = textLayoutResult.getPathForRange(start, end)
184                 canvas.drawPath(selectionPath, paint)
185             }
186         }
187 
188         /**
189          * Notify system that focused input area.
190          *
191          * @param value The editor model
192          * @param textDelegate The text delegate
193          * @param layoutCoordinates The layout coordinates
194          * @param textInputSession The current input session.
195          * @param hasFocus True if focus is gained.
196          * @param offsetMapping The mapper from/to editing buffer to/from visible text.
197          */
198         // TODO(b/262648050) Try to find a better API.
199         @JvmStatic
notifyFocusedRectnull200         internal fun notifyFocusedRect(
201             value: TextFieldValue,
202             textDelegate: TextDelegate,
203             textLayoutResult: TextLayoutResult,
204             layoutCoordinates: LayoutCoordinates,
205             textInputSession: TextInputSession,
206             hasFocus: Boolean,
207             offsetMapping: OffsetMapping
208         ) {
209             if (!hasFocus) {
210                 return
211             }
212 
213             textInputSession.notifyFocusedRect(
214                 focusedRectInRoot(
215                     layoutResult = textLayoutResult,
216                     layoutCoordinates = layoutCoordinates,
217                     focusOffset = offsetMapping.originalToTransformed(value.selection.max),
218                     sizeForDefaultText = {
219                         computeSizeForDefaultText(
220                             textDelegate.style,
221                             textDelegate.density,
222                             textDelegate.fontFamilyResolver
223                         )
224                     }
225                 )
226             )
227         }
228 
229         /**
230          * Notify the input service of layout and position changes.
231          *
232          * @param textInputSession the current input session
233          * @param textFieldValue the editor state
234          * @param offsetMapping the offset mapping for the visual transformation
235          * @param textLayoutResult the layout result
236          */
237         @JvmStatic
updateTextLayoutResultnull238         internal fun updateTextLayoutResult(
239             textInputSession: TextInputSession,
240             textFieldValue: TextFieldValue,
241             offsetMapping: OffsetMapping,
242             textLayoutResult: TextLayoutResultProxy
243         ) {
244             textLayoutResult.innerTextFieldCoordinates?.let { innerTextFieldCoordinates ->
245                 if (!innerTextFieldCoordinates.isAttached) return
246                 textLayoutResult.decorationBoxCoordinates?.let { decorationBoxCoordinates ->
247                     textInputSession.updateTextLayoutResult(
248                         textFieldValue,
249                         offsetMapping,
250                         textLayoutResult.value,
251                         { matrix ->
252                             if (innerTextFieldCoordinates.isAttached) {
253                                 innerTextFieldCoordinates
254                                     .findRootCoordinates()
255                                     .transformFrom(innerTextFieldCoordinates, matrix)
256                             }
257                         },
258                         innerTextFieldCoordinates.visibleBounds(),
259                         innerTextFieldCoordinates.localBoundingBoxOf(
260                             decorationBoxCoordinates,
261                             clipBounds = false
262                         )
263                     )
264                 }
265             }
266         }
267 
268         /**
269          * Called when edit operations are passed from TextInputService
270          *
271          * @param ops A list of edit operations.
272          * @param editProcessor The edit processor
273          * @param onValueChange The callback called when the new editor state arrives.
274          */
275         @JvmStatic
onEditCommandnull276         internal fun onEditCommand(
277             ops: List<EditCommand>,
278             editProcessor: EditProcessor,
279             onValueChange: (TextFieldValue) -> Unit,
280             session: TextInputSession?
281         ) {
282             val newValue = editProcessor.apply(ops)
283 
284             // Android: Some IME calls getTextBeforeCursor API just after the setComposingText. The
285             // getTextBeforeCursor may return the text without a text set by setComposingText
286             // because the text field state in the application code is updated on the next time
287             // composition. On the other hand, some IME gets confused and cancel the composition
288             // because the text set by setComposingText is not available.
289             // To avoid this problem, update the state in the TextInputService to the latest
290             // plausible state. When the real state comes, the TextInputService will compare and
291             // update the state if it is modified by developers.
292             session?.updateState(null, newValue)
293             onValueChange(newValue)
294         }
295 
296         /**
297          * Sets the cursor position. Should be called when TextField has focus.
298          *
299          * @param position The event position in composable coordinate.
300          * @param textLayoutResult The text layout result proxy
301          * @param editProcessor The edit processor
302          * @param offsetMapping The offset map
303          * @param onValueChange The callback called when the new editor state arrives.
304          */
305         @JvmStatic
setCursorOffsetnull306         internal fun setCursorOffset(
307             position: Offset,
308             textLayoutResult: TextLayoutResultProxy,
309             editProcessor: EditProcessor,
310             offsetMapping: OffsetMapping,
311             onValueChange: (TextFieldValue) -> Unit
312         ) {
313             val offset =
314                 offsetMapping.transformedToOriginal(textLayoutResult.getOffsetForPosition(position))
315             onValueChange(editProcessor.toTextFieldValue().copy(selection = TextRange(offset)))
316         }
317 
318         /**
319          * Starts a new input connection.
320          *
321          * @param textInputService The text input service
322          * @param value The editor state
323          * @param editProcessor The edit processor
324          * @param onValueChange The callback called when the new editor state arrives.
325          * @param onImeActionPerformed The callback called when the editor action arrives.
326          * @param imeOptions Keyboard configuration such as single line, auto correct etc.
327          */
328         @JvmStatic
restartInputnull329         internal fun restartInput(
330             textInputService: TextInputService,
331             value: TextFieldValue,
332             editProcessor: EditProcessor,
333             imeOptions: ImeOptions,
334             onValueChange: (TextFieldValue) -> Unit,
335             onImeActionPerformed: (ImeAction) -> Unit
336         ): TextInputSession {
337             var session: TextInputSession? = null
338             session =
339                 textInputService.startInput(
340                     value = value,
341                     imeOptions = imeOptions,
342                     onEditCommand = { onEditCommand(it, editProcessor, onValueChange, session) },
343                     onImeActionPerformed = onImeActionPerformed
344                 )
345             return session
346         }
347 
348         /**
349          * Called when the composable gained input focus
350          *
351          * @param textInputService The text input service
352          * @param value The editor state
353          * @param editProcessor The edit processor
354          * @param onValueChange The callback called when the new editor state arrives.
355          * @param onImeActionPerformed The callback called when the editor action arrives.
356          * @param imeOptions Keyboard configuration such as single line, auto correct etc.
357          */
358         @JvmStatic
onFocusnull359         internal fun onFocus(
360             textInputService: TextInputService,
361             value: TextFieldValue,
362             editProcessor: EditProcessor,
363             imeOptions: ImeOptions,
364             onValueChange: (TextFieldValue) -> Unit,
365             onImeActionPerformed: (ImeAction) -> Unit
366         ): TextInputSession {
367             // The keyboard will automatically be shown when the new IME connection is started.
368             return restartInput(
369                 textInputService = textInputService,
370                 value = value,
371                 editProcessor = editProcessor,
372                 imeOptions = imeOptions,
373                 onValueChange = onValueChange,
374                 onImeActionPerformed = onImeActionPerformed
375             )
376         }
377 
378         /**
379          * Called when the composable loses input focus
380          *
381          * @param textInputSession The current input session.
382          * @param editProcessor The edit processor
383          * @param onValueChange The callback called when the new editor state arrives.
384          */
385         @JvmStatic
onBlurnull386         internal fun onBlur(
387             textInputSession: TextInputSession,
388             editProcessor: EditProcessor,
389             onValueChange: (TextFieldValue) -> Unit
390         ) {
391             onValueChange(editProcessor.toTextFieldValue().copy(composition = null))
392             // Don't hide the keyboard when losing focus. If the target system needs that behavior,
393             // it can be implemented in the PlatformTextInputService.
394             textInputSession.dispose()
395         }
396 
397         /**
398          * Apply the composition text decoration (undeline) to the transformed text.
399          *
400          * @param compositionRange An input state
401          * @param transformed A transformed text
402          * @return The transformed text with composition decoration.
403          */
applyCompositionDecorationnull404         fun applyCompositionDecoration(
405             compositionRange: TextRange,
406             transformed: TransformedText
407         ): TransformedText {
408             val startPositionTransformed =
409                 transformed.offsetMapping.originalToTransformed(compositionRange.start)
410             val endPositionTransformed =
411                 transformed.offsetMapping.originalToTransformed(compositionRange.end)
412 
413             // coerce into a valid range with start <= end
414             val start = min(startPositionTransformed, endPositionTransformed)
415             val coercedEnd = max(startPositionTransformed, endPositionTransformed)
416             return TransformedText(
417                 AnnotatedString.Builder(transformed.text)
418                     .apply {
419                         addStyle(
420                             SpanStyle(textDecoration = TextDecoration.Underline),
421                             start,
422                             coercedEnd
423                         )
424                     }
425                     .toAnnotatedString(),
426                 transformed.offsetMapping
427             )
428         }
429     }
430 }
431 
432 /** Computes the bounds of the area where text editing is in progress, relative to the root. */
focusedRectInRootnull433 internal fun focusedRectInRoot(
434     layoutResult: TextLayoutResult,
435     layoutCoordinates: LayoutCoordinates,
436     focusOffset: Int,
437     sizeForDefaultText: () -> IntSize
438 ): Rect {
439     val bbox =
440         when {
441             focusOffset < layoutResult.layoutInput.text.length -> {
442                 layoutResult.getBoundingBox(focusOffset)
443             }
444             focusOffset != 0 -> {
445                 layoutResult.getBoundingBox(focusOffset - 1)
446             }
447             else -> { // empty text.
448                 val size = sizeForDefaultText()
449                 Rect(0f, 0f, 1.0f, size.height.toFloat())
450             }
451         }
452     val globalLT = layoutCoordinates.localToRoot(Offset(bbox.left, bbox.top))
453     return Rect(Offset(globalLT.x, globalLT.y), Size(bbox.width, bbox.height))
454 }
455