1 /* <lambda>null2 * 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.focus 18 19 import androidx.collection.mutableScatterMapOf 20 import androidx.compose.runtime.collection.mutableVectorOf 21 import androidx.compose.ui.ComposeUiFlags 22 import androidx.compose.ui.ExperimentalComposeUiApi 23 import androidx.compose.ui.internal.checkPreconditionNotNull 24 25 /** 26 * This manager provides a way to ensure that only one focus transaction is running at a time. We 27 * use this to prevent re-entrant focus operations. Starting a new transaction automatically cancels 28 * the previous transaction and reverts any focus state changes made during that transaction. 29 */ 30 internal class FocusTransactionManager { 31 private val states = mutableScatterMapOf<FocusTargetNode, FocusStateImpl>() 32 private val cancellationListener = mutableVectorOf<() -> Unit>() 33 var ongoingTransaction = false 34 private set 35 36 /** 37 * An indicator of changes to the transaction. When any state changes, the generation changes. 38 */ 39 var generation = 0 40 private set 41 42 /** 43 * Stars a new transaction, which allows you to change the focus state. Calling this function 44 * causes any ongoing focus transaction to be cancelled. If an [onCancelled] lambda is 45 * specified, it will be called if this transaction is cancelled by a new invocation to 46 * [withNewTransaction]. 47 */ 48 inline fun <T> withNewTransaction( 49 noinline onCancelled: (() -> Unit)? = null, 50 block: () -> T 51 ): T = 52 try { 53 if (ongoingTransaction) cancelTransaction() 54 beginTransaction() 55 onCancelled?.let { cancellationListener += it } 56 block() 57 } finally { 58 commitTransaction() 59 } 60 61 /** 62 * If another transaction is ongoing, this runs the specified [block] within that transaction, 63 * and it commits any changes to focus state at the end of that transaction. If there is no 64 * ongoing transaction, this will start a new transaction. If an [onCancelled] lambda is 65 * specified, it will be called if this transaction is cancelled by a new invocation to 66 * [withNewTransaction]. 67 */ 68 inline fun <T> withExistingTransaction( 69 noinline onCancelled: (() -> Unit)? = null, 70 block: () -> T 71 ): T { 72 onCancelled?.let { cancellationListener += it } 73 return if (ongoingTransaction) block() 74 else 75 try { 76 beginTransaction() 77 block() 78 } finally { 79 commitTransaction() 80 } 81 } 82 83 /** 84 * The focus state for the specified [node][FocusTargetNode] if the state was changed during the 85 * current transaction. 86 */ 87 var FocusTargetNode.uncommittedFocusState: FocusStateImpl? 88 get() = 89 if (@OptIn(ExperimentalComposeUiApi::class) ComposeUiFlags.isTrackFocusEnabled) { 90 error("uncommittedFocusState must not be accessed when isTrackFocusEnabled is on") 91 } else { 92 states[this] 93 } 94 set(value) { 95 if (!@OptIn(ExperimentalComposeUiApi::class) ComposeUiFlags.isTrackFocusEnabled) { 96 val currentFocusState = states[this] ?: FocusStateImpl.Inactive 97 if (currentFocusState != value) { 98 generation++ 99 } 100 states[this] = checkPreconditionNotNull(value) { "requires a non-null focus state" } 101 } 102 } 103 104 private fun beginTransaction() { 105 ongoingTransaction = true 106 } 107 108 private fun commitTransaction() { 109 states.forEachKey { focusTargetNode -> focusTargetNode.commitFocusState() } 110 states.clear() 111 ongoingTransaction = false 112 cancellationListener.clear() 113 } 114 115 private fun cancelTransaction() { 116 states.clear() 117 ongoingTransaction = false 118 cancellationListener.forEach { it() } 119 cancellationListener.clear() 120 } 121 } 122