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