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