1 /*
2  * 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.ui.text.input
18 
19 import android.view.inputmethod.CursorAnchorInfo
20 import androidx.compose.ui.geometry.Rect
21 import androidx.compose.ui.graphics.Matrix
22 import androidx.compose.ui.graphics.setFrom
23 import androidx.compose.ui.input.pointer.MatrixPositionCalculator
24 import androidx.compose.ui.text.TextLayoutResult
25 
26 @Deprecated(
27     "Only exists to support the legacy TextInputService APIs. It is not used by any Compose " +
28         "code. A copy of this class in foundation is used by the legacy BasicTextField."
29 )
30 internal class CursorAnchorInfoController(
31     private val rootPositionCalculator: MatrixPositionCalculator,
32     @Suppress("DEPRECATION") private val inputMethodManager: InputMethodManager
33 ) {
34     private val lock = Any()
35 
36     private var monitorEnabled = false
37     private var hasPendingImmediateRequest = false
38 
39     private var includeInsertionMarker = false
40     private var includeCharacterBounds = false
41     private var includeEditorBounds = false
42     private var includeLineBounds = false
43 
44     private var textFieldValue: TextFieldValue? = null
45     private var textLayoutResult: TextLayoutResult? = null
46     private var offsetMapping: OffsetMapping? = null
<lambda>null47     private var textFieldToRootTransform: (Matrix) -> Unit = {}
48     private var innerTextFieldBounds: Rect? = null
49     private var decorationBoxBounds: Rect? = null
50 
51     private val builder = CursorAnchorInfo.Builder()
52     private val matrix = Matrix()
53     private val androidMatrix = android.graphics.Matrix()
54 
55     /**
56      * Requests [CursorAnchorInfo] updates to be provided to the [InputMethodManager].
57      *
58      * Combinations of [immediate] and [monitor] are used to specify when to provide updates. If
59      * these are both false, then no further updates will be provided.
60      *
61      * @param immediate whether to update with the current [CursorAnchorInfo] immediately, or as
62      *   soon as available
63      * @param monitor whether to provide [CursorAnchorInfo] updates for all future layout or
64      *   position changes
65      * @param includeInsertionMarker whether to include insertion marker (i.e. cursor) location
66      *   information
67      * @param includeCharacterBounds whether to include character bounds information for the
68      *   composition range
69      * @param includeEditorBounds whether to include editor bounds information
70      * @param includeLineBounds whether to include line bounds information
71      */
requestUpdatenull72     fun requestUpdate(
73         immediate: Boolean,
74         monitor: Boolean,
75         includeInsertionMarker: Boolean,
76         includeCharacterBounds: Boolean,
77         includeEditorBounds: Boolean,
78         includeLineBounds: Boolean
79     ) =
80         synchronized(lock) {
81             this.includeInsertionMarker = includeInsertionMarker
82             this.includeCharacterBounds = includeCharacterBounds
83             this.includeEditorBounds = includeEditorBounds
84             this.includeLineBounds = includeLineBounds
85 
86             if (immediate) {
87                 hasPendingImmediateRequest = true
88                 if (textFieldValue != null) {
89                     updateCursorAnchorInfo()
90                 }
91             }
92             monitorEnabled = monitor
93         }
94 
95     /**
96      * Notify the controller of layout and position changes.
97      *
98      * @param textFieldValue the text field's [TextFieldValue]
99      * @param offsetMapping the offset mapping for the visual transformation
100      * @param textLayoutResult the text field's [TextLayoutResult]
101      * @param textFieldToRootTransform function that modifies a matrix to be a transformation matrix
102      *   from local coordinates to the root composable coordinates
103      * @param innerTextFieldBounds visible bounds of the text field in local coordinates, or an
104      *   empty rectangle if the text field is not visible
105      * @param decorationBoxBounds visible bounds of the decoration box in local coordinates, or an
106      *   empty rectangle if the decoration box is not visible
107      */
updateTextLayoutResultnull108     fun updateTextLayoutResult(
109         textFieldValue: TextFieldValue,
110         offsetMapping: OffsetMapping,
111         textLayoutResult: TextLayoutResult,
112         textFieldToRootTransform: (Matrix) -> Unit,
113         innerTextFieldBounds: Rect,
114         decorationBoxBounds: Rect
115     ) =
116         synchronized(lock) {
117             this.textFieldValue = textFieldValue
118             this.offsetMapping = offsetMapping
119             this.textLayoutResult = textLayoutResult
120             this.textFieldToRootTransform = textFieldToRootTransform
121             this.innerTextFieldBounds = innerTextFieldBounds
122             this.decorationBoxBounds = decorationBoxBounds
123 
124             if (hasPendingImmediateRequest || monitorEnabled) {
125                 updateCursorAnchorInfo()
126             }
127         }
128 
129     /**
130      * Invalidate the last received layout and position data.
131      *
132      * This should be called when the [TextFieldValue] has changed, so the last received layout and
133      * position data is no longer valid. [CursorAnchorInfo] updates will not be sent until new
134      * layout and position data is received.
135      */
invalidatenull136     fun invalidate() =
137         synchronized(lock) {
138             textFieldValue = null
139             offsetMapping = null
140             textLayoutResult = null
141             textFieldToRootTransform = {}
142             innerTextFieldBounds = null
143             decorationBoxBounds = null
144         }
145 
updateCursorAnchorInfonull146     private fun updateCursorAnchorInfo() {
147         if (!inputMethodManager.isActive()) return
148 
149         // Sets matrix to transform text field local coordinates to the root composable coordinates.
150         textFieldToRootTransform(matrix)
151         // Updates matrix to transform text field local coordinates to screen coordinates.
152         rootPositionCalculator.localToScreen(matrix)
153         androidMatrix.setFrom(matrix)
154 
155         @Suppress("DEPRECATION")
156         inputMethodManager.updateCursorAnchorInfo(
157             builder.build(
158                 textFieldValue!!,
159                 offsetMapping!!,
160                 textLayoutResult!!,
161                 androidMatrix,
162                 innerTextFieldBounds!!,
163                 decorationBoxBounds!!,
164                 includeInsertionMarker,
165                 includeCharacterBounds,
166                 includeEditorBounds,
167                 includeLineBounds
168             )
169         )
170 
171         hasPendingImmediateRequest = false
172     }
173 }
174