1 /*
<lambda>null2  * Copyright 2021 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.selection
18 
19 import androidx.collection.LongObjectMap
20 import androidx.collection.emptyLongObjectMap
21 import androidx.collection.mutableLongObjectMapOf
22 import androidx.compose.foundation.AtomicLong
23 import androidx.compose.foundation.internal.requirePrecondition
24 import androidx.compose.runtime.getValue
25 import androidx.compose.runtime.mutableStateOf
26 import androidx.compose.runtime.saveable.Saver
27 import androidx.compose.runtime.setValue
28 import androidx.compose.ui.geometry.Offset
29 import androidx.compose.ui.layout.LayoutCoordinates
30 
31 internal class SelectionRegistrarImpl private constructor(initialIncrementId: Long) :
32     SelectionRegistrar {
33     companion object {
34         val Saver =
35             Saver<SelectionRegistrarImpl, Long>(
36                 save = { it.incrementId.get() },
37                 restore = { SelectionRegistrarImpl(it) }
38             )
39     }
40 
41     constructor() : this(initialIncrementId = 1L)
42 
43     /** A flag to check if the [Selectable]s have already been sorted. */
44     internal var sorted: Boolean = false
45 
46     /**
47      * This is essentially the list of registered components that want to handle text selection that
48      * are below the SelectionContainer.
49      */
50     private val _selectables = mutableListOf<Selectable>()
51 
52     /** Getter for handlers that returns a List. */
53     internal val selectables: List<Selectable>
54         get() = _selectables
55 
56     private val _selectableMap = mutableLongObjectMapOf<Selectable>()
57 
58     /** A map from selectable keys to subscribed selectables. */
59     internal val selectableMap: LongObjectMap<Selectable>
60         get() = _selectableMap
61 
62     /**
63      * The incremental id to be assigned to each selectable. It starts from 1 and 0 is used to
64      * denote an invalid id.
65      *
66      * @see SelectionRegistrar.InvalidSelectableId
67      */
68     private var incrementId = AtomicLong(initialIncrementId)
69 
70     /** The callback to be invoked when the position change was triggered. */
71     internal var onPositionChangeCallback: ((Long) -> Unit)? = null
72 
73     /** The callback to be invoked when the selection is initiated. */
74     internal var onSelectionUpdateStartCallback:
75         ((Boolean, LayoutCoordinates, Offset, SelectionAdjustment) -> Unit)? =
76         null
77 
78     /** The callback to be invoked when the selection is initiated with selectAll [Selection]. */
79     internal var onSelectionUpdateSelectAll: ((Boolean, Long) -> Unit)? = null
80 
81     /**
82      * The callback to be invoked when the selection is updated. If the first offset is null it
83      * means that the start of selection is unknown for the caller.
84      */
85     internal var onSelectionUpdateCallback:
86         ((Boolean, LayoutCoordinates, Offset, Offset, Boolean, SelectionAdjustment) -> Boolean)? =
87         null
88 
89     /** The callback to be invoked when selection update finished. */
90     internal var onSelectionUpdateEndCallback: (() -> Unit)? = null
91 
92     /** The callback to be invoked when one of the selectable has changed. */
93     internal var onSelectableChangeCallback: ((Long) -> Unit)? = null
94 
95     /**
96      * The callback to be invoked after a selectable is unsubscribed from this [SelectionRegistrar].
97      */
98     internal var afterSelectableUnsubscribe: ((Long) -> Unit)? = null
99 
100     override var subselections: LongObjectMap<Selection> by mutableStateOf(emptyLongObjectMap())
101 
102     override fun subscribe(selectable: Selectable): Selectable {
103         requirePrecondition(selectable.selectableId != SelectionRegistrar.InvalidSelectableId) {
104             "The selectable contains an invalid id: ${selectable.selectableId}"
105         }
106         requirePrecondition(!_selectableMap.containsKey(selectable.selectableId)) {
107             "Another selectable with the id: $selectable.selectableId has already subscribed."
108         }
109         _selectableMap[selectable.selectableId] = selectable
110         _selectables.add(selectable)
111         sorted = false
112         return selectable
113     }
114 
115     override fun unsubscribe(selectable: Selectable) {
116         if (!_selectableMap.containsKey(selectable.selectableId)) return
117         _selectables.remove(selectable)
118         _selectableMap.remove(selectable.selectableId)
119         afterSelectableUnsubscribe?.invoke(selectable.selectableId)
120     }
121 
122     override fun nextSelectableId(): Long {
123         var id = incrementId.getAndIncrement()
124         while (id == SelectionRegistrar.InvalidSelectableId) {
125             id = incrementId.getAndIncrement()
126         }
127         return id
128     }
129 
130     /**
131      * Sort the list of registered [Selectable]s in [SelectionRegistrar]. Currently the order of
132      * selectables is geometric-based.
133      */
134     fun sort(containerLayoutCoordinates: LayoutCoordinates): List<Selectable> {
135         if (!sorted) {
136             // Sort selectables by y-coordinate first, and then x-coordinate, to match English
137             // hand-writing habit.
138             _selectables.sortWith { a: Selectable, b: Selectable ->
139                 val layoutCoordinatesA = a.getLayoutCoordinates()
140                 val layoutCoordinatesB = b.getLayoutCoordinates()
141 
142                 val positionA =
143                     if (layoutCoordinatesA != null) {
144                         containerLayoutCoordinates.localPositionOf(layoutCoordinatesA, Offset.Zero)
145                     } else {
146                         Offset.Zero
147                     }
148                 val positionB =
149                     if (layoutCoordinatesB != null) {
150                         containerLayoutCoordinates.localPositionOf(layoutCoordinatesB, Offset.Zero)
151                     } else {
152                         Offset.Zero
153                     }
154 
155                 if (positionA.y == positionB.y) {
156                     compareValues(positionA.x, positionB.x)
157                 } else {
158                     compareValues(positionA.y, positionB.y)
159                 }
160             }
161             sorted = true
162         }
163         return selectables
164     }
165 
166     override fun notifyPositionChange(selectableId: Long) {
167         // Set the variable sorted to be false, when the global position of a registered
168         // selectable changes.
169         sorted = false
170         onPositionChangeCallback?.invoke(selectableId)
171     }
172 
173     override fun notifySelectionUpdateStart(
174         layoutCoordinates: LayoutCoordinates,
175         startPosition: Offset,
176         adjustment: SelectionAdjustment,
177         isInTouchMode: Boolean
178     ) {
179         onSelectionUpdateStartCallback?.invoke(
180             isInTouchMode,
181             layoutCoordinates,
182             startPosition,
183             adjustment
184         )
185     }
186 
187     override fun notifySelectionUpdateSelectAll(selectableId: Long, isInTouchMode: Boolean) {
188         onSelectionUpdateSelectAll?.invoke(isInTouchMode, selectableId)
189     }
190 
191     override fun notifySelectionUpdate(
192         layoutCoordinates: LayoutCoordinates,
193         newPosition: Offset,
194         previousPosition: Offset,
195         isStartHandle: Boolean,
196         adjustment: SelectionAdjustment,
197         isInTouchMode: Boolean
198     ): Boolean {
199         return onSelectionUpdateCallback?.invoke(
200             isInTouchMode,
201             layoutCoordinates,
202             newPosition,
203             previousPosition,
204             isStartHandle,
205             adjustment
206         ) ?: true
207     }
208 
209     override fun notifySelectionUpdateEnd() {
210         onSelectionUpdateEndCallback?.invoke()
211     }
212 
213     override fun notifySelectableChange(selectableId: Long) {
214         onSelectableChangeCallback?.invoke(selectableId)
215     }
216 }
217