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.material3.adaptive.layout
18 
19 import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
20 import androidx.compose.runtime.Immutable
21 import androidx.compose.runtime.saveable.Saver
22 import androidx.compose.runtime.saveable.listSaver
23 import androidx.compose.ui.util.fastForEachReversed
24 
25 /**
26  * Calculates the current adapted value of [ThreePaneScaffold] according to the given
27  * [maxHorizontalPartitions], [adaptStrategies] and [currentDestination]. The returned value can be
28  * used as a unique representation of the current layout structure.
29  *
30  * The function will treat the current destination as the highest priority and then adapt the rest
31  * panes according to the order of [ThreePaneScaffoldRole.Primary],
32  * [ThreePaneScaffoldRole.Secondary] and [ThreePaneScaffoldRole.Tertiary]. If there are still
33  * remaining partitions to put the pane, the pane will be set as [PaneAdaptedValue.Expanded],
34  * otherwise it will be adapted according to its associated [AdaptStrategy].
35  *
36  * @param maxHorizontalPartitions The maximum allowed partitions along the horizontal axis, i.e.,
37  *   how many expanded panes can be shown at the same time.
38  * @param adaptStrategies The adapt strategies of each pane role that [ThreePaneScaffold] supports,
39  *   the default value will be [ThreePaneScaffoldDefaults.adaptStrategies].
40  * @param currentDestination The current destination item, which will be treated as having the
41  *   highest priority, can be `null`.
42  * @param maxVerticalPartitions The maximum allowed partitions along the vertical axis, by default
43  *   it will be 1 and in this case no reflowed panes will be allowed; if the value equals to or
44  *   larger than 2, reflowed panes are allowed, besides the expanded pane in the same horizontal
45  *   partition.
46  */
47 @ExperimentalMaterial3AdaptiveApi
48 fun calculateThreePaneScaffoldValue(
49     maxHorizontalPartitions: Int,
50     adaptStrategies: ThreePaneScaffoldAdaptStrategies,
51     currentDestination: ThreePaneScaffoldDestinationItem<*>?,
52     maxVerticalPartitions: Int = 1
53 ): ThreePaneScaffoldValue =
54     calculateThreePaneScaffoldValue(
55         maxHorizontalPartitions,
56         adaptStrategies,
57         listOfNotNull(currentDestination),
58         maxVerticalPartitions
59     )
60 
61 /**
62  * Calculates the current adapted value of [ThreePaneScaffold] according to the given
63  * [maxHorizontalPartitions], [adaptStrategies] and [destinationHistory]. The returned value can be
64  * used as a unique representation of the current layout structure.
65  *
66  * The function will treat the current focus as the highest priority and then adapt the rest panes
67  * according to the order of [ThreePaneScaffoldRole.Primary], [ThreePaneScaffoldRole.Secondary] and
68  * [ThreePaneScaffoldRole.Tertiary]. If there are still remaining partitions to put the pane, the
69  * pane will be set as [PaneAdaptedValue.Expanded], otherwise it will be adapted according to its
70  * associated [AdaptStrategy].
71  *
72  * @param maxHorizontalPartitions The maximum allowed partitions along the horizontal axis, i.e.,
73  *   how many expanded panes can be shown at the same time.
74  * @param adaptStrategies The adapt strategies of each pane role that [ThreePaneScaffold] supports,
75  *   the default value will be [ThreePaneScaffoldDefaults.adaptStrategies].
76  * @param destinationHistory The history of past destination items. The last destination will have
77  *   the highest priority, and the second last destination will have the second highest priority,
78  *   and so forth until all panes have a priority assigned. Note that the last destination is
79  *   supposed to be the last item of the provided list.
80  * @param maxVerticalPartitions The maximum allowed partitions along the vertical axis, by default
81  *   it will be 1 and in this case no reflowed panes will be allowed; if the value equals to or
82  *   larger than 2, reflowed panes are allowed, besides the expanded pane in the same horizontal
83  *   partition.
84  */
85 @ExperimentalMaterial3AdaptiveApi
86 fun calculateThreePaneScaffoldValue(
87     maxHorizontalPartitions: Int,
88     adaptStrategies: ThreePaneScaffoldAdaptStrategies,
89     destinationHistory: List<ThreePaneScaffoldDestinationItem<*>>,
90     maxVerticalPartitions: Int = 1
91 ): ThreePaneScaffoldValue {
92     var expandedCount = 0
93     var primaryPaneAdaptedValue: PaneAdaptedValue? = null
94     var secondaryPaneAdaptedValue: PaneAdaptedValue? = null
95     var tertiaryPaneAdaptedValue: PaneAdaptedValue? = null
96 
97     fun getAdaptedValue(role: ThreePaneScaffoldRole) =
98         when (role) {
99             ThreePaneScaffoldRole.Primary -> primaryPaneAdaptedValue
100             ThreePaneScaffoldRole.Secondary -> secondaryPaneAdaptedValue
101             ThreePaneScaffoldRole.Tertiary -> tertiaryPaneAdaptedValue
102         }
103 
104     fun setAdaptedValue(role: ThreePaneScaffoldRole, value: PaneAdaptedValue) {
105         when (role) {
106             ThreePaneScaffoldRole.Primary -> primaryPaneAdaptedValue = value
107             ThreePaneScaffoldRole.Secondary -> secondaryPaneAdaptedValue = value
108             ThreePaneScaffoldRole.Tertiary -> tertiaryPaneAdaptedValue = value
109         }
110     }
111 
112     var checkReflowedPane =
113         maxHorizontalPartitions == 1 &&
114             maxVerticalPartitions > 1 &&
115             (adaptStrategies[ThreePaneScaffoldRole.Primary] is AdaptStrategy.Reflow ||
116                 adaptStrategies[ThreePaneScaffoldRole.Secondary] is AdaptStrategy.Reflow ||
117                 adaptStrategies[ThreePaneScaffoldRole.Tertiary] is AdaptStrategy.Reflow)
118 
119     run {
120         forEachPaneByPriority(destinationHistory) { pane ->
121             val hasAvailablePartition = expandedCount < maxHorizontalPartitions
122             if (!hasAvailablePartition && !checkReflowedPane) {
123                 return@run // No need to check more panes, break;
124             }
125             if (getAdaptedValue(pane) != null) {
126                 return@forEachPaneByPriority // Pane already adapted, continue;
127             }
128             var reflowedPane: ThreePaneScaffoldRole? = null
129             var anchorPane: ThreePaneScaffoldRole = pane
130             var anchorPaneValue: PaneAdaptedValue? = null
131             if (checkReflowedPane) {
132                 (adaptStrategies[pane] as? AdaptStrategy.Reflow)?.apply {
133                     (this.targetPane as? ThreePaneScaffoldRole)?.apply {
134                         reflowedPane = pane
135                         anchorPane = this
136                         anchorPaneValue = getAdaptedValue(anchorPane)
137                     }
138                 }
139             }
140             when (anchorPaneValue) {
141                 null ->
142                     if (hasAvailablePartition) {
143                         // Expand the anchor pane to reflow the pane
144                         setAdaptedValue(anchorPane, PaneAdaptedValue.Expanded)
145                         expandedCount++
146                     } else {
147                         // Cannot expand the anchor pane, continue;
148                         return@forEachPaneByPriority
149                     }
150                 PaneAdaptedValue.Expanded -> {
151                     // Anchor pane is expanded, do nothing
152                 }
153                 else -> return@forEachPaneByPriority // Anchor pane is not expanded, continue;
154             }
155             reflowedPane?.apply {
156                 setAdaptedValue(this, PaneAdaptedValue.Reflowed(anchorPane))
157                 checkReflowedPane = false
158             }
159         }
160     }
161     return ThreePaneScaffoldValue(
162         primary = primaryPaneAdaptedValue ?: PaneAdaptedValue.Hidden,
163         secondary = secondaryPaneAdaptedValue ?: PaneAdaptedValue.Hidden,
164         tertiary = tertiaryPaneAdaptedValue ?: PaneAdaptedValue.Hidden,
165     )
166 }
167 
168 @ExperimentalMaterial3AdaptiveApi
forEachPaneByPrioritynull169 private inline fun forEachPaneByPriority(
170     destinationHistory: List<ThreePaneScaffoldDestinationItem<*>>,
171     action: (ThreePaneScaffoldRole) -> Unit
172 ) {
173     destinationHistory.fastForEachReversed { action(it.pane) }
174     action(ThreePaneScaffoldRole.Primary)
175     action(ThreePaneScaffoldRole.Secondary)
176     action(ThreePaneScaffoldRole.Tertiary)
177 }
178 
179 /**
180  * The adapted value of [ThreePaneScaffold]. It contains each pane's adapted value.
181  * [ThreePaneScaffold] will use the adapted values to decide which panes should be displayed and how
182  * they should be displayed. With other input parameters of [ThreePaneScaffold] fixed, each possible
183  * instance of this class should represent a unique state of [ThreePaneScaffold] and developers can
184  * compare two [ThreePaneScaffoldValue] to decide if there is a layout structure change.
185  *
186  * For a Material-opinionated layout, it's suggested to use [calculateThreePaneScaffoldValue] to
187  * calculate the current scaffold value.
188  *
189  * @param primary [PaneAdaptedValue] of the primary pane of [ThreePaneScaffold]
190  * @param secondary [PaneAdaptedValue] of the secondary pane of [ThreePaneScaffold]
191  * @param tertiary [PaneAdaptedValue] of the tertiary pane of [ThreePaneScaffold]
192  * @constructor create an instance of [ThreePaneScaffoldValue]
193  */
194 @ExperimentalMaterial3AdaptiveApi
195 @Immutable
196 class ThreePaneScaffoldValue(
197     val primary: PaneAdaptedValue,
198     val secondary: PaneAdaptedValue,
199     val tertiary: PaneAdaptedValue
200 ) : PaneScaffoldValue<ThreePaneScaffoldRole>, PaneExpansionStateKeyProvider {
<lambda>null201     internal val expandedCount by lazy {
202         var count = 0
203         if (primary == PaneAdaptedValue.Expanded) {
204             count++
205         }
206         if (secondary == PaneAdaptedValue.Expanded) {
207             count++
208         }
209         if (tertiary == PaneAdaptedValue.Expanded) {
210             count++
211         }
212         count
213     }
214 
<lambda>null215     override val paneExpansionStateKey by lazy {
216         if (expandedCount != 2) {
217             PaneExpansionStateKey.Default
218         } else {
219             val expandedPanes = Array<ThreePaneScaffoldRole?>(2) { null }
220             var count = 0
221             if (primary == PaneAdaptedValue.Expanded) {
222                 expandedPanes[count++] = ThreePaneScaffoldRole.Primary
223             }
224             if (secondary == PaneAdaptedValue.Expanded) {
225                 expandedPanes[count++] = ThreePaneScaffoldRole.Secondary
226             }
227             if (tertiary == PaneAdaptedValue.Expanded) {
228                 expandedPanes[count] = ThreePaneScaffoldRole.Tertiary
229             }
230             TwoPaneExpansionStateKeyImpl(expandedPanes[0]!!, expandedPanes[1]!!)
231         }
232     }
233 
equalsnull234     override fun equals(other: Any?): Boolean {
235         if (this === other) return true
236         if (other !is ThreePaneScaffoldValue) return false
237         if (primary != other.primary) return false
238         if (secondary != other.secondary) return false
239         if (tertiary != other.tertiary) return false
240         return true
241     }
242 
hashCodenull243     override fun hashCode(): Int {
244         var result = primary.hashCode()
245         result = 31 * result + secondary.hashCode()
246         result = 31 * result + tertiary.hashCode()
247         return result
248     }
249 
toStringnull250     override fun toString(): String {
251         return "ThreePaneScaffoldValue(primary=$primary, " +
252             "secondary=$secondary, " +
253             "tertiary=$tertiary)"
254     }
255 
getnull256     override operator fun get(role: ThreePaneScaffoldRole): PaneAdaptedValue =
257         when (role) {
258             ThreePaneScaffoldRole.Primary -> primary
259             ThreePaneScaffoldRole.Secondary -> secondary
260             ThreePaneScaffoldRole.Tertiary -> tertiary
261         }
262 }
263 
264 @OptIn(ExperimentalMaterial3AdaptiveApi::class)
265 internal class TwoPaneExpansionStateKeyImpl(
266     val firstExpandedPane: ThreePaneScaffoldRole,
267     val secondExpandedPane: ThreePaneScaffoldRole
268 ) : PaneExpansionStateKey {
hashCodenull269     override fun hashCode(): Int {
270         return firstExpandedPane.hashCode() * 31 + secondExpandedPane.hashCode()
271     }
272 
equalsnull273     override fun equals(other: Any?): Boolean {
274         if (this === other) return true
275         val otherKey = other as? TwoPaneExpansionStateKeyImpl ?: return false
276         return firstExpandedPane == otherKey.firstExpandedPane &&
277             secondExpandedPane == otherKey.secondExpandedPane
278     }
279 
280     companion object {
savernull281         fun saver(): Saver<TwoPaneExpansionStateKeyImpl, Any> =
282             listSaver(
283                 save = { listOf(it.firstExpandedPane, it.secondExpandedPane) },
<lambda>null284                 restore = {
285                     TwoPaneExpansionStateKeyImpl(
286                         firstExpandedPane = it[0],
287                         secondExpandedPane = it[1]
288                     )
289                 }
290             )
291     }
292 }
293