1 /*
<lambda>null2  * Copyright 2024 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.input.TextFieldBuffer.ChangeList
20 import androidx.compose.runtime.collection.mutableVectorOf
21 import androidx.compose.ui.text.TextRange
22 
23 /**
24  * Keeps track of changes made to a text buffer via [trackChange], and reports changes as a
25  * [ChangeList].
26  *
27  * @param initialChanges If non-null, used to initialize this tracker by copying changes.
28  */
29 internal class ChangeTracker(initialChanges: ChangeTracker? = null) : ChangeList {
30 
31     private var _changes = mutableVectorOf<Change>()
32     private var _changesTemp = mutableVectorOf<Change>()
33 
34     init {
35         initialChanges?._changes?.forEach {
36             _changes += Change(it.preStart, it.preEnd, it.originalStart, it.originalEnd)
37         }
38     }
39 
40     override val changeCount: Int
41         get() = _changes.size
42 
43     /**
44      * This function deals with three "coordinate spaces":
45      * - `original`: The text before any changes were made.
46      * - `pre`: The text before this change is applied, but with all previous changes applied.
47      * - `post`: The text after this change is applied, including all the previous changes.
48      *
49      * When this function is called, the existing changes map ranges in `original` to ranges in
50      * `pre`. The new change is a mapping from `pre` to `post`. This function must determine the
51      * corresponding range in `original` for this change, and convert all other changes' `pre`
52      * ranges to `post`. It must also ensure that any adjacent or overlapping ranges are merged to
53      * ensure the [ChangeList] invariant that all changes are non-overlapping.
54      *
55      * The algorithm works as follows:
56      * 1. Find all the changes that are adjacent to or overlap with this one. This search is
57      *    performed in the `pre` space since that's the space the new change shares with the
58      *    existing changes.
59      * 2. Merge all the changes from (1) into a single range in the `original` and `pre` spaces.
60      * 3. Merge the new change with the change from (2), updating the end of the range to account
61      *    for the new text.
62      * 3. Offset all remaining changes are to account for the new text.
63      */
64     fun trackChange(preStart: Int, preEnd: Int, postLength: Int) {
65         if (preStart == preEnd && postLength == 0) {
66             // Ignore noop changes.
67             return
68         }
69         val preMin = minOf(preStart, preEnd)
70         val preMax = maxOf(preStart, preEnd)
71 
72         var i = 0
73         var recordedNewChange = false
74         val postDelta = postLength - (preMax - preMin)
75 
76         var mergedOverlappingChange: Change? = null
77         while (i < _changes.size) {
78             val change = _changes[i]
79 
80             // Merge adjacent and overlapping changes as we go.
81             if (
82                 change.preStart in preMin..preMax ||
83                     change.preEnd in preMin..preMax ||
84                     preMin in change.preStart..change.preEnd ||
85                     preMax in change.preStart..change.preEnd
86             ) {
87                 if (mergedOverlappingChange == null) {
88                     mergedOverlappingChange = change
89                 } else {
90                     mergedOverlappingChange.preEnd = change.preEnd
91                     mergedOverlappingChange.originalEnd = change.originalEnd
92                 }
93                 // Don't append overlapping changes to the temp list until we're finished merging.
94                 i++
95                 continue
96             }
97 
98             if (change.preStart > preMax && !recordedNewChange) {
99                 // First non-overlapping change after the new one – record the change before
100                 // proceeding.
101                 appendNewChange(mergedOverlappingChange, preMin, preMax, postDelta)
102                 recordedNewChange = true
103             }
104 
105             if (recordedNewChange) {
106                 change.preStart += postDelta
107                 change.preEnd += postDelta
108             }
109             _changesTemp += change
110             i++
111         }
112 
113         if (!recordedNewChange) {
114             // The new change is after or overlapping all previous changes so it hasn't been
115             // appended yet.
116             appendNewChange(mergedOverlappingChange, preMin, preMax, postDelta)
117         }
118 
119         // Swap the lists.
120         val oldChanges = _changes
121         _changes = _changesTemp
122         _changesTemp = oldChanges
123         _changesTemp.clear()
124     }
125 
126     fun clearChanges() {
127         _changes.clear()
128     }
129 
130     override fun getRange(changeIndex: Int): TextRange =
131         _changes[changeIndex].let { TextRange(it.preStart, it.preEnd) }
132 
133     override fun getOriginalRange(changeIndex: Int): TextRange =
134         _changes[changeIndex].let { TextRange(it.originalStart, it.originalEnd) }
135 
136     override fun toString(): String = buildString {
137         append("ChangeList(changes=[")
138         _changes.forEachIndexed { i, change ->
139             append(
140                 "(${change.originalStart},${change.originalEnd})->" +
141                     "(${change.preStart},${change.preEnd})"
142             )
143             if (i < changeCount - 1) append(", ")
144         }
145         append("])")
146     }
147 
148     private fun appendNewChange(
149         mergedOverlappingChange: Change?,
150         preMin: Int,
151         preMax: Int,
152         postDelta: Int
153     ) {
154         var originalDelta =
155             if (_changesTemp.isEmpty()) 0
156             else {
157                 _changesTemp.last().let { it.preEnd - it.originalEnd }
158             }
159         val newChange: Change
160         if (mergedOverlappingChange == null) {
161             // There were no overlapping changes, so allocate a new one.
162             val originalStart = preMin - originalDelta
163             val originalEnd = originalStart + (preMax - preMin)
164             newChange =
165                 Change(
166                     preStart = preMin,
167                     preEnd = preMax + postDelta,
168                     originalStart = originalStart,
169                     originalEnd = originalEnd
170                 )
171         } else {
172             newChange = mergedOverlappingChange
173             // Convert the merged overlapping changes to the `post` space.
174             // Merge the new changed with the merged overlapping changes.
175             if (newChange.preStart > preMin) {
176                 // The new change starts before the merged overlapping set.
177                 newChange.preStart = preMin
178                 newChange.originalStart = preMin
179             }
180             if (preMax > newChange.preEnd) {
181                 // The new change ends after the merged overlapping set.
182                 originalDelta = newChange.preEnd - newChange.originalEnd
183                 newChange.preEnd = preMax
184                 newChange.originalEnd = preMax - originalDelta
185             }
186             newChange.preEnd += postDelta
187         }
188         _changesTemp += newChange
189     }
190 
191     private data class Change(
192         var preStart: Int,
193         var preEnd: Int,
194         var originalStart: Int,
195         var originalEnd: Int
196     )
197 }
198