1 /*
2  * 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 @file:Suppress("DEPRECATED") // Deprecated import WindowWidthSizeClass.
18 
19 package androidx.compose.material3.adaptive.layout
20 
21 import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
22 import androidx.compose.material3.adaptive.Posture
23 import androidx.compose.material3.adaptive.WindowAdaptiveInfo
24 import androidx.compose.material3.adaptive.allVerticalHingeBounds
25 import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
26 import androidx.compose.material3.adaptive.occludingVerticalHingeBounds
27 import androidx.compose.material3.adaptive.separatingVerticalHingeBounds
28 import androidx.compose.runtime.Immutable
29 import androidx.compose.ui.geometry.Rect
30 import androidx.compose.ui.unit.Dp
31 import androidx.compose.ui.unit.dp
32 import kotlin.jvm.JvmInline
33 
34 /**
35  * Calculates the recommended [PaneScaffoldDirective] from a given [WindowAdaptiveInfo]. Use this
36  * method with [currentWindowAdaptiveInfo] to acquire Material-recommended adaptive layout settings
37  * of the current activity window.
38  *
39  * See more details on the [Material design guideline site]
40  * (https://m3.material.io/foundations/layout/applying-layout/window-size-classes).
41  *
42  * @param windowAdaptiveInfo [WindowAdaptiveInfo] that collects useful information in making layout
43  *   adaptation decisions like [WindowSizeClass].
44  * @param verticalHingePolicy [HingePolicy] that decides how layouts are supposed to address
45  *   vertical hinges.
46  * @return an [PaneScaffoldDirective] to be used to decide adaptive layout states.
47  */
48 @ExperimentalMaterial3AdaptiveApi
49 @Suppress("DEPRECATION") // WindowWidthSizeClass is deprecated
calculatePaneScaffoldDirectivenull50 fun calculatePaneScaffoldDirective(
51     windowAdaptiveInfo: WindowAdaptiveInfo,
52     verticalHingePolicy: HingePolicy = HingePolicy.AvoidSeparating
53 ): PaneScaffoldDirective {
54     val maxHorizontalPartitions: Int
55     val horizontalPartitionSpacerSize: Dp
56     when (windowAdaptiveInfo.windowSizeClass.windowWidthSizeClass) {
57         androidx.window.core.layout.WindowWidthSizeClass.COMPACT -> {
58             maxHorizontalPartitions = 1
59             horizontalPartitionSpacerSize = 0.dp
60         }
61         androidx.window.core.layout.WindowWidthSizeClass.MEDIUM -> {
62             maxHorizontalPartitions = 1
63             horizontalPartitionSpacerSize = 0.dp
64         }
65         else -> {
66             maxHorizontalPartitions = 2
67             horizontalPartitionSpacerSize = 24.dp
68         }
69     }
70     val maxVerticalPartitions: Int
71     val verticalPartitionSpacerSize: Dp
72 
73     // TODO(conradchen): Confirm the table top mode settings
74     if (
75         windowAdaptiveInfo.windowPosture.isTabletop ||
76             (maxHorizontalPartitions == 1 &&
77                 windowAdaptiveInfo.windowSizeClass.windowHeightSizeClass ==
78                     androidx.window.core.layout.WindowHeightSizeClass.EXPANDED)
79     ) {
80         maxVerticalPartitions = 2
81         verticalPartitionSpacerSize = 24.dp
82     } else {
83         maxVerticalPartitions = 1
84         verticalPartitionSpacerSize = 0.dp
85     }
86 
87     // TODO(conradchen): add 412.dp for L/XL window size class when they are available
88     val defaultPanePreferredWidth = PaneScaffoldDirective.DefaultPreferredWidth
89 
90     val defaultPanePreferredHeight = PaneScaffoldDirective.DefaultPreferredHeight
91 
92     return PaneScaffoldDirective(
93         maxHorizontalPartitions = maxHorizontalPartitions,
94         horizontalPartitionSpacerSize = horizontalPartitionSpacerSize,
95         maxVerticalPartitions = maxVerticalPartitions,
96         verticalPartitionSpacerSize = verticalPartitionSpacerSize,
97         defaultPanePreferredWidth = defaultPanePreferredWidth,
98         defaultPanePreferredHeight = defaultPanePreferredHeight,
99         excludedBounds =
100             getExcludedVerticalBounds(windowAdaptiveInfo.windowPosture, verticalHingePolicy),
101     )
102 }
103 
104 /**
105  * Calculates the recommended [PaneScaffoldDirective] from a given [WindowAdaptiveInfo]. Use this
106  * method with [currentWindowAdaptiveInfo] to acquire Material-recommended dense-mode adaptive
107  * layout settings of the current activity window. Note that this function results in a dual-pane
108  * layout when the [WindowWidthSizeClass] is [WindowWidthSizeClass.MEDIUM], while
109  * [calculatePaneScaffoldDirective] results in a single-pane layout instead. We recommend to use
110  * [calculatePaneScaffoldDirective], unless you have a strong use case to show two panes on a
111  * medium-width window, which can make your layout look too packed.
112  *
113  * See more details on the [Material design guideline site]
114  * (https://m3.material.io/foundations/layout/applying-layout/window-size-classes).
115  *
116  * @param windowAdaptiveInfo [WindowAdaptiveInfo] that collects useful information in making layout
117  *   adaptation decisions like [WindowSizeClass].
118  * @param verticalHingePolicy [HingePolicy] that decides how layouts are supposed to address
119  *   vertical hinges.
120  * @return an [PaneScaffoldDirective] to be used to decide adaptive layout states.
121  */
122 @ExperimentalMaterial3AdaptiveApi
123 @Suppress("DEPRECATION") // WindowWidthSizeClass is deprecated
calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidthnull124 fun calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth(
125     windowAdaptiveInfo: WindowAdaptiveInfo,
126     verticalHingePolicy: HingePolicy = HingePolicy.AvoidSeparating
127 ): PaneScaffoldDirective {
128     val isMediumWidth =
129         windowAdaptiveInfo.windowSizeClass.windowWidthSizeClass ==
130             androidx.window.core.layout.WindowWidthSizeClass.MEDIUM
131     val isTableTop = windowAdaptiveInfo.windowPosture.isTabletop
132     return with(calculatePaneScaffoldDirective(windowAdaptiveInfo, verticalHingePolicy)) {
133         copy(
134             maxHorizontalPartitions = if (isMediumWidth) 2 else maxHorizontalPartitions,
135             horizontalPartitionSpacerSize =
136                 if (isMediumWidth) {
137                     24.dp
138                 } else {
139                     horizontalPartitionSpacerSize
140                 },
141             maxVerticalPartitions = if (isMediumWidth && !isTableTop) 1 else maxVerticalPartitions,
142             verticalPartitionSpacerSize =
143                 if (isMediumWidth && !isTableTop) 0.dp else verticalPartitionSpacerSize
144         )
145     }
146 }
147 
getExcludedVerticalBoundsnull148 private fun getExcludedVerticalBounds(posture: Posture, hingePolicy: HingePolicy): List<Rect> {
149     return when (hingePolicy) {
150         HingePolicy.AvoidSeparating -> posture.separatingVerticalHingeBounds
151         HingePolicy.AvoidOccluding -> posture.occludingVerticalHingeBounds
152         HingePolicy.AlwaysAvoid -> posture.allVerticalHingeBounds
153         else -> emptyList()
154     }
155 }
156 
157 /**
158  * Top-level directives about how a pane scaffold should be arranged and spaced, like how many
159  * partitions the layout can be split into and what should be the gutter size.
160  *
161  * @property maxHorizontalPartitions the max number of partitions along the horizontal axis the
162  *   layout can be split into.
163  * @property horizontalPartitionSpacerSize Size of the spacers between horizontal partitions. It's
164  *   equivalent to the left/right margins the horizontal partitions.
165  * @property maxVerticalPartitions the max number of partitions along the vertical axis the layout
166  *   can be split into.
167  * @property verticalPartitionSpacerSize Size of the spacers between vertical partitions. It's
168  *   equivalent to the top/bottom margins of the vertical partitions.
169  * @property defaultPanePreferredWidth Default preferred width of panes that will be used by the
170  *   scaffold if there's no [PaneScaffoldScope.preferredWidth] provided with a pane; see
171  *   [PaneScaffoldScope.preferredWidth] for more info about how and when preferred width will be
172  *   used.
173  * @property defaultPanePreferredHeight Default preferred height of panes that will be used by the
174  *   scaffold if there's no [PaneScaffoldScope.preferredHeight] provided with a pane; see
175  *   [PaneScaffoldScope.preferredHeight] for more info about how and when preferred height will be
176  *   used.
177  * @property excludedBounds the bounds of all areas in the window that the layout needs to avoid
178  *   displaying anything upon it. Usually these bounds represent where physical hinges are.
179  */
180 @Immutable
181 class PaneScaffoldDirective(
182     val maxHorizontalPartitions: Int,
183     val horizontalPartitionSpacerSize: Dp,
184     val maxVerticalPartitions: Int,
185     val verticalPartitionSpacerSize: Dp,
186     val defaultPanePreferredWidth: Dp,
187     val defaultPanePreferredHeight: Dp,
188     val excludedBounds: List<Rect>
189 ) {
190     constructor(
191         maxHorizontalPartitions: Int,
192         horizontalPartitionSpacerSize: Dp,
193         maxVerticalPartitions: Int,
194         verticalPartitionSpacerSize: Dp,
195         defaultPanePreferredWidth: Dp,
196         excludedBounds: List<Rect>,
197     ) : this(
198         maxHorizontalPartitions = maxHorizontalPartitions,
199         horizontalPartitionSpacerSize = horizontalPartitionSpacerSize,
200         maxVerticalPartitions = maxVerticalPartitions,
201         verticalPartitionSpacerSize = verticalPartitionSpacerSize,
202         defaultPanePreferredWidth = defaultPanePreferredWidth,
203         defaultPanePreferredHeight = DefaultPreferredHeight,
204         excludedBounds = excludedBounds,
205     )
206 
207     /**
208      * Returns a new copy of [PaneScaffoldDirective] with specified fields overwritten. Use this
209      * method to create a custom [PaneScaffoldDirective] from the default instance or the result of
210      * [calculatePaneScaffoldDirective].
211      *
212      * @param maxHorizontalPartitions the max number of partitions along the horizontal axis the
213      *   layout can be split into.
214      * @param horizontalPartitionSpacerSize Size of the spacers between horizontal partitions. It's
215      *   equivalent to the left/right margins the horizontal partitions.
216      * @param maxVerticalPartitions the max number of partitions along the vertical axis the layout
217      *   can be split into.
218      * @param verticalPartitionSpacerSize Size of the spacers between vertical partitions. It's
219      *   equivalent to the top/bottom margins of the vertical partitions.
220      * @param defaultPanePreferredWidth Default preferred width of panes that will be used by the
221      *   scaffold if there's no [PaneScaffoldScope.preferredWidth] provided with a pane.
222      * @param excludedBounds the bounds of all areas in the window that the layout needs to avoid
223      *   displaying anything upon it. Usually these bounds represent where physical hinges are.
224      * @param defaultPanePreferredHeight Default preferred height of panes that will be used by the
225      *   scaffold if there's no [PaneScaffoldScope.preferredHeight] provided with a pane.
226      */
copynull227     fun copy(
228         maxHorizontalPartitions: Int = this.maxHorizontalPartitions,
229         horizontalPartitionSpacerSize: Dp = this.horizontalPartitionSpacerSize,
230         maxVerticalPartitions: Int = this.maxVerticalPartitions,
231         verticalPartitionSpacerSize: Dp = this.verticalPartitionSpacerSize,
232         defaultPanePreferredWidth: Dp = this.defaultPanePreferredWidth,
233         excludedBounds: List<Rect> = this.excludedBounds,
234         defaultPanePreferredHeight: Dp = this.defaultPanePreferredHeight
235     ): PaneScaffoldDirective =
236         PaneScaffoldDirective(
237             maxHorizontalPartitions = maxHorizontalPartitions,
238             horizontalPartitionSpacerSize = horizontalPartitionSpacerSize,
239             maxVerticalPartitions = maxVerticalPartitions,
240             verticalPartitionSpacerSize = verticalPartitionSpacerSize,
241             defaultPanePreferredWidth = defaultPanePreferredWidth,
242             defaultPanePreferredHeight = defaultPanePreferredHeight,
243             excludedBounds = excludedBounds,
244         )
245 
246     /**
247      * Returns a new copy of [PaneScaffoldDirective] with specified fields overwritten. Use this
248      * method to create a custom [PaneScaffoldDirective] from the default instance or the result of
249      * [calculatePaneScaffoldDirective].
250      *
251      * @param maxHorizontalPartitions the max number of partitions along the horizontal axis the
252      *   layout can be split into.
253      * @param horizontalPartitionSpacerSize Size of the spacers between horizontal partitions. It's
254      *   equivalent to the left/right margins the horizontal partitions.
255      * @param maxVerticalPartitions the max number of partitions along the vertical axis the layout
256      *   can be split into.
257      * @param verticalPartitionSpacerSize Size of the spacers between vertical partitions. It's
258      *   equivalent to the top/bottom margins of the vertical partitions.
259      * @param defaultPanePreferredWidth Default preferred width of panes that will be used by the
260      *   scaffold if there's no [PaneScaffoldScope.preferredWidth] provided with a pane.
261      * @param excludedBounds the bounds of all areas in the window that the layout needs to avoid
262      *   displaying anything upon it. Usually these bounds represent where physical hinges are.
263      */
264     @Deprecated(
265         "Maintained for binary compatibility. Use version with defaultPanePreferredHeight instead.",
266         level = DeprecationLevel.HIDDEN
267     )
268     fun copy(
269         maxHorizontalPartitions: Int = this.maxHorizontalPartitions,
270         horizontalPartitionSpacerSize: Dp = this.horizontalPartitionSpacerSize,
271         maxVerticalPartitions: Int = this.maxVerticalPartitions,
272         verticalPartitionSpacerSize: Dp = this.verticalPartitionSpacerSize,
273         defaultPanePreferredWidth: Dp = this.defaultPanePreferredWidth,
274         excludedBounds: List<Rect> = this.excludedBounds
275     ): PaneScaffoldDirective =
276         PaneScaffoldDirective(
277             maxHorizontalPartitions = maxHorizontalPartitions,
278             horizontalPartitionSpacerSize = horizontalPartitionSpacerSize,
279             maxVerticalPartitions = maxVerticalPartitions,
280             verticalPartitionSpacerSize = verticalPartitionSpacerSize,
281             defaultPanePreferredWidth = defaultPanePreferredWidth,
282             excludedBounds = excludedBounds,
283         )
284 
285     override fun equals(other: Any?): Boolean {
286         if (this === other) return true
287         if (other !is PaneScaffoldDirective) return false
288         if (maxHorizontalPartitions != other.maxHorizontalPartitions) return false
289         if (horizontalPartitionSpacerSize != other.horizontalPartitionSpacerSize) return false
290         if (maxVerticalPartitions != other.maxVerticalPartitions) return false
291         if (verticalPartitionSpacerSize != other.verticalPartitionSpacerSize) return false
292         if (defaultPanePreferredWidth != other.defaultPanePreferredWidth) return false
293         if (defaultPanePreferredHeight != other.defaultPanePreferredHeight) return false
294         if (excludedBounds != other.excludedBounds) return false
295         return true
296     }
297 
hashCodenull298     override fun hashCode(): Int {
299         var result = maxHorizontalPartitions
300         result = 31 * result + horizontalPartitionSpacerSize.hashCode()
301         result = 31 * result + maxVerticalPartitions
302         result = 31 * result + verticalPartitionSpacerSize.hashCode()
303         result = 31 * result + defaultPanePreferredWidth.hashCode()
304         result = 31 * result + defaultPanePreferredHeight.hashCode()
305         result = 31 * result + excludedBounds.hashCode()
306         return result
307     }
308 
toStringnull309     override fun toString(): String {
310         return "PaneScaffoldDirective(maxHorizontalPartitions=$maxHorizontalPartitions, " +
311             "horizontalPartitionSpacerSize=$horizontalPartitionSpacerSize, " +
312             "maxVerticalPartitions=$maxVerticalPartitions, " +
313             "verticalPartitionSpacerSize=$verticalPartitionSpacerSize, " +
314             "defaultPanePreferredWidth=$defaultPanePreferredWidth, " +
315             "defaultPanePreferredHeight=$defaultPanePreferredHeight, " +
316             "number of excluded bounds=${excludedBounds.size})"
317     }
318 
319     companion object {
320         internal val DefaultPreferredWidth = 360.dp
321         internal val DefaultPreferredHeight = 420.dp
322 
323         /**
324          * A default instance of [PaneScaffoldDirective] that suggests a single-pane layout that
325          * occupies the full window. To create a customized [PaneScaffoldDirective], you can use
326          * [PaneScaffoldDirective.copy] on the default instance to create a copy with custom values.
327          */
328         val Default =
329             PaneScaffoldDirective(
330                 maxHorizontalPartitions = 1,
331                 horizontalPartitionSpacerSize = 0.dp,
332                 maxVerticalPartitions = 1,
333                 verticalPartitionSpacerSize = 0.dp,
334                 defaultPanePreferredWidth = DefaultPreferredWidth,
335                 defaultPanePreferredHeight = DefaultPreferredHeight,
336                 excludedBounds = emptyList()
337             )
338     }
339 }
340 
341 /** Policies that indicate how hinges are supposed to be addressed in an adaptive layout. */
342 @Immutable
343 @JvmInline
344 value class HingePolicy private constructor(private val value: Int) {
toStringnull345     override fun toString(): String {
346         return "HingePolicy." +
347             when (this) {
348                 AlwaysAvoid -> "AlwaysAvoid"
349                 AvoidSeparating -> "AvoidOccludingAndSeparating"
350                 AvoidOccluding -> "AvoidOccludingOnly"
351                 NeverAvoid -> "NeverAvoid"
352                 else -> ""
353             }
354     }
355 
356     companion object {
357         /** When rendering content in a layout, always avoid where hinges are. */
358         val AlwaysAvoid = HingePolicy(0)
359         /**
360          * When rendering content in a layout, avoid hinges that are separating. Note that an
361          * occluding hinge is supposed to be separating as well but not vice versa.
362          */
363         val AvoidSeparating = HingePolicy(1)
364         /**
365          * When rendering content in a layout, avoid hinges that are occluding. Note that an
366          * occluding hinge is supposed to be separating as well but not vice versa.
367          */
368         val AvoidOccluding = HingePolicy(2)
369         /** When rendering content in a layout, never avoid any hinges, separating or not. */
370         val NeverAvoid = HingePolicy(3)
371     }
372 }
373