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.foundation.text.input.internal
18 
19 import androidx.compose.foundation.text.LegacyTextFieldState
20 import androidx.compose.foundation.text.selection.TextFieldSelectionManager
21 import androidx.compose.runtime.getValue
22 import androidx.compose.runtime.mutableStateOf
23 import androidx.compose.runtime.setValue
24 import androidx.compose.ui.Modifier
25 import androidx.compose.ui.layout.LayoutCoordinates
26 import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
27 import androidx.compose.ui.node.GlobalPositionAwareModifierNode
28 import androidx.compose.ui.node.ModifierNodeElement
29 import androidx.compose.ui.node.currentValueOf
30 import androidx.compose.ui.platform.InspectorInfo
31 import androidx.compose.ui.platform.LocalSoftwareKeyboardController
32 import androidx.compose.ui.platform.LocalViewConfiguration
33 import androidx.compose.ui.platform.PlatformTextInputModifierNode
34 import androidx.compose.ui.platform.PlatformTextInputSession
35 import androidx.compose.ui.platform.SoftwareKeyboardController
36 import androidx.compose.ui.platform.ViewConfiguration
37 import androidx.compose.ui.platform.establishTextInputSession
38 import kotlinx.coroutines.CoroutineStart
39 import kotlinx.coroutines.Job
40 import kotlinx.coroutines.launch
41 
42 /**
43  * Connects a [LegacyPlatformTextInputServiceAdapter] to the PlatformTextInput system. This modifier
44  * must be applied to the text field in order for the [LegacyPlatformTextInputServiceAdapter] to
45  * function.
46  */
legacyTextInputAdapternull47 internal fun Modifier.legacyTextInputAdapter(
48     serviceAdapter: LegacyPlatformTextInputServiceAdapter,
49     legacyTextFieldState: LegacyTextFieldState,
50     textFieldSelectionManager: TextFieldSelectionManager
51 ): Modifier =
52     this then
53         LegacyAdaptingPlatformTextInputModifier(
54             serviceAdapter,
55             legacyTextFieldState,
56             textFieldSelectionManager
57         )
58 
59 private data class LegacyAdaptingPlatformTextInputModifier(
60     val serviceAdapter: LegacyPlatformTextInputServiceAdapter,
61     val legacyTextFieldState: LegacyTextFieldState,
62     val textFieldSelectionManager: TextFieldSelectionManager
63 ) : ModifierNodeElement<LegacyAdaptingPlatformTextInputModifierNode>() {
64 
65     override fun create(): LegacyAdaptingPlatformTextInputModifierNode {
66         return LegacyAdaptingPlatformTextInputModifierNode(
67             serviceAdapter,
68             legacyTextFieldState,
69             textFieldSelectionManager
70         )
71     }
72 
73     override fun update(node: LegacyAdaptingPlatformTextInputModifierNode) {
74         node.setServiceAdapter(serviceAdapter)
75         node.legacyTextFieldState = legacyTextFieldState
76         node.textFieldSelectionManager = textFieldSelectionManager
77     }
78 
79     override fun InspectorInfo.inspectableProperties() {
80         // Not a public-facing modifier.
81     }
82 }
83 
84 /**
85  * Exposes [PlatformTextInputModifierNode] capabilities and other information from the modifier
86  * system such as position and some composition locals to implementations of
87  * [LegacyPlatformTextInputServiceAdapter].
88  */
89 internal class LegacyAdaptingPlatformTextInputModifierNode(
90     private var serviceAdapter: LegacyPlatformTextInputServiceAdapter,
91     override var legacyTextFieldState: LegacyTextFieldState,
92     override var textFieldSelectionManager: TextFieldSelectionManager
93 ) :
94     Modifier.Node(),
95     PlatformTextInputModifierNode,
96     CompositionLocalConsumerModifierNode,
97     GlobalPositionAwareModifierNode,
98     LegacyPlatformTextInputServiceAdapter.LegacyPlatformTextInputNode {
99 
100     override var layoutCoordinates: LayoutCoordinates? by mutableStateOf(null)
101         private set
102 
103     override val softwareKeyboardController: SoftwareKeyboardController?
104         get() = currentValueOf(LocalSoftwareKeyboardController)
105 
setServiceAdapternull106     fun setServiceAdapter(serviceAdapter: LegacyPlatformTextInputServiceAdapter) {
107         if (isAttached) {
108             this.serviceAdapter.stopInput()
109             this.serviceAdapter.unregisterModifier(this)
110         }
111         this.serviceAdapter = serviceAdapter
112         if (isAttached) {
113             this.serviceAdapter.registerModifier(this)
114         }
115     }
116 
117     override val viewConfiguration: ViewConfiguration
118         get() = currentValueOf(LocalViewConfiguration)
119 
onAttachnull120     override fun onAttach() {
121         serviceAdapter.registerModifier(this)
122     }
123 
onDetachnull124     override fun onDetach() {
125         serviceAdapter.unregisterModifier(this)
126     }
127 
onGloballyPositionednull128     override fun onGloballyPositioned(coordinates: LayoutCoordinates) {
129         this.layoutCoordinates = coordinates
130     }
131 
launchTextInputSessionnull132     override fun launchTextInputSession(
133         block: suspend PlatformTextInputSession.() -> Nothing
134     ): Job? {
135         if (!isAttached) return null
136         return coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) {
137             establishTextInputSession(block)
138         }
139     }
140 }
141